export interface EncryptedPayload { iv: string ciphertext: string } const SALT_LENGTH = 32 const PBKDF2_ITERATIONS = 100000 const PBKDF2_HASH = 'SHA-256' const KEY_LENGTH = 32 /** * Derive an encryption key from a recovery phrase using PBKDF2 */ export async function deriveKeyFromPhrase(phrase: string[]): Promise { const phraseString = phrase.join(' ') const encoder = new TextEncoder() const password = encoder.encode(phraseString) // Generate a deterministic salt from the phrase itself // This ensures the same phrase always generates the same key const saltBuffer = await crypto.subtle.digest('SHA-256', password) const saltArray = new Uint8Array(saltBuffer) const salt = saltArray.slice(0, SALT_LENGTH) // Import password as key material const keyMaterial = await crypto.subtle.importKey( 'raw', password, 'PBKDF2', false, ['deriveBits', 'deriveKey'] ) // Derive key using PBKDF2 const derivedKey = await crypto.subtle.deriveKey( { name: 'PBKDF2', salt, iterations: PBKDF2_ITERATIONS, hash: PBKDF2_HASH, }, keyMaterial, { name: 'AES-GCM', length: KEY_LENGTH * 8 }, false, ['encrypt', 'decrypt'] ) return derivedKey } /** * Encrypt nsec with derived key */ export async function encryptNsec(derivedKey: CryptoKey, nsecHex: string): Promise { const encoder = new TextEncoder() const data = encoder.encode(nsecHex) const iv = crypto.getRandomValues(new Uint8Array(12)) const encrypted = await crypto.subtle.encrypt( { name: 'AES-GCM', iv }, derivedKey, data ) const encryptedArray = new Uint8Array(encrypted) // Convert to base64 for storage function toBase64(bytes: Uint8Array): string { let binary = '' bytes.forEach((b) => { binary += String.fromCharCode(b) }) return btoa(binary) } return { iv: toBase64(iv), ciphertext: toBase64(encryptedArray), } } /** * Decrypt nsec with derived key */ export async function decryptNsec(derivedKey: CryptoKey, payload: EncryptedPayload): Promise { 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 } const iv = fromBase64(payload.iv) const ciphertext = fromBase64(payload.ciphertext) // Ensure iv and ciphertext are proper ArrayBuffer views const ivBuffer = iv.buffer instanceof ArrayBuffer ? iv.buffer : new ArrayBuffer(iv.byteLength) const ivView = new Uint8Array(ivBuffer, 0, iv.byteLength) ivView.set(iv) const cipherBuffer = ciphertext.buffer instanceof ArrayBuffer ? ciphertext.buffer : new ArrayBuffer(ciphertext.byteLength) const cipherView = new Uint8Array(cipherBuffer, 0, ciphertext.byteLength) cipherView.set(ciphertext) const decrypted = await crypto.subtle.decrypt( { name: 'AES-GCM', iv: ivView }, derivedKey, cipherView ) const decoder = new TextDecoder() return decoder.decode(decrypted) }