unjwt 0.5.40 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/README.md +147 -6
  2. package/dist/_chunks/_crypto.mjs +1014 -0
  3. package/dist/_chunks/error.d.mts +67 -0
  4. package/dist/_chunks/index.d.mts +149 -45
  5. package/dist/_chunks/jwk.d.mts +154 -57
  6. package/dist/_chunks/jws.mjs +729 -683
  7. package/dist/_chunks/utils.mjs +74 -70
  8. package/dist/{_chunks/jws.d.mts → adapters/h3v1/index.d.mts} +98 -58
  9. package/dist/{_chunks/jws2.mjs → adapters/h3v1/index.mjs} +198 -85
  10. package/dist/adapters/h3v2/index.d.mts +213 -0
  11. package/dist/adapters/{h3v2.mjs → h3v2/index.mjs} +195 -89
  12. package/dist/core/_crypto/index.d.mts +71 -0
  13. package/dist/core/_crypto/index.mjs +2 -0
  14. package/dist/core/jwe.d.mts +40 -0
  15. package/dist/{_chunks → core}/jwe.mjs +61 -32
  16. package/dist/core/jwk.d.mts +231 -0
  17. package/dist/core/jwk.mjs +686 -0
  18. package/dist/core/jws.d.mts +31 -0
  19. package/dist/core/jws.mjs +104 -0
  20. package/dist/{_chunks/index2.d.mts → core/utils/index.d.mts} +13 -12
  21. package/dist/core/utils/index.mjs +2 -0
  22. package/dist/index.d.mts +7 -6
  23. package/dist/index.mjs +5 -5
  24. package/package.json +48 -40
  25. package/dist/_chunks/chunk.mjs +0 -13
  26. package/dist/_chunks/jwe.d.mts +0 -43
  27. package/dist/_chunks/jwk.mjs +0 -1592
  28. package/dist/adapters/h3.d.mts +0 -3
  29. package/dist/adapters/h3.mjs +0 -3
  30. package/dist/adapters/h3v1.d.mts +0 -3
  31. package/dist/adapters/h3v1.mjs +0 -3
  32. package/dist/adapters/h3v2.d.mts +0 -183
  33. package/dist/jwe.d.mts +0 -3
  34. package/dist/jwe.mjs +0 -2
  35. package/dist/jwk.d.mts +0 -3
  36. package/dist/jwk.mjs +0 -2
  37. package/dist/jws.d.mts +0 -3
  38. package/dist/jws.mjs +0 -2
  39. package/dist/utils.d.mts +0 -3
  40. package/dist/utils.mjs +0 -2
package/README.md CHANGED
@@ -7,7 +7,7 @@
7
7
  A collection of low-level JWT ([RFC 7519](https://datatracker.ietf.org/doc/html/rfc7519)) utilities using the Web Crypto API. Supports:
8
8
 
9
9
  - **JWS** ([RFC 7515](https://datatracker.ietf.org/doc/html/rfc7515)) — sign and verify tokens using HMAC, RSA, RSA-PSS, ECDSA, and EdDSA algorithms
10
- - **JWE** ([RFC 7516](https://datatracker.ietf.org/doc/html/rfc7516)) — encrypt and decrypt data using AES Key Wrap, AES-GCM KW, RSA-OAEP, PBES2, and ECDH-ES key management with AES-GCM or AES-CBC+HMAC-SHA2 content encryption
10
+ - **JWE** ([RFC 7516](https://datatracker.ietf.org/doc/html/rfc7516)) — encrypt and decrypt data using Direct (`dir`), AES Key Wrap, AES-GCM KW, RSA-OAEP, PBES2, and ECDH-ES key management with AES-GCM or AES-CBC+HMAC-SHA2 content encryption
11
11
  - **JWK** ([RFC 7517](https://datatracker.ietf.org/doc/html/rfc7517)) — generate, import, export, wrap, and unwrap keys (CryptoKey, JWK, PEM)
12
12
  - **Framework adapters** for cookie-based JWT sessions:
13
13
  - [H3 v1](./skills/unjwt/references/adapters-h3.md) (Nuxt v4, Nitro v2)
@@ -37,6 +37,7 @@ npx skills add sandros94/unjwt
37
37
  ```ts
38
38
  import { sign, verify } from "unjwt/jws";
39
39
  import { generateJWK } from "unjwt/jwk";
40
+ // or import both from the root: import { sign, verify, generateJWK } from "unjwt";
40
41
 
41
42
  // Generate a key (HMAC, RSA, ECDSA, EdDSA, etc.)
42
43
  const key = await generateJWK("HS256");
@@ -74,12 +75,27 @@ const { payload } = await verify(
74
75
  );
75
76
  ```
76
77
 
78
+ **JWKSet for key rotation and JWKS endpoint consumption:**
79
+
80
+ When a `JWKSet` is passed (directly or returned from a lookup function), `verify` tries each matching key in order until one succeeds. If the token carries a `kid`, only keys with that exact `kid` are tried — no retry. If `kid` is absent, all keys in the set whose `alg` is compatible become candidates. This makes transparent key rotation possible without any retry logic in userland.
81
+
82
+ ```ts
83
+ // Fetch all public keys from a JWKS endpoint — library picks the right one
84
+ const jwks = await fetch("https://auth.example.com/.well-known/jwks.json").then((r) => r.json());
85
+ const { payload } = await verify(token, jwks);
86
+
87
+ // Rotation window: old tokens (no kid) are tried against all keys automatically
88
+ const rotatingSet = { keys: [newKey, legacyKey] };
89
+ const { payload: p } = await verify(oldToken, rotatingSet);
90
+ ```
91
+
77
92
  ### JWE — Encrypt & Decrypt
78
93
 
79
94
  > Full reference: [skills/unjwt/references/jwe.md](./skills/unjwt/references/jwe.md)
80
95
 
81
96
  ```ts
82
97
  import { encrypt, decrypt } from "unjwt/jwe";
98
+ // or: import { encrypt, decrypt } from "unjwt";
83
99
 
84
100
  // Password-based encryption (simplest — uses PBES2)
85
101
  const token = await encrypt({ secret: "sensitive data" }, "my-password");
@@ -105,6 +121,119 @@ const ecKeys = await generateJWK("ECDH-ES+A256KW");
105
121
  const token3 = await encrypt({ data: "secret" }, ecKeys.publicKey);
106
122
  ```
107
123
 
124
+ ### ECDH-ES — End-to-End Encrypted Messaging
125
+
126
+ In end-to-end encryption (E2EE), each participant holds a **key pair**: the **private key** never leaves the owner's system; the **public key** is distributed freely. A sender encrypts a message using the recipient's public key, producing a token that only the recipient — holding the matching private key — can decrypt.
127
+
128
+ #### One-time key setup
129
+
130
+ Each participant generates a key pair once and persists it. The public key is shared out-of-band — published to a server endpoint, written to a config file, exchanged at registration, etc.:
131
+
132
+ ```ts
133
+ import { generateJWK } from "unjwt/jwk";
134
+
135
+ const myKeys = await generateJWK("ECDH-ES+A256KW", { kid: "alice-2025" });
136
+ // → { privateKey: JWK_EC_Private, publicKey: JWK_EC_Public }
137
+
138
+ // Keep myKeys.privateKey in secure storage — never expose it
139
+ // Share myKeys.publicKey freely (e.g., publish at /.well-known/jwks.json)
140
+ ```
141
+
142
+ #### Sending to one recipient
143
+
144
+ For two-party communication, `encrypt` and `decrypt` handle everything. Pass the recipient's public key — a one-time ephemeral key pair is generated internally per message:
145
+
146
+ ```ts
147
+ import { encrypt, decrypt } from "unjwt/jwe";
148
+
149
+ // Sender (Alice) ————————————————————————————————————
150
+ // Fetch Bob's public key (from his public endpoint, shared config, etc.)
151
+ const bobPublicKey = /* ... */;
152
+
153
+ const token = await encrypt({ message: "Hello Bob!" }, bobPublicKey);
154
+ // Send `token` to Bob over any channel — only Bob can decrypt it
155
+
156
+ // Recipient (Bob) ————————————————————————————————————
157
+ const { payload } = await decrypt(token, bobPrivateKey);
158
+ console.log(payload.message); // "Hello Bob!"
159
+ ```
160
+
161
+ Each token carries a fresh ephemeral public key (`epk`) in its header. Bob uses that `epk` together with his private key to re-derive the same shared secret Alice used, then unwraps the Content Encryption Key. At no point does Alice have Bob's private key, and Bob cannot forge a message appearing to come from Alice.
162
+
163
+ #### Sending to multiple recipients
164
+
165
+ **Simple — one token per recipient.** Encrypt the payload independently for each party. Each token is decryptable only by its intended recipient:
166
+
167
+ ```ts
168
+ const recipients = [
169
+ { name: "bob", publicKey: bobPublicKey },
170
+ { name: "charlie", publicKey: charliePublicKey },
171
+ ];
172
+
173
+ const tokens = await Promise.all(
174
+ recipients.map(({ publicKey }) => encrypt({ message: "Hello everyone!" }, publicKey)),
175
+ );
176
+ // Deliver tokens[0] to Bob, tokens[1] to Charlie
177
+ ```
178
+
179
+ This is the right choice for most use cases. The only trade-off is proportional size: N recipients produce N tokens.
180
+
181
+ **Shared ciphertext — one payload, N wrapped keys.** When the payload is large or bandwidth matters, encrypt the payload once with a random Content Encryption Key (`dir`), then wrap that key separately per recipient. Everyone receives the same ciphertext, each with their own individually wrapped key:
182
+
183
+ ```ts
184
+ import { wrapKey, unwrapKey } from "unjwt/jwk";
185
+ import { randomBytes } from "unjwt/utils";
186
+ import { encrypt, decrypt } from "unjwt/jwe";
187
+
188
+ const enc = "A256GCM";
189
+
190
+ // 1. Generate a random CEK once for this message (32 bytes = 256-bit for A256GCM)
191
+ const cek = randomBytes(32);
192
+
193
+ // 2. Encrypt the payload ONCE using the CEK directly (alg: "dir")
194
+ const ciphertext = await encrypt({ message: "Hello everyone!" }, cek, { alg: "dir", enc });
195
+
196
+ // 3. Wrap the CEK individually for each recipient
197
+ const wrappedKeys = await Promise.all(
198
+ recipients.map(async ({ name, publicKey }) => {
199
+ const { encryptedKey, epk } = await wrapKey("ECDH-ES+A256KW", cek, publicKey);
200
+ return { name, encryptedKey, epk };
201
+ }),
202
+ );
203
+ // Deliver `ciphertext` (same for all) + each recipient's own { encryptedKey, epk }
204
+
205
+ // Recipient side ——————————————————————————————————————————————
206
+ const mine = wrappedKeys.find((w) => w.name === myName)!;
207
+ const rawCek = await unwrapKey("ECDH-ES+A256KW", mine.encryptedKey, myPrivateKey, {
208
+ format: "raw",
209
+ epk: mine.epk,
210
+ enc,
211
+ });
212
+ const { payload } = await decrypt(ciphertext, rawCek);
213
+ console.log(payload.message); // "Hello everyone!"
214
+ ```
215
+
216
+ > This pattern is the manual foundation of [JWE JSON Serialization](https://www.rfc-editor.org/rfc/rfc7516#section-3.3) (RFC 7516 §3.3), which packages the ciphertext and all wrapped keys into a single `recipients[]` JSON structure. Full JWE JSON Serialization support is planned for a future release; `wrapKey` and `unwrapKey` provide the building blocks for those who need to construct or consume the JSON form today.
217
+
218
+ #### `deriveSharedSecret` — raw key material
219
+
220
+ `wrapKey` and `unwrapKey` already handle the full ECDH Concat KDF + AES-KW cycle internally. `deriveSharedSecret` exposes the KDF step alone, returning the raw derived bytes before any key-wrapping step:
221
+
222
+ ```ts
223
+ import { deriveSharedSecret } from "unjwt/jwk";
224
+
225
+ // Both sides independently derive the exact same bytes
226
+ const aliceView = await deriveSharedSecret(
227
+ bobPublicKey,
228
+ aliceEphemeralPrivateKey,
229
+ "ECDH-ES+A256KW",
230
+ );
231
+ const bobView = await deriveSharedSecret(aliceEphemeralPublicKey, bobPrivateKey, "ECDH-ES+A256KW");
232
+ // aliceView and bobView are identical Uint8Arrays
233
+ ```
234
+
235
+ Use `deriveSharedSecret` when you need the shared secret itself rather than a wrapped key: custom hybrid protocols that use the derived bytes directly, non-standard wrapping schemes, or verifying the key agreement step in isolation.
236
+
108
237
  ### JWK — Key Management
109
238
 
110
239
  > Full reference: [skills/unjwt/references/jwk.md](./skills/unjwt/references/jwk.md)
@@ -115,12 +244,16 @@ import {
115
244
  generateJWK,
116
245
  importKey,
117
246
  exportKey,
118
- importJWKFromPEM,
119
- exportJWKToPEM,
247
+ importFromPEM,
248
+ exportToPEM,
120
249
  wrapKey,
121
250
  unwrapKey,
122
251
  deriveKeyFromPassword,
252
+ getJWKsFromSet,
253
+ deriveSharedSecret,
254
+ configureJWKCache,
123
255
  } from "unjwt/jwk";
256
+ // All of the above are also importable from "unjwt" directly.
124
257
 
125
258
  // Generate keys as CryptoKey or JWK
126
259
  const hmacKey = await generateKey("HS256"); // CryptoKey
@@ -128,13 +261,20 @@ const rsaPair = await generateKey("RS256"); // CryptoKeyPair
128
261
  const ecJwk = await generateJWK("ES256", { kid: "k1" }); // { privateKey: JWK, publicKey: JWK }
129
262
 
130
263
  // PEM conversion
131
- const jwk = await importJWKFromPEM(pemString, "spki", "RS256", undefined, { kid: "rsa-1" });
132
- const pem = await exportJWKToPEM(jwk, "spki");
264
+ const jwk = await importFromPEM(pemString, "spki", "RS256", { jwkParams: { kid: "rsa-1" } });
265
+ const pem = await exportToPEM(jwk, "spki");
133
266
 
134
267
  // Key wrapping
135
268
  const cek = crypto.getRandomValues(new Uint8Array(32));
136
269
  const { encryptedKey } = await wrapKey("A256KW", cek, aesKey);
137
- const unwrapped = await unwrapKey("A256KW", encryptedKey, aesKey, { returnAs: false });
270
+ const unwrapped = await unwrapKey("A256KW", encryptedKey, aesKey, { format: "raw" });
271
+
272
+ // ECDH-ES shared secret (e.g. for multi-recipient JWE)
273
+ const secret = await deriveSharedSecret(
274
+ recipientPublicKey,
275
+ senderEphemeralPrivate,
276
+ "ECDH-ES+A256KW",
277
+ );
138
278
  ```
139
279
 
140
280
  ### Utility Functions
@@ -155,6 +295,7 @@ import {
155
295
  textEncoder,
156
296
  textDecoder,
157
297
  } from "unjwt/utils";
298
+ // Most of the above are also importable from "unjwt" directly.
158
299
 
159
300
  const bytes = randomBytes(32);
160
301
  const encoded = base64UrlEncode(bytes);