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:

OptionTypeDefaultWhat it does
algorithmsKeyManagementAlgorithm[]inferred from the keyThe alg values to accept. Required for raw bytes.
encryptionAlgorithmsContentEncryptionAlgorithm[]any supported encThe enc values to accept.
minIterationsnumber1000Lowest PBES2 p2c accepted.
maxIterationsnumber600_000Highest PBES2 p2c accepted.
limits.keyCandidatesnumber8Most JWK Set entries one token may be tried against.
limits.pbes2Iterationsnumber2_400_000PBKDF2 iterations one call may spend across all attempts.
returnCekbooleanfalseAdd the raw content key and the AAD to the result.
extractablebooleantrueWhether the unwrapped content CryptoKey is extractable.
keyUsageKeyUsage[]["decrypt"]Usages of the unwrapped content key.
unwrappedKeyAlgorithmWeb Crypto import algorithmfrom encHow 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 header

returnCek 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 use absent or "enc", and key_ops absent or including unwrapKey, decrypt, deriveBits or deriveKey.
  • A password string is accepted. It works only for a PBES2 token: for any other alg the inferred allow-list is the three PBES2 algorithms, so the token is refused with ERR_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:

  • p2c outside minIterations to maxIterations (1000 to 600,000 by default) is ERR_JWE_INVALID.
  • limits.pbes2Iterations caps the total across a call. Four attempts at the default count fit, which matters for JWK Sets and multi-recipient tokens. Running out is ERR_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_FAILED

For 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:

  • alg is supported and not "none", enc is supported and allowed, and there is no zip.
  • crit names only present, understood extensions, and typ matches when you set it.
  • dir and bare ECDH-ES carry an empty encrypted key segment.
  • Every segment is canonical base64url, and the IV and tag have the length enc fixes.
  • An ECDH-ES epk is a public EC or OKP key on the recipient's curve, and apu and apv are at most 1024 bytes.

After decryption the payload is decoded under the payload mode and the claims run as on verify().

#Errors

CodeWhen
ERR_INVALID_OPTIONA malformed or empty option, a bad limits value, returnCek with a non-extractable key.
ERR_JWE_INVALIDNot 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_FOUNDNo JWK Set candidate, too many candidates, or a lookup that threw or returned nothing.
ERR_JWK_INVALIDA public key, a string under a non-PBES2 alg, a key in the wrong role.
ERR_JWE_DECRYPTION_FAILEDThe 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_MISSINGA claim check failed.

#See also