JWE

JWE — encrypted tokens

A JSON Web Encryption (RFC 7516) is a JWT whose payload is encrypted: no one can read it without the right key, and no one can tamper with it without the tag failing.

Import:

import { encrypt, decrypt } from "unjwt/jwe";
// or from the flat barrel:
import { encrypt, decrypt } from "unjwt";

Basic usage

The shortest path — password-based encryption (PBES2 under the hood):

basic.ts
import { encrypt, decrypt } from "unjwt/jwe";

const token = await encrypt({ secret: "sensitive" }, "my-password");
const { payload } = await decrypt(token, "my-password");

console.log(payload);
// { secret: "sensitive" }

The token looks like a JWT (dotted segments) but the third segment onwards is ciphertext — base64url-decoding the payload gives you random bytes, not claims.

How a JWE is built

Every JWE encrypts its payload in two steps:

Content encryption — a random Content Encryption Key (CEK) encrypts the payload with an authenticated cipher (AES-GCM or AES-CBC+HMAC-SHA2). The result is the ciphertext segment plus an IV and authentication tag.

Key management — the CEK itself is delivered to the recipient, either wrapped by a key they hold, or derived from a shared secret with them. That's what the alg header describes.

So every JWE header has two algorithm fields:

  • alg — how the CEK is delivered. Determines what kind of recipient key is required.
  • enc — how the CEK encrypts the payload. Determines the content cipher.
{ "alg": "PBES2-HS256+A128KW", "enc": "A128GCM", "p2s": "...", "p2c": 600000 }

You'll see these in every example below. Full list in Algorithms.

Choosing the alg family

You have…Use alg
A password stringPBES2-* (inferred automatically for passwords)
A shared secret key (both sides hold it)A128KW/A256KW (Key Wrap) or dir (direct)
The recipient's RSA public keyRSA-OAEP-256 / RSA-OAEP-512
The recipient's EC public keyECDH-ES or ECDH-ES+A256KW
A prearranged CEK used directly as the keydir

Algorithm inference handles most of this for you — passing a JWK with a alg field set picks the right path. See Encrypting →.

The three encryption patterns

The rest of JWE is just variations on three recurring shapes:

1. Password-based

One secret known to both parties. Simplest, slowest (by design).

const token = await encrypt({ data: "x" }, "my-password");
const { payload } = await decrypt(token, "my-password");

2. Shared key (symmetric)

A key that both parties already hold — you generated and distributed it out of band.

const key = await generateJWK("A256KW");
const token = await encrypt({ data: "x" }, key);
const { payload } = await decrypt(token, key);

3. Public-key (asymmetric)

The sender only needs the recipient's public key; only the holder of the matching private key can decrypt. Essential for end-to-end encryption.

const { publicKey, privateKey } = await generateJWK("RSA-OAEP-256");
// ... publicKey distributed out-of-band ...

const token = await encrypt({ data: "x" }, publicKey);
const { payload } = await decrypt(token, privateKey);

Elliptic-curve equivalent:

const { publicKey, privateKey } = await generateJWK("ECDH-ES+A256KW");
const token = await encrypt({ data: "x" }, publicKey);
const { payload } = await decrypt(token, privateKey);

See ECDH-ES → for the full end-to-end messaging story.

Going further