argon2.ts
1import { argon2id } from "hash-wasm";23// Argon2id params: m=65536 KiB (64 MiB), t=3, p=4, 32-byte output.4// Meets the OWASP "password manager" tier (m=65536, t=3, p=1) and exceeds it5// on parallelism. Changing any of these changes the derived key — existing6// vaults on disk can only be unlocked with the params they were sealed under.7// Raising them tightens attacker cost; lowering them is a regression.8async function argon2idDerive(9 password: string,10 salt: Uint8Array11): Promise<Uint8Array> {12 return argon2id({13 password,14 salt,15 parallelism: 4,16 iterations: 3,17 memorySize: 65536,18 hashLength: 32,19 outputType: "binary",20 });21}2223export async function deriveKey(24 password: string,25 salt: Uint8Array26): Promise<Uint8Array> {27 // LV-1: NFC-normalize before the KDF. The same visually-identical28 // password typed on a keyboard/IME that emits a different normalization29 // form (NFD on macOS dead-keys, etc.) must derive the same key — and the30 // same bytes as the Android/iOS clients, which apply the identical31 // normalization (cross-platform parity).32 return argon2idDerive(password.normalize("NFC"), salt);33}3435// Per-vault salt: 16 bytes from the OS CSPRNG. A fresh salt is generated at36// vault creation and on every changePassword. Prevents cross-user37// precomputation attacks against the same master passphrase.38export function generateSalt(): Uint8Array {39 return crypto.getRandomValues(new Uint8Array(16));40}