Algorithms
JWE algorithms
A JWE's header carries two algorithm identifiers:
alg— how the Content Encryption Key (CEK) is delivered.enc— which cipher encrypts the payload with the CEK.
Both are defined in RFC 7518.
Key management (alg)
| Family | Identifiers | Key type | Notes |
|---|---|---|---|
| Direct | dir | Symmetric — the key IS the CEK | Smallest token; requires pre-shared CEK. |
| RSA-OAEP | RSA-OAEP, RSA-OAEP-256, RSA-OAEP-384, RSA-OAEP-512 | RSA keypair | Public-key encryption. Prefer -256 or higher for new keys. |
| AES Key Wrap | A128KW, A192KW, A256KW | Symmetric AES | Wraps the CEK with an AES key you already share. |
| AES-GCM Key Wrap | A128GCMKW, A192GCMKW, A256GCMKW | Symmetric AES | Authenticated variant of AES Key Wrap. |
| PBES2 | PBES2-HS256+A128KW, PBES2-HS384+A192KW, PBES2-HS512+A256KW | Password | Password-based; uses PBKDF2 + AES-KW. |
| ECDH-ES | ECDH-ES, ECDH-ES+A128KW, ECDH-ES+A192KW, ECDH-ES+A256KW | EC or OKP keypair | Diffie-Hellman. Ephemeral key generated per message. |
Deprecated / avoid
RSA1_5(RSA PKCS#1 v1.5) — not supported by unjwt. Vulnerable to Bleichenbacher attacks. UseRSA-OAEP-256or higher.A128CBC-HS256as a key wrap — exists only as a historical content-encryption alg, not listed here asalg.
Content encryption (enc)
| Family | Identifiers | Notes |
|---|---|---|
| AES-GCM | A128GCM, A192GCM, A256GCM | AEAD. Fast, modern. Prefer A256GCM for new keys. |
| AES-CBC + HMAC-SHA2 | A128CBC-HS256, A192CBC-HS384, A256CBC-HS512 | Composite construction. Required for some interop. |
Both are authenticated (they fail decryption if the ciphertext is modified), but AES-GCM does it in one pass while AES-CBC+HMAC does encryption and authentication separately (bigger CEK, more bytes in the token).
Defaults for unjwt: single-recipient encrypt() falls back to A128GCM when enc is not specified and the JWK carries no enc hint; encryptMulti() defaults to A256GCM. Use A128CBC-HS256 only when a counterparty requires it.
Choosing alg
You control both sides of the channel — use a symmetric key.
const aesKey = await generateJWK("A256KW");
// Same aesKey encrypts and decrypts. Share securely out-of-band.
const token = await encrypt(payload, aesKey);
The encryption key is a password (human-typed or human-remembered) — let unjwt use PBES2:
const token = await encrypt(payload, "my-strong-password");
// Uses PBES2-HS256+A128KW under the hood with p2c=600_000
Recipients can't share a symmetric key (multi-party systems, federated identity) — use a public-key scheme. ECDH-ES+A256KW is the modern default:
const { publicKey, privateKey } = await generateJWK("ECDH-ES+A256KW");
Interop with a consumer that only supports RSA — RSA-OAEP-256 (or higher).
You already have a pre-shared CEK (e.g. derived from an out-of-band exchange) — use dir. See Direct encryption below.
Choosing enc
In 2026 the answer is almost always A256GCM:
- It's AEAD (one pass; smaller token).
256is the safe default for new keys.- AES-GCM is hardware-accelerated on every modern CPU.
Pick A128CBC-HS256 only if:
- Counterparty requires it for interop.
- You're pairing it explicitly with
PBES2-HS256+A128KWfor classical interop. - Your platform lacks AES-GCM hardware support (rare).
Direct encryption (dir)
With alg = "dir", the encryptedKey segment of the token is empty — the recipient's key is the CEK. It's the smallest JWE possible but requires coordination.
import { generateKey } from "unjwt/jwk";
// 1. Generate a CEK (must match `enc`)
const cek = await generateKey("A256GCM"); // CryptoKey with 256-bit random bytes
// 2. Both sides hold `cek`
const token = await encrypt({ secret: "x" }, cek, { alg: "dir", enc: "A256GCM" });
const { payload } = await decrypt(token, cek);
Use dir when:
- You're operating on a pre-shared CEK (e.g. derived from a session exchange).
- You control both sides and want the smallest overhead.
- Multi-recipient is out of scope —
diris forbidden in multi-recipient envelopes.
Password-based (PBES2)
PBKDF2 with SHA-2, producing an AES-KW key that wraps the CEK. Three variants pair the hash strength with the AES-KW size:
PBES2-HS256+A128KW→ PBKDF2-HMAC-SHA256 + AES-128 wrap.PBES2-HS384+A192KW→ PBKDF2-HMAC-SHA384 + AES-192 wrap.PBES2-HS512+A256KW→ PBKDF2-HMAC-SHA512 + AES-256 wrap.
The token header carries the salt (p2s) and iteration count (p2c) the decryptor needs. unjwt defaults to p2c: 600_000 per OWASP; on decrypt, it also enforces a floor (1000) and ceiling (1_000_000) to limit attacker-controlled CPU burn.
Full combination rules
Not every alg/enc pair is valid. The constraints unjwt enforces:
alg: "dir"requiresencto be set and both parties to hold the matching key.alg: "ECDH-ES"(no key wrap) requiresencto be set and is single-recipient only.alg: "ECDH-ES+A*KW"generates a random CEK;enccan be any content-encryption alg.alg: "RSA-OAEP*"withenc: "A*CBC-HS*"works, but prefer AES-GCM.
When in doubt, let encrypt() infer from a JWK generated by generateJWK() — it picks sensible defaults.
See also
- Encrypting → — the producer side.
- Decrypting → — allowlists and DoS bounds.
- ECDH-ES → — the public-key workflow.
- JWK generation → — per-algorithm key creation.