import { importRaw, getRandomValues, decryptBuffer, encryptBuffer, deriveBits, type IKey } from './shared'; type Algorithms = { name: 'AES-GCM'; length: 256 } | { name: 'AES-CBC'; length: 256 }; export type DerivedKey = IKey< TAlgorithm, false, 'secret', TAlgorithm['name'] extends 'AES-CBC' ? ['decrypt'] : ['encrypt', 'decrypt'] >; export type Options = { salt: Uint8Array; iterations: number; }; export type EncryptedContent = { iv: Uint8Array; ciphertext: Uint8Array; }; type Narrow = { [P in keyof T]: P extends keyof U ? U[P] : T[P]; }; export type BaseKey = IKey< { readonly name: 'PBKDF2'; }, false, 'secret', ['deriveBits'] >; export const importBaseKey = async (keyData: Uint8Array): Promise => { const baseKey = await importRaw(keyData, { name: 'PBKDF2' }, false, ['deriveBits']); return baseKey; }; type Throws = F extends (...args: infer TArgs) => infer TRet ? (...args: TArgs) => TRet & never : never; type FixedSizeArrayBuffer = Narrow< ArrayBuffer, { resize: Throws; readonly byteLength: N; get maxByteLength(): N; get resizable(): false; get detached(): false; } >; export type DerivedBits = FixedSizeArrayBuffer<32>; export const derive = async (key: BaseKey, options: Options): Promise => { const bits = await deriveBits( { name: key.algorithm.name, hash: 'SHA-256', salt: options.salt, iterations: options.iterations }, key, 256, ); return bits as DerivedBits; }; export const importKey = async (derivedBits: DerivedBits, algorithm: T): Promise> => { const usages: ['decrypt'] | ['encrypt', 'decrypt'] = algorithm.name === 'AES-CBC' ? ['decrypt'] : ['encrypt', 'decrypt']; const key = await importRaw(derivedBits, algorithm satisfies AesKeyGenParams, false, usages); return key as DerivedKey; }; export const decrypt = async (key: DerivedKey, content: EncryptedContent): Promise> => { const decrypted = await decryptBuffer( key, { name: key.algorithm.name, iv: content.iv } satisfies AesCbcParams | AesGcmParams, content.ciphertext, ); return new Uint8Array(decrypted); }; export const encrypt = async ( key: DerivedKey<{ name: 'AES-GCM'; length: 256 }>, data: Uint8Array, ): Promise => { // Always use AES-GCM for new data const iv = getRandomValues(new Uint8Array(12)); const ciphertext = await encryptBuffer(key, { name: 'AES-GCM', iv }, data); return { iv, ciphertext: new Uint8Array(ciphertext) }; };