Key Derivation

How passwords are stretched with Argon2id before becoming keys — the Argon2Kdf primitive (Argon2KdfOptions), the WebAssembly-vs-pure-JS backend, and argon2BackendInfo() (Argon2BackendInfo) for detecting which path ran.

A password is never used as a key directly. Every sealed record is protected by a key stretched from the password with Argon2id, the memory-hard KDF from RFC 9106. This page covers the Argon2Kdf primitive that performs that stretch, the two interchangeable backends it can run on, and argon2BackendInfo() for telling which one actually ran.

You rarely call Argon2Kdf yourself — a provider derives keys for you during unlock. It is exposed for custom encryption work, and understanding the backend resolution matters because one of the two paths is far slower than the other.

Argon2id parameters

Argon2Kdf is configured through Argon2KdfOptions. Each field is optional and falls back to a default drawn from RFC 9106's recommendations.

Argon2KdfOptions
interface Argon2KdfOptions {
  /** Iterations (time cost). Default: 3. */
  time?: number;
  /** Memory cost in KiB. Default: 65536 (64 MiB). */
  memory?: number;
  /** Parallelism. Default: 1. */
  parallelism?: number;
  /** Output hash length in bytes. Default: 32. */
  hashLength?: number;
}
OptionDefaultNotes
time3Argon2 iteration (t) cost.
memory65536 (64 MiB)Memory (m) cost, in KiB.
parallelism1Lanes (p).
hashLength32Derived key length in bytes.

Parameters are validated before any work runs. An out-of-spec value fails loudly rather than silently degrading — for example a time of 0 would otherwise skip the work loop entirely and return the same constant for every password.

Argon2Kdf

Argon2Kdf implements the synchronous Cipher contract, with its id set to "argon2id". Because a KDF is one-way, only the forward direction does anything.

argon2-kdf.ts
import { Argon2Kdf } from "wative-core";

const kdf = new Argon2Kdf({ time: 3, memory: 64 * 1024 });

// encrypt(password, salt): derive a key. Both arguments are Uint8Array.
const key = kdf.encrypt(passwordBytes, saltBytes); // 32-byte Uint8Array

// decrypt() always throws — Argon2 is irreversible.
kdf.decrypt(key, saltBytes); // WativeError("ALGORITHM_IRREVERSIBLE")
MemberSignatureNotes
encryptencrypt(password: Uint8Array, salt: Uint8Array): Uint8ArrayDerives the key.
decryptdecrypt(input: Uint8Array, secret: Uint8Array): neverAlways throws WativeError("ALGORITHM_IRREVERSIBLE").

encrypt enforces its inputs. Both arguments must be a Uint8Array, and the salt must be at least 16 bytes — RFC 9106 §3.1 mandates that minimum for Argon2id, and shorter salts are rejected so sealed records stay portable to strict verifiers. A bad argument throws WativeError("ENCRYPT_FAILED").

WebAssembly vs pure-JS backend

Key derivation runs through one of two byte-identical backends. The default is resolved once per process, synchronously:

  • wasm — a vendored WebAssembly Argon2 build. Chosen when it both compiles and reproduces a known-answer vector. This is the normal, fast path.
  • noble — a pure-JavaScript implementation (@noble/hashes). Produces identical output, but roughly 17× slower.

The library degrades to noble rather than throwing, because the situations that block WebAssembly are not rescuable by trying harder:

The fallback is announced once on the console. If you would rather fail loudly than run slowly, set the environment variable WATIVE_REQUIRE_WASM_ARGON2=1 and the default resolution throws WativeError("UNSUPPORTED_OP") instead of degrading. See Environments for where each backend applies.

argon2BackendInfo()

argon2BackendInfo(): Argon2BackendInfo reports which implementation the process settled on and why — the way to detect a silent fallback programmatically.

Argon2BackendInfo
interface Argon2BackendInfo {
  readonly backend: string;          // "wasm" | "noble" | "unresolved"
  readonly wasm: boolean;            // true only when wasm compiled AND verified
  readonly reason?: string;          // present only on a fallback / unresolved
  readonly overrides: readonly string[];
}
FieldMeaning
backendThe default backend id: "wasm", "noble", or "unresolved".
wasmtrue only when the WebAssembly module compiled and reproduced its known vector.
reasonOn a fallback, why WebAssembly could not be used; on "unresolved", why nothing has resolved yet.
overridesBackend ids installed on individual providers. Non-empty means some derives do not use backend.
detect-backend.ts
import { argon2BackendInfo } from "wative-core";

const info = argon2BackendInfo();
if (info.backend === "noble") {
  console.warn(`Argon2 is running in pure JS: ${info.reason}`);
}

The V3 provider override

The backend field is the process-wide default, which is not necessarily what every derive uses. A provider may carry its own backend, and HybridProviderV3 does: it seals with native Argon2id bindings rather than the resolved default.

So a process using HybridProviderV3 will report backend: "wasm" while every one of its derives runs natively. The overrides array is what tells you that — it lists the backend ids installed per-provider (for example "node-rs"). A non-empty overrides means some key derivations in the process bypass backend entirely.

See also

Last updated on