import { getPublicKey, getSharedSecret } from '@noble/secp256k1'; import { bytesToHex, hexToBytes } from '@noble/hashes/utils'; /** * Encrypt data with AES-GCM using a shared secret derived from ECDH. */ export async function encryptWithECDH( data: string, recipientPublicKey: string, senderPrivateKey: string, ): Promise<{ encrypted: string; iv: string; publicKey: string; }> { const recipientPubKey = hexToBytes(recipientPublicKey); const senderPrivKey = hexToBytes(senderPrivateKey); const sharedSecret = getSharedSecret(senderPrivKey, recipientPubKey, true); const keyMaterial = await crypto.subtle.importKey( 'raw', sharedSecret.slice(0, 32) as BufferSource, { name: 'HKDF' }, false, ['deriveBits', 'deriveKey'], ); const derivedKey = await crypto.subtle.deriveKey( { name: 'HKDF', hash: 'SHA-256', salt: new Uint8Array(32), info: new Uint8Array(0), }, keyMaterial, { name: 'AES-GCM', length: 256 }, false, ['encrypt'], ); const iv = crypto.getRandomValues(new Uint8Array(12)); const encoder = new TextEncoder(); const dataBytes = encoder.encode(data); const encrypted = await crypto.subtle.encrypt( { name: 'AES-GCM', iv, }, derivedKey, dataBytes, ); return { encrypted: bytesToHex(new Uint8Array(encrypted)), iv: bytesToHex(iv), publicKey: bytesToHex(getPublicKey(senderPrivKey, true)), }; } /** * Decrypt data with AES-GCM using a shared secret derived from ECDH. */ export async function decryptWithECDH( encrypted: string, iv: string, senderPublicKey: string, recipientPrivateKey: string, ): Promise { const senderPubKey = hexToBytes(senderPublicKey); const recipientPrivKey = hexToBytes(recipientPrivateKey); const sharedSecret = getSharedSecret(recipientPrivKey, senderPubKey, true); const keyMaterial = await crypto.subtle.importKey( 'raw', sharedSecret.slice(0, 32) as BufferSource, { name: 'HKDF' }, false, ['deriveBits', 'deriveKey'], ); const derivedKey = await crypto.subtle.deriveKey( { name: 'HKDF', hash: 'SHA-256', salt: new Uint8Array(32), info: new Uint8Array(0), }, keyMaterial, { name: 'AES-GCM', length: 256 }, false, ['decrypt'], ); const encryptedBytes = hexToBytes(encrypted); const ivBytes = hexToBytes(iv); const decrypted = await crypto.subtle.decrypt( { name: 'AES-GCM', iv: ivBytes as BufferSource, }, derivedKey, encryptedBytes as BufferSource, ); const decoder = new TextDecoder(); return decoder.decode(decrypted); } /** * Encrypt data for "publish to all" scenario. * Uses a symmetric key that can be shared via ECDH with multiple recipients. */ export async function encryptForAll( data: string, encryptionKey: Uint8Array, ): Promise<{ encrypted: string; iv: string; }> { const key = await crypto.subtle.importKey( 'raw', encryptionKey as BufferSource, { name: 'AES-GCM' }, false, ['encrypt'], ); const iv = crypto.getRandomValues(new Uint8Array(12)); const encoder = new TextEncoder(); const dataBytes = encoder.encode(data); const encrypted = await crypto.subtle.encrypt( { name: 'AES-GCM', iv: iv as BufferSource, }, key, dataBytes, ); return { encrypted: bytesToHex(new Uint8Array(encrypted)), iv: bytesToHex(iv), }; } /** * Decrypt data encrypted with encryptForAll. */ export async function decryptForAll( encrypted: string, iv: string, encryptionKey: Uint8Array, ): Promise { const key = await crypto.subtle.importKey( 'raw', encryptionKey as BufferSource, { name: 'AES-GCM' }, false, ['decrypt'], ); const encryptedBytes = hexToBytes(encrypted); const ivBytes = hexToBytes(iv); const decrypted = await crypto.subtle.decrypt( { name: 'AES-GCM', iv: ivBytes as BufferSource, }, key, encryptedBytes as BufferSource, ); const decoder = new TextDecoder(); return decoder.decode(decrypted); }