argon2.ts

1.8 KB · raw

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/**36 * LV-1 legacy fallback ONLY: derive WITHOUT normalization, for unlocking a37 * vault sealed before NFC normalization existed with a non-NFC password.38 * Every normal path uses {@link deriveKey}. Do not use this elsewhere.39 */40export async function deriveKeyRaw(41  password: string,42  salt: Uint8Array43): Promise<Uint8Array> {44  return argon2idDerive(password, salt);45}4647// Per-vault salt: 16 bytes from the OS CSPRNG. A fresh salt is generated at48// vault creation and on every changePassword. Prevents cross-user49// precomputation attacks against the same master passphrase.50export function generateSalt(): Uint8Array {51  return crypto.getRandomValues(new Uint8Array(16));52}