Encrypting

encrypt(payload, key, options?)

Produces a compact JWE — five base64url segments: <header>.<encryptedKey>.<iv>.<ciphertext>.<tag>.

Parameters

NameType
payloadstring | Uint8Array | Record<string, unknown>
keystring | JWEEncryptJWK | CryptoKey | Uint8Array
options.algKeyManagementAlgorithm — required when inference fails
options.encContentEncryptionAlgorithm — required when alg = "dir"
OthersSee Full signature below

JWEEncryptJWK narrows the JWK by family: an oct key with a JWE symmetric alg (AES-KW, AES-GCM, AES-GCM-KW, AES-CBC-HMAC, PBES2, or "dir"), or a public asymmetric JWK whose alg is RSA-OAEP (JWK_RSA_Public<JWK_RSA_ENC>) or ECDH-ES (JWK_EC_Public<JWK_ECDH_ES> / JWK_OKP_Public<JWK_ECDH_ES>). HMAC and other non-JWE algs are rejected at the type level.

Returns Promise<string> — the compact JWE.

Algorithm inference

The shape of key determines alg, and then alg determines whether enc is inferable:

key shapeInferred algenc default?
string (password)PBES2-HS256+A128KWA128GCM
JWK with alg fieldkey.algDepends on alg
CryptoKey with usages: ["encrypt"]Derived from algorithm contextUsually required
CryptoKey | JWK_oct with alg = "dir""dir"Required — no default

Pass options.alg / options.enc explicitly when inference is impossible.

Key shapes

Password (PBES2)

The shortest path — unjwt derives a CEK-wrapping key via PBKDF2:

const token = await encrypt({ data: "x" }, "my-password");
// Uses: alg = PBES2-HS256+A128KW, enc = A128GCM, p2c = 600_000

The p2s (salt) and p2c (iteration count) are written into the header so the decryptor can regenerate the same wrapping key.

p2c defaults to 600 000 — the OWASP PBKDF2 recommendation for HMAC-SHA256. Lower it only for legacy interop. PBKDF2 is intentionally slow, so a password-protected token takes ~tens of milliseconds to produce on a modern laptop.

Symmetric JWK

A key generated by generateJWK("A256KW") or similar:

const aesKey = await generateJWK("A256KW"); // key.alg === "A256KW"
const token = await encrypt({ data: "x" }, aesKey); // alg inferred; enc defaults to A128GCM

Asymmetric JWK — public key

For RSA-OAEP and ECDH-ES:

const { publicKey, privateKey } = await generateJWK("RSA-OAEP-256");
const token = await encrypt({ data: "x" }, publicKey);
// Recipient decrypts with privateKey

Direct encryption — dir

With alg = "dir", the provided key is the CEK. No wrapping happens; encryptedKey in the token is empty.

import { generateKey } from "unjwt/jwk";

const cek = await generateKey("A256GCM"); // CryptoKey suitable as CEK for A256GCM
const token = await encrypt({ data: "x" }, cek, { alg: "dir", enc: "A256GCM" });

When passing a JWK_oct directly, you can hint enc via the non-standard jwk.enc field (unjwt reads it):

const cekJwk = { ...(await generateJWK("A256GCM")), enc: "A256GCM" };
const token = await encrypt({ data: "x" }, cekJwk, { alg: "dir" });

dir produces the smallest token (the encryptedKey segment is empty) but requires both parties to hold the exact same CEK ahead of time.

expiresIn — the same as JWS

If the payload is a JSON object:

await encrypt({ sub: "u1" }, key, { expiresIn: "1h" });

Accepted forms: 30, "30s", "10m", "2h", "7D", "1W", "3M", "1Y".

Custom protected header

The JWE protected header is part of the AAD (Additional Authenticated Data) — anything you put there is bound to the ciphertext and can't be altered without invalidating the tag.

const token = await encrypt({ sub: "u1" }, key, {
  protectedHeader: {
    kid: "my-key",
    typ: "JWE", // default for object payloads
    cty: "application/json",
  },
});

The library manages these fields and rejects them in protectedHeader:

  • alg, enc — set via options.alg/options.enc.
  • iv, tag — computed during encryption.
  • p2s, p2c — set for PBES2.
  • epk, apu, apv — set for ECDH-ES.

Custom CEK / IV

For test reproducibility, deterministic benchmarks, or interop scenarios:

import { secureRandomBytes } from "unjwt/utils";

const token = await encrypt({ data: "x" }, key, {
  cek: secureRandomBytes(32), // supply your own CEK
  contentEncryptionIV: secureRandomBytes(12), // supply your own IV
});
Never reuse a CEK or IV across messages in production. The primary purpose of these options is testability. In normal use, let unjwt generate fresh random values.

PBES2 parameters

Override p2c or provide a fixed salt:

const token = await encrypt({ data: "x" }, "password", {
  p2s: secureRandomBytes(16),
  p2c: 100_000, // lower — only for legacy interop
});

Raising p2c on the sender side means proportionally more CPU for the decryptor too — unjwt enforces a ceiling by default (see maxIterations on decrypt).

ECDH-ES parameters

For ECDH-ES / ECDH-ES+A*KW keys:

const token = await encrypt({ data: "x" }, ecPublicKey, {
  enc: "A256GCM", // required at top level for bare "ECDH-ES"
  ecdh: {
    ephemeralKey: senderEphemeralKeypair, // override if you need a specific key
    partyUInfo: new TextEncoder().encode("alice@example.com"),
    partyVInfo: new TextEncoder().encode("bob@example.com"),
  },
});

partyUInfo / partyVInfo bind the derived key to specific sender/recipient identities (part of NIST SP 800-56A Concat KDF). They're optional, but they're how you prevent cross-context key reuse.

Full pattern: ECDH-ES →.

Full signature

interface JWEEncryptOptions {
  alg?: KeyManagementAlgorithm;
  enc?: ContentEncryptionAlgorithm;
  currentDate?: Date;
  expiresIn?: ExpiresIn;
  expiresAt?: Date;
  notBeforeIn?: ExpiresIn;
  notBeforeAt?: Date;
  protectedHeader?: StrictOmit<
    JWEHeaderParameters,
    "alg" | "enc" | "iv" | "tag" | "p2s" | "p2c" | "epk" | "apu" | "apv"
  >;
  keyManagementIV?: Uint8Array<ArrayBuffer>;
  p2s?: Uint8Array<ArrayBuffer>;
  p2c?: number;
  ecdh?: {
    ephemeralKey?:
      | CryptoKey
      | JWK_EC_Private
      | CryptoKeyPair
      | {
          publicKey: CryptoKey | JWK_EC_Public;
          privateKey: CryptoKey | JWK_EC_Private;
        };
    partyUInfo?: Uint8Array<ArrayBuffer>;
    partyVInfo?: Uint8Array<ArrayBuffer>;
  };
  cek?: Uint8Array<ArrayBuffer>;
  contentEncryptionIV?: Uint8Array<ArrayBuffer>;
}

See also