
# 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.

```ts
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

```ts
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()`](/jwt/jws/verify#options) 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()`](/jwt/jws/verify#return-shape). `decrypt<MyClaims>(token, key)` types the claims. `protectedHeader` always carries `alg` and `enc`.

```ts
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()`](/jwt/jws/verify#keys), 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`.

```ts
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](/jwt/jws/verify#algorithm-allow-list): 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.

```ts
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](/jwt/jwe/multi) tokens. Running out is `ERR_JWE_INVALID`.

```ts
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](https://www.rfc-editor.org/rfc/rfc7516#section-11.5)).

```ts
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()`](/jwt/jws/verify#claims).

## 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()`](/jwt/jwe/encrypt) for the producing side.
- [`decryptMulti()`](/jwt/jwe/multi) for JSON serializations.
- [Errors](/errors) for `isJWTError` and the full code list.
