# JWE algorithms

A JWE's header carries **two** algorithm identifiers:

- **alg** — how the Content Encryption Key (CEK) is delivered.
- **enc** — which cipher encrypts the payload with the CEK.

Both are defined in [RFC 7518](https://www.rfc-editor.org/rfc/rfc7518).

## Key management (`alg`)

<table>
<thead>
  <tr>
    <th>
      Family
    </th>
    
    <th>
      Identifiers
    </th>
    
    <th>
      Key type
    </th>
    
    <th>
      Notes
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      Direct
    </td>
    
    <td>
      <code>
        dir
      </code>
    </td>
    
    <td>
      Symmetric — the key IS the CEK
    </td>
    
    <td>
      Smallest token; requires pre-shared CEK.
    </td>
  </tr>
  
  <tr>
    <td>
      RSA-OAEP
    </td>
    
    <td>
      <code>
        RSA-OAEP
      </code>
      
      , <code>
        RSA-OAEP-256
      </code>
      
      , <code>
        RSA-OAEP-384
      </code>
      
      , <code>
        RSA-OAEP-512
      </code>
    </td>
    
    <td>
      RSA keypair
    </td>
    
    <td>
      Public-key encryption. Prefer <code>
        -256
      </code>
      
       or higher for new keys.
    </td>
  </tr>
  
  <tr>
    <td>
      AES Key Wrap
    </td>
    
    <td>
      <code>
        A128KW
      </code>
      
      , <code>
        A192KW
      </code>
      
      , <code>
        A256KW
      </code>
    </td>
    
    <td>
      Symmetric AES
    </td>
    
    <td>
      Wraps the CEK with an AES key you already share.
    </td>
  </tr>
  
  <tr>
    <td>
      AES-GCM Key Wrap
    </td>
    
    <td>
      <code>
        A128GCMKW
      </code>
      
      , <code>
        A192GCMKW
      </code>
      
      , <code>
        A256GCMKW
      </code>
    </td>
    
    <td>
      Symmetric AES
    </td>
    
    <td>
      Authenticated variant of AES Key Wrap.
    </td>
  </tr>
  
  <tr>
    <td>
      PBES2
    </td>
    
    <td>
      <code>
        PBES2-HS256+A128KW
      </code>
      
      , <code>
        PBES2-HS384+A192KW
      </code>
      
      , <code>
        PBES2-HS512+A256KW
      </code>
    </td>
    
    <td>
      Password
    </td>
    
    <td>
      Password-based; uses PBKDF2 + AES-KW.
    </td>
  </tr>
  
  <tr>
    <td>
      ECDH-ES
    </td>
    
    <td>
      <code>
        ECDH-ES
      </code>
      
      , <code>
        ECDH-ES+A128KW
      </code>
      
      , <code>
        ECDH-ES+A192KW
      </code>
      
      , <code>
        ECDH-ES+A256KW
      </code>
    </td>
    
    <td>
      EC or OKP keypair
    </td>
    
    <td>
      Diffie-Hellman. Ephemeral key generated per message.
    </td>
  </tr>
</tbody>
</table>

### Deprecated / avoid

- `RSA1_5` (RSA PKCS#1 v1.5) — **not supported by unjwt**. Vulnerable to Bleichenbacher attacks. Use `RSA-OAEP-256` or higher.
- `A128CBC-HS256` as a *key wrap* — exists only as a historical content-encryption alg, not listed here as `alg`.

## Content encryption (`enc`)

<table>
<thead>
  <tr>
    <th>
      Family
    </th>
    
    <th>
      Identifiers
    </th>
    
    <th>
      Notes
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      AES-GCM
    </td>
    
    <td>
      <code>
        A128GCM
      </code>
      
      , <code>
        A192GCM
      </code>
      
      , <code>
        A256GCM
      </code>
    </td>
    
    <td>
      AEAD. Fast, modern. Prefer <code>
        A256GCM
      </code>
      
       for new keys.
    </td>
  </tr>
  
  <tr>
    <td>
      AES-CBC + HMAC-SHA2
    </td>
    
    <td>
      <code>
        A128CBC-HS256
      </code>
      
      , <code>
        A192CBC-HS384
      </code>
      
      , <code>
        A256CBC-HS512
      </code>
    </td>
    
    <td>
      Composite construction. Required for some interop.
    </td>
  </tr>
</tbody>
</table>

Both are authenticated (they fail decryption if the ciphertext is modified), but AES-GCM does it in one pass while AES-CBC+HMAC does encryption and authentication separately (bigger CEK, more bytes in the token).

**Defaults for unjwt:** single-recipient `encrypt()` falls back to `A128GCM` when `enc` is not specified and the JWK carries no `enc` hint; `encryptMulti()` defaults to `A256GCM`. Use `A128CBC-HS256` only when a counterparty requires it.

## Choosing `alg`

**You control both sides of the channel** — use a symmetric key.

```ts
const aesKey = await generateJWK("A256KW");
// Same aesKey encrypts and decrypts. Share securely out-of-band.
const token = await encrypt(payload, aesKey);
```

**The encryption key is a password (human-typed or human-remembered)** — let unjwt use PBES2:

```ts
const token = await encrypt(payload, "my-strong-password");
// Uses PBES2-HS256+A128KW under the hood with p2c=600_000
```

**Recipients can't share a symmetric key** (multi-party systems, federated identity) — use a public-key scheme. `ECDH-ES+A256KW` is the modern default:

```ts
const { publicKey, privateKey } = await generateJWK("ECDH-ES+A256KW");
```

**Interop with a consumer that only supports RSA** — `RSA-OAEP-256` (or higher).

**You already have a pre-shared CEK** (e.g. derived from an out-of-band exchange) — use `dir`. See [Direct encryption](#direct-encryption-dir) below.

## Choosing `enc`

In 2026 the answer is almost always `A256GCM`:

- It's AEAD (one pass; smaller token).
- `256` is the safe default for new keys.
- AES-GCM is hardware-accelerated on every modern CPU.

Pick `A128CBC-HS256` **only** if:

- Counterparty requires it for interop.
- You're pairing it explicitly with `PBES2-HS256+A128KW` for classical interop.
- Your platform lacks AES-GCM hardware support (rare).

## Direct encryption (`dir`)

With `alg = "dir"`, the `encryptedKey` segment of the token is empty — the recipient's key is the CEK. It's the smallest JWE possible but requires coordination.

```ts
import { generateKey } from "unjwt/jwk";

// 1. Generate a CEK (must match `enc`)
const cek = await generateKey("A256GCM"); // CryptoKey with 256-bit random bytes

// 2. Both sides hold `cek`
const token = await encrypt({ secret: "x" }, cek, { alg: "dir", enc: "A256GCM" });
const { payload } = await decrypt(token, cek);
```

Use `dir` when:

- You're operating on a pre-shared CEK (e.g. derived from a session exchange).
- You control both sides and want the smallest overhead.
- Multi-recipient is out of scope — `dir` is **forbidden** in multi-recipient envelopes.

## Password-based (PBES2)

PBKDF2 with SHA-2, producing an AES-KW key that wraps the CEK. Three variants pair the hash strength with the AES-KW size:

- `PBES2-HS256+A128KW` → PBKDF2-HMAC-SHA256 + AES-128 wrap.
- `PBES2-HS384+A192KW` → PBKDF2-HMAC-SHA384 + AES-192 wrap.
- `PBES2-HS512+A256KW` → PBKDF2-HMAC-SHA512 + AES-256 wrap.

The token header carries the salt (`p2s`) and iteration count (`p2c`) the decryptor needs. unjwt defaults to `p2c: 600_000` per [OWASP](https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html#pbkdf2); on decrypt, it also enforces a floor (`1000`) and ceiling (`1_000_000`) to limit attacker-controlled CPU burn.

<note>

PBES2 is intentionally slow. A single password-based encryption takes ~tens of milliseconds on a modern laptop. That's a feature, not a bug — it makes offline password cracking expensive.

</note>

## Full combination rules

Not every `alg`/`enc` pair is valid. The constraints unjwt enforces:

- `alg: "dir"` requires `enc` to be set and both parties to hold the matching key.
- `alg: "ECDH-ES"` (no key wrap) requires `enc` to be set and is single-recipient only.
- `alg: "ECDH-ES+A*KW"` generates a random CEK; `enc` can be any content-encryption alg.
- `alg: "RSA-OAEP*"` with `enc: "A*CBC-HS*"` works, but prefer AES-GCM.

When in doubt, let `encrypt()` infer from a JWK generated by [`generateJWK()`](/jwk/generating) — it picks sensible defaults.

## See also

- [Encrypting →](/jwt/jwe/encrypting) — the producer side.
- [Decrypting →](/jwt/jwe/decrypting) — allowlists and DoS bounds.
- [ECDH-ES →](/jwt/jwe/ecdh-es) — the public-key workflow.
- [JWK generation →](/jwk/generating) — per-algorithm key creation.
