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
| Name | Type |
|---|---|
token | string — the compact JWE |
key | CryptoKey | JWKSet | JWEDecryptJWK | string | Uint8Array | JWKLookupFunction |
options.algorithms | KeyManagementAlgorithm[] — allowlist for alg |
options.encryptionAlgorithms | ContentEncryptionAlgorithm[] — allowlist for enc |
options.validateClaims | boolean — force-skip claim validation |
options.forceUint8Array | boolean — return bytes instead of parsed JSON |
options.returnCek | boolean — include cek and aad in the result |
options.minIterations | number — PBES2 p2c floor (default 1000 per RFC 7518) |
options.maxIterations | number — PBES2 p2c ceiling (default 1_000_000) |
| Claim options | Same 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/Uint8Arraypassword →["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:
const { payload } = await decrypt(token, lookupFn, {
algorithms: ["RSA-OAEP-256"],
encryptionAlgorithms: ["A256GCM"],
});
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:
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
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
- Encrypting → — the producer side.
- Multi-recipient → — for General JSON Serialization input.
- Algorithms → — picking
algandenc.