// TODO: Consider whether we should shuffle this to Stack Util (as opposed to Primitive). // It is async which breaks from our Primitive patterns. import { utf8ToBin } from '@bitauth/libauth'; // NOTE: We need to provide a default salt otherwise we would be deriving a key per each operation. // This would be far too computationally expensive. // TODO: Allow the salt to be customized in an options argument to the constructor. export const DEFAULT_XO_SALT = 'cash.xo.default_salt_for_encryption_aabbccddeeff00112233445566778899'; export type EncrypterOpts = { algorithm: string; keyLength: number; salt: Uint8Array; ivLength: number; iterations: number; }; export class Encrypter { constructor( private key: CryptoKey, public opts: EncrypterOpts, ) {} static async fromBytes(key: Uint8Array, opts: Partial = {}) { // Merge the provided options into our default options. const encryptionOpts = { algorithm: 'AES-GCM', keyLength: 256, salt: utf8ToBin(DEFAULT_XO_SALT), ivLength: 16, iterations: 100_000, ...opts, }; // Create a key derivation key const keyMaterialBase = await crypto.subtle.importKey( 'raw', key, 'PBKDF2', false, ['deriveBits', 'deriveKey'], ); // Derive the actual encryption key const derivedKey = await crypto.subtle.deriveKey( { name: 'PBKDF2', salt: encryptionOpts.salt, iterations: encryptionOpts.iterations, hash: 'SHA-256', }, keyMaterialBase, { name: encryptionOpts.algorithm, length: encryptionOpts.keyLength, }, false, ['encrypt', 'decrypt'], ); return new Encrypter(derivedKey, encryptionOpts); } static async fromPassphrase( passphrase: string, opts: Partial = {}, ) { const key = utf8ToBin(passphrase); return this.fromBytes(key, opts); } async encrypt(payload: Uint8Array | string): Promise { // Convert string payload to Uint8Array if needed const data = typeof payload === 'string' ? utf8ToBin(payload) : payload; // Generate random IV const iv = crypto.getRandomValues(new Uint8Array(this.opts.ivLength)); // Encrypt the data const encryptedData = await crypto.subtle.encrypt( { name: this.opts.algorithm, iv, }, this.key, data, ); // Combine IV and encrypted data const result = new Uint8Array(iv.length + encryptedData.byteLength); result.set(iv); result.set(new Uint8Array(encryptedData), iv.length); return result; } async decrypt(payload: Uint8Array | string): Promise { // Ensure payload is Uint8Array if (typeof payload === 'string') { throw new Error('Encrypted payload must be Uint8Array'); } // Extract IV and encrypted data const iv = payload.slice(0, this.opts.ivLength); const encryptedData = payload.slice(this.opts.ivLength); // Decrypt the data const decryptedData = await crypto.subtle.decrypt( { name: this.opts.algorithm, iv, }, this.key, encryptedData, ); return new Uint8Array(decryptedData); } }