unjwt 0.5.40 → 0.6.0
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.
- package/README.md +147 -6
- package/dist/_chunks/_crypto.mjs +1014 -0
- package/dist/_chunks/error.d.mts +67 -0
- package/dist/_chunks/index.d.mts +149 -45
- package/dist/_chunks/jwk.d.mts +154 -57
- package/dist/_chunks/jws.mjs +729 -683
- package/dist/_chunks/utils.mjs +74 -70
- package/dist/{_chunks/jws.d.mts → adapters/h3v1/index.d.mts} +98 -58
- package/dist/{_chunks/jws2.mjs → adapters/h3v1/index.mjs} +198 -85
- package/dist/adapters/h3v2/index.d.mts +213 -0
- package/dist/adapters/{h3v2.mjs → h3v2/index.mjs} +195 -89
- package/dist/core/_crypto/index.d.mts +71 -0
- package/dist/core/_crypto/index.mjs +2 -0
- package/dist/core/jwe.d.mts +40 -0
- package/dist/{_chunks → core}/jwe.mjs +61 -32
- package/dist/core/jwk.d.mts +231 -0
- package/dist/core/jwk.mjs +686 -0
- package/dist/core/jws.d.mts +31 -0
- package/dist/core/jws.mjs +104 -0
- package/dist/{_chunks/index2.d.mts → core/utils/index.d.mts} +13 -12
- package/dist/core/utils/index.mjs +2 -0
- package/dist/index.d.mts +7 -6
- package/dist/index.mjs +5 -5
- package/package.json +49 -40
- package/dist/_chunks/chunk.mjs +0 -13
- package/dist/_chunks/jwe.d.mts +0 -43
- package/dist/_chunks/jwk.mjs +0 -1592
- package/dist/adapters/h3.d.mts +0 -3
- package/dist/adapters/h3.mjs +0 -3
- package/dist/adapters/h3v1.d.mts +0 -3
- package/dist/adapters/h3v1.mjs +0 -3
- package/dist/adapters/h3v2.d.mts +0 -183
- package/dist/jwe.d.mts +0 -3
- package/dist/jwe.mjs +0 -2
- package/dist/jwk.d.mts +0 -3
- package/dist/jwk.mjs +0 -2
- package/dist/jws.d.mts +0 -3
- package/dist/jws.mjs +0 -2
- package/dist/utils.d.mts +0 -3
- 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
|
-
|
|
119
|
-
|
|
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
|
|
132
|
-
const pem = await
|
|
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, {
|
|
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);
|