encrypt()

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

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

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

OptionTypeDefaultWhat it does
algKeyManagementAlgorithmfrom the keyHow the content key reaches the recipient.
encContentEncryptionAlgorithmthe key's enc, else A256GCMHow the payload is sealed. Required for dir when the key names none.
protectedHeaderJWEHeaderParametersnoneExtra header members. alg, enc, iv, tag, p2s, p2c, epk, apu, apv are written by the library.
expiresIn, expiresAt, notBeforeIn, notBeforeAt, currentDatenoneTime claims, as on sign().
p2cnumber600_000PBES2 iteration count, between 1000 and 600,000.
p2sUint8Array16 random bytesPBES2 salt, at least 8 bytes.
keyManagementIVUint8ArrayrandomIV for A*GCMKW key wrapping.
ecdh.partyUInfoUint8ArraynoneECDH-ES apu, at most 1024 bytes.
ecdh.partyVInfoUint8ArraynoneECDH-ES apv, at most 1024 bytes.
ecdh.ephemeralKeykey pair, private JWK or CryptoKeygeneratedThe sender's ephemeral ECDH key. Leave it to the library.
cekUint8ArrayrandomThe content key. Must be the length enc needs.
contentEncryptionIVUint8ArrayrandomThe 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

Keyalg when omittedenc when omitted
A password stringPBES2-HS256+A128KWA256GCM
A JWK with a key-wrapping alg (A256KW, RSA-OAEP-256, ECDH-ES+A256KW, …)the JWK's algthe JWK's enc, else A256GCM
An oct JWK whose alg is an enc (A256GCM, A128CBC-HS256, …)dirthat same algorithm
A CryptoKeynone, pass algA256GCM (dir needs enc)
A Uint8Arraynone, pass algA256GCM (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.

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() 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) and writes the salt and count into the header.

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 cover deriving a reusable JWK instead.

#Header members written for you

alg familyMembers added
PBES2-*p2s (salt), p2c (count)
A*GCMKWiv and tag of the key-wrapping step
ECDH-ES*epk (ephemeral public key), and apu / apv when given
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.
  • crit follows the same rules as on sign().
  • zip is refused, since compression is not implemented.
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 for the JSON form.

#Errors

CodeWhen
ERR_JWE_ALG_MISSINGNo alg and none can be read from the key.
ERR_JWE_ENC_MISSINGalg: "dir" with no enc from the options or the key.
ERR_JWE_ALG_NOT_ALLOWEDalg is "none" or outside the table, such as RSA1_5.
ERR_JWE_INVALIDAn unsupported enc.
ERR_JWK_INVALIDA 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_OPTIONp2c 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_INVALIDExclusive time options passed together, or a negative notBeforeIn.