// Password wraps protect serialized wallet secrets with authenticated AES-GCM. // PBKDF2 parameters and wire fields stay centralized so legacy linking-key and // v2 wallet-material records remain interoperable with their own readers. import { bytesToHex, hexToBytes, utf8ToBytes } from '@noble/hashes/utils.js'; const PBKDF2_ITERATIONS = 210_000; const deriveAesKeyFromPassword = (password: string, salt: Uint8Array): Promise => crypto.subtle .importKey('raw', utf8ToBytes(password), 'PBKDF2', false, ['deriveKey']) .then((baseKey) => crypto.subtle.deriveKey( { name: 'PBKDF2', salt: new Uint8Array(salt), iterations: PBKDF2_ITERATIONS, hash: 'SHA-256', }, baseKey, { name: 'AES-GCM', length: 256 }, false, ['encrypt', 'decrypt'], ), ); export type EncryptedSecretParts = { readonly salt: string; readonly iv: string; readonly ciphertext: string; }; export const encryptSecretParts = async ( value: string, password: string, ): Promise => { const salt = crypto.getRandomValues(new Uint8Array(16)); const iv = crypto.getRandomValues(new Uint8Array(12)); const aesKey = await deriveAesKeyFromPassword(password, salt); const ciphertext = new Uint8Array( await crypto.subtle.encrypt({ name: 'AES-GCM', iv }, aesKey, utf8ToBytes(value)), ); return { salt: bytesToHex(salt), iv: bytesToHex(iv), ciphertext: bytesToHex(ciphertext), }; }; export const decryptSecretParts = async ( parts: EncryptedSecretParts, password: string, ): Promise => { const salt = hexToBytes(parts.salt); const iv = hexToBytes(parts.iv); const aesKey = await deriveAesKeyFromPassword(password, salt); const plaintext = await crypto.subtle.decrypt( { name: 'AES-GCM', iv }, aesKey, hexToBytes(parts.ciphertext), ); return new TextDecoder().decode(plaintext); };