/** * PBKDF2 key derivation using Web Crypto API. * * Matches Django-CFG backend key derivation for decryption compatibility. */ /** * Derive an encryption key using PBKDF2. * * Uses Web Crypto API for secure key derivation that matches * the Django-CFG backend implementation. * * @param password - The password/secret key to derive from * @param salt - Salt bytes for key derivation * @param iterations - Number of PBKDF2 iterations (default: 100000) * @param keyLength - Desired key length in bytes (default: 32 for AES-256) * @returns Promise resolving to derived key as CryptoKey * * @example * ```typescript * const salt = new TextEncoder().encode('my-salt'); * const key = await deriveKey('secret', salt, 100000); * ``` */ export async function deriveKey( password: string, salt: Uint8Array, iterations: number = 100000, keyLength: number = 32 ): Promise { const encoder = new TextEncoder(); const passwordBuffer = encoder.encode(password); // Import password as raw key material const keyMaterial = await crypto.subtle.importKey( 'raw', passwordBuffer, 'PBKDF2', false, ['deriveBits', 'deriveKey'] ); // Derive AES-GCM key using PBKDF2 return crypto.subtle.deriveKey( { name: 'PBKDF2', salt: salt.buffer as ArrayBuffer, iterations: iterations, hash: 'SHA-256', }, keyMaterial, { name: 'AES-GCM', length: keyLength * 8 }, false, ['decrypt'] ); } /** * Derive raw key bytes using PBKDF2. * * @param password - The password/secret key to derive from * @param salt - Salt bytes for key derivation * @param iterations - Number of PBKDF2 iterations (default: 100000) * @param keyLength - Desired key length in bytes (default: 32 for AES-256) * @returns Promise resolving to derived key as Uint8Array */ export async function deriveKeyBytes( password: string, salt: Uint8Array, iterations: number = 100000, keyLength: number = 32 ): Promise { const encoder = new TextEncoder(); const passwordBuffer = encoder.encode(password); // Import password as raw key material const keyMaterial = await crypto.subtle.importKey( 'raw', passwordBuffer, 'PBKDF2', false, ['deriveBits'] ); // Derive raw bits const keyBits = await crypto.subtle.deriveBits( { name: 'PBKDF2', salt: salt.buffer as ArrayBuffer, iterations: iterations, hash: 'SHA-256', }, keyMaterial, keyLength * 8 ); return new Uint8Array(keyBits); } /** * Build a deterministic salt from context components. * * Matches Django-CFG backend salt generation for key derivation. * * @param keyPrefix - Key prefix (default: "djangocfg_encryption") * @param userId - Optional user ID for per-user keys * @param sessionId - Optional session ID for per-session keys * @returns Salt as Uint8Array (first 16 bytes of SHA-256 hash) */ export async function buildSalt( keyPrefix: string = 'djangocfg_encryption', userId?: string | number, sessionId?: string ): Promise { const parts = [keyPrefix]; if (sessionId) { parts.push(`session:${sessionId}`); } else if (userId !== undefined) { parts.push(`user:${userId}`); } else { parts.push('global'); } const saltInput = parts.join(':'); const encoder = new TextEncoder(); const inputBuffer = encoder.encode(saltInput); // SHA-256 hash and take first 16 bytes const hashBuffer = await crypto.subtle.digest('SHA-256', inputBuffer); return new Uint8Array(hashBuffer).slice(0, 16); } /** * Derive encryption key from Django-CFG config. * * Convenience function that matches backend key derivation. * * @param config - Configuration object with secretKey and optional context * @returns Promise resolving to CryptoKey for decryption * * @example * ```typescript * const key = await deriveKeyFromConfig({ * secretKey: 'django-secret-key', * userId: 123, * iterations: 100000 * }); * ``` */ export async function deriveKeyFromConfig(config: { secretKey: string; userId?: string | number; sessionId?: string; iterations?: number; keyPrefix?: string; }): Promise { const { secretKey, userId, sessionId, iterations = 100000, keyPrefix = 'djangocfg_encryption', } = config; const salt = await buildSalt(keyPrefix, userId, sessionId); return deriveKey(secretKey, salt, iterations); }