2025-12-23 02:20:57 +01:00

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
}