import { argon2id } from "hash-wasm"; // Argon2id params: m=65536 KiB (64 MiB), t=3, p=4, 32-byte output. // Meets the OWASP "password manager" tier (m=65536, t=3, p=1) and exceeds it // on parallelism. Changing any of these changes the derived key — existing // vaults on disk can only be unlocked with the params they were sealed under. // Raising them tightens attacker cost; lowering them is a regression. async function argon2idDerive( password: string, salt: Uint8Array ): Promise { return argon2id({ password, salt, parallelism: 4, iterations: 3, memorySize: 65536, hashLength: 32, outputType: "binary", }); } export async function deriveKey( password: string, salt: Uint8Array ): Promise { // LV-1: NFC-normalize before the KDF. The same visually-identical // password typed on a keyboard/IME that emits a different normalization // form (NFD on macOS dead-keys, etc.) must derive the same key — and the // same bytes as the Android/iOS clients, which apply the identical // normalization (cross-platform parity). return argon2idDerive(password.normalize("NFC"), salt); } // Per-vault salt: 16 bytes from the OS CSPRNG. A fresh salt is generated at // vault creation and on every changePassword. Prevents cross-user // precomputation attacks against the same master passphrase. export function generateSalt(): Uint8Array { return crypto.getRandomValues(new Uint8Array(16)); }