Encrypting
encrypt(payload, key, options?)
Produces a compact JWE — five base64url segments: <header>.<encryptedKey>.<iv>.<ciphertext>.<tag>.
Parameters
| Name | Type |
|---|---|
payload | string | Uint8Array | Record<string, unknown> |
key | string | JWEEncryptJWK | CryptoKey | Uint8Array |
options.alg | KeyManagementAlgorithm — required when inference fails |
options.enc | ContentEncryptionAlgorithm — required when alg = "dir" |
| Others | See 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 shape | Inferred alg | enc default? |
|---|---|---|
string (password) | PBES2-HS256+A128KW | A128GCM |
JWK with alg field | key.alg | Depends on alg |
CryptoKey with usages: ["encrypt"] | Derived from algorithm context | Usually 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 viaoptions.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
});
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
- Decrypting → — the consumer side.
- Algorithms → — picking
algandenc. - ECDH-ES → — end-to-end encryption.
- Multi-recipient → — one ciphertext, many recipients.