
# encrypt()

> Encrypt a payload into a compact JWE string, with `alg` read from the key or the password and `enc` defaulting to `A256GCM`.

```ts
import { encrypt } from "unjwt/jwe";
import { generateJWK } from "unjwt/jwk";

const key = await generateJWK("A256KW");
const token = await encrypt({ sub: "user_1", email: "ada@example.com" }, key, { expiresIn: "1h" });
// "eyJraWQiOiJBdHBEUWVQ…"  (five segments)
// header: { kid: "AtpDQeP1…", alg: "A256KW", enc: "A256GCM", typ: "JWT" }
```

## Signature

```ts
function encrypt(
  payload: string | Uint8Array | Record<string, unknown>,
  key: JWEEncryptJWK | CryptoKey | Uint8Array | string,
  options?: JWEEncryptOptions, // `alg` is required for a CryptoKey or raw bytes
): Promise<string>;
```

`JWEEncryptJWK` is an `oct` JWK with a JWE algorithm, or a public RSA, EC or OKP JWK with an `RSA-OAEP*` or `ECDH-ES*` algorithm. A signing JWK is a type error.

## Options

| Option                                                                | Type                                 | Default                         | What it does                                                                                                   |
| --------------------------------------------------------------------- | ------------------------------------ | ------------------------------- | -------------------------------------------------------------------------------------------------------------- |
| `alg`                                                                 | `KeyManagementAlgorithm`             | from the key                    | How the content key reaches the recipient.                                                                     |
| `enc`                                                                 | `ContentEncryptionAlgorithm`         | the key's `enc`, else `A256GCM` | How the payload is sealed. Required for `dir` when the key names none.                                         |
| `protectedHeader`                                                     | `JWEHeaderParameters`                | none                            | Extra header members. `alg`, `enc`, `iv`, `tag`, `p2s`, `p2c`, `epk`, `apu`, `apv` are written by the library. |
| `expiresIn`, `expiresAt`, `notBeforeIn`, `notBeforeAt`, `currentDate` |                                      | none                            | Time claims, as on [`sign()`](/jwt/jws/sign#options).                                                          |
| `p2c`                                                                 | `number`                             | `600_000`                       | PBES2 iteration count, between 1000 and 600,000.                                                               |
| `p2s`                                                                 | `Uint8Array`                         | 16 random bytes                 | PBES2 salt, at least 8 bytes.                                                                                  |
| `keyManagementIV`                                                     | `Uint8Array`                         | random                          | IV for `A*GCMKW` key wrapping.                                                                                 |
| `ecdh.partyUInfo`                                                     | `Uint8Array`                         | none                            | ECDH-ES `apu`, at most 1024 bytes.                                                                             |
| `ecdh.partyVInfo`                                                     | `Uint8Array`                         | none                            | ECDH-ES `apv`, at most 1024 bytes.                                                                             |
| `ecdh.ephemeralKey`                                                   | key pair, private JWK or `CryptoKey` | generated                       | The sender's ephemeral ECDH key. Leave it to the library.                                                      |
| `cek`                                                                 | `Uint8Array`                         | random                          | The content key. Must be the length `enc` needs.                                                               |
| `contentEncryptionIV`                                                 | `Uint8Array`                         | random                          | The content IV. Must be the length `enc` needs.                                                                |

Leave `cek`, `contentEncryptionIV`, `keyManagementIV` and `ecdh.ephemeralKey` unset outside tests. Reusing an IV with the same key breaks AES-GCM.

## The key and the inferred `alg`

| Key                                                                             | `alg` when omitted   | `enc` when omitted              |
| ------------------------------------------------------------------------------- | -------------------- | ------------------------------- |
| A password string                                                               | `PBES2-HS256+A128KW` | `A256GCM`                       |
| A JWK with a key-wrapping `alg` (`A256KW`, `RSA-OAEP-256`, `ECDH-ES+A256KW`, …) | the JWK's `alg`      | the JWK's `enc`, else `A256GCM` |
| An `oct` JWK whose `alg` is an `enc` (`A256GCM`, `A128CBC-HS256`, …)            | `dir`                | that same algorithm             |
| A `CryptoKey`                                                                   | none, pass `alg`     | `A256GCM` (`dir` needs `enc`)   |
| A `Uint8Array`                                                                  | none, pass `alg`     | `A256GCM` (`dir` needs `enc`)   |

An asymmetric JWK must be the public half. A private JWK is `ERR_JWK_INVALID`, so a private key is not handed to an encryption path by mistake.

```ts
const rsa = await generateJWK("RSA-OAEP-256");
await encrypt({ sub: "u" }, rsa.publicKey);
// header: { kid: "8iPQ21lj…", alg: "RSA-OAEP-256", enc: "A256GCM", typ: "JWT" }

const cek = await generateJWK("A256GCM");
await encrypt({ sub: "u" }, cek);
// header: { kid: "mYJBQ80p…", alg: "dir", enc: "A256GCM", typ: "JWT" }   encrypted key segment empty

const bytes = crypto.getRandomValues(new Uint8Array(32));
await encrypt({ sub: "u" }, bytes, { alg: "dir", enc: "A128CBC-HS256" });
// header: { alg: "dir", enc: "A128CBC-HS256", typ: "JWT" }

await encrypt({ sub: "u" }, bytes);
// JWTError ERR_JWE_ALG_MISSING: JWE "alg" (Key Management Algorithm) must be provided in options or inferable from the key
```

A CEK JWK serves `dir` with its own `enc` only. Asking for another `enc`, or using it as a wrapping key, is `ERR_JWK_INVALID`. Raw bytes carry no role, so the matching [`decrypt()`](/jwt/jwe/decrypt) needs the same `algorithms: [alg]`.

## Passwords and PBES2

A string key is a password and nothing else. `encrypt` derives a wrapping key from it with PBKDF2 ([RFC 7518 §4.8](https://www.rfc-editor.org/rfc/rfc7518#section-4.8)) and writes the salt and count into the header.

```ts
await encrypt({ sub: "u" }, "correct horse battery staple", { p2c: 210_000 });
// header: { p2c: 210000, p2s: "EDcynTCb…", alg: "PBES2-HS256+A128KW", enc: "A256GCM", typ: "JWT" }

await encrypt({ sub: "u" }, "pw", { p2c: 1_000_000 });
// JWTError ERR_INVALID_OPTION: "p2c" must be between 1000 and 600000 iterations (got 1000000).

await encrypt({ sub: "u" }, "pw", { alg: "A256KW" });
// JWTError ERR_JWK_INVALID: String keys are not accepted: pass an oct JWK or a Uint8Array secret, or import PEM text with importPEM().
```

The ceiling of 600,000 is the default `maxIterations` of `decrypt`, so every token `encrypt` mints opens with default options. A lower `p2c` is faster for both sides and weaker against guessing. Prefer a generated key whenever a person does not have to type the secret. [Password-derived keys](/jwk/password) cover deriving a reusable JWK instead.

## Header members written for you

| `alg` family | Members added                                              |
| ------------ | ---------------------------------------------------------- |
| `PBES2-*`    | `p2s` (salt), `p2c` (count)                                |
| `A*GCMKW`    | `iv` and `tag` of the key-wrapping step                    |
| `ECDH-ES*`   | `epk` (ephemeral public key), and `apu` / `apv` when given |

```ts
await encrypt({ sub: "u" }, await generateJWK("A128GCMKW"));
// header: { kid: "nRoV3Jw2…", iv: "FQ0pBiSj…", tag: "TsYSA7zg…", alg: "A128GCMKW", enc: "A256GCM", typ: "JWT" }

const ec = await generateJWK("ECDH-ES+A256KW");
await encrypt({ sub: "u" }, ec.publicKey, {
  ecdh: {
    partyUInfo: new TextEncoder().encode("alice"),
    partyVInfo: new TextEncoder().encode("bob"),
  },
});
// header: { kid: "wEB_iEjz…", epk: { kty: "EC", crv: "P-256", x: "qI4QBn…", y: "yW46SQ…" },
//           apu: "YWxpY2U", apv: "Ym9i", alg: "ECDH-ES+A256KW", enc: "A256GCM", typ: "JWT" }
```

`apu` and `apv` name the two parties and are mixed into the derived key, so a token made for one exchange does not open in another. An empty one is left out. Bare `ECDH-ES` uses the derived key as the content key, so its encrypted key segment is empty, as with `dir`.

## The protected header

`encrypt` builds the header in this order, later steps winning: the JWK's `kid`, your `protectedHeader`, the key-management members, then `alg` and `enc`.

- **`typ`** defaults to `"JWT"` for an object payload and is left out for a string or bytes.
- **`cty`** is never added. Set `cty: "JWT"` for a [nested token](/jwt#sign-then-encrypt).
- **`crit`** follows the same rules as on [`sign()`](/jwt/jws/sign#the-protected-header).
- **`zip`** is refused, since compression is not implemented.

```ts
await encrypt("hello", key);
// header: { kid: "AtpDQeP1…", alg: "A256KW", enc: "A256GCM" }

await encrypt({ sub: "u" }, key, { protectedHeader: { typ: "secevent+jwt", kid: "k-2026" } });
// header: { kid: "k-2026", typ: "secevent+jwt", alg: "A256KW", enc: "A256GCM" }
```

## Return value

A compact string of five base64url segments: header, encrypted key, IV, ciphertext, tag. The encrypted key is empty for `dir` and bare `ECDH-ES`. Use [`encryptMulti`](/jwt/jwe/multi) for the JSON form.

## Errors

| Code                      | When                                                                                                                                                                                                                         |
| ------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `ERR_JWE_ALG_MISSING`     | No `alg` and none can be read from the key.                                                                                                                                                                                  |
| `ERR_JWE_ENC_MISSING`     | `alg: "dir"` with no `enc` from the options or the key.                                                                                                                                                                      |
| `ERR_JWE_ALG_NOT_ALLOWED` | `alg` is `"none"` or outside the [table](/jwt/jwe/algorithms), such as `RSA1_5`.                                                                                                                                             |
| `ERR_JWE_INVALID`         | An unsupported `enc`.                                                                                                                                                                                                        |
| `ERR_JWK_INVALID`         | A string under a non-PBES2 `alg`, a private key, a key of the wrong size or role, an RSA key under 2048 bits, an ephemeral key on another curve.                                                                             |
| `ERR_INVALID_OPTION`      | `p2c` or `p2s` out of bounds, `apu`/`apv` not bytes or over 1024 bytes, a `cek`, IV or `dir` key of the wrong length, `zip`, a bad `crit` or `typ`, a header or payload that is not JSON-serializable, a malformed duration. |
| `ERR_JWT_CLAIM_INVALID`   | Exclusive time options passed together, or a negative `notBeforeIn`.                                                                                                                                                         |
