decrypt()
Open a compact JWE with a key, a password, a JWK Set or a key lookup, validate its header and claims, and return the payload.
import { decrypt } from "unjwt/jwe";
const { payload, protectedHeader } = await decrypt(token, privateKey);
// payload: { sub: "user_1", iat: 1792068606, exp: 1792072206 }
// protectedHeader: { kid: "cmQvr9e4…", alg: "RSA-OAEP-256", enc: "A256GCM", typ: "JWT" }#Signature
function decrypt<T extends object = JWTClaims>(
jwe: string,
key: JWEDecryptJWK | JWKSet | CryptoKey | Uint8Array | string | JWEKeyLookup,
options?: JWEDecryptOptions,
): Promise<{
payload: T | string | Uint8Array;
protectedHeader: JWEProtectedHeader;
cek?: Uint8Array; // with returnCek
aad?: Uint8Array; // with returnCek
}>;JWEDecryptJWK is the private counterpart of the key encrypt took: an oct JWK with a JWE algorithm, or a private RSA, EC or OKP JWK.
#Options
Every option of verify() applies with the same default: payload, validateClaims, audience, issuer, subject, maxTokenAge, clockTolerance, requiredClaims, currentDate, typ, recognizedHeaders and allowSymmetricJWKSet. These are specific to JWE:
| Option | Type | Default | What it does |
|---|---|---|---|
algorithms | KeyManagementAlgorithm[] | inferred from the key | The alg values to accept. Required for raw bytes. |
encryptionAlgorithms | ContentEncryptionAlgorithm[] | any supported enc | The enc values to accept. |
minIterations | number | 1000 | Lowest PBES2 p2c accepted. |
maxIterations | number | 600_000 | Highest PBES2 p2c accepted. |
limits.keyCandidates | number | 8 | Most JWK Set entries one token may be tried against. |
limits.pbes2Iterations | number | 2_400_000 | PBKDF2 iterations one call may spend across all attempts. |
returnCek | boolean | false | Add the raw content key and the AAD to the result. |
extractable | boolean | true | Whether the unwrapped content CryptoKey is extractable. |
keyUsage | KeyUsage[] | ["decrypt"] | Usages of the unwrapped content key. |
unwrappedKeyAlgorithm | Web Crypto import algorithm | from enc | How to import the unwrapped content key. |
#Return shape
payload follows the payload mode exactly as on verify(). decrypt<MyClaims>(token, key) types the claims. protectedHeader always carries alg and enc.
const text = await encrypt("plain text", aesKey);
await decrypt(text, aesKey);
// JWTError ERR_JWE_INVALID: Invalid JWE: Payload is not a JSON object.
(await decrypt(text, aesKey, { payload: "string" })).payload; // "plain text"
const withCek = await decrypt(token, privateKey, { returnCek: true });
withCek.cek?.length; // 32
withCek.aad?.length; // 136, the ASCII bytes of the encoded headerreturnCek exists for interop tests and key escrow. A content key that cannot leave Web Crypto (a non-extractable dir key, or extractable: false) makes it throw ERR_INVALID_OPTION.
#Keys
The key forms and the JWK Set rules match verify(), with two differences:
- A JWK Set entry must have
useabsent or"enc", andkey_opsabsent or includingunwrapKey,decrypt,deriveBitsorderiveKey. - A password string is accepted. It works only for a PBES2 token: for any other
algthe inferred allow-list is the three PBES2 algorithms, so the token is refused withERR_JWE_ALG_NOT_ALLOWED.
An asymmetric key must be the private half. A public JWK is ERR_JWK_INVALID.
await decrypt(token, { keys: [other.privateKey, privateKey] }); // picks privateKey by kid
await decrypt(token, { keys: [other.privateKey] });
// JWTError ERR_JWK_KEY_NOT_FOUND: No key found in JWK Set with kid "cmQvr9e4…".#Allow-lists
algorithms follows the verify rules: the option first, then the JWK's own alg, then inference from the key shape. One difference: an oct JWK whose alg is a content algorithm (A256GCM) accepts dir. Raw bytes infer nothing.
await decrypt(aesToken, rawBytes);
// JWTError ERR_JWE_ALG_NOT_ALLOWED: Cannot infer allowed key management algorithms from this key; pass "options.algorithms" explicitly.
await decrypt(aesToken, rawBytes, { algorithms: ["A256KW"] }); // ok
await decrypt(token, privateKey, { encryptionAlgorithms: ["A128GCM"] });
// JWTError ERR_JWE_ALG_NOT_ALLOWED: Content encryption algorithm not allowed: A256GCM#PBES2 bounds
The PBES2 count p2c sits in the header, which the sender writes. An attacker could ask for billions of PBKDF2 iterations to stall your server, so decrypt checks it before deriving anything:
p2coutsideminIterationstomaxIterations(1000 to 600,000 by default) isERR_JWE_INVALID.limits.pbes2Iterationscaps the total across a call. Four attempts at the default count fit, which matters for JWK Sets and multi-recipient tokens. Running out isERR_JWE_INVALID.
const pw = await encrypt({ sub: "u" }, "hunter2-but-longer", { p2c: 1000 });
await decrypt(pw, "hunter2-but-longer", { minIterations: 10_000 });
// JWTError ERR_JWE_INVALID: PBES2 "p2c" below the minimum of 10000 iterations (got 1000).Raise maxIterations only to open tokens from a producer that mints above 600,000.
#One failure for every wrong key
A wrong key, a wrong password, or a tampered ciphertext, IV or tag all end in the same code, ERR_JWE_DECRYPTION_FAILED. The error does not say which step failed, so it cannot be used as an oracle to probe the key (RFC 7516 §11.5).
await decrypt(token, other.privateKey); // JWTError ERR_JWE_DECRYPTION_FAILED
await decrypt(pw, "wrong"); // JWTError ERR_JWE_DECRYPTION_FAILED
await decrypt(tampered, privateKey); // JWTError ERR_JWE_DECRYPTION_FAILEDFor a JWK Set, cause holds one JWTError per tried key, in order. Problems you control stay distinct: a public key where a private one belongs is ERR_JWK_INVALID, and a disallowed algorithm is ERR_JWE_ALG_NOT_ALLOWED.
#Checks before any key work
Everything the header decides is settled before a key is resolved:
algis supported and not"none",encis supported and allowed, and there is nozip.critnames only present, understood extensions, andtypmatches when you set it.dirand bareECDH-EScarry an empty encrypted key segment.- Every segment is canonical base64url, and the IV and tag have the length
encfixes. - An ECDH-ES
epkis a public EC or OKP key on the recipient's curve, andapuandapvare at most 1024 bytes.
After decryption the payload is decoded under the payload mode and the claims run as on verify().
#Errors
| Code | When |
|---|---|
ERR_INVALID_OPTION | A malformed or empty option, a bad limits value, returnCek with a non-extractable key. |
ERR_JWE_INVALID | Not five segments, a bad header, unsupported enc, zip, a crit or typ violation, malformed segments, a PBES2 count out of bounds or over budget, a payload that does not match the mode. |
ERR_JWE_ALG_NOT_ALLOWED | "none", an unsupported alg, an alg or enc outside the allow-lists, or a key nothing can be inferred from. |
ERR_JWK_KEY_NOT_FOUND | No JWK Set candidate, too many candidates, or a lookup that threw or returned nothing. |
ERR_JWK_INVALID | A public key, a string under a non-PBES2 alg, a key in the wrong role. |
ERR_JWE_DECRYPTION_FAILED | The key could not unwrap the content key, or the content did not authenticate. |
ERR_JWT_EXPIRED, ERR_JWT_NBF, ERR_JWT_CLAIM_INVALID, ERR_JWT_CLAIM_MISSING | A claim check failed. |
#See also
encrypt()for the producing side.decryptMulti()for JSON serializations.- Errors for
isJWTErrorand the full code list.