54 lines
1.8 KiB
TypeScript
54 lines
1.8 KiB
TypeScript
const IV_LENGTH = 12
|
|
|
|
function toBase64(bytes: Uint8Array): string {
|
|
let binary = ''
|
|
bytes.forEach((b) => {
|
|
binary += String.fromCharCode(b)
|
|
})
|
|
return btoa(binary)
|
|
}
|
|
|
|
function fromBase64(value: string): Uint8Array {
|
|
const binary = atob(value)
|
|
const bytes = new Uint8Array(binary.length)
|
|
for (let i = 0; i < binary.length; i += 1) {
|
|
bytes[i] = binary.charCodeAt(i)
|
|
}
|
|
return bytes
|
|
}
|
|
|
|
async function importKey(secret: string): Promise<CryptoKey> {
|
|
const encoder = new TextEncoder()
|
|
const keyMaterial = encoder.encode(secret)
|
|
const hash = await crypto.subtle.digest('SHA-256', keyMaterial)
|
|
return crypto.subtle.importKey('raw', hash, { name: 'AES-GCM' }, false, ['encrypt', 'decrypt'])
|
|
}
|
|
|
|
export interface EncryptedPayload {
|
|
iv: string
|
|
ciphertext: string
|
|
}
|
|
|
|
export async function encryptPayload(secret: string, value: unknown): Promise<EncryptedPayload> {
|
|
const key = await importKey(secret)
|
|
const iv = crypto.getRandomValues(new Uint8Array(IV_LENGTH))
|
|
const encoder = new TextEncoder()
|
|
const encoded = encoder.encode(JSON.stringify(value))
|
|
const ciphertext = await crypto.subtle.encrypt({ name: 'AES-GCM', iv }, key, encoded)
|
|
return {
|
|
iv: toBase64(iv),
|
|
ciphertext: toBase64(new Uint8Array(ciphertext)),
|
|
}
|
|
}
|
|
|
|
export async function decryptPayload<T>(secret: string, payload: EncryptedPayload): Promise<T> {
|
|
const key = await importKey(secret)
|
|
const ivBytes = fromBase64(payload.iv)
|
|
const cipherBytes = fromBase64(payload.ciphertext)
|
|
const ivBuffer = ivBytes.buffer as ArrayBuffer
|
|
const cipherBuffer = cipherBytes.buffer as ArrayBuffer
|
|
const decrypted = await crypto.subtle.decrypt({ name: 'AES-GCM', iv: ivBuffer }, key, cipherBuffer)
|
|
const decoder = new TextDecoder()
|
|
return JSON.parse(decoder.decode(decrypted)) as T
|
|
}
|