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:
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
| Option | Required? | Default | Effect |
|---|---|---|---|
salt | Yes | — | Binds derivations to a specific context. |
iterations | Yes | — | Higher = more work for attackers and you. |
extractable | No | true | Web Crypto extractable flag. |
keyUsage | No | Derived from the AES-KW alg | Web Crypto KeyUsage[]. |
| JWK metadata | No | — | kid, use, x5c, etc. merged in. |
alg, kty, key_ops, and ext are managed by the library.
deriveKeyFromPassword — raw CryptoKey output
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:
alg | PRF | Output alg | Output key size |
|---|---|---|---|
PBES2-HS256+A128KW | HMAC-SHA256 | A128KW | 16 bytes |
PBES2-HS384+A192KW | HMAC-SHA384 | A192KW | 24 bytes |
PBES2-HS512+A256KW | HMAC-SHA512 | A256KW | 32 bytes |
Iteration count — what to pick
- Default:
600_000— current OWASP recommendation for PBKDF2-HMAC-SHA256. - Minimum safe:
1000per 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.
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.