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)

FamilyIdentifiersKey typeNotes
DirectdirSymmetric — the key IS the CEKSmallest token; requires pre-shared CEK.
RSA-OAEPRSA-OAEP, RSA-OAEP-256, RSA-OAEP-384, RSA-OAEP-512RSA keypairPublic-key encryption. Prefer -256 or higher for new keys.
AES Key WrapA128KW, A192KW, A256KWSymmetric AESWraps the CEK with an AES key you already share.
AES-GCM Key WrapA128GCMKW, A192GCMKW, A256GCMKWSymmetric AESAuthenticated variant of AES Key Wrap.
PBES2PBES2-HS256+A128KW, PBES2-HS384+A192KW, PBES2-HS512+A256KWPasswordPassword-based; uses PBKDF2 + AES-KW.
ECDH-ESECDH-ES, ECDH-ES+A128KW, ECDH-ES+A192KW, ECDH-ES+A256KWEC or OKP keypairDiffie-Hellman. Ephemeral key generated per message.

Deprecated / avoid

  • RSA1_5 (RSA PKCS#1 v1.5) — not supported by unjwt. Vulnerable to Bleichenbacher attacks. Use RSA-OAEP-256 or higher.
  • A128CBC-HS256 as a key wrap — exists only as a historical content-encryption alg, not listed here as alg.

Content encryption (enc)

FamilyIdentifiersNotes
AES-GCMA128GCM, A192GCM, A256GCMAEAD. Fast, modern. Prefer A256GCM for new keys.
AES-CBC + HMAC-SHA2A128CBC-HS256, A192CBC-HS384, A256CBC-HS512Composite 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 RSARSA-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).
  • 256 is 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+A128KW for 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 — dir is 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.

PBES2 is intentionally slow. A single password-based encryption takes ~tens of milliseconds on a modern laptop. That's a feature, not a bug — it makes offline password cracking expensive.

Full combination rules

Not every alg/enc pair is valid. The constraints unjwt enforces:

  • alg: "dir" requires enc to be set and both parties to hold the matching key.
  • alg: "ECDH-ES" (no key wrap) requires enc to be set and is single-recipient only.
  • alg: "ECDH-ES+A*KW" generates a random CEK; enc can be any content-encryption alg.
  • alg: "RSA-OAEP*" with enc: "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