Files
seedpgp-web/src/lib/pbkdf2.ts
LC mac aa06c9ae27 feat: fix CompactSeedQR binary QR code scanning with jsQR library
- Replace BarcodeDetector with jsQR for raw binary byte access
- BarcodeDetector forced UTF-8 decoding which corrupted binary data
- jsQR's binaryData property preserves raw bytes without text conversion
- Fix regex bug: use single backslash \x00 instead of \x00 for binary detection
- Add debug logging for scan data inspection
- QR generation already worked (Krux-compatible), only scanning was broken

Resolves binary QR code scanning for 12/24-word CompactSeedQR format.
Tested with Krux device - full bidirectional compatibility confirmed.
2026-02-07 04:22:56 +08:00

88 lines
2.5 KiB
TypeScript

/**
* @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<ArrayBuffer> {
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<Uint8Array> {
// 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<Uint8Array> {
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;
}