Password derivation

A password is not a cryptographic key — it's a low-entropy secret that a human might actually remember. To turn a password into a key suitable for encryption, unjwt uses PBKDF2 (Password-Based Key Derivation Function 2, RFC 8018), the same primitive that powers PBES2 JWE.

import { deriveJWKFromPassword, deriveKeyFromPassword } from "unjwt/jwk";

When you'd reach for these

encrypt(payload, "my-password") already calls PBKDF2 internally — you don't need to derive a key just to encrypt a JWE token.

These functions matter when you want to:

  • Persist a password-derived key and reuse it for many encrypt/decrypt operations without re-running PBKDF2 each time.
  • Use a password-derived key for a non-JWE purpose — signing, a custom cipher, HMAC over something else.
  • Inspect the derived key (e.g. to transmit it alongside the ciphertext in a custom envelope).

deriveJWKFromPassword — the common case

Returns a JWK_oct (symmetric JWK) directly, with alg baked in:

derive-jwk.ts
import { deriveJWKFromPassword } from "unjwt/jwk";
import { secureRandomBytes } from "unjwt/utils";

const salt = secureRandomBytes(16); // MUST be random, MUST be unique per password
const jwk = await deriveJWKFromPassword("my-strong-password", "PBES2-HS256+A128KW", {
  salt,
  iterations: 600_000, // OWASP-recommended default
  kid: "derived-key",
});
// jwk.kty === "oct", jwk.alg === "A128KW" (after PBES2 unwrap),
// jwk.k contains the derived 128-bit material

Options

OptionRequired?DefaultEffect
saltYesBinds derivations to a specific context.
iterationsYesHigher = more work for attackers and you.
extractableNotrueWeb Crypto extractable flag.
keyUsageNoDerived from the AES-KW algWeb Crypto KeyUsage[].
JWK metadataNokid, use, x5c, etc. merged in.

alg, kty, key_ops, and ext are managed by the library.

deriveKeyFromPassword — raw CryptoKey output

derive-cryptokey.ts
import { deriveKeyFromPassword } from "unjwt/jwk";

const cryptoKey = await deriveKeyFromPassword("my-password", "PBES2-HS256+A128KW", {
  salt,
  iterations: 600_000,
});
// CryptoKey suitable for direct use with Web Crypto APIs

Pass toJWK: true to get a JWK back instead (in which case deriveJWKFromPassword is shorter).

What the alg parameter means here

PBES2 names are composite: PBES2-HS256+A128KW means:

  • PBKDF2 with HMAC-SHA256 as its PRF, producing
  • an AES-128 Key Wrap key.

Only these three values are valid for password derivation:

algPRFOutput algOutput key size
PBES2-HS256+A128KWHMAC-SHA256A128KW16 bytes
PBES2-HS384+A192KWHMAC-SHA384A192KW24 bytes
PBES2-HS512+A256KWHMAC-SHA512A256KW32 bytes

Iteration count — what to pick

  • Default: 600_000 — current OWASP recommendation for PBKDF2-HMAC-SHA256.
  • Minimum safe: 1000 per RFC 7518 §4.8.1.2, but that's a floor from 2015 — don't ship that in 2026.
  • Legacy interop: you might see 10 000–100 000 from older systems. Document it; plan to migrate.
Password-derived keys are only as strong as the password. PBKDF2 makes brute-forcing expensive, not impossible. A 6-character password is guessable in minutes regardless of iteration count.

Salt — why it must be random

The salt prevents:

  • Rainbow tables (precomputed derivations of common passwords).
  • Cross-use detection — without salt, "same password" derivations would be identical across users, leaking information.

Rules:

Random, via crypto.getRandomValues (or secureRandomBytes()).

Unique per derivation — never reuse a salt across passwords.

≥ 16 bytes (128 bits). Larger doesn't hurt.

Store/transmit the salt alongside the ciphertext — it's not secret.

PBES2 JWE tokens carry their salt in the p2s header field automatically. You only manage salts manually when using deriveJWKFromPassword outside the PBES2 flow.

See also