/** * 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'; // ============================================================================ // 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; } 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; /** * 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 { // 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, 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); this.derivedKeys = { encryptionKey, signingKeyPair, didSeed, did, fromPRF: this.fromPRF, }; 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 ); // For now, generate a non-deterministic key pair // TODO: Use @noble/curves for deterministic generation from seed // This is a placeholder - in production, use the seed deterministically const keyPair = await crypto.subtle.generateKey( { name: 'ECDSA', namedCurve: 'P-256', }, false, // Private key not extractable ['sign', 'verify'] ); // Store seed hash for verification console.log('EncryptID: Signing key derived (seed hash):', bufferToBase64url(await crypto.subtle.digest('SHA-256', seed)).slice(0, 16)); return keyPair; } /** * 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 { // Ed25519 public key generation would go here // For now, we'll create a placeholder using the seed hash // TODO: Use @noble/ed25519 for proper Ed25519 key generation const publicKeyHash = await crypto.subtle.digest('SHA-256', seed); const publicKeyBytes = new Uint8Array(publicKeyHash).slice(0, 32); // Multicodec prefix for Ed25519 public key: 0xed01 const multicodecPrefix = new Uint8Array([0xed, 0x01]); const multicodecKey = new Uint8Array(34); multicodecKey.set(multicodecPrefix); multicodecKey.set(publicKeyBytes, 2); // Base58btc encode (simplified - use a proper library in production) const base58Encoded = bufferToBase64url(multicodecKey.buffer) .replace(/-/g, '') .replace(/_/g, ''); return `did:key:z${base58Encoded}`; } /** * Clear all keys from memory */ clear(): void { 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; } 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, }, 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; } 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'] ); } // ============================================================================ // 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; } }