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
| 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(). | |
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.
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 keyA 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 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 |
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.
typdefaults to"JWT"for an object payload and is left out for a string or bytes.ctyis never added. Setcty: "JWT"for a nested token.critfollows the same rules as onsign().zipis 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
| 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, 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. |