/** * @file pbkdf2.ts * @summary A pure-JS implementation of PBKDF2-HMAC-SHA256 using the Web Crypto API. * This is used as a fallback to test for platform inconsistencies in native PBKDF2. * Adapted from public domain examples and RFC 2898. */ /** * Performs HMAC-SHA256 on a given key and data. * @param key The HMAC key. * @param data The data to hash. * @returns A promise that resolves to the HMAC-SHA256 digest as an ArrayBuffer. */ async function hmacSha256(key: CryptoKey, data: ArrayBuffer): Promise { return crypto.subtle.sign('HMAC', key, data); } /** * The F function for PBKDF2 (PRF). * T_1 = F(P, S, c, 1) * T_2 = F(P, S, c, 2) * ... * F(P, S, c, i) = U_1 \xor U_2 \xor ... \xor U_c * U_1 = PRF(P, S || INT_32_BE(i)) * U_2 = PRF(P, U_1) * ... * U_c = PRF(P, U_{c-1}) */ async function F(passwordKey: CryptoKey, salt: Uint8Array, iterations: number, i: number): Promise { // S || INT_32_BE(i) const saltI = new Uint8Array(salt.length + 4); saltI.set(salt, 0); const i_be = new DataView(saltI.buffer, salt.length, 4); i_be.setUint32(0, i, false); // false for big-endian // U_1 let U = new Uint8Array(await hmacSha256(passwordKey, saltI.buffer)); // T let T = U.slice(); for (let c = 1; c < iterations; c++) { // U_c = PRF(P, U_{c-1}) U = new Uint8Array(await hmacSha256(passwordKey, U.buffer)); // T = T \xor U_c for (let j = 0; j < T.length; j++) { T[j] ^= U[j]; } } return T; } /** * Derives a key using PBKDF2-HMAC-SHA256. * @param password The password string. * @param salt The salt bytes. * @param iterations The number of iterations. * @param keyLenBytes The desired key length in bytes. * @returns A promise that resolves to the derived key as a Uint8Array. */ export async function pbkdf2HmacSha256(password: string, salt: Uint8Array, iterations: number, keyLenBytes: number): Promise { const passwordBytes = new TextEncoder().encode(password); const passwordKey = await crypto.subtle.importKey( 'raw', passwordBytes, { name: 'HMAC', hash: 'SHA-256' }, false, ['sign'] ); const hLen = 32; // SHA-256 output length in bytes const l = Math.ceil(keyLenBytes / hLen); const r = keyLenBytes - (l - 1) * hLen; const blocks: Uint8Array[] = []; for (let i = 1; i <= l; i++) { blocks.push(await F(passwordKey, salt, iterations, i)); } const T = new Uint8Array(keyLenBytes); for(let i = 0; i < l - 1; i++) { T.set(blocks[i], i * hLen); } T.set(blocks[l-1].slice(0, r), (l-1) * hLen); return T; }