/** * EncryptID Key Derivation Module * * Derives application-specific cryptographic keys from WebAuthn PRF output * or passphrase fallback. This is Layer 2 of the EncryptID architecture. */ import { bufferToBase64url, base64urlToBuffer } from './webauthn'; import { deriveEOAFromPRF } from './eoa-derivation'; import { p256 } from '@noble/curves/p256'; import { ed25519 } from '@noble/curves/ed25519'; // ============================================================================ // TYPES // ============================================================================ export interface DerivedKeys { /** AES-256-GCM key for file/data encryption */ encryptionKey: CryptoKey; /** ECDSA P-256 key pair for signing */ signingKeyPair: CryptoKeyPair; /** Ed25519 seed for DID key (raw bytes) */ didSeed: Uint8Array; /** The DID identifier (did:key:z6Mk...) */ did: string; /** Whether keys were derived from PRF (true) or passphrase (false) */ fromPRF: boolean; /** secp256k1 private key for EOA wallet operations (derived from PRF only) */ eoaPrivateKey?: Uint8Array; /** Ethereum address derived from the EOA private key */ eoaAddress?: string; } export interface EncryptedData { ciphertext: ArrayBuffer; iv: Uint8Array; tag?: ArrayBuffer; // For AEAD modes, tag is included in ciphertext } export interface SignedData { data: ArrayBuffer; signature: ArrayBuffer; publicKey: ArrayBuffer; } // ============================================================================ // KEY DERIVATION // ============================================================================ /** * EncryptID Key Manager * * Handles derivation and management of all cryptographic keys * from a master secret (PRF output or passphrase-derived). */ export class EncryptIDKeyManager { private masterKey: CryptoKey | null = null; private derivedKeys: DerivedKeys | null = null; private fromPRF: boolean = false; private prfOutputRaw: Uint8Array | null = null; /** * Initialize from WebAuthn PRF output * * This is the preferred path - keys are derived directly from * the hardware-backed PRF extension output. */ async initFromPRF(prfOutput: ArrayBuffer): Promise { // Copy raw PRF output for EOA derivation (uses @noble/hashes directly) // Must copy — not just wrap — so clear() doesn't zero the caller's buffer this.prfOutputRaw = new Uint8Array(new Uint8Array(prfOutput)); // Import PRF output as HKDF key material this.masterKey = await crypto.subtle.importKey( 'raw', prfOutput, { name: 'HKDF' }, false, // Not extractable ['deriveKey', 'deriveBits'] ); this.fromPRF = true; this.derivedKeys = null; // Clear any existing derived keys console.log('EncryptID: Key manager initialized from PRF output'); } /** * Initialize from passphrase (fallback for non-PRF authenticators) * * Uses PBKDF2 with high iteration count to derive master key * from user's passphrase. The salt should be stored securely. */ async initFromPassphrase( passphrase: string, salt: Uint8Array ): Promise { const encoder = new TextEncoder(); // Import passphrase as PBKDF2 key const passphraseKey = await crypto.subtle.importKey( 'raw', encoder.encode(passphrase), { name: 'PBKDF2' }, false, ['deriveBits'] ); // Derive master key material using PBKDF2 // OWASP 2023 recommends 600,000 iterations for PBKDF2-SHA256 const masterKeyMaterial = await crypto.subtle.deriveBits( { name: 'PBKDF2', salt: salt as BufferSource, iterations: 600000, hash: 'SHA-256', }, passphraseKey, 256 // 32 bytes ); // Import as HKDF key for further derivation this.masterKey = await crypto.subtle.importKey( 'raw', masterKeyMaterial, { name: 'HKDF' }, false, ['deriveKey', 'deriveBits'] ); this.fromPRF = false; this.derivedKeys = null; console.log('EncryptID: Key manager initialized from passphrase'); } /** * Generate a random salt for passphrase-based derivation */ static generateSalt(): Uint8Array { return crypto.getRandomValues(new Uint8Array(32)); } /** * Check if the key manager is initialized */ isInitialized(): boolean { return this.masterKey !== null; } /** * Get or derive all application keys */ async getKeys(): Promise { if (!this.masterKey) { throw new Error('Key manager not initialized'); } // Return cached keys if available if (this.derivedKeys) { return this.derivedKeys; } // Derive all keys const [encryptionKey, signingKeyPair, didSeed] = await Promise.all([ this.deriveEncryptionKey(), this.deriveSigningKeyPair(), this.deriveDIDSeed(), ]); // Generate DID from seed const did = await this.generateDID(didSeed); // Derive EOA key (PRF-only — passphrase path doesn't get wallet keys) const eoa = this.deriveEOAKey(); this.derivedKeys = { encryptionKey, signingKeyPair, didSeed, did, fromPRF: this.fromPRF, ...(eoa && { eoaPrivateKey: eoa.privateKey, eoaAddress: eoa.address }), }; return this.derivedKeys; } /** * Derive AES-256-GCM encryption key */ private async deriveEncryptionKey(): Promise { if (!this.masterKey) { throw new Error('Key manager not initialized'); } const encoder = new TextEncoder(); return crypto.subtle.deriveKey( { name: 'HKDF', hash: 'SHA-256', salt: encoder.encode('encryptid-encryption-key-v1'), info: encoder.encode('AES-256-GCM'), }, this.masterKey, { name: 'AES-GCM', length: 256, }, false, // Not extractable ['encrypt', 'decrypt', 'wrapKey', 'unwrapKey'] ); } /** * Derive ECDSA P-256 signing key pair * * Note: WebCrypto doesn't support deterministic key generation, * so we derive a seed and use it to generate the key pair. * For production, consider using a library like @noble/curves. */ private async deriveSigningKeyPair(): Promise { if (!this.masterKey) { throw new Error('Key manager not initialized'); } const encoder = new TextEncoder(); // Derive seed for signing key const seed = await crypto.subtle.deriveBits( { name: 'HKDF', hash: 'SHA-256', salt: encoder.encode('encryptid-signing-key-v1'), info: encoder.encode('ECDSA-P256-seed'), }, this.masterKey, 256 ); // Derive deterministic P-256 key pair from seed using @noble/curves const seedBytes = new Uint8Array(seed); const publicKeyUncompressed = p256.getPublicKey(seedBytes, false); // 65 bytes: 04 || x || y // Import into WebCrypto as ECDSA key pair for sign/verify operations // PKCS8 wrapping for private key import const pkcs8 = buildP256Pkcs8(seedBytes); const privateKey = await crypto.subtle.importKey( 'pkcs8', pkcs8, { name: 'ECDSA', namedCurve: 'P-256' }, false, // Not extractable ['sign'] ); const publicKey = await crypto.subtle.importKey( 'raw', publicKeyUncompressed as BufferSource, { name: 'ECDSA', namedCurve: 'P-256' }, true, ['verify'] ); return { privateKey, publicKey }; } /** * Derive Ed25519 seed for DID key */ private async deriveDIDSeed(): Promise { if (!this.masterKey) { throw new Error('Key manager not initialized'); } const encoder = new TextEncoder(); const seed = await crypto.subtle.deriveBits( { name: 'HKDF', hash: 'SHA-256', salt: encoder.encode('encryptid-did-key-v1'), info: encoder.encode('Ed25519-seed'), }, this.masterKey, 256 ); return new Uint8Array(seed); } /** * Generate did:key identifier from Ed25519 seed * * Format: did:key:z6Mk... (multicodec ed25519-pub + base58btc) */ private async generateDID(seed: Uint8Array): Promise { // Derive Ed25519 public key from seed using @noble/curves const publicKeyBytes = ed25519.getPublicKey(seed); // Multicodec prefix for Ed25519 public key: 0xed01 const multicodecKey = new Uint8Array(34); multicodecKey[0] = 0xed; multicodecKey[1] = 0x01; multicodecKey.set(publicKeyBytes, 2); // Encode as base58btc (did:key spec requires 'z' prefix + base58btc) return `did:key:z${base58btcEncode(multicodecKey)}`; } /** * Derive secp256k1 EOA key from PRF output. * * Only available when initialized from PRF (not passphrase). * Uses @noble/curves + @noble/hashes for deterministic derivation * with domain-separated HKDF salt/info. */ private deriveEOAKey(): { privateKey: Uint8Array; publicKey: Uint8Array; address: string } | null { if (!this.fromPRF || !this.prfOutputRaw) { return null; } const result = deriveEOAFromPRF(this.prfOutputRaw); console.log('EncryptID: EOA key derived, address:', result.address); return result; } /** * Clear all keys from memory */ clear(): void { // Zero out sensitive key material if (this.derivedKeys?.eoaPrivateKey) { this.derivedKeys.eoaPrivateKey.fill(0); } if (this.derivedKeys?.didSeed) { this.derivedKeys.didSeed.fill(0); } if (this.prfOutputRaw) { this.prfOutputRaw.fill(0); this.prfOutputRaw = null; } this.masterKey = null; this.derivedKeys = null; this.fromPRF = false; console.log('EncryptID: Key manager cleared'); } } // ============================================================================ // ENCRYPTION UTILITIES // ============================================================================ /** * Encrypt data using the encryption key */ export async function encryptData( key: CryptoKey, data: ArrayBuffer | Uint8Array | string ): Promise { // Convert string to ArrayBuffer if needed let plaintext: ArrayBuffer; if (typeof data === 'string') { plaintext = new TextEncoder().encode(data).buffer; } else if (data instanceof Uint8Array) { plaintext = data.buffer as ArrayBuffer; } else { plaintext = data; } // Generate random IV const iv = crypto.getRandomValues(new Uint8Array(12)); // Encrypt const ciphertext = await crypto.subtle.encrypt( { name: 'AES-GCM', iv: iv, }, key, plaintext ); return { ciphertext, iv }; } /** * Decrypt data using the encryption key */ export async function decryptData( key: CryptoKey, encrypted: EncryptedData ): Promise { return crypto.subtle.decrypt( { name: 'AES-GCM', iv: encrypted.iv as BufferSource, }, key, encrypted.ciphertext ); } /** * Decrypt data and return as string */ export async function decryptDataAsString( key: CryptoKey, encrypted: EncryptedData ): Promise { const plaintext = await decryptData(key, encrypted); return new TextDecoder().decode(plaintext); } // ============================================================================ // SIGNING UTILITIES // ============================================================================ /** * Sign data using the signing key */ export async function signData( keyPair: CryptoKeyPair, data: ArrayBuffer | Uint8Array | string ): Promise { // Convert string to ArrayBuffer if needed let dataBuffer: ArrayBuffer; if (typeof data === 'string') { dataBuffer = new TextEncoder().encode(data).buffer; } else if (data instanceof Uint8Array) { dataBuffer = data.buffer as ArrayBuffer; } else { dataBuffer = data; } // Sign const signature = await crypto.subtle.sign( { name: 'ECDSA', hash: 'SHA-256', }, keyPair.privateKey, dataBuffer ); // Export public key for verification const publicKey = await crypto.subtle.exportKey('raw', keyPair.publicKey); return { data: dataBuffer, signature, publicKey, }; } /** * Verify a signature */ export async function verifySignature( signed: SignedData ): Promise { // Import public key const publicKey = await crypto.subtle.importKey( 'raw', signed.publicKey, { name: 'ECDSA', namedCurve: 'P-256', }, false, ['verify'] ); // Verify return crypto.subtle.verify( { name: 'ECDSA', hash: 'SHA-256', }, publicKey, signed.signature, signed.data ); } // ============================================================================ // KEY WRAPPING (FOR SHARING) // ============================================================================ /** * Wrap a key for sharing with another user * * Used in rfiles to share encrypted files - wrap the file key * with the recipient's public key. */ export async function wrapKeyForRecipient( keyToWrap: CryptoKey, recipientPublicKey: CryptoKey ): Promise { return crypto.subtle.wrapKey( 'raw', keyToWrap, recipientPublicKey, { name: 'RSA-OAEP', } ); } /** * Unwrap a key that was shared with us */ export async function unwrapSharedKey( wrappedKey: ArrayBuffer, privateKey: CryptoKey ): Promise { return crypto.subtle.unwrapKey( 'raw', wrappedKey, privateKey, { name: 'RSA-OAEP', }, { name: 'AES-GCM', length: 256, }, false, ['encrypt', 'decrypt'] ); } // ============================================================================ // HELPERS // ============================================================================ /** * Build a PKCS#8 DER wrapper around a raw P-256 private key (32 bytes) * so it can be imported via WebCrypto's importKey('pkcs8', ...). * * Structure: SEQUENCE { version, AlgorithmIdentifier { ecPublicKey, P-256 }, OCTET STRING { SEQUENCE { version, privateKey } } } */ function buildP256Pkcs8(rawPrivateKey: Uint8Array): ArrayBuffer { // DER-encoded PKCS#8 template for P-256 with a 32-byte private key // Pre-computed ASN.1 header (version + algorithm identifier) const header = new Uint8Array([ 0x30, 0x41, // SEQUENCE (65 bytes total) 0x02, 0x01, 0x00, // INTEGER version = 0 0x30, 0x13, // SEQUENCE (AlgorithmIdentifier) 0x06, 0x07, // OID 1.2.840.10045.2.1 (ecPublicKey) 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x02, 0x01, 0x06, 0x08, // OID 1.2.840.10045.3.1.7 (P-256) 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x03, 0x01, 0x07, 0x04, 0x27, // OCTET STRING (39 bytes) 0x30, 0x25, // SEQUENCE (37 bytes) 0x02, 0x01, 0x01, // INTEGER version = 1 0x04, 0x20, // OCTET STRING (32 bytes) — the private key ]); const result = new Uint8Array(header.length + 32); result.set(header); result.set(rawPrivateKey, header.length); return result.buffer; } /** * Base58btc encoding (Bitcoin alphabet). * Minimal implementation — no external dependency needed. */ const BASE58_ALPHABET = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'; function base58btcEncode(bytes: Uint8Array): string { // Count leading zeros let zeroes = 0; for (const b of bytes) { if (b !== 0) break; zeroes++; } // Convert to base58 using bigint arithmetic let num = BigInt(0); for (const b of bytes) { num = num * 256n + BigInt(b); } let encoded = ''; while (num > 0n) { const remainder = Number(num % 58n); num = num / 58n; encoded = BASE58_ALPHABET[remainder] + encoded; } // Prepend '1' for each leading zero byte return '1'.repeat(zeroes) + encoded; } // ============================================================================ // SINGLETON INSTANCE // ============================================================================ // Global key manager instance let keyManagerInstance: EncryptIDKeyManager | null = null; /** * Get the global EncryptID key manager instance */ export function getKeyManager(): EncryptIDKeyManager { if (!keyManagerInstance) { keyManagerInstance = new EncryptIDKeyManager(); } return keyManagerInstance; } /** * Reset the global key manager (e.g., on logout) */ export function resetKeyManager(): void { if (keyManagerInstance) { keyManagerInstance.clear(); keyManagerInstance = null; } }