const PBKDF2_ITERATIONS = 100_000; const SALT_LENGTH = 16; const IV_LENGTH = 12; const KEY_LENGTH = 256; export interface EncryptedPayload { ciphertext: string; iv: string; salt: string; } /** * Derive AES-GCM key from password using PBKDF2-HMAC-SHA256. */ async function deriveKey( password: string, salt: Uint8Array, ): Promise { const enc = new TextEncoder(); const keyMaterial = await crypto.subtle.importKey( 'raw', enc.encode(password), 'PBKDF2', false, ['deriveBits', 'deriveKey'], ); return crypto.subtle.deriveKey( { name: 'PBKDF2', salt: salt as BufferSource, iterations: PBKDF2_ITERATIONS, hash: 'SHA-256', }, keyMaterial, { name: 'AES-GCM', length: KEY_LENGTH }, false, ['encrypt', 'decrypt'], ); } /** * Encrypt private key (hex string) with password. * Returns base64-encoded ciphertext, iv, and salt. */ export async function encryptPrivateKey( privateKeyHex: string, password: string, ): Promise { const salt = crypto.getRandomValues(new Uint8Array(SALT_LENGTH)); const iv = crypto.getRandomValues(new Uint8Array(IV_LENGTH)); const key = await deriveKey(password, salt); const enc = new TextEncoder(); const plaintext = enc.encode(privateKeyHex); const ciphertext = await crypto.subtle.encrypt( { name: 'AES-GCM', iv }, key, plaintext, ); return { ciphertext: b64Encode(new Uint8Array(ciphertext)), iv: b64Encode(iv), salt: b64Encode(salt), }; } /** * Decrypt private key from payload using password. * Returns hex string or throws. */ export async function decryptPrivateKey( payload: EncryptedPayload, password: string, ): Promise { const salt = b64Decode(payload.salt); const iv = b64Decode(payload.iv); const ciphertext = b64Decode(payload.ciphertext); const key = await deriveKey(password, salt); const decrypted = await crypto.subtle.decrypt( { name: 'AES-GCM', iv: iv as BufferSource }, key, ciphertext as BufferSource, ); return new TextDecoder().decode(decrypted); } function b64Encode(bytes: Uint8Array): string { let binary = ''; for (let i = 0; i < bytes.length; i++) { binary += String.fromCharCode(bytes[i] ?? 0); } return btoa(binary); } function b64Decode(str: string): Uint8Array { const bin = atob(str); const out = new Uint8Array(bin.length); for (let i = 0; i < bin.length; i++) { out[i] = bin.charCodeAt(i); } return out; }