Decrypting

decrypt(token, key, options?)

Parses a compact JWE, validates header allowlists, decrypts the payload, and validates JWT claims (when the decrypted payload is an object).

Parameters

NameType
tokenstring — the compact JWE
keyCryptoKey | JWKSet | JWEDecryptJWK | string | Uint8Array | JWKLookupFunction
options.algorithmsKeyManagementAlgorithm[] — allowlist for alg
options.encryptionAlgorithmsContentEncryptionAlgorithm[] — allowlist for enc
options.validateClaimsboolean — force-skip claim validation
options.forceUint8Arrayboolean — return bytes instead of parsed JSON
options.returnCekboolean — include cek and aad in the result
options.minIterationsnumber — PBES2 p2c floor (default 1000 per RFC 7518)
options.maxIterationsnumber — PBES2 p2c ceiling (default 1_000_000)
Claim optionsSame as verify()audience, issuer, subject, maxTokenAge, etc.

JWEDecryptJWK is the private-side counterpart of JWEEncryptJWK — an oct JWK with a JWE symmetric alg, or a private asymmetric JWK whose alg is RSA-OAEP or ECDH-ES. JWKSet stays fully permissive (JWK[]); wire JWKS are heterogeneous and the runtime filters candidates per header.

Returns Promise<{ payload, protectedHeader, cek?, aad? }>.

The simple case

const { payload, protectedHeader } = await decrypt(token, key);

If the key is a JWK with an alg field (as generateJWK() produces), unjwt infers the expected alg and proceeds. For passwords and raw bytes, alg is inferred when possible (passwords → PBES2 variants; Uint8Array → symmetric unwrap or dir).

Algorithm allowlists

Two allowlists protect the decryptor:

  • algorithms — which key-management (alg) values are acceptable.
  • encryptionAlgorithms — which content-encryption (enc) values are acceptable.

When omitted, unjwt calls inferJWEAllowedAlgorithms to derive an alg allowlist from the key shape:

  • string / Uint8Array password → ["PBES2-HS256+A128KW", "PBES2-HS384+A192KW", "PBES2-HS512+A256KW", "dir"]
  • Symmetric JWK with a specific wrap alg → that alg plus "dir"
  • RSA private JWK → the matching RSA-OAEP* variants
  • EC/OKP private JWK → the matching ECDH-ES* variants

If inference fails (lookup function, ambiguous key), pass explicitly:

explicit-allowlist.ts
const { payload } = await decrypt(token, lookupFn, {
  algorithms: ["RSA-OAEP-256"],
  encryptionAlgorithms: ["A256GCM"],
});
Setting encryptionAlgorithms is always a good idea — it's the only protection against a malicious token using a weaker-than-intended content cipher. ["A256GCM"] is a strong default.

Dynamic key resolution

Same as verify() — pass a function that receives the header:

lookup.ts
const { payload } = await decrypt(
  token,
  async (header, _rawToken) => {
    return await keyStore.get(header.kid!);
  },
  { algorithms: ["ECDH-ES+A256KW"], encryptionAlgorithms: ["A256GCM"] },
);

A JWKLookupFunction can return a single key or a JWKSet; if it returns a set, the per-kid / per-alg retry logic applies.

JWKSet — rotation and multi-key decryption

When you pass a JWKSet (directly or returned from a lookup), unjwt selects candidates like it does on the sign side:

Token's header has kid — only keys with that kid are tried.

No kid — all keys whose alg matches the token's alg are candidates, tried in order.

No candidates — throws ERR_JWK_KEY_NOT_FOUND before any crypto runs.

Useful when you rotate encryption keys: the new key wraps future tokens, but older tokens (wrapped with the previous key) still decrypt because both keys live in the set.

PBES2 DoS protection

The PBES2 p2c (iteration count) header field is attacker-controlled — a malicious sender could craft a token with p2c: 1_000_000_000 to burn your CPU on decryption. unjwt guards against this by default:

minIterations: 1000; // RFC 7518 §4.8.1.2 mandated floor
maxIterations: 1_000_000; // sane ceiling

Any token outside this range is rejected with ERR_JWE_P2C_OUT_OF_BOUNDS before PBKDF2 runs. Override cautiously:

const { payload } = await decrypt(token, "password", {
  minIterations: 100_000, // require at least this many iterations
  maxIterations: 2_000_000, // allow stronger-than-default tokens
});

Returning the CEK

For custom verification flows (integrity checks over aad, manual key extraction), pass returnCek: true:

const { payload, cek, aad } = await decrypt(token, key, { returnCek: true });
// cek: Uint8Array — the derived Content Encryption Key
// aad: Uint8Array — the authenticated additional data (protected header bytes)

Most callers never need this.

Claim validation

Identical to JWS verify: when the decrypted payload is a JSON object, exp, nbf, and iat are validated automatically, plus any of audience / issuer / subject / maxTokenAge / requiredClaims / typ you pass.

Pass validateClaims: false to opt out (e.g. when you're decrypting arbitrary bytes rather than claim-bearing JSON).

Payload typing

typed.ts
interface Session {
  userId: string;
  role: "admin" | "user";
}

const { payload } = await decrypt<Session>(token, key);
payload.role; // "admin" | "user"

Force bytes:

const { payload } = await decrypt(token, key, { forceUint8Array: true });
payload instanceof Uint8Array; // true

Full signature

interface JWEDecryptOptions extends JWTClaimValidationOptions {
  algorithms?: KeyManagementAlgorithm[];
  encryptionAlgorithms?: ContentEncryptionAlgorithm[];
  unwrappedKeyAlgorithm?: Parameters<typeof crypto.subtle.importKey>[2];
  keyUsage?: KeyUsage[];
  extractable?: boolean;
  forceUint8Array?: boolean;
  validateClaims?: boolean;
  returnCek?: boolean;
  minIterations?: number;
  maxIterations?: number;
}

interface JWEDecryptResult<T> {
  payload: T;
  protectedHeader: JWEProtectedHeader; // alg and enc required
  cek?: Uint8Array; // only when returnCek: true
  aad?: Uint8Array; // only when returnCek: true
}

See also