unjwt 0.5.39 → 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 +156 -13
- package/dist/_chunks/_crypto.mjs +1014 -0
- package/dist/_chunks/error.d.mts +67 -0
- package/dist/_chunks/index.d.mts +631 -219
- package/dist/_chunks/jwk.d.mts +220 -87
- package/dist/_chunks/jws.mjs +729 -682
- package/dist/_chunks/utils.mjs +74 -69
- package/dist/adapters/h3v1/index.d.mts +282 -0
- package/dist/{_chunks/h3v1.mjs → adapters/h3v1/index.mjs} +204 -91
- package/dist/adapters/h3v2/index.d.mts +213 -0
- package/dist/adapters/{h3v2.mjs → h3v2/index.mjs} +215 -113
- 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 +50 -41
- package/dist/_chunks/chunk.mjs +0 -13
- package/dist/_chunks/jwe.d.mts +0 -43
- package/dist/_chunks/jwk.mjs +0 -1592
- package/dist/_chunks/jws.d.mts +0 -46
- 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 -184
- package/dist/jwe.d.mts +0 -3
- package/dist/jwe.mjs +0 -4
- package/dist/jwk.d.mts +0 -3
- package/dist/jwk.mjs +0 -3
- package/dist/jws.d.mts +0 -3
- package/dist/jws.mjs +0 -4
- package/dist/utils.d.mts +0 -3
- package/dist/utils.mjs +0 -2
package/dist/_chunks/jws.mjs
CHANGED
|
@@ -1,3 +1,36 @@
|
|
|
1
|
+
//#region src/core/error.ts
|
|
2
|
+
/**
|
|
3
|
+
* Base error class for all unjwt errors. Every error thrown by the library is
|
|
4
|
+
* an instance of `JWTError`, so a single `instanceof` check is sufficient to
|
|
5
|
+
* distinguish library errors from other thrown values. Use the `code` property
|
|
6
|
+
* (or the `isJWTError` type guard) to narrow to a specific error condition.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* import { JWTError, isJWTError } from "unjwt/jws";
|
|
10
|
+
*
|
|
11
|
+
* try {
|
|
12
|
+
* await verify(token, key);
|
|
13
|
+
* } catch (err) {
|
|
14
|
+
* if (isJWTError(err, "ERR_JWT_EXPIRED")) {
|
|
15
|
+
* console.log(err.cause.jti); // string | undefined
|
|
16
|
+
* }
|
|
17
|
+
* }
|
|
18
|
+
*/
|
|
19
|
+
var JWTError = class extends Error {
|
|
20
|
+
code;
|
|
21
|
+
cause;
|
|
22
|
+
constructor(message, code, cause) {
|
|
23
|
+
super(message);
|
|
24
|
+
this.name = "JWTError";
|
|
25
|
+
this.code = code;
|
|
26
|
+
this.cause = cause;
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
function isJWTError(error, code) {
|
|
30
|
+
if (!(error instanceof JWTError)) return false;
|
|
31
|
+
return code === void 0 || error.code === code;
|
|
32
|
+
}
|
|
33
|
+
//#endregion
|
|
1
34
|
//#region src/core/utils/jwt.ts
|
|
2
35
|
/**
|
|
3
36
|
* Apply default typ/cty semantics shared by JWS & JWE.
|
|
@@ -106,23 +139,31 @@ function validateJwtClaims(jwtClaims, options = {}) {
|
|
|
106
139
|
if (options.subject) allRequiredClaims.add("sub");
|
|
107
140
|
if (options.maxTokenAge) allRequiredClaims.add("iat");
|
|
108
141
|
for (const claimName of allRequiredClaims) if (!(claimName in jwtClaims)) missingClaims.add(claimName);
|
|
109
|
-
if (missingClaims.size > 0) throw new
|
|
142
|
+
if (missingClaims.size > 0) throw new JWTError(`Missing required JWT Claims: ${[...missingClaims].join(", ")}`, "ERR_JWT_CLAIM_MISSING");
|
|
110
143
|
if (options.issuer) {
|
|
111
144
|
const expectedIssuers = maybeArray(options.issuer);
|
|
112
|
-
if (!jwtClaims.iss || !expectedIssuers.includes(jwtClaims.iss)) throw new
|
|
145
|
+
if (!jwtClaims.iss || !expectedIssuers.includes(jwtClaims.iss)) throw new JWTError(`Invalid JWT "iss" (Issuer) Claim: Expected ${expectedIssuers.join(" or ")}, got ${jwtClaims.iss}`, "ERR_JWT_CLAIM_INVALID");
|
|
113
146
|
}
|
|
114
|
-
if (options.subject && jwtClaims.sub !== options.subject) throw new
|
|
147
|
+
if (options.subject && jwtClaims.sub !== options.subject) throw new JWTError(`Invalid JWT "sub" (Subject) Claim: Expected ${options.subject}, got ${jwtClaims.sub}`, "ERR_JWT_CLAIM_INVALID");
|
|
115
148
|
if (options.audience) {
|
|
116
149
|
const expectedAudiences = maybeArray(options.audience);
|
|
117
150
|
const claimAudience = maybeArray(jwtClaims.aud || []);
|
|
118
|
-
if (!claimAudience.some((aud) => expectedAudiences.includes(aud))) throw new
|
|
151
|
+
if (!claimAudience.some((aud) => expectedAudiences.includes(aud))) throw new JWTError(`Invalid JWT "aud" (Audience) Claim: Expected ${expectedAudiences.join(" or ")}, got ${claimAudience.join(", ")}`, "ERR_JWT_CLAIM_INVALID");
|
|
119
152
|
}
|
|
120
|
-
if (typeof jwtClaims.nbf === "number" && jwtClaims.nbf > currentTime + clockTolerance) throw new
|
|
121
|
-
if (typeof jwtClaims.exp === "number" && jwtClaims.exp <= currentTime - clockTolerance) throw new
|
|
153
|
+
if (typeof jwtClaims.nbf === "number" && jwtClaims.nbf > currentTime + clockTolerance) throw new JWTError(`JWT "nbf" (Not Before) Claim validation failed: Token is not yet valid (nbf: ${(/* @__PURE__ */ new Date(jwtClaims.nbf * 1e3)).toISOString()})`, "ERR_JWT_NBF");
|
|
154
|
+
if (typeof jwtClaims.exp === "number" && jwtClaims.exp <= currentTime - clockTolerance) throw new JWTError(`JWT "exp" (Expiration Time) Claim validation failed: Token has expired (exp: ${(/* @__PURE__ */ new Date(jwtClaims.exp * 1e3)).toISOString()})`, "ERR_JWT_EXPIRED", {
|
|
155
|
+
jti: jwtClaims.jti,
|
|
156
|
+
iat: jwtClaims.iat,
|
|
157
|
+
exp: jwtClaims.exp
|
|
158
|
+
});
|
|
122
159
|
if (options.maxTokenAge) {
|
|
123
|
-
if (typeof jwtClaims.iat !== "number") throw new
|
|
124
|
-
if (jwtClaims.iat > currentTime + clockTolerance) throw new
|
|
125
|
-
if (jwtClaims.iat < currentTime - computeMaxTokenAgeSeconds(options.maxTokenAge) - clockTolerance) throw new
|
|
160
|
+
if (typeof jwtClaims.iat !== "number") throw new JWTError("JWT \"iat\" (Issued At) Claim must be a number when maxTokenAge is set.", "ERR_JWT_CLAIM_INVALID");
|
|
161
|
+
if (jwtClaims.iat > currentTime + clockTolerance) throw new JWTError(`JWT "iat" (Issued At) Claim validation failed: Token was issued in the future (iat: ${(/* @__PURE__ */ new Date(jwtClaims.iat * 1e3)).toISOString()})`, "ERR_JWT_CLAIM_INVALID");
|
|
162
|
+
if (jwtClaims.iat < currentTime - computeMaxTokenAgeSeconds(options.maxTokenAge) - clockTolerance) throw new JWTError(`JWT "iat" (Issued At) Claim validation failed: Token is too old (maxTokenAge: ${options.maxTokenAge}s, iat: ${(/* @__PURE__ */ new Date(jwtClaims.iat * 1e3)).toISOString()})`, "ERR_JWT_EXPIRED", {
|
|
163
|
+
jti: jwtClaims.jti,
|
|
164
|
+
iat: jwtClaims.iat,
|
|
165
|
+
exp: jwtClaims.exp
|
|
166
|
+
});
|
|
126
167
|
}
|
|
127
168
|
}
|
|
128
169
|
const BASE_HEADER_PARAMS = [
|
|
@@ -137,11 +178,11 @@ const BASE_HEADER_PARAMS = [
|
|
|
137
178
|
"x5u"
|
|
138
179
|
];
|
|
139
180
|
/** Validate critical headers in JWS semantics. */
|
|
140
|
-
function validateCriticalHeadersJWS(protectedHeader,
|
|
181
|
+
function validateCriticalHeadersJWS(protectedHeader, recognizedHeaders = []) {
|
|
141
182
|
if (!protectedHeader.crit) return;
|
|
142
183
|
const missingHeaderParams = /* @__PURE__ */ new Set();
|
|
143
184
|
const recognizedParams = new Set([
|
|
144
|
-
...
|
|
185
|
+
...recognizedHeaders,
|
|
145
186
|
...BASE_HEADER_PARAMS,
|
|
146
187
|
"b64"
|
|
147
188
|
]);
|
|
@@ -149,14 +190,14 @@ function validateCriticalHeadersJWS(protectedHeader, requiredHeaders = []) {
|
|
|
149
190
|
if (recognizedParams.has(param) && (param in protectedHeader || param === "b64")) continue;
|
|
150
191
|
missingHeaderParams.add(param);
|
|
151
192
|
}
|
|
152
|
-
if (missingHeaderParams.size > 0) throw new
|
|
193
|
+
if (missingHeaderParams.size > 0) throw new JWTError(`Missing critical header parameters: ${[...missingHeaderParams].join(", ")}`, "ERR_JWS_INVALID");
|
|
153
194
|
}
|
|
154
195
|
/** Validate critical headers in JWE semantics. */
|
|
155
|
-
function validateCriticalHeadersJWE(protectedHeader,
|
|
156
|
-
if (protectedHeader.crit && !
|
|
196
|
+
function validateCriticalHeadersJWE(protectedHeader, recognizedHeaders) {
|
|
197
|
+
if (protectedHeader.crit && !recognizedHeaders) throw new JWTError(`Unprocessed critical header parameters: ${protectedHeader.crit.join(", ")}`, "ERR_JWE_INVALID");
|
|
157
198
|
if (!protectedHeader.crit) return;
|
|
158
199
|
const understoodParams = new Set([
|
|
159
|
-
...
|
|
200
|
+
...recognizedHeaders || [],
|
|
160
201
|
...BASE_HEADER_PARAMS,
|
|
161
202
|
"enc",
|
|
162
203
|
"iv",
|
|
@@ -168,40 +209,38 @@ function validateCriticalHeadersJWE(protectedHeader, understoodFromOptions) {
|
|
|
168
209
|
"apv"
|
|
169
210
|
]);
|
|
170
211
|
for (const critParam of protectedHeader.crit) {
|
|
171
|
-
if (!Object.prototype.hasOwnProperty.call(protectedHeader, critParam)) throw new
|
|
172
|
-
if (!understoodParams.has(critParam)) throw new
|
|
212
|
+
if (!Object.prototype.hasOwnProperty.call(protectedHeader, critParam)) throw new JWTError(`Critical header parameter "${critParam}" listed in "crit" but not present in the protected header.`, "ERR_JWE_INVALID");
|
|
213
|
+
if (!understoodParams.has(critParam)) throw new JWTError(`Unprocessed critical header parameters: ${critParam}`, "ERR_JWE_INVALID");
|
|
173
214
|
}
|
|
174
215
|
}
|
|
175
216
|
//#endregion
|
|
176
217
|
//#region src/core/utils/sanitize.ts
|
|
177
218
|
/**
|
|
178
|
-
*
|
|
219
|
+
* Originally derived from unsecure@0.0.4
|
|
179
220
|
* Licensed under the MIT License.
|
|
180
221
|
*
|
|
181
222
|
* Source: https://github.com/sandros94/unsecure/blob/913ed5580c551153a19154b4916c67a8efb2be85/src/sanitize.ts
|
|
182
223
|
*/
|
|
183
224
|
/**
|
|
184
|
-
*
|
|
185
|
-
*
|
|
186
|
-
*
|
|
225
|
+
* Returns a deep structural copy of `obj` with prototype-pollution vectors
|
|
226
|
+
* stripped at every level (`__proto__`, `prototype`, `constructor`).
|
|
227
|
+
*
|
|
228
|
+
* The input is never modified. Arrays are copied element-by-element;
|
|
229
|
+
* nested objects are recursed with a WeakSet cycle guard.
|
|
187
230
|
*/
|
|
188
231
|
function sanitizeObject(obj) {
|
|
189
232
|
if (!obj || typeof obj !== "object") return obj;
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
if (
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
seen ??= /* @__PURE__ */ new WeakSet();
|
|
200
|
-
if (!seen.has(v)) {
|
|
201
|
-
seen.add(v);
|
|
202
|
-
sanitizeKeys(v, seen);
|
|
203
|
-
}
|
|
233
|
+
return _sanitizeCopy(obj, /* @__PURE__ */ new WeakSet());
|
|
234
|
+
}
|
|
235
|
+
function _sanitizeCopy(current, seen) {
|
|
236
|
+
seen.add(current);
|
|
237
|
+
if (Array.isArray(current)) return current.map((item) => item && typeof item === "object" && !seen.has(item) ? _sanitizeCopy(item, seen) : item);
|
|
238
|
+
const result = {};
|
|
239
|
+
for (const [key, value] of Object.entries(current)) {
|
|
240
|
+
if (key === "__proto__" || key === "prototype" || key === "constructor") continue;
|
|
241
|
+
result[key] = value && typeof value === "object" && !seen.has(value) ? _sanitizeCopy(value, seen) : value;
|
|
204
242
|
}
|
|
243
|
+
return result;
|
|
205
244
|
}
|
|
206
245
|
//#endregion
|
|
207
246
|
//#region src/core/utils/index.ts
|
|
@@ -337,157 +376,61 @@ function isPublicJWK(key) {
|
|
|
337
376
|
return false;
|
|
338
377
|
}
|
|
339
378
|
//#endregion
|
|
340
|
-
//#region src/core/
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
*/
|
|
346
|
-
function unusable(name, prop = "algorithm.name") {
|
|
347
|
-
return /* @__PURE__ */ new TypeError(`CryptoKey does not support this operation, its ${prop} must be ${name}`);
|
|
348
|
-
}
|
|
349
|
-
function isAlgorithm(algorithm, name) {
|
|
350
|
-
return algorithm.name === name;
|
|
351
|
-
}
|
|
352
|
-
function getHashLength(hash) {
|
|
353
|
-
return Number.parseInt(hash.name.slice(4), 10);
|
|
379
|
+
//#region src/core/_crypto/_sign-verify.ts
|
|
380
|
+
async function sign$1(alg, key, data) {
|
|
381
|
+
checkSigCryptoKey(key, alg, "sign");
|
|
382
|
+
const signature = await crypto.subtle.sign(subtleAlgorithm(alg, key.algorithm), key, data);
|
|
383
|
+
return new Uint8Array(signature);
|
|
354
384
|
}
|
|
355
|
-
function
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
385
|
+
async function verify$1(alg, key, signature, data) {
|
|
386
|
+
checkSigCryptoKey(key, alg, "verify");
|
|
387
|
+
try {
|
|
388
|
+
return await crypto.subtle.verify(subtleAlgorithm(alg, key.algorithm), key, signature, data);
|
|
389
|
+
} catch {
|
|
390
|
+
return false;
|
|
361
391
|
}
|
|
362
392
|
}
|
|
363
|
-
function checkUsage(key, usage) {
|
|
364
|
-
if (usage && !key.usages.includes(usage)) throw new TypeError(`CryptoKey does not support this operation, its usages must include ${usage}.`);
|
|
365
|
-
}
|
|
366
393
|
function checkSigCryptoKey(key, alg, usage) {
|
|
367
394
|
switch (alg) {
|
|
368
395
|
case "HS256":
|
|
369
396
|
case "HS384":
|
|
370
397
|
case "HS512": {
|
|
371
|
-
if (!isAlgorithm(key.algorithm, "HMAC")) throw unusable("HMAC");
|
|
398
|
+
if (!isAlgorithm$3(key.algorithm, "HMAC")) throw unusable$4("HMAC");
|
|
372
399
|
const expected = Number.parseInt(alg.slice(2), 10);
|
|
373
|
-
if (getHashLength(key.algorithm.hash) !== expected) throw unusable(`SHA-${expected}`, "algorithm.hash");
|
|
400
|
+
if (getHashLength$1(key.algorithm.hash) !== expected) throw unusable$4(`SHA-${expected}`, "algorithm.hash");
|
|
374
401
|
break;
|
|
375
402
|
}
|
|
376
403
|
case "RS256":
|
|
377
404
|
case "RS384":
|
|
378
405
|
case "RS512": {
|
|
379
|
-
if (!isAlgorithm(key.algorithm, "RSASSA-PKCS1-v1_5")) throw unusable("RSASSA-PKCS1-v1_5");
|
|
406
|
+
if (!isAlgorithm$3(key.algorithm, "RSASSA-PKCS1-v1_5")) throw unusable$4("RSASSA-PKCS1-v1_5");
|
|
380
407
|
const expected = Number.parseInt(alg.slice(2), 10);
|
|
381
|
-
if (getHashLength(key.algorithm.hash) !== expected) throw unusable(`SHA-${expected}`, "algorithm.hash");
|
|
408
|
+
if (getHashLength$1(key.algorithm.hash) !== expected) throw unusable$4(`SHA-${expected}`, "algorithm.hash");
|
|
382
409
|
break;
|
|
383
410
|
}
|
|
384
411
|
case "PS256":
|
|
385
412
|
case "PS384":
|
|
386
413
|
case "PS512": {
|
|
387
|
-
if (!isAlgorithm(key.algorithm, "RSA-PSS")) throw unusable("RSA-PSS");
|
|
414
|
+
if (!isAlgorithm$3(key.algorithm, "RSA-PSS")) throw unusable$4("RSA-PSS");
|
|
388
415
|
const expected = Number.parseInt(alg.slice(2), 10);
|
|
389
|
-
if (getHashLength(key.algorithm.hash) !== expected) throw unusable(`SHA-${expected}`, "algorithm.hash");
|
|
416
|
+
if (getHashLength$1(key.algorithm.hash) !== expected) throw unusable$4(`SHA-${expected}`, "algorithm.hash");
|
|
390
417
|
break;
|
|
391
418
|
}
|
|
392
419
|
case "Ed25519":
|
|
393
420
|
case "EdDSA":
|
|
394
|
-
if (!isAlgorithm(key.algorithm, "Ed25519")) throw unusable("Ed25519");
|
|
421
|
+
if (!isAlgorithm$3(key.algorithm, "Ed25519")) throw unusable$4("Ed25519");
|
|
395
422
|
break;
|
|
396
423
|
case "ES256":
|
|
397
424
|
case "ES384":
|
|
398
425
|
case "ES512": {
|
|
399
|
-
if (!isAlgorithm(key.algorithm, "ECDSA")) throw unusable("ECDSA");
|
|
426
|
+
if (!isAlgorithm$3(key.algorithm, "ECDSA")) throw unusable$4("ECDSA");
|
|
400
427
|
const expected = getNamedCurve$1(alg);
|
|
401
|
-
if (key.algorithm.namedCurve !== expected) throw unusable(expected, "algorithm.namedCurve");
|
|
402
|
-
break;
|
|
403
|
-
}
|
|
404
|
-
default: throw new TypeError("CryptoKey does not support this operation");
|
|
405
|
-
}
|
|
406
|
-
checkUsage(key, usage);
|
|
407
|
-
}
|
|
408
|
-
function checkEncCryptoKey(key, alg, usage) {
|
|
409
|
-
switch (alg) {
|
|
410
|
-
case "A128GCM":
|
|
411
|
-
case "A192GCM":
|
|
412
|
-
case "A256GCM": {
|
|
413
|
-
if (!isAlgorithm(key.algorithm, "AES-GCM")) throw unusable("AES-GCM");
|
|
414
|
-
const expected = Number.parseInt(alg.slice(1, 4), 10);
|
|
415
|
-
if (key.algorithm.length !== expected) throw unusable(expected, "algorithm.length");
|
|
416
|
-
break;
|
|
417
|
-
}
|
|
418
|
-
case "A128KW":
|
|
419
|
-
case "A192KW":
|
|
420
|
-
case "A256KW": {
|
|
421
|
-
if (!isAlgorithm(key.algorithm, "AES-KW")) throw unusable("AES-KW");
|
|
422
|
-
const expected = Number.parseInt(alg.slice(1, 4), 10);
|
|
423
|
-
if (key.algorithm.length !== expected) throw unusable(expected, "algorithm.length");
|
|
424
|
-
break;
|
|
425
|
-
}
|
|
426
|
-
case "ECDH":
|
|
427
|
-
switch (key.algorithm.name) {
|
|
428
|
-
case "ECDH":
|
|
429
|
-
case "X25519": break;
|
|
430
|
-
default: throw unusable("ECDH or X25519");
|
|
431
|
-
}
|
|
432
|
-
break;
|
|
433
|
-
case "PBES2-HS256+A128KW":
|
|
434
|
-
case "PBES2-HS384+A192KW":
|
|
435
|
-
case "PBES2-HS512+A256KW":
|
|
436
|
-
if (!isAlgorithm(key.algorithm, "PBKDF2")) throw unusable("PBKDF2");
|
|
437
|
-
break;
|
|
438
|
-
case "RSA-OAEP":
|
|
439
|
-
case "RSA-OAEP-256":
|
|
440
|
-
case "RSA-OAEP-384":
|
|
441
|
-
case "RSA-OAEP-512": {
|
|
442
|
-
if (!isAlgorithm(key.algorithm, "RSA-OAEP")) throw unusable("RSA-OAEP");
|
|
443
|
-
const expected = Number.parseInt(alg.slice(9), 10) || 1;
|
|
444
|
-
if (getHashLength(key.algorithm.hash) !== expected) throw unusable(`SHA-${expected}`, "algorithm.hash");
|
|
428
|
+
if (key.algorithm.namedCurve !== expected) throw unusable$4(expected, "algorithm.namedCurve");
|
|
445
429
|
break;
|
|
446
430
|
}
|
|
447
431
|
default: throw new TypeError("CryptoKey does not support this operation");
|
|
448
432
|
}
|
|
449
|
-
checkUsage(key, usage);
|
|
450
|
-
}
|
|
451
|
-
//#endregion
|
|
452
|
-
//#region src/core/jose/sign-verify.ts
|
|
453
|
-
/**
|
|
454
|
-
* This is a fork of Jose library's utility functions.
|
|
455
|
-
* @source https://github.com/panva/jose/tree/69b7960c67e05be55fa2ec31c74b987696c20c60/src/lib/sign.ts
|
|
456
|
-
* @source https://github.com/panva/jose/tree/69b7960c67e05be55fa2ec31c74b987696c20c60/src/lib/verify.ts
|
|
457
|
-
* @license MIT https://github.com/panva/jose/blob/69b7960c67e05be55fa2ec31c74b987696c20c60/LICENSE.md
|
|
458
|
-
*/
|
|
459
|
-
async function sign$1(alg, key, data) {
|
|
460
|
-
const cryptoKey = await getSignVerifyKey(alg, key, "sign");
|
|
461
|
-
checkKeyLength(alg, cryptoKey);
|
|
462
|
-
const signature = await crypto.subtle.sign(subtleAlgorithm(alg, cryptoKey.algorithm), cryptoKey, data);
|
|
463
|
-
return new Uint8Array(signature);
|
|
464
|
-
}
|
|
465
|
-
async function verify$1(alg, key, signature, data) {
|
|
466
|
-
const cryptoKey = await getSignVerifyKey(alg, key, "verify");
|
|
467
|
-
checkKeyLength(alg, cryptoKey);
|
|
468
|
-
const algorithm = subtleAlgorithm(alg, cryptoKey.algorithm);
|
|
469
|
-
try {
|
|
470
|
-
return await crypto.subtle.verify(algorithm, cryptoKey, signature, data);
|
|
471
|
-
} catch {
|
|
472
|
-
return false;
|
|
473
|
-
}
|
|
474
|
-
}
|
|
475
|
-
function checkKeyLength(alg, key) {
|
|
476
|
-
if (alg.startsWith("RS") || alg.startsWith("PS")) {
|
|
477
|
-
const { modulusLength } = key.algorithm;
|
|
478
|
-
if (typeof modulusLength !== "number" || modulusLength < 2048) throw new TypeError(`${alg} requires key modulusLength to be 2048 bits or larger`);
|
|
479
|
-
}
|
|
480
|
-
}
|
|
481
|
-
async function getSignVerifyKey(alg, key, usage) {
|
|
482
|
-
if (key instanceof Uint8Array) {
|
|
483
|
-
if (!alg.startsWith("HS")) throw new TypeError("Key must be of type CryptoKey or JSON Web Key for non-HMAC algorithms");
|
|
484
|
-
return crypto.subtle.importKey("raw", key, {
|
|
485
|
-
hash: `SHA-${alg.slice(-3)}`,
|
|
486
|
-
name: "HMAC"
|
|
487
|
-
}, false, [usage]);
|
|
488
|
-
}
|
|
489
|
-
checkSigCryptoKey(key, alg, usage);
|
|
490
|
-
return key;
|
|
433
|
+
checkUsage$4(key, usage);
|
|
491
434
|
}
|
|
492
435
|
function subtleAlgorithm(alg, algorithm) {
|
|
493
436
|
const hash = `SHA-${alg.slice(-3)}`;
|
|
@@ -523,44 +466,28 @@ function subtleAlgorithm(alg, algorithm) {
|
|
|
523
466
|
default: throw new Error(`alg ${alg} is not supported either by JOSE or your javascript runtime`);
|
|
524
467
|
}
|
|
525
468
|
}
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
/**
|
|
529
|
-
* This is a fork of Jose library's utility functions.
|
|
530
|
-
* @source https://github.com/panva/jose/tree/69b7960c67e05be55fa2ec31c74b987696c20c60/src/lib/buffer_utils.ts
|
|
531
|
-
* @license MIT https://github.com/panva/jose/blob/69b7960c67e05be55fa2ec31c74b987696c20c60/LICENSE.md
|
|
532
|
-
*/
|
|
533
|
-
const MAX_INT32 = 2 ** 32;
|
|
534
|
-
function writeUInt32BE(buf, value, offset) {
|
|
535
|
-
if (value < 0 || value >= MAX_INT32) throw new RangeError(`value must be >= 0 and <= ${MAX_INT32 - 1}. Received ${value}`);
|
|
536
|
-
buf.set([
|
|
537
|
-
value >>> 24,
|
|
538
|
-
value >>> 16,
|
|
539
|
-
value >>> 8,
|
|
540
|
-
value & 255
|
|
541
|
-
], offset);
|
|
469
|
+
function unusable$4(name, prop = "algorithm.name") {
|
|
470
|
+
return /* @__PURE__ */ new TypeError(`CryptoKey does not support this operation, its ${prop} must be ${name}`);
|
|
542
471
|
}
|
|
543
|
-
function
|
|
544
|
-
|
|
545
|
-
const low = value % MAX_INT32;
|
|
546
|
-
const buf = new Uint8Array(8);
|
|
547
|
-
writeUInt32BE(buf, high, 0);
|
|
548
|
-
writeUInt32BE(buf, low, 4);
|
|
549
|
-
return buf;
|
|
472
|
+
function isAlgorithm$3(algorithm, name) {
|
|
473
|
+
return algorithm.name === name;
|
|
550
474
|
}
|
|
551
|
-
function
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
475
|
+
function getHashLength$1(hash) {
|
|
476
|
+
return Number.parseInt(hash.name.slice(4), 10);
|
|
477
|
+
}
|
|
478
|
+
function getNamedCurve$1(alg) {
|
|
479
|
+
switch (alg) {
|
|
480
|
+
case "ES256": return "P-256";
|
|
481
|
+
case "ES384": return "P-384";
|
|
482
|
+
case "ES512": return "P-521";
|
|
483
|
+
default: throw new Error("unreachable");
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
function checkUsage$4(key, usage) {
|
|
487
|
+
if (usage && !key.usages.includes(usage)) throw new TypeError(`CryptoKey does not support this operation, its usages must include ${usage}.`);
|
|
555
488
|
}
|
|
556
489
|
//#endregion
|
|
557
|
-
//#region src/core/
|
|
558
|
-
/**
|
|
559
|
-
* This is a fork of Jose library's utility functions.
|
|
560
|
-
* @source https://github.com/panva/jose/tree/69b7960c67e05be55fa2ec31c74b987696c20c60/src/lib/cek.ts
|
|
561
|
-
* @source https://github.com/panva/jose/tree/69b7960c67e05be55fa2ec31c74b987696c20c60/src/lib/iv.ts
|
|
562
|
-
* @license MIT https://github.com/panva/jose/blob/69b7960c67e05be55fa2ec31c74b987696c20c60/LICENSE.md
|
|
563
|
-
*/
|
|
490
|
+
//#region src/core/_crypto/_aes.ts
|
|
564
491
|
const IV_BIT_LENGTHS = {
|
|
565
492
|
A128GCM: 96,
|
|
566
493
|
A128GCMKW: 96,
|
|
@@ -603,14 +530,112 @@ function checkCEKLength(cek, expected) {
|
|
|
603
530
|
const actual = cek.byteLength << 3;
|
|
604
531
|
if (actual !== expected) throw new Error(`Invalid Content Encryption Key length. Expected ${expected} bits, got ${actual} bits`);
|
|
605
532
|
}
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
533
|
+
function checkAESCryptoKey(key, alg, usage) {
|
|
534
|
+
switch (alg) {
|
|
535
|
+
case "A128GCM":
|
|
536
|
+
case "A192GCM":
|
|
537
|
+
case "A256GCM": {
|
|
538
|
+
if (!isAlgorithm$2(key.algorithm, "AES-GCM")) throw unusable$3("AES-GCM");
|
|
539
|
+
const expected = Number.parseInt(alg.slice(1, 4), 10);
|
|
540
|
+
if (key.algorithm.length !== expected) throw unusable$3(expected, "algorithm.length");
|
|
541
|
+
break;
|
|
542
|
+
}
|
|
543
|
+
case "A128KW":
|
|
544
|
+
case "A192KW":
|
|
545
|
+
case "A256KW": {
|
|
546
|
+
if (!isAlgorithm$2(key.algorithm, "AES-KW")) throw unusable$3("AES-KW");
|
|
547
|
+
const expected = Number.parseInt(alg.slice(1, 4), 10);
|
|
548
|
+
if (key.algorithm.length !== expected) throw unusable$3(expected, "algorithm.length");
|
|
549
|
+
break;
|
|
550
|
+
}
|
|
551
|
+
default: throw new TypeError("CryptoKey does not support this operation");
|
|
552
|
+
}
|
|
553
|
+
checkUsage$3(key, usage);
|
|
554
|
+
}
|
|
555
|
+
async function aesKwWrap(alg, key, cek) {
|
|
556
|
+
const cryptoKey = await getAESKWCryptoKey(key, alg, { usage: "wrapKey" });
|
|
557
|
+
checkAESKWKeySize(cryptoKey, alg);
|
|
558
|
+
const cryptoKeyCek = await crypto.subtle.importKey("raw", cek, {
|
|
559
|
+
hash: "SHA-256",
|
|
560
|
+
name: "HMAC"
|
|
561
|
+
}, true, ["sign"]);
|
|
562
|
+
return new Uint8Array(await crypto.subtle.wrapKey("raw", cryptoKeyCek, cryptoKey, "AES-KW"));
|
|
563
|
+
}
|
|
564
|
+
async function aesKwUnwrap(alg, key, encryptedKey) {
|
|
565
|
+
const cryptoKey = await getAESKWCryptoKey(key, alg, { usage: "unwrapKey" });
|
|
566
|
+
checkAESKWKeySize(cryptoKey, alg);
|
|
567
|
+
const cryptoKeyCek = await crypto.subtle.unwrapKey("raw", encryptedKey, cryptoKey, "AES-KW", {
|
|
568
|
+
hash: "SHA-256",
|
|
569
|
+
name: "HMAC"
|
|
570
|
+
}, true, ["sign"]);
|
|
571
|
+
return new Uint8Array(await crypto.subtle.exportKey("raw", cryptoKeyCek));
|
|
572
|
+
}
|
|
573
|
+
async function gcmkwEncrypt(alg, key, plaintext, iv) {
|
|
574
|
+
const wrapped = await encrypt$1(alg.slice(0, 7), plaintext, key, iv, new Uint8Array(0));
|
|
575
|
+
return {
|
|
576
|
+
encryptedKey: wrapped.ciphertext,
|
|
577
|
+
iv: base64UrlEncode(wrapped.iv),
|
|
578
|
+
tag: base64UrlEncode(wrapped.tag)
|
|
579
|
+
};
|
|
580
|
+
}
|
|
581
|
+
async function gcmkwDecrypt(alg, key, ciphertext, iv, tag) {
|
|
582
|
+
return decrypt$1(alg.slice(0, 7), key, ciphertext, iv, tag, new Uint8Array(0));
|
|
583
|
+
}
|
|
584
|
+
async function encrypt$1(enc, plaintext, cek, iv, aad) {
|
|
585
|
+
if (!isCryptoKey(cek) && !(cek instanceof Uint8Array)) throw new TypeError("Key must be one of type: CryptoKey or Uint8Array");
|
|
586
|
+
if (iv) checkIvLength(enc, iv);
|
|
587
|
+
else iv = generateIV(enc);
|
|
588
|
+
switch (enc) {
|
|
589
|
+
case "A128CBC-HS256":
|
|
590
|
+
case "A192CBC-HS384":
|
|
591
|
+
case "A256CBC-HS512":
|
|
592
|
+
if (cek instanceof Uint8Array) checkCEKLength(cek, Number.parseInt(enc.slice(-3), 10));
|
|
593
|
+
return cbcEncrypt(enc, plaintext, cek, iv, aad);
|
|
594
|
+
case "A128GCM":
|
|
595
|
+
case "A192GCM":
|
|
596
|
+
case "A256GCM":
|
|
597
|
+
if (cek instanceof Uint8Array) checkCEKLength(cek, Number.parseInt(enc.slice(1, 4), 10));
|
|
598
|
+
return gcmEncrypt(enc, plaintext, cek, iv, aad);
|
|
599
|
+
default: throw new Error("Unsupported JWE Content Encryption Algorithm");
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
async function decrypt$1(enc, cek, ciphertext, iv, tag, aad) {
|
|
603
|
+
if (!isCryptoKey(cek) && !(cek instanceof Uint8Array)) throw new TypeError("Key must be one of type: CryptoKey or Uint8Array");
|
|
604
|
+
if (!iv) throw new Error("JWE Initialization Vector missing");
|
|
605
|
+
if (!tag) throw new Error("JWE Authentication Tag missing");
|
|
606
|
+
checkIvLength(enc, iv);
|
|
607
|
+
switch (enc) {
|
|
608
|
+
case "A128CBC-HS256":
|
|
609
|
+
case "A192CBC-HS384":
|
|
610
|
+
case "A256CBC-HS512":
|
|
611
|
+
if (cek instanceof Uint8Array) checkCEKLength(cek, Number.parseInt(enc.slice(-3), 10));
|
|
612
|
+
return cbcDecrypt(enc, cek, ciphertext, iv, tag, aad);
|
|
613
|
+
case "A128GCM":
|
|
614
|
+
case "A192GCM":
|
|
615
|
+
case "A256GCM":
|
|
616
|
+
if (cek instanceof Uint8Array) checkCEKLength(cek, Number.parseInt(enc.slice(1, 4), 10));
|
|
617
|
+
return gcmDecrypt(enc, cek, ciphertext, iv, tag, aad);
|
|
618
|
+
default: throw new Error("Unsupported JWE Content Encryption Algorithm");
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
function unusable$3(name, prop = "algorithm.name") {
|
|
622
|
+
return /* @__PURE__ */ new TypeError(`CryptoKey does not support this operation, its ${prop} must be ${name}`);
|
|
623
|
+
}
|
|
624
|
+
function isAlgorithm$2(algorithm, name) {
|
|
625
|
+
return algorithm.name === name;
|
|
626
|
+
}
|
|
627
|
+
function checkUsage$3(key, usage) {
|
|
628
|
+
if (usage && !key.usages.includes(usage)) throw new TypeError(`CryptoKey does not support this operation, its usages must include ${usage}.`);
|
|
629
|
+
}
|
|
630
|
+
async function getAESKWCryptoKey(key, alg, options = {}) {
|
|
631
|
+
const { extractable = true, usage } = options;
|
|
632
|
+
if (key instanceof Uint8Array) return crypto.subtle.importKey("raw", key, "AES-KW", extractable, [usage ?? "wrapKey"]);
|
|
633
|
+
checkAESCryptoKey(key, alg, usage);
|
|
634
|
+
return key;
|
|
635
|
+
}
|
|
636
|
+
function checkAESKWKeySize(key, alg) {
|
|
637
|
+
if (key.algorithm.length !== Number.parseInt(alg.slice(1, 4), 10)) throw new TypeError(`Invalid key size for alg: ${alg}`);
|
|
638
|
+
}
|
|
614
639
|
async function importCBCKeys(enc, cek, usage) {
|
|
615
640
|
const keySize = Number.parseInt(enc.slice(1, 4), 10);
|
|
616
641
|
const [encKey, macKey] = await Promise.all([crypto.subtle.importKey("raw", cek.subarray(keySize >> 3), "AES-CBC", false, [usage]), crypto.subtle.importKey("raw", cek.subarray(0, keySize >> 3), {
|
|
@@ -644,7 +669,7 @@ async function gcmEncrypt(enc, plaintext, cek, iv, aad) {
|
|
|
644
669
|
let encKey;
|
|
645
670
|
if (cek instanceof Uint8Array) encKey = await crypto.subtle.importKey("raw", cek, "AES-GCM", false, ["encrypt"]);
|
|
646
671
|
else {
|
|
647
|
-
|
|
672
|
+
checkAESCryptoKey(cek, enc, "encrypt");
|
|
648
673
|
encKey = cek;
|
|
649
674
|
}
|
|
650
675
|
const encrypted = new Uint8Array(await crypto.subtle.encrypt({
|
|
@@ -660,50 +685,6 @@ async function gcmEncrypt(enc, plaintext, cek, iv, aad) {
|
|
|
660
685
|
iv
|
|
661
686
|
};
|
|
662
687
|
}
|
|
663
|
-
async function encrypt$1(enc, plaintext, cek, iv, aad) {
|
|
664
|
-
if (!isCryptoKey(cek) && !(cek instanceof Uint8Array)) throw new TypeError("Key must be one of type: CryptoKey or Uint8Array");
|
|
665
|
-
if (iv) checkIvLength(enc, iv);
|
|
666
|
-
else iv = generateIV(enc);
|
|
667
|
-
switch (enc) {
|
|
668
|
-
case "A128CBC-HS256":
|
|
669
|
-
case "A192CBC-HS384":
|
|
670
|
-
case "A256CBC-HS512":
|
|
671
|
-
if (cek instanceof Uint8Array) checkCEKLength(cek, Number.parseInt(enc.slice(-3), 10));
|
|
672
|
-
return cbcEncrypt(enc, plaintext, cek, iv, aad);
|
|
673
|
-
case "A128GCM":
|
|
674
|
-
case "A192GCM":
|
|
675
|
-
case "A256GCM":
|
|
676
|
-
if (cek instanceof Uint8Array) checkCEKLength(cek, Number.parseInt(enc.slice(1, 4), 10));
|
|
677
|
-
return gcmEncrypt(enc, plaintext, cek, iv, aad);
|
|
678
|
-
default: throw new Error("Unsupported JWE Content Encryption Algorithm");
|
|
679
|
-
}
|
|
680
|
-
}
|
|
681
|
-
/**
|
|
682
|
-
* Decrypt fork
|
|
683
|
-
*/
|
|
684
|
-
let _timingSafeKey;
|
|
685
|
-
function getTimingSafeKey() {
|
|
686
|
-
if (!_timingSafeKey) _timingSafeKey = crypto.subtle.generateKey({
|
|
687
|
-
name: "HMAC",
|
|
688
|
-
hash: "SHA-256"
|
|
689
|
-
}, false, ["sign"]);
|
|
690
|
-
return _timingSafeKey;
|
|
691
|
-
}
|
|
692
|
-
async function timingSafeEqual(a, b) {
|
|
693
|
-
if (!(a instanceof Uint8Array)) throw new TypeError("First argument must be a buffer");
|
|
694
|
-
if (!(b instanceof Uint8Array)) throw new TypeError("Second argument must be a buffer");
|
|
695
|
-
const algorithm = {
|
|
696
|
-
name: "HMAC",
|
|
697
|
-
hash: "SHA-256"
|
|
698
|
-
};
|
|
699
|
-
const key = await getTimingSafeKey();
|
|
700
|
-
const aHmac = new Uint8Array(await crypto.subtle.sign(algorithm, key, a));
|
|
701
|
-
const bHmac = new Uint8Array(await crypto.subtle.sign(algorithm, key, b));
|
|
702
|
-
let out = 0;
|
|
703
|
-
let i = -1;
|
|
704
|
-
while (++i < 32) out |= aHmac[i] ^ bHmac[i];
|
|
705
|
-
return out === 0;
|
|
706
|
-
}
|
|
707
688
|
async function cbcDecrypt(enc, cek, ciphertext, iv, tag, aad) {
|
|
708
689
|
if (!(cek instanceof Uint8Array)) throw new TypeError("CBC key must be of type Uint8Array");
|
|
709
690
|
const { encKey, macKey, keySize } = await importCBCKeys(enc, cek, "decrypt");
|
|
@@ -728,7 +709,7 @@ async function gcmDecrypt(enc, cek, ciphertext, iv, tag, aad) {
|
|
|
728
709
|
let encKey;
|
|
729
710
|
if (cek instanceof Uint8Array) encKey = await crypto.subtle.importKey("raw", cek, "AES-GCM", false, ["decrypt"]);
|
|
730
711
|
else {
|
|
731
|
-
|
|
712
|
+
checkAESCryptoKey(cek, enc, "decrypt");
|
|
732
713
|
encKey = cek;
|
|
733
714
|
}
|
|
734
715
|
try {
|
|
@@ -742,27 +723,62 @@ async function gcmDecrypt(enc, cek, ciphertext, iv, tag, aad) {
|
|
|
742
723
|
throw new Error("JWE Decryption Failed");
|
|
743
724
|
}
|
|
744
725
|
}
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
if (!
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
726
|
+
let _timingSafeKey;
|
|
727
|
+
function getTimingSafeKey() {
|
|
728
|
+
if (!_timingSafeKey) _timingSafeKey = crypto.subtle.generateKey({
|
|
729
|
+
name: "HMAC",
|
|
730
|
+
hash: "SHA-256"
|
|
731
|
+
}, false, ["sign"]);
|
|
732
|
+
return _timingSafeKey;
|
|
733
|
+
}
|
|
734
|
+
async function timingSafeEqual(a, b) {
|
|
735
|
+
if (!(a instanceof Uint8Array)) throw new TypeError("First argument must be a buffer");
|
|
736
|
+
if (!(b instanceof Uint8Array)) throw new TypeError("Second argument must be a buffer");
|
|
737
|
+
const algorithm = {
|
|
738
|
+
name: "HMAC",
|
|
739
|
+
hash: "SHA-256"
|
|
740
|
+
};
|
|
741
|
+
const key = await getTimingSafeKey();
|
|
742
|
+
const aHmac = new Uint8Array(await crypto.subtle.sign(algorithm, key, a));
|
|
743
|
+
const bHmac = new Uint8Array(await crypto.subtle.sign(algorithm, key, b));
|
|
744
|
+
let out = 0;
|
|
745
|
+
let i = -1;
|
|
746
|
+
while (++i < 32) out |= aHmac[i] ^ bHmac[i];
|
|
747
|
+
return out === 0;
|
|
748
|
+
}
|
|
749
|
+
function writeUInt32BE$1(buf, value, offset) {
|
|
750
|
+
if (value < 0 || value >= 2 ** 32) throw new RangeError(`value must be >= 0 and <= ${2 ** 32 - 1}. Received ${value}`);
|
|
751
|
+
buf.set([
|
|
752
|
+
value >>> 24,
|
|
753
|
+
value >>> 16,
|
|
754
|
+
value >>> 8,
|
|
755
|
+
value & 255
|
|
756
|
+
], offset);
|
|
757
|
+
}
|
|
758
|
+
function uint64be(value) {
|
|
759
|
+
const high = Math.floor(value / 2 ** 32);
|
|
760
|
+
const low = value % 2 ** 32;
|
|
761
|
+
const buf = new Uint8Array(8);
|
|
762
|
+
writeUInt32BE$1(buf, high, 0);
|
|
763
|
+
writeUInt32BE$1(buf, low, 4);
|
|
764
|
+
return buf;
|
|
763
765
|
}
|
|
764
766
|
//#endregion
|
|
765
|
-
//#region src/core/
|
|
767
|
+
//#region src/core/_crypto/_key-codec.ts
|
|
768
|
+
async function jwkTokey(jwk) {
|
|
769
|
+
if (!jwk.alg) throw new TypeError("\"alg\" argument is required when \"jwk.alg\" is not present");
|
|
770
|
+
const { algorithm, keyUsages } = subtleMapping(jwk);
|
|
771
|
+
return crypto.subtle.importKey("jwk", jwk, algorithm, jwk.ext ?? (jwk.d ? false : true), jwk.key_ops ?? keyUsages);
|
|
772
|
+
}
|
|
773
|
+
async function keyToJWK(key) {
|
|
774
|
+
if (key instanceof Uint8Array) return {
|
|
775
|
+
kty: "oct",
|
|
776
|
+
k: base64UrlEncode(key)
|
|
777
|
+
};
|
|
778
|
+
if (!isCryptoKey(key)) throw new TypeError("Key must be one of type: CryptoKey or Uint8Array");
|
|
779
|
+
if (!key.extractable) throw new TypeError("non-extractable CryptoKey cannot be exported as a JWK");
|
|
780
|
+
return await crypto.subtle.exportKey("jwk", key);
|
|
781
|
+
}
|
|
766
782
|
function subtleMapping(jwk) {
|
|
767
783
|
let algorithm;
|
|
768
784
|
let keyUsages;
|
|
@@ -860,72 +876,47 @@ function subtleMapping(jwk) {
|
|
|
860
876
|
keyUsages
|
|
861
877
|
};
|
|
862
878
|
}
|
|
863
|
-
async function jwkTokey(jwk) {
|
|
864
|
-
if (!jwk.alg) throw new TypeError("\"alg\" argument is required when \"jwk.alg\" is not present");
|
|
865
|
-
const { algorithm, keyUsages } = subtleMapping(jwk);
|
|
866
|
-
return crypto.subtle.importKey("jwk", jwk, algorithm, jwk.ext ?? (jwk.d ? false : true), jwk.key_ops ?? keyUsages);
|
|
867
|
-
}
|
|
868
|
-
//#endregion
|
|
869
|
-
//#region src/core/jose/key_to_jwk.ts
|
|
870
|
-
async function keyToJWK(key) {
|
|
871
|
-
if (key instanceof Uint8Array) return {
|
|
872
|
-
kty: "oct",
|
|
873
|
-
k: base64UrlEncode(key)
|
|
874
|
-
};
|
|
875
|
-
if (!isCryptoKey(key)) throw new TypeError("Key must be one of type: CryptoKey or Uint8Array");
|
|
876
|
-
if (!key.extractable) throw new TypeError("non-extractable CryptoKey cannot be exported as a JWK");
|
|
877
|
-
return await crypto.subtle.exportKey("jwk", key);
|
|
878
|
-
}
|
|
879
879
|
//#endregion
|
|
880
|
-
//#region src/core/
|
|
880
|
+
//#region src/core/_crypto/_rsa.ts
|
|
881
881
|
async function encryptRSAES(alg, key, cek) {
|
|
882
|
-
|
|
883
|
-
|
|
882
|
+
checkRSACryptoKey(key, alg, "encrypt");
|
|
883
|
+
checkRSAModulusLength(alg, key);
|
|
884
884
|
return new Uint8Array(await crypto.subtle.encrypt("RSA-OAEP", key, cek));
|
|
885
885
|
}
|
|
886
886
|
async function decryptRSAES(alg, key, encryptedKey) {
|
|
887
|
-
|
|
888
|
-
|
|
887
|
+
checkRSACryptoKey(key, alg, "decrypt");
|
|
888
|
+
checkRSAModulusLength(alg, key);
|
|
889
889
|
return new Uint8Array(await crypto.subtle.decrypt("RSA-OAEP", key, encryptedKey));
|
|
890
890
|
}
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
* @license MIT https://github.com/panva/jose/blob/69b7960c67e05be55fa2ec31c74b987696c20c60/LICENSE.md
|
|
897
|
-
*/
|
|
898
|
-
async function encryptIV(alg, key, plaintext, iv) {
|
|
899
|
-
const wrapped = await encrypt$1(alg.slice(0, 7), plaintext, key, iv, new Uint8Array(0));
|
|
900
|
-
return {
|
|
901
|
-
encryptedKey: wrapped.ciphertext,
|
|
902
|
-
iv: base64UrlEncode(wrapped.iv),
|
|
903
|
-
tag: base64UrlEncode(wrapped.tag)
|
|
904
|
-
};
|
|
891
|
+
function checkRSACryptoKey(key, alg, usage) {
|
|
892
|
+
if (!isAlgorithm$1(key.algorithm, "RSA-OAEP")) throw unusable$2("RSA-OAEP");
|
|
893
|
+
const expected = Number.parseInt(alg.slice(9), 10) || 1;
|
|
894
|
+
if (getHashLength(key.algorithm.hash) !== expected) throw unusable$2(`SHA-${expected}`, "algorithm.hash");
|
|
895
|
+
checkUsage$2(key, usage);
|
|
905
896
|
}
|
|
906
|
-
|
|
907
|
-
|
|
897
|
+
function checkRSAModulusLength(alg, key) {
|
|
898
|
+
if (alg.startsWith("RS") || alg.startsWith("PS") || alg.startsWith("RSA")) {
|
|
899
|
+
const { modulusLength } = key.algorithm;
|
|
900
|
+
if (typeof modulusLength !== "number" || modulusLength < 2048) throw new TypeError(`${alg} requires key modulusLength to be 2048 bits or larger`);
|
|
901
|
+
}
|
|
908
902
|
}
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
function lengthAndInput(input) {
|
|
912
|
-
return concatUint8Arrays(uint32be(input.length), input);
|
|
903
|
+
function unusable$2(name, prop = "algorithm.name") {
|
|
904
|
+
return /* @__PURE__ */ new TypeError(`CryptoKey does not support this operation, its ${prop} must be ${name}`);
|
|
913
905
|
}
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
res.set(await digest("sha256", buf), iter * 32);
|
|
923
|
-
}
|
|
924
|
-
return res.slice(0, bits >> 3);
|
|
906
|
+
function isAlgorithm$1(algorithm, name) {
|
|
907
|
+
return algorithm.name === name;
|
|
908
|
+
}
|
|
909
|
+
function getHashLength(hash) {
|
|
910
|
+
return Number.parseInt(hash.name.slice(4), 10);
|
|
911
|
+
}
|
|
912
|
+
function checkUsage$2(key, usage) {
|
|
913
|
+
if (usage && !key.usages.includes(usage)) throw new TypeError(`CryptoKey does not support this operation, its usages must include ${usage}.`);
|
|
925
914
|
}
|
|
915
|
+
//#endregion
|
|
916
|
+
//#region src/core/_crypto/_ecdh.ts
|
|
926
917
|
async function deriveECDHESKey(publicKey, privateKey, algorithm, keyLength, apu = new Uint8Array(0), apv = new Uint8Array(0)) {
|
|
927
|
-
|
|
928
|
-
|
|
918
|
+
checkECDHCryptoKey(publicKey, "ECDH");
|
|
919
|
+
checkECDHCryptoKey(privateKey, "ECDH", "deriveBits");
|
|
929
920
|
const value = concatUint8Arrays(lengthAndInput(textEncoder.encode(algorithm)), lengthAndInput(apu), lengthAndInput(apv), uint32be(keyLength));
|
|
930
921
|
const length = publicKey.algorithm.name === "X25519" ? 256 : Math.ceil(Number.parseInt(publicKey.algorithm.namedCurve.slice(-3), 10) / 8) << 3;
|
|
931
922
|
return concatKdf(new Uint8Array(await crypto.subtle.deriveBits({
|
|
@@ -933,7 +924,7 @@ async function deriveECDHESKey(publicKey, privateKey, algorithm, keyLength, apu
|
|
|
933
924
|
public: publicKey
|
|
934
925
|
}, privateKey, length)), keyLength, value);
|
|
935
926
|
}
|
|
936
|
-
function
|
|
927
|
+
function isECDHKeyAllowed(key) {
|
|
937
928
|
switch (key.algorithm.namedCurve) {
|
|
938
929
|
case "P-256":
|
|
939
930
|
case "P-384":
|
|
@@ -941,38 +932,65 @@ function allowed(key) {
|
|
|
941
932
|
default: return key.algorithm.name === "X25519";
|
|
942
933
|
}
|
|
943
934
|
}
|
|
935
|
+
function checkECDHCryptoKey(key, alg, usage) {
|
|
936
|
+
switch (key.algorithm.name) {
|
|
937
|
+
case "ECDH":
|
|
938
|
+
case "X25519": break;
|
|
939
|
+
default: throw unusable$1("ECDH or X25519");
|
|
940
|
+
}
|
|
941
|
+
checkUsage$1(key, usage);
|
|
942
|
+
}
|
|
943
|
+
function unusable$1(name, prop = "algorithm.name") {
|
|
944
|
+
return /* @__PURE__ */ new TypeError(`CryptoKey does not support this operation, its ${prop} must be ${name}`);
|
|
945
|
+
}
|
|
946
|
+
function checkUsage$1(key, usage) {
|
|
947
|
+
if (usage && !key.usages.includes(usage)) throw new TypeError(`CryptoKey does not support this operation, its usages must include ${usage}.`);
|
|
948
|
+
}
|
|
949
|
+
async function concatKdf(secret, bits, value) {
|
|
950
|
+
const iterations = Math.ceil((bits >> 3) / 32);
|
|
951
|
+
const res = new Uint8Array(iterations * 32);
|
|
952
|
+
for (let iter = 0; iter < iterations; iter++) {
|
|
953
|
+
const buf = new Uint8Array(4 + secret.length + value.length);
|
|
954
|
+
buf.set(uint32be(iter + 1));
|
|
955
|
+
buf.set(secret, 4);
|
|
956
|
+
buf.set(value, 4 + secret.length);
|
|
957
|
+
res.set(await digest("sha256", buf), iter * 32);
|
|
958
|
+
}
|
|
959
|
+
return res.slice(0, bits >> 3);
|
|
960
|
+
}
|
|
961
|
+
function lengthAndInput(input) {
|
|
962
|
+
return concatUint8Arrays(uint32be(input.length), input);
|
|
963
|
+
}
|
|
944
964
|
async function digest(algorithm, data) {
|
|
945
|
-
|
|
946
|
-
|
|
965
|
+
return new Uint8Array(await crypto.subtle.digest(`SHA-${algorithm.slice(-3)}`, data));
|
|
966
|
+
}
|
|
967
|
+
function writeUInt32BE(buf, value, offset) {
|
|
968
|
+
if (value < 0 || value >= 2 ** 32) throw new RangeError(`value must be >= 0 and <= ${2 ** 32 - 1}. Received ${value}`);
|
|
969
|
+
buf.set([
|
|
970
|
+
value >>> 24,
|
|
971
|
+
value >>> 16,
|
|
972
|
+
value >>> 8,
|
|
973
|
+
value & 255
|
|
974
|
+
], offset);
|
|
975
|
+
}
|
|
976
|
+
function uint32be(value) {
|
|
977
|
+
const buf = new Uint8Array(4);
|
|
978
|
+
writeUInt32BE(buf, value, 0);
|
|
979
|
+
return buf;
|
|
947
980
|
}
|
|
948
981
|
//#endregion
|
|
949
|
-
//#region src/core/
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
p2s: base64UrlEncode(p2s)
|
|
962
|
-
};
|
|
963
|
-
} else return { encryptedKey: await _wrap(alg, key, cek) };
|
|
964
|
-
}
|
|
965
|
-
async function unwrap(alg, key, encryptedKey, p2c, p2s) {
|
|
966
|
-
if (p2c && p2s) {
|
|
967
|
-
const derived = await deriveKey(p2s, alg, p2c, key);
|
|
968
|
-
return _unwrap(alg.slice(-6), derived, encryptedKey);
|
|
969
|
-
} else return _unwrap(alg, key, encryptedKey);
|
|
970
|
-
}
|
|
971
|
-
function getCryptoKey(key, alg, options = {}) {
|
|
972
|
-
const { importAlg = "PBKDF2", extractable = false, usage = "deriveBits" } = options;
|
|
973
|
-
if (key instanceof Uint8Array) return crypto.subtle.importKey("raw", key, importAlg, extractable, [usage]);
|
|
974
|
-
checkEncCryptoKey(key, alg, usage);
|
|
975
|
-
return key;
|
|
982
|
+
//#region src/core/_crypto/_pbes2.ts
|
|
983
|
+
async function pbes2Wrap(alg, key, cek, p2c, p2s) {
|
|
984
|
+
const derived = await deriveKey(p2s, alg, p2c, key);
|
|
985
|
+
return {
|
|
986
|
+
encryptedKey: await aesKwWrap(alg.slice(-6), derived, cek),
|
|
987
|
+
p2c,
|
|
988
|
+
p2s: base64UrlEncode(p2s)
|
|
989
|
+
};
|
|
990
|
+
}
|
|
991
|
+
async function pbes2Unwrap(alg, key, encryptedKey, p2c, p2s) {
|
|
992
|
+
const derived = await deriveKey(p2s, alg, p2c, key);
|
|
993
|
+
return aesKwUnwrap(alg.slice(-6), derived, encryptedKey);
|
|
976
994
|
}
|
|
977
995
|
async function deriveKey(p2s, alg, p2c, key) {
|
|
978
996
|
if (!(p2s instanceof Uint8Array) || p2s.length < 8) throw new Error("PBES2 Salt Input must be 8 or more octets");
|
|
@@ -985,92 +1003,44 @@ async function deriveKey(p2s, alg, p2c, key) {
|
|
|
985
1003
|
name: "PBKDF2",
|
|
986
1004
|
salt
|
|
987
1005
|
};
|
|
988
|
-
const cryptoKey = await
|
|
1006
|
+
const cryptoKey = await getPBKDF2CryptoKey(key, alg);
|
|
989
1007
|
return new Uint8Array(await crypto.subtle.deriveBits(subtleAlg, cryptoKey, keylen));
|
|
990
1008
|
}
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
function checkKeySize(key, alg) {
|
|
995
|
-
if (key.algorithm.length !== Number.parseInt(alg.slice(1, 4), 10)) throw new TypeError(`Invalid key size for alg: ${alg}`);
|
|
996
|
-
}
|
|
997
|
-
async function _wrap(alg, key, cek) {
|
|
998
|
-
const cryptoKey = await getCryptoKey(key, alg, {
|
|
999
|
-
importAlg: "AES-KW",
|
|
1000
|
-
extractable: true,
|
|
1001
|
-
usage: "wrapKey"
|
|
1002
|
-
});
|
|
1003
|
-
checkKeySize(cryptoKey, alg);
|
|
1004
|
-
const cryptoKeyCek = await crypto.subtle.importKey("raw", cek, {
|
|
1005
|
-
hash: "SHA-256",
|
|
1006
|
-
name: "HMAC"
|
|
1007
|
-
}, true, ["sign"]);
|
|
1008
|
-
return new Uint8Array(await crypto.subtle.wrapKey("raw", cryptoKeyCek, cryptoKey, "AES-KW"));
|
|
1009
|
-
}
|
|
1010
|
-
async function _unwrap(alg, key, encryptedKey) {
|
|
1011
|
-
const cryptoKey = await getCryptoKey(key, alg, {
|
|
1012
|
-
importAlg: "AES-KW",
|
|
1013
|
-
extractable: true,
|
|
1014
|
-
usage: "unwrapKey"
|
|
1015
|
-
});
|
|
1016
|
-
checkKeySize(cryptoKey, alg);
|
|
1017
|
-
const cryptoKeyCek = await crypto.subtle.unwrapKey("raw", encryptedKey, cryptoKey, "AES-KW", {
|
|
1018
|
-
hash: "SHA-256",
|
|
1019
|
-
name: "HMAC"
|
|
1020
|
-
}, true, ["sign"]);
|
|
1021
|
-
return new Uint8Array(await crypto.subtle.exportKey("raw", cryptoKeyCek));
|
|
1009
|
+
function checkPBES2CryptoKey(key, alg, usage) {
|
|
1010
|
+
if (!isAlgorithm(key.algorithm, "PBKDF2")) throw unusable("PBKDF2");
|
|
1011
|
+
checkUsage(key, usage);
|
|
1022
1012
|
}
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
const PUBLIC_EPK_FIELDS = new Set([
|
|
1026
|
-
"kty",
|
|
1027
|
-
"crv",
|
|
1028
|
-
"x",
|
|
1029
|
-
"y",
|
|
1030
|
-
"kid",
|
|
1031
|
-
"use",
|
|
1032
|
-
"key_ops",
|
|
1033
|
-
"alg",
|
|
1034
|
-
"x5c",
|
|
1035
|
-
"x5t",
|
|
1036
|
-
"x5t#S256",
|
|
1037
|
-
"x5u"
|
|
1038
|
-
]);
|
|
1039
|
-
function toPublicEpkJwk(jwk) {
|
|
1040
|
-
const filtered = {};
|
|
1041
|
-
for (const [key, value] of Object.entries(jwk)) if (value !== void 0 && PUBLIC_EPK_FIELDS.has(key)) filtered[key] = value;
|
|
1042
|
-
const { kty, crv, x, y } = filtered;
|
|
1043
|
-
if (typeof kty !== "string") throw new TypeError("ECDH-ES ephemeral key JWK must include the \"kty\" parameter.");
|
|
1044
|
-
if (typeof crv !== "string") throw new TypeError("ECDH-ES ephemeral key JWK must include the \"crv\" parameter.");
|
|
1045
|
-
if (typeof x !== "string") throw new TypeError("ECDH-ES ephemeral key JWK must include the \"x\" coordinate.");
|
|
1046
|
-
if (kty === "EC" && typeof y !== "string") throw new TypeError("ECDH-ES EC ephemeral key JWK must include both \"x\" and \"y\" coordinates.");
|
|
1047
|
-
const publicJwk = {
|
|
1048
|
-
...filtered,
|
|
1049
|
-
kty,
|
|
1050
|
-
crv,
|
|
1051
|
-
x
|
|
1052
|
-
};
|
|
1053
|
-
if (typeof y === "string") publicJwk.y = y;
|
|
1054
|
-
else delete publicJwk.y;
|
|
1055
|
-
return sanitizeObject(publicJwk);
|
|
1013
|
+
function unusable(name, prop = "algorithm.name") {
|
|
1014
|
+
return /* @__PURE__ */ new TypeError(`CryptoKey does not support this operation, its ${prop} must be ${name}`);
|
|
1056
1015
|
}
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
if (!(normalized instanceof CryptoKey)) throw new TypeError("ECDH-ES ephemeral private key must be a CryptoKey or convertible JWK.");
|
|
1060
|
-
if (normalized.type !== "private") throw new TypeError("ECDH-ES ephemeral private key must have type \"private\".");
|
|
1061
|
-
return normalized;
|
|
1016
|
+
function isAlgorithm(algorithm, name) {
|
|
1017
|
+
return algorithm.name === name;
|
|
1062
1018
|
}
|
|
1063
|
-
|
|
1064
|
-
if (
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
if (!(normalized instanceof CryptoKey)) throw new TypeError("ECDH-ES ephemeral public key must be a CryptoKey or convertible JWK.");
|
|
1072
|
-
return exportPublicJwkFrom(alg, normalized);
|
|
1019
|
+
function checkUsage(key, usage) {
|
|
1020
|
+
if (usage && !key.usages.includes(usage)) throw new TypeError(`CryptoKey does not support this operation, its usages must include ${usage}.`);
|
|
1021
|
+
}
|
|
1022
|
+
async function getPBKDF2CryptoKey(key, alg, options = {}) {
|
|
1023
|
+
const { extractable = false, usage = "deriveBits" } = options;
|
|
1024
|
+
if (key instanceof Uint8Array) return crypto.subtle.importKey("raw", key, "PBKDF2", extractable, [usage]);
|
|
1025
|
+
checkPBES2CryptoKey(key, alg, usage);
|
|
1026
|
+
return key;
|
|
1073
1027
|
}
|
|
1028
|
+
//#endregion
|
|
1029
|
+
//#region src/core/_crypto/_key-encryption.ts
|
|
1030
|
+
const PUBLIC_EPK_FIELDS = new Set([
|
|
1031
|
+
"kty",
|
|
1032
|
+
"crv",
|
|
1033
|
+
"x",
|
|
1034
|
+
"y",
|
|
1035
|
+
"kid",
|
|
1036
|
+
"use",
|
|
1037
|
+
"key_ops",
|
|
1038
|
+
"alg",
|
|
1039
|
+
"x5c",
|
|
1040
|
+
"x5t",
|
|
1041
|
+
"x5t#S256",
|
|
1042
|
+
"x5u"
|
|
1043
|
+
]);
|
|
1074
1044
|
async function encryptKey(alg, enc, key, providedCek, providedParameters = {}) {
|
|
1075
1045
|
let encryptedKey;
|
|
1076
1046
|
let parameters = {};
|
|
@@ -1084,7 +1054,7 @@ async function encryptKey(alg, enc, key, providedCek, providedParameters = {}) {
|
|
|
1084
1054
|
case "ECDH-ES+A192KW":
|
|
1085
1055
|
case "ECDH-ES+A256KW": {
|
|
1086
1056
|
assertCryptoKey(key);
|
|
1087
|
-
if (!
|
|
1057
|
+
if (!isECDHKeyAllowed(key)) throw new Error("ECDH with the provided key is not allowed or not supported by your javascript runtime");
|
|
1088
1058
|
const { apu, apv, epk: providedEpk, epkPrivateKey } = providedParameters;
|
|
1089
1059
|
if (providedEpk && !epkPrivateKey) throw new TypeError("ECDH-ES custom ephemeral key material must include the matching private key.");
|
|
1090
1060
|
let ephemeralPrivateKey;
|
|
@@ -1107,7 +1077,7 @@ async function encryptKey(alg, enc, key, providedCek, providedParameters = {}) {
|
|
|
1107
1077
|
break;
|
|
1108
1078
|
}
|
|
1109
1079
|
cek = providedCek || generateCEK(enc);
|
|
1110
|
-
encryptedKey =
|
|
1080
|
+
encryptedKey = await aesKwWrap(alg.slice(-6), sharedSecret, cek);
|
|
1111
1081
|
break;
|
|
1112
1082
|
}
|
|
1113
1083
|
case "RSA-OAEP":
|
|
@@ -1123,21 +1093,21 @@ async function encryptKey(alg, enc, key, providedCek, providedParameters = {}) {
|
|
|
1123
1093
|
case "PBES2-HS512+A256KW": {
|
|
1124
1094
|
cek = providedCek || generateCEK(enc);
|
|
1125
1095
|
const { p2c, p2s } = providedParameters;
|
|
1126
|
-
({encryptedKey, ...parameters} = await
|
|
1096
|
+
({encryptedKey, ...parameters} = await pbes2Wrap(alg, key, cek, p2c, p2s));
|
|
1127
1097
|
break;
|
|
1128
1098
|
}
|
|
1129
1099
|
case "A128KW":
|
|
1130
1100
|
case "A192KW":
|
|
1131
1101
|
case "A256KW":
|
|
1132
1102
|
cek = providedCek || generateCEK(enc);
|
|
1133
|
-
encryptedKey =
|
|
1103
|
+
encryptedKey = await aesKwWrap(alg, key, cek);
|
|
1134
1104
|
break;
|
|
1135
1105
|
case "A128GCMKW":
|
|
1136
1106
|
case "A192GCMKW":
|
|
1137
1107
|
case "A256GCMKW": {
|
|
1138
1108
|
cek = providedCek || generateCEK(enc);
|
|
1139
1109
|
const { iv } = providedParameters;
|
|
1140
|
-
({encryptedKey, ...parameters} = await
|
|
1110
|
+
({encryptedKey, ...parameters} = await gcmkwEncrypt(alg, key, cek, iv));
|
|
1141
1111
|
break;
|
|
1142
1112
|
}
|
|
1143
1113
|
default: throw new Error("Invalid or unsupported \"alg\" (JWE Algorithm) header value");
|
|
@@ -1160,67 +1130,72 @@ async function normalizeKey(key, alg) {
|
|
|
1160
1130
|
}
|
|
1161
1131
|
throw new Error("unreachable");
|
|
1162
1132
|
}
|
|
1133
|
+
function toPublicEpkJwk(jwk) {
|
|
1134
|
+
const filtered = {};
|
|
1135
|
+
for (const [key, value] of Object.entries(jwk)) if (value !== void 0 && PUBLIC_EPK_FIELDS.has(key)) filtered[key] = value;
|
|
1136
|
+
const { kty, crv, x, y } = filtered;
|
|
1137
|
+
if (typeof kty !== "string") throw new TypeError("ECDH-ES ephemeral key JWK must include the \"kty\" parameter.");
|
|
1138
|
+
if (typeof crv !== "string") throw new TypeError("ECDH-ES ephemeral key JWK must include the \"crv\" parameter.");
|
|
1139
|
+
if (typeof x !== "string") throw new TypeError("ECDH-ES ephemeral key JWK must include the \"x\" coordinate.");
|
|
1140
|
+
if (kty === "EC" && typeof y !== "string") throw new TypeError("ECDH-ES EC ephemeral key JWK must include both \"x\" and \"y\" coordinates.");
|
|
1141
|
+
const publicJwk = {
|
|
1142
|
+
...filtered,
|
|
1143
|
+
kty,
|
|
1144
|
+
crv,
|
|
1145
|
+
x
|
|
1146
|
+
};
|
|
1147
|
+
if (typeof y === "string") publicJwk.y = y;
|
|
1148
|
+
else delete publicJwk.y;
|
|
1149
|
+
return sanitizeObject(publicJwk);
|
|
1150
|
+
}
|
|
1151
|
+
async function ensurePrivateKey(alg, value) {
|
|
1152
|
+
const normalized = await normalizeKey(value, alg);
|
|
1153
|
+
if (!(normalized instanceof CryptoKey)) throw new TypeError("ECDH-ES ephemeral private key must be a CryptoKey or convertible JWK.");
|
|
1154
|
+
if (normalized.type !== "private") throw new TypeError("ECDH-ES ephemeral private key must have type \"private\".");
|
|
1155
|
+
return normalized;
|
|
1156
|
+
}
|
|
1157
|
+
async function exportPublicJwkFrom(alg, value) {
|
|
1158
|
+
if (isCryptoKey(value)) try {
|
|
1159
|
+
return toPublicEpkJwk(await keyToJWK(value));
|
|
1160
|
+
} catch (error_) {
|
|
1161
|
+
throw new TypeError("ECDH-ES ephemeral CryptoKey must be extractable to export as JWK.", { cause: error_ instanceof Error ? error_ : void 0 });
|
|
1162
|
+
}
|
|
1163
|
+
if (isJWK(value) && isAsymmetricJWK(value) && value.kty === "EC") return toPublicEpkJwk(value);
|
|
1164
|
+
const normalized = await normalizeKey(value, alg);
|
|
1165
|
+
if (!(normalized instanceof CryptoKey)) throw new TypeError("ECDH-ES ephemeral public key must be a CryptoKey or convertible JWK.");
|
|
1166
|
+
return exportPublicJwkFrom(alg, normalized);
|
|
1167
|
+
}
|
|
1163
1168
|
//#endregion
|
|
1164
|
-
//#region src/core/
|
|
1165
|
-
/**
|
|
1166
|
-
* This is a fork of Jose library's utility functions.
|
|
1167
|
-
* @source https://github.com/panva/jose/tree/69b7960c67e05be55fa2ec31c74b987696c20c60/src/lib/asn1.ts
|
|
1168
|
-
* @license MIT https://github.com/panva/jose/blob/69b7960c67e05be55fa2ec31c74b987696c20c60/LICENSE.md
|
|
1169
|
-
*/
|
|
1170
|
-
const formatPEM = (b64, descriptor) => {
|
|
1171
|
-
return `-----BEGIN ${descriptor}-----\n${(b64.match(/.{1,64}/g) || []).join("\n")}\n-----END ${descriptor}-----`;
|
|
1172
|
-
};
|
|
1173
|
-
const genericExport = async (keyType, keyFormat, key) => {
|
|
1174
|
-
if (!isCryptoKey(key)) throw new TypeError(`Key must be ${key} of type CryptoKey`);
|
|
1175
|
-
if (!key.extractable) throw new TypeError("CryptoKey is not extractable");
|
|
1176
|
-
if (key.type !== keyType) throw new TypeError(`key is not a ${keyType} key`);
|
|
1177
|
-
return formatPEM(base64Encode(new Uint8Array(await crypto.subtle.exportKey(keyFormat, key))), `${keyType.toUpperCase()} KEY`);
|
|
1178
|
-
};
|
|
1169
|
+
//#region src/core/_crypto/_pem.ts
|
|
1179
1170
|
const toSPKI = (key) => {
|
|
1180
1171
|
return genericExport("public", "spki", key);
|
|
1181
1172
|
};
|
|
1182
1173
|
const toPKCS8 = (key) => {
|
|
1183
1174
|
return genericExport("private", "pkcs8", key);
|
|
1184
1175
|
};
|
|
1185
|
-
const
|
|
1186
|
-
|
|
1187
|
-
oid.unshift(oid.length);
|
|
1188
|
-
oid.unshift(6);
|
|
1189
|
-
}
|
|
1190
|
-
const i = keyData.indexOf(oid[0], from);
|
|
1191
|
-
if (i === -1) return false;
|
|
1192
|
-
const sub = keyData.subarray(i, i + oid.length);
|
|
1193
|
-
if (sub.length !== oid.length) return false;
|
|
1194
|
-
return sub.every((value, index) => value === oid[index]) || findOid(keyData, oid, i + 1);
|
|
1176
|
+
const fromPKCS8 = (pem, alg, options) => {
|
|
1177
|
+
return genericImport(/(?:-----(?:BEGIN|END) PRIVATE KEY-----|\s)/g, "pkcs8", pem, alg, options);
|
|
1195
1178
|
};
|
|
1196
|
-
const
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
1,
|
|
1206
|
-
7
|
|
1207
|
-
]): return "P-256";
|
|
1208
|
-
case findOid(keyData, [
|
|
1209
|
-
43,
|
|
1210
|
-
129,
|
|
1211
|
-
4,
|
|
1212
|
-
0,
|
|
1213
|
-
34
|
|
1214
|
-
]): return "P-384";
|
|
1215
|
-
case findOid(keyData, [
|
|
1216
|
-
43,
|
|
1217
|
-
129,
|
|
1218
|
-
4,
|
|
1219
|
-
0,
|
|
1220
|
-
35
|
|
1221
|
-
]): return "P-521";
|
|
1222
|
-
default: return;
|
|
1179
|
+
const fromSPKI = (pem, alg, options) => {
|
|
1180
|
+
return genericImport(/(?:-----(?:BEGIN|END) PUBLIC KEY-----|\s)/g, "spki", pem, alg, options);
|
|
1181
|
+
};
|
|
1182
|
+
const fromX509 = (pem, alg, options) => {
|
|
1183
|
+
let spki;
|
|
1184
|
+
try {
|
|
1185
|
+
spki = getSPKI(pem);
|
|
1186
|
+
} catch (error_) {
|
|
1187
|
+
throw new TypeError("Failed to parse the X.509 certificate", { cause: error_ });
|
|
1223
1188
|
}
|
|
1189
|
+
return fromSPKI(spki, alg, options);
|
|
1190
|
+
};
|
|
1191
|
+
const formatPEM = (b64, descriptor) => {
|
|
1192
|
+
return `-----BEGIN ${descriptor}-----\n${(b64.match(/.{1,64}/g) || []).join("\n")}\n-----END ${descriptor}-----`;
|
|
1193
|
+
};
|
|
1194
|
+
const genericExport = async (keyType, keyFormat, key) => {
|
|
1195
|
+
if (!isCryptoKey(key)) throw new TypeError(`Key must be ${key} of type CryptoKey`);
|
|
1196
|
+
if (!key.extractable) throw new TypeError("CryptoKey is not extractable");
|
|
1197
|
+
if (key.type !== keyType) throw new TypeError(`key is not a ${keyType} key`);
|
|
1198
|
+
return formatPEM(base64Encode(new Uint8Array(await crypto.subtle.exportKey(keyFormat, key))), `${keyType.toUpperCase()} KEY`);
|
|
1224
1199
|
};
|
|
1225
1200
|
const genericImport = async (replace, keyFormat, pem, alg, options) => {
|
|
1226
1201
|
let algorithm;
|
|
@@ -1298,13 +1273,47 @@ const genericImport = async (replace, keyFormat, pem, alg, options) => {
|
|
|
1298
1273
|
}
|
|
1299
1274
|
return crypto.subtle.importKey(keyFormat, keyData, algorithm, options?.extractable ?? (isPublic ? true : false), keyUsages);
|
|
1300
1275
|
};
|
|
1301
|
-
const
|
|
1302
|
-
|
|
1276
|
+
const findOid = (keyData, oid, from = 0) => {
|
|
1277
|
+
if (from === 0) {
|
|
1278
|
+
oid.unshift(oid.length);
|
|
1279
|
+
oid.unshift(6);
|
|
1280
|
+
}
|
|
1281
|
+
const i = keyData.indexOf(oid[0], from);
|
|
1282
|
+
if (i === -1) return false;
|
|
1283
|
+
const sub = keyData.subarray(i, i + oid.length);
|
|
1284
|
+
if (sub.length !== oid.length) return false;
|
|
1285
|
+
return sub.every((value, index) => value === oid[index]) || findOid(keyData, oid, i + 1);
|
|
1303
1286
|
};
|
|
1304
|
-
const
|
|
1305
|
-
|
|
1287
|
+
const getNamedCurve = (keyData) => {
|
|
1288
|
+
switch (true) {
|
|
1289
|
+
case findOid(keyData, [
|
|
1290
|
+
42,
|
|
1291
|
+
134,
|
|
1292
|
+
72,
|
|
1293
|
+
206,
|
|
1294
|
+
61,
|
|
1295
|
+
3,
|
|
1296
|
+
1,
|
|
1297
|
+
7
|
|
1298
|
+
]): return "P-256";
|
|
1299
|
+
case findOid(keyData, [
|
|
1300
|
+
43,
|
|
1301
|
+
129,
|
|
1302
|
+
4,
|
|
1303
|
+
0,
|
|
1304
|
+
34
|
|
1305
|
+
]): return "P-384";
|
|
1306
|
+
case findOid(keyData, [
|
|
1307
|
+
43,
|
|
1308
|
+
129,
|
|
1309
|
+
4,
|
|
1310
|
+
0,
|
|
1311
|
+
35
|
|
1312
|
+
]): return "P-521";
|
|
1313
|
+
default: return;
|
|
1314
|
+
}
|
|
1306
1315
|
};
|
|
1307
|
-
|
|
1316
|
+
const getElement = (seq) => {
|
|
1308
1317
|
const result = [];
|
|
1309
1318
|
let next = 0;
|
|
1310
1319
|
while (next < seq.length) {
|
|
@@ -1313,8 +1322,8 @@ function getElement(seq) {
|
|
|
1313
1322
|
next += nextPart.byteLength;
|
|
1314
1323
|
}
|
|
1315
1324
|
return result;
|
|
1316
|
-
}
|
|
1317
|
-
|
|
1325
|
+
};
|
|
1326
|
+
const parseElement = (bytes) => {
|
|
1318
1327
|
let position = 0;
|
|
1319
1328
|
let tag = bytes[0] & 31;
|
|
1320
1329
|
position++;
|
|
@@ -1358,13 +1367,13 @@ function parseElement(bytes) {
|
|
|
1358
1367
|
contents: bytes.subarray(position, byteLength),
|
|
1359
1368
|
raw: bytes.subarray(0, byteLength)
|
|
1360
1369
|
};
|
|
1361
|
-
}
|
|
1362
|
-
|
|
1370
|
+
};
|
|
1371
|
+
const spkiFromX509 = (buf) => {
|
|
1363
1372
|
const tbsCertificate = getElement(getElement(parseElement(buf).contents)[0].contents);
|
|
1364
1373
|
return base64Encode(tbsCertificate[tbsCertificate[0].raw[0] === 160 ? 6 : 5].raw);
|
|
1365
|
-
}
|
|
1374
|
+
};
|
|
1366
1375
|
let createPublicKey;
|
|
1367
|
-
|
|
1376
|
+
const getSPKI = (x509) => {
|
|
1368
1377
|
try {
|
|
1369
1378
|
createPublicKey ??= globalThis.process?.getBuiltinModule?.("node:crypto")?.createPublicKey;
|
|
1370
1379
|
} catch {
|
|
@@ -1377,15 +1386,6 @@ function getSPKI(x509) {
|
|
|
1377
1386
|
});
|
|
1378
1387
|
} catch {}
|
|
1379
1388
|
return formatPEM(spkiFromX509(base64Decode(x509.replace(/(?:-----(?:BEGIN|END) CERTIFICATE-----|\s)/g, ""), false)), "PUBLIC KEY");
|
|
1380
|
-
}
|
|
1381
|
-
const fromX509 = (pem, alg, options) => {
|
|
1382
|
-
let spki;
|
|
1383
|
-
try {
|
|
1384
|
-
spki = getSPKI(pem);
|
|
1385
|
-
} catch (error_) {
|
|
1386
|
-
throw new TypeError("Failed to parse the X.509 certificate", { cause: error_ });
|
|
1387
|
-
}
|
|
1388
|
-
return fromSPKI(spki, alg, options);
|
|
1389
1389
|
};
|
|
1390
1390
|
//#endregion
|
|
1391
1391
|
//#region src/core/jwk.ts
|
|
@@ -1399,25 +1399,13 @@ async function generateKey(alg, options = {}) {
|
|
|
1399
1399
|
}
|
|
1400
1400
|
const { algorithm, keyUsages } = getGenerateKeyParams(alg, options);
|
|
1401
1401
|
const key = await crypto.subtle.generateKey(algorithm, defaultExtractable, keyUsages);
|
|
1402
|
-
if (exportToJWK) {
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
const [publicKey, privateKey] = await Promise.all([exportKey(key.publicKey, {
|
|
1410
|
-
...additionalKeyParams,
|
|
1411
|
-
alg
|
|
1412
|
-
}), exportKey(key.privateKey, {
|
|
1413
|
-
...additionalKeyParams,
|
|
1414
|
-
alg
|
|
1415
|
-
})]);
|
|
1416
|
-
return {
|
|
1417
|
-
privateKey,
|
|
1418
|
-
publicKey
|
|
1419
|
-
};
|
|
1420
|
-
}
|
|
1402
|
+
if (exportToJWK) if (key instanceof CryptoKey) return exportKey(key, { alg });
|
|
1403
|
+
else {
|
|
1404
|
+
const [publicKey, privateKey] = await Promise.all([exportKey(key.publicKey, { alg }), exportKey(key.privateKey, { alg })]);
|
|
1405
|
+
return {
|
|
1406
|
+
privateKey,
|
|
1407
|
+
publicKey
|
|
1408
|
+
};
|
|
1421
1409
|
}
|
|
1422
1410
|
return key;
|
|
1423
1411
|
}
|
|
@@ -1430,14 +1418,31 @@ async function generateKey(alg, options = {}) {
|
|
|
1430
1418
|
* @returns A Promise resolving to the generated JWK or JWK pair representation.
|
|
1431
1419
|
*/
|
|
1432
1420
|
async function generateJWK(alg, jwkParams, options = {}) {
|
|
1433
|
-
const
|
|
1434
|
-
|
|
1435
|
-
...
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1421
|
+
const extraParams = {
|
|
1422
|
+
kid: typeof jwkParams?.kid === "string" ? jwkParams.kid : crypto.randomUUID(),
|
|
1423
|
+
...jwkParams
|
|
1424
|
+
};
|
|
1425
|
+
const result = await generateKey(alg, {
|
|
1426
|
+
...options,
|
|
1427
|
+
toJWK: true
|
|
1440
1428
|
});
|
|
1429
|
+
if (result && typeof result === "object" && "privateKey" in result && "publicKey" in result) {
|
|
1430
|
+
const pair = result;
|
|
1431
|
+
return {
|
|
1432
|
+
privateKey: {
|
|
1433
|
+
...extraParams,
|
|
1434
|
+
...pair.privateKey
|
|
1435
|
+
},
|
|
1436
|
+
publicKey: {
|
|
1437
|
+
...extraParams,
|
|
1438
|
+
...pair.publicKey
|
|
1439
|
+
}
|
|
1440
|
+
};
|
|
1441
|
+
}
|
|
1442
|
+
return {
|
|
1443
|
+
...extraParams,
|
|
1444
|
+
...result
|
|
1445
|
+
};
|
|
1441
1446
|
}
|
|
1442
1447
|
async function deriveKeyFromPassword(password, alg, options) {
|
|
1443
1448
|
const { salt, iterations, extractable, keyUsage } = options;
|
|
@@ -1449,16 +1454,11 @@ async function deriveKeyFromPassword(password, alg, options) {
|
|
|
1449
1454
|
const finalUsages = keyUsage ?? ["wrapKey", "unwrapKey"];
|
|
1450
1455
|
const finalExtractable = exportToJWK ? true : extractable === true;
|
|
1451
1456
|
const derivedKey = await crypto.subtle.importKey("raw", derivedBytes, { name: "AES-KW" }, finalExtractable, finalUsages);
|
|
1452
|
-
if (exportToJWK) {
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
...jwk,
|
|
1458
|
-
alg: wrappingAlg,
|
|
1459
|
-
kty: "oct"
|
|
1460
|
-
};
|
|
1461
|
-
}
|
|
1457
|
+
if (exportToJWK) return {
|
|
1458
|
+
...await keyToJWK(derivedKey),
|
|
1459
|
+
alg: wrappingAlg,
|
|
1460
|
+
kty: "oct"
|
|
1461
|
+
};
|
|
1462
1462
|
return derivedKey;
|
|
1463
1463
|
}
|
|
1464
1464
|
/**
|
|
@@ -1471,36 +1471,67 @@ async function deriveKeyFromPassword(password, alg, options) {
|
|
|
1471
1471
|
* @returns A Promise resolving to the derived JWK (JWK_oct).
|
|
1472
1472
|
*/
|
|
1473
1473
|
async function deriveJWKFromPassword(password, alg, options, jwkParams) {
|
|
1474
|
-
const
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
toJWK: jwkParams || true
|
|
1474
|
+
const result = await deriveKeyFromPassword(password, alg, {
|
|
1475
|
+
...options,
|
|
1476
|
+
toJWK: true
|
|
1478
1477
|
});
|
|
1478
|
+
return jwkParams ? {
|
|
1479
|
+
...jwkParams,
|
|
1480
|
+
...result
|
|
1481
|
+
} : result;
|
|
1479
1482
|
}
|
|
1480
1483
|
/**
|
|
1481
|
-
*
|
|
1484
|
+
* Default JWK import cache backed by a WeakMap.
|
|
1485
|
+
*
|
|
1486
|
+
* The outer WeakMap is keyed by the JWK object reference — a cache hit only
|
|
1487
|
+
* occurs when the exact same object variable is passed to {@link importKey}.
|
|
1488
|
+
* Reconstructing a structurally identical JWK (e.g. `{ ...jwk }`) will miss
|
|
1489
|
+
* the cache.
|
|
1490
|
+
*
|
|
1491
|
+
* The inner `Record<string, CryptoKey>` uses a plain object rather than `Map`
|
|
1492
|
+
* because typical entries have 1–2 algorithm strings per key. V8 applies
|
|
1493
|
+
* hidden-class optimisation to small plain objects, making property access
|
|
1494
|
+
* faster than `Map.get()` at this cardinality.
|
|
1482
1495
|
*/
|
|
1483
|
-
|
|
1484
|
-
|
|
1496
|
+
var WeakMapJWKCache = class {
|
|
1497
|
+
_map = /* @__PURE__ */ new WeakMap();
|
|
1498
|
+
get(jwk, alg) {
|
|
1499
|
+
return this._map.get(jwk)?.[alg];
|
|
1500
|
+
}
|
|
1501
|
+
set(jwk, alg, key) {
|
|
1502
|
+
let entry = this._map.get(jwk);
|
|
1503
|
+
if (!entry) this._map.set(jwk, { [alg]: key });
|
|
1504
|
+
else entry[alg] = key;
|
|
1505
|
+
}
|
|
1506
|
+
};
|
|
1507
|
+
let _activeCache = new WeakMapJWKCache();
|
|
1508
|
+
async function importKey(key, algOrOptions) {
|
|
1509
|
+
const alg = typeof algOrOptions === "string" ? algOrOptions : void 0;
|
|
1485
1510
|
if (typeof key === "string") key = textEncoder.encode(key);
|
|
1486
1511
|
if (key instanceof Uint8Array) return key;
|
|
1487
1512
|
if (isCryptoKey(key)) return key;
|
|
1488
|
-
if (isJWK(key)) if ("k" in key && typeof key.k === "string")
|
|
1489
|
-
|
|
1513
|
+
if (isJWK(key)) if ("k" in key && typeof key.k === "string") {
|
|
1514
|
+
const rawBytes = base64UrlDecode(key.k, false);
|
|
1515
|
+
if (typeof algOrOptions === "object" && algOrOptions.asCryptoKey) {
|
|
1516
|
+
const { algorithm, usage, extractable = false } = algOrOptions;
|
|
1517
|
+
return crypto.subtle.importKey("raw", rawBytes, algorithm, extractable, usage);
|
|
1518
|
+
}
|
|
1519
|
+
return rawBytes;
|
|
1520
|
+
} else {
|
|
1490
1521
|
if (!key.alg && !alg) throw new TypeError("Algorithm must be provided when importing non-oct JWK");
|
|
1491
1522
|
const effectiveAlg = key.alg || alg;
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1523
|
+
if (_activeCache) {
|
|
1524
|
+
const cached = _activeCache.get(key, effectiveAlg);
|
|
1525
|
+
if (cached) return cached;
|
|
1526
|
+
}
|
|
1495
1527
|
const cryptoKey = await jwkTokey(key.alg ? key : {
|
|
1496
1528
|
...key,
|
|
1497
1529
|
alg
|
|
1498
1530
|
});
|
|
1499
|
-
if (
|
|
1500
|
-
else cached[effectiveAlg] = cryptoKey;
|
|
1531
|
+
if (_activeCache) _activeCache.set(key, effectiveAlg, cryptoKey);
|
|
1501
1532
|
return cryptoKey;
|
|
1502
1533
|
}
|
|
1503
|
-
throw new
|
|
1534
|
+
throw new JWTError("Invalid key type provided to importKey", "ERR_JWK_INVALID");
|
|
1504
1535
|
}
|
|
1505
1536
|
/**
|
|
1506
1537
|
* Exports a CryptoKey to a JWK (JSON Web Key) format.
|
|
@@ -1522,41 +1553,37 @@ async function resolveWrappingKey(alg, key) {
|
|
|
1522
1553
|
if (typeof key === "string") throw new TypeError(`Key must be CryptoKey, JWK, or Uint8Array for ${alg}`);
|
|
1523
1554
|
return importKey(key, alg);
|
|
1524
1555
|
}
|
|
1525
|
-
/**
|
|
1526
|
-
* Unwraps a Content Encryption Key (CEK) using the specified algorithm and unwrapping key.
|
|
1527
|
-
*
|
|
1528
|
-
* @param alg The JWA key management algorithm (e.g., "A128KW", "RSA-OAEP", "PBES2-HS256+A128KW").
|
|
1529
|
-
* @param wrappedKey The wrapped key (ciphertext) as Uint8Array.
|
|
1530
|
-
* @param unwrappingKey The key used to unwrap the CEK (CryptoKey, JWK, or password string/Uint8Array for PBES2).
|
|
1531
|
-
* @param options Additional options required by certain algorithms (e.g., iv, tag, p2s, p2c, epk).
|
|
1532
|
-
* @returns A Promise resolving to the unwrapped key (CEK) as a CryptoKey or Uint8Array.
|
|
1533
|
-
*/
|
|
1534
1556
|
async function unwrapKey(alg, wrappedKey, unwrappingKey, options = {}) {
|
|
1535
|
-
const
|
|
1557
|
+
const returnRaw = options.format === "raw";
|
|
1536
1558
|
const defaultExtractable = options.extractable !== false;
|
|
1537
1559
|
const importedUnwrappingKey = isCryptoKey(unwrappingKey) ? unwrappingKey : await resolveWrappingKey(alg, unwrappingKey);
|
|
1538
1560
|
let unwrappedCekBytes;
|
|
1539
1561
|
switch (alg) {
|
|
1562
|
+
case "dir":
|
|
1563
|
+
if (importedUnwrappingKey instanceof Uint8Array) unwrappedCekBytes = importedUnwrappingKey;
|
|
1564
|
+
else {
|
|
1565
|
+
if (!returnRaw) return importedUnwrappingKey;
|
|
1566
|
+
unwrappedCekBytes = new Uint8Array(await crypto.subtle.exportKey("raw", importedUnwrappingKey));
|
|
1567
|
+
}
|
|
1568
|
+
break;
|
|
1540
1569
|
case "A128KW":
|
|
1541
1570
|
case "A192KW":
|
|
1542
1571
|
case "A256KW":
|
|
1572
|
+
unwrappedCekBytes = await aesKwUnwrap(alg, importedUnwrappingKey, wrappedKey);
|
|
1573
|
+
break;
|
|
1543
1574
|
case "PBES2-HS256+A128KW":
|
|
1544
1575
|
case "PBES2-HS384+A192KW":
|
|
1545
1576
|
case "PBES2-HS512+A256KW": {
|
|
1546
1577
|
const { p2s, p2c } = options;
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
if (!p2s || typeof p2c !== "number") throw new Error("PBES2 requires 'p2s' (salt) and 'p2c' (count) options");
|
|
1550
|
-
p2sBytes = typeof p2s === "string" ? base64UrlDecode(p2s, false) : p2s;
|
|
1551
|
-
}
|
|
1552
|
-
unwrappedCekBytes = await unwrap(alg, importedUnwrappingKey, wrappedKey, p2c, p2sBytes);
|
|
1578
|
+
if (!p2s || typeof p2c !== "number") throw new Error("PBES2 requires 'p2s' (salt) and 'p2c' (count) options");
|
|
1579
|
+
unwrappedCekBytes = await pbes2Unwrap(alg, importedUnwrappingKey, wrappedKey, p2c, typeof p2s === "string" ? base64UrlDecode(p2s, false) : p2s);
|
|
1553
1580
|
break;
|
|
1554
1581
|
}
|
|
1555
1582
|
case "A128GCMKW":
|
|
1556
1583
|
case "A192GCMKW":
|
|
1557
1584
|
case "A256GCMKW":
|
|
1558
1585
|
if (!options.iv || !options.tag) throw new Error("AES-GCMKW requires 'iv' and 'tag' options for unwrapping");
|
|
1559
|
-
unwrappedCekBytes = await
|
|
1586
|
+
unwrappedCekBytes = await gcmkwDecrypt(alg, importedUnwrappingKey, wrappedKey, typeof options.iv === "string" ? base64UrlDecode(options.iv, false) : options.iv, typeof options.tag === "string" ? base64UrlDecode(options.tag, false) : options.tag);
|
|
1560
1587
|
break;
|
|
1561
1588
|
case "RSA-OAEP":
|
|
1562
1589
|
case "RSA-OAEP-256":
|
|
@@ -1564,7 +1591,7 @@ async function unwrapKey(alg, wrappedKey, unwrappingKey, options = {}) {
|
|
|
1564
1591
|
case "RSA-OAEP-512": {
|
|
1565
1592
|
if (!(importedUnwrappingKey instanceof CryptoKey)) throw new TypeError("RSA-OAEP requires the unwrapping key to be provided as a CryptoKey");
|
|
1566
1593
|
unwrappedCekBytes = await decryptRSAES(alg, importedUnwrappingKey, wrappedKey);
|
|
1567
|
-
if (
|
|
1594
|
+
if (returnRaw) break;
|
|
1568
1595
|
const inferredAlgorithm = options.unwrappedKeyAlgorithm || inferAesImportAlgorithm(options.enc, unwrappedCekBytes.length);
|
|
1569
1596
|
if (!inferredAlgorithm) throw new Error("Unable to infer algorithm for RSA-OAEP unwrapped key. Provide \"unwrappedKeyAlgorithm\" in options.");
|
|
1570
1597
|
const keyUsages = options.keyUsage || ["encrypt", "decrypt"];
|
|
@@ -1576,7 +1603,7 @@ async function unwrapKey(alg, wrappedKey, unwrappingKey, options = {}) {
|
|
|
1576
1603
|
case "ECDH-ES+A256KW": {
|
|
1577
1604
|
if (!options.epk) throw new Error("ECDH-ES requires 'epk' (Ephemeral Public Key) option");
|
|
1578
1605
|
if (!(importedUnwrappingKey instanceof CryptoKey)) throw new TypeError("ECDH-ES requires the unwrapping key to be a CryptoKey");
|
|
1579
|
-
if (!
|
|
1606
|
+
if (!isECDHKeyAllowed(importedUnwrappingKey)) throw new Error("ECDH with the provided key is not allowed or not supported");
|
|
1580
1607
|
const epkCandidate = options.epk instanceof CryptoKey ? options.epk : await normalizeKey(options.epk, alg);
|
|
1581
1608
|
if (!(epkCandidate instanceof CryptoKey)) throw new TypeError("Failed to normalize ECDH ephemeral public key");
|
|
1582
1609
|
const apuBytes = typeof options.apu === "string" ? base64UrlDecode(options.apu, false) : options.apu ?? new Uint8Array(0);
|
|
@@ -1588,13 +1615,13 @@ async function unwrapKey(alg, wrappedKey, unwrappingKey, options = {}) {
|
|
|
1588
1615
|
else {
|
|
1589
1616
|
const kwAlg = alg.slice(-6);
|
|
1590
1617
|
if (!(wrappedKey instanceof Uint8Array) || wrappedKey.length === 0) throw new Error("ECDH-ES key agreement with key wrapping requires an encrypted key");
|
|
1591
|
-
unwrappedCekBytes = await
|
|
1618
|
+
unwrappedCekBytes = await aesKwUnwrap(kwAlg, sharedSecret, wrappedKey);
|
|
1592
1619
|
}
|
|
1593
1620
|
break;
|
|
1594
1621
|
}
|
|
1595
1622
|
default: throw new Error(`Unsupported key unwrapping algorithm: ${alg}`);
|
|
1596
1623
|
}
|
|
1597
|
-
if (
|
|
1624
|
+
if (returnRaw) return unwrappedCekBytes;
|
|
1598
1625
|
return await crypto.subtle.importKey("raw", unwrappedCekBytes, options.unwrappedKeyAlgorithm || { name: "AES-GCM" }, defaultExtractable, options.keyUsage || ["encrypt", "decrypt"]);
|
|
1599
1626
|
}
|
|
1600
1627
|
/**
|
|
@@ -1608,35 +1635,25 @@ async function unwrapKey(alg, wrappedKey, unwrappingKey, options = {}) {
|
|
|
1608
1635
|
* @param jwkExtras Additional properties to merge into the resulting JWK.
|
|
1609
1636
|
* @returns A Promise resolving to the imported key as a JWK.
|
|
1610
1637
|
*/
|
|
1611
|
-
async function
|
|
1638
|
+
async function importFromPEM(pem, pemType, alg, options) {
|
|
1639
|
+
const extractable = options?.extractable !== false;
|
|
1612
1640
|
let cryptoKey;
|
|
1613
|
-
const defaultExtractable = importOptions?.extractable !== false;
|
|
1614
1641
|
switch (pemType) {
|
|
1615
1642
|
case "pkcs8":
|
|
1616
|
-
cryptoKey = await fromPKCS8(pem, alg, {
|
|
1617
|
-
...importOptions,
|
|
1618
|
-
extractable: defaultExtractable
|
|
1619
|
-
});
|
|
1643
|
+
cryptoKey = await fromPKCS8(pem, alg, { extractable });
|
|
1620
1644
|
break;
|
|
1621
1645
|
case "spki":
|
|
1622
|
-
cryptoKey = await fromSPKI(pem, alg, {
|
|
1623
|
-
...importOptions,
|
|
1624
|
-
extractable: defaultExtractable
|
|
1625
|
-
});
|
|
1646
|
+
cryptoKey = await fromSPKI(pem, alg, { extractable });
|
|
1626
1647
|
break;
|
|
1627
1648
|
case "x509":
|
|
1628
|
-
cryptoKey = await fromX509(pem, alg, {
|
|
1629
|
-
...importOptions,
|
|
1630
|
-
extractable: defaultExtractable
|
|
1631
|
-
});
|
|
1649
|
+
cryptoKey = await fromX509(pem, alg, { extractable });
|
|
1632
1650
|
break;
|
|
1633
1651
|
default: throw new TypeError(`Unsupported PEM type: ${pemType}`);
|
|
1634
1652
|
}
|
|
1635
|
-
|
|
1653
|
+
return exportKey(cryptoKey, {
|
|
1636
1654
|
alg,
|
|
1637
|
-
...
|
|
1638
|
-
};
|
|
1639
|
-
return exportKey(cryptoKey, finalJWKExtras);
|
|
1655
|
+
...options?.jwkParams
|
|
1656
|
+
});
|
|
1640
1657
|
}
|
|
1641
1658
|
/**
|
|
1642
1659
|
* Exports a JWK to a PEM-encoded string.
|
|
@@ -1647,7 +1664,7 @@ async function importJWKFromPEM(pem, pemType, alg, importOptions, jwkExtras) {
|
|
|
1647
1664
|
* required to correctly convert it to a CryptoKey first.
|
|
1648
1665
|
* @returns A Promise resolving to the PEM-encoded key string.
|
|
1649
1666
|
*/
|
|
1650
|
-
async function
|
|
1667
|
+
async function exportToPEM(jwk, pemFormat, algForCryptoKeyImport) {
|
|
1651
1668
|
if (jwk.kty === "oct") throw new TypeError("Octet (symmetric) JWKs (kty: 'oct') cannot be exported to PKCS8 or SPKI PEM formats.");
|
|
1652
1669
|
const effectiveAlg = jwk.alg || algForCryptoKeyImport;
|
|
1653
1670
|
if (!effectiveAlg && (jwk.kty === "RSA" || jwk.kty === "EC" || jwk.kty === "OKP")) throw new TypeError("Algorithm (alg) must be provided in the JWK or as a parameter for converting this JWK type to a CryptoKey.");
|
|
@@ -1668,43 +1685,19 @@ async function exportJWKToPEM(jwk, pemFormat, algForCryptoKeyImport) {
|
|
|
1668
1685
|
}
|
|
1669
1686
|
}
|
|
1670
1687
|
/**
|
|
1671
|
-
*
|
|
1672
|
-
*
|
|
1688
|
+
* Returns all JWKs from a JWK Set, optionally narrowed by a predicate.
|
|
1689
|
+
*
|
|
1690
|
+
* Useful for multi-key verification retry, key rotation tooling, and
|
|
1691
|
+
* constructing multi-recipient JWE JSON Serialization structures.
|
|
1673
1692
|
*
|
|
1674
|
-
* @param jwkSet The
|
|
1675
|
-
* @param
|
|
1676
|
-
*
|
|
1677
|
-
* provided, it must contain a 'kid' property. It can optionally contain
|
|
1678
|
-
* 'alg' (Algorithm) and 'kty' (Key Type) to further refine the search.
|
|
1679
|
-
* @returns The matching JWK from the set.
|
|
1680
|
-
* @throws TypeError if `jwkSet` is invalid or `kidOrProtectedHeader` is not a
|
|
1681
|
-
* string or a valid header object.
|
|
1682
|
-
* @throws Error if no key is found matching the provided criteria, or if the
|
|
1683
|
-
* header object is missing the 'kid' property.
|
|
1693
|
+
* @param jwkSet The JWK Set to search.
|
|
1694
|
+
* @param filter Optional predicate `(jwk: JWK) => boolean`. Returns all keys when omitted.
|
|
1695
|
+
* @returns An array of matching JWKs.
|
|
1684
1696
|
*/
|
|
1685
|
-
function
|
|
1697
|
+
function getJWKsFromSet(jwkSet, filter) {
|
|
1686
1698
|
if (!jwkSet || !isJWKSet(jwkSet)) throw new TypeError("Invalid JWK Set provided");
|
|
1687
|
-
if (
|
|
1688
|
-
|
|
1689
|
-
const selectedKey = jwkSet.keys.find((k) => k.kid === kid);
|
|
1690
|
-
if (!selectedKey) throw new Error(`No key found in JWK Set with kid "${kid}".`);
|
|
1691
|
-
return selectedKey;
|
|
1692
|
-
} else if (typeof kidOrProtectedHeader === "object") {
|
|
1693
|
-
const { kid, alg, kty } = kidOrProtectedHeader;
|
|
1694
|
-
if (!kid) throw new Error("JWS Protected Header is missing 'kid' (Key ID) and a JWK Set was provided. Cannot select key from JWK Set automatically.");
|
|
1695
|
-
const selectedKey = jwkSet.keys.find((k) => {
|
|
1696
|
-
return k.kid === kid && (!alg || k.alg === alg) && (!kty || k.kty === kty);
|
|
1697
|
-
});
|
|
1698
|
-
if (!selectedKey) {
|
|
1699
|
-
let errorMessage = `No key found in JWK Set with kid "${kid}"`;
|
|
1700
|
-
if (alg) errorMessage += ` and alg "${alg}"`;
|
|
1701
|
-
if (kty) errorMessage += ` and kty "${kty}"`;
|
|
1702
|
-
errorMessage += ".";
|
|
1703
|
-
throw new Error(errorMessage);
|
|
1704
|
-
}
|
|
1705
|
-
return selectedKey;
|
|
1706
|
-
}
|
|
1707
|
-
throw new TypeError("`kidOrProtectedHeader` must be a string (kid) or an object (JOSE Protected Header).");
|
|
1699
|
+
if (!filter) return [...jwkSet.keys];
|
|
1700
|
+
return jwkSet.keys.filter(filter);
|
|
1708
1701
|
}
|
|
1709
1702
|
function getGenerateKeyParams(alg, options) {
|
|
1710
1703
|
let algorithm;
|
|
@@ -1865,20 +1858,23 @@ function inferAesImportAlgorithm(enc, cekLengthBytes) {
|
|
|
1865
1858
|
//#endregion
|
|
1866
1859
|
//#region src/core/jwe.ts
|
|
1867
1860
|
async function encrypt(payload, key, options = {}) {
|
|
1868
|
-
const { protectedHeader:
|
|
1861
|
+
const { protectedHeader: userHeader, cek: providedCek, contentEncryptionIV: providedContentIV, keyManagementIV, p2s, p2c, ecdh } = options;
|
|
1869
1862
|
let { alg, enc } = options;
|
|
1870
1863
|
if (!alg) {
|
|
1871
1864
|
if (typeof key === "string" || key instanceof Uint8Array) alg = "PBES2-HS256+A128KW";
|
|
1872
1865
|
else if (isJWK(key)) alg = key.alg === "A128GCM" || key.alg === "A192GCM" || key.alg === "A256GCM" ? `${key.alg}KW` : key.alg;
|
|
1873
1866
|
}
|
|
1874
1867
|
if (!alg) throw new TypeError("JWE \"alg\" (Key Management Algorithm) must be provided in options or inferable from the key");
|
|
1875
|
-
if (!enc)
|
|
1868
|
+
if (!enc) if (alg === "dir") {
|
|
1869
|
+
enc = isJWK(key) && "enc" in key ? key.enc : void 0;
|
|
1870
|
+
if (!enc) throw new TypeError("JWE \"enc\" must be provided when alg is \"dir\"");
|
|
1871
|
+
} else enc = isJWK(key) && "enc" in key ? key.enc : "A128GCM";
|
|
1876
1872
|
const jweKeyManagementParams = {};
|
|
1877
1873
|
if (keyManagementIV) jweKeyManagementParams.iv = keyManagementIV;
|
|
1878
1874
|
if (p2s) jweKeyManagementParams.p2s = p2s;
|
|
1879
1875
|
else if (alg?.startsWith("PBES2")) jweKeyManagementParams.p2s = randomBytes(16);
|
|
1880
1876
|
if (p2c) jweKeyManagementParams.p2c = p2c;
|
|
1881
|
-
else if (alg?.startsWith("PBES2")) jweKeyManagementParams.p2c =
|
|
1877
|
+
else if (alg?.startsWith("PBES2")) jweKeyManagementParams.p2c = 6e5;
|
|
1882
1878
|
if (ecdh?.partyUInfo) jweKeyManagementParams.apu = ecdh.partyUInfo;
|
|
1883
1879
|
if (ecdh?.partyVInfo) jweKeyManagementParams.apv = ecdh.partyVInfo;
|
|
1884
1880
|
if (ecdh?.ephemeralKey) {
|
|
@@ -1888,21 +1884,14 @@ async function encrypt(payload, key, options = {}) {
|
|
|
1888
1884
|
}
|
|
1889
1885
|
const wrappingKeyMaterial = await importKey(key, alg);
|
|
1890
1886
|
const { cek: finalCek, encryptedKey: jweEncryptedKey, parameters: keyManagementHeaderParams } = await encryptKey(alg, enc, wrappingKeyMaterial, providedCek, jweKeyManagementParams);
|
|
1891
|
-
const
|
|
1892
|
-
...additionalProtectedHeader,
|
|
1893
|
-
...keyManagementHeaderParams,
|
|
1894
|
-
alg,
|
|
1895
|
-
enc
|
|
1896
|
-
};
|
|
1897
|
-
if (isJWK(key) && key.kid) jweProtectedHeader.kid = key.kid;
|
|
1898
|
-
const protectedHeader = applyTypCtyDefaults(jweProtectedHeader, payload);
|
|
1887
|
+
const protectedHeader = _buildJWEHeader(alg, enc, key, userHeader, keyManagementHeaderParams, payload);
|
|
1899
1888
|
const plaintextBytes = getPlaintextBytes(computeJwtTimeClaims(payload, protectedHeader.typ, options.expiresIn, options.currentDate) || payload);
|
|
1900
1889
|
const protectedHeaderEncoded = base64UrlEncode(JSON.stringify(protectedHeader));
|
|
1901
1890
|
const aadBytes = textEncoder.encode(protectedHeaderEncoded);
|
|
1902
1891
|
const contentIVBytes = providedContentIV ?? generateIV(enc);
|
|
1903
1892
|
const { ciphertext: contentCiphertext, tag: contentAuthTag, iv: actualContentIV } = await encrypt$1(enc, plaintextBytes, finalCek, contentIVBytes, aadBytes);
|
|
1904
|
-
if (!actualContentIV) throw new
|
|
1905
|
-
if (!contentAuthTag) throw new
|
|
1893
|
+
if (!actualContentIV) throw new JWTError("Content encryption IV was not generated or returned.", "ERR_JWE_INVALID");
|
|
1894
|
+
if (!contentAuthTag) throw new JWTError("Content encryption auth tag was not generated or returned.", "ERR_JWE_INVALID");
|
|
1906
1895
|
return [
|
|
1907
1896
|
protectedHeaderEncoded,
|
|
1908
1897
|
jweEncryptedKey ? base64UrlEncode(jweEncryptedKey) : "",
|
|
@@ -1913,28 +1902,28 @@ async function encrypt(payload, key, options = {}) {
|
|
|
1913
1902
|
}
|
|
1914
1903
|
async function decrypt(jwe, key, options = {}) {
|
|
1915
1904
|
const parts = jwe.split(".");
|
|
1916
|
-
if (parts.length !== 5) throw new
|
|
1905
|
+
if (parts.length !== 5) throw new JWTError("Invalid JWE: Must contain five sections (RFC7516, section-3).", "ERR_JWE_INVALID");
|
|
1917
1906
|
const [protectedHeaderEncoded, encryptedKeyEncoded, ivEncoded, ciphertextEncoded, authTagEncoded] = parts;
|
|
1918
1907
|
let protectedHeader;
|
|
1919
1908
|
try {
|
|
1920
1909
|
const protectedHeaderJson = base64UrlDecode(protectedHeaderEncoded);
|
|
1921
1910
|
protectedHeader = sanitizeObject(JSON.parse(protectedHeaderJson));
|
|
1922
1911
|
} catch {
|
|
1923
|
-
throw new
|
|
1912
|
+
throw new JWTError("Invalid JWE: Protected header could not be decoded.", "ERR_JWE_INVALID");
|
|
1924
1913
|
}
|
|
1925
|
-
if (!protectedHeader || typeof protectedHeader !== "object" || !protectedHeader.alg || !protectedHeader.enc) throw new
|
|
1914
|
+
if (!protectedHeader || typeof protectedHeader !== "object" || !protectedHeader.alg || !protectedHeader.enc) throw new JWTError("Invalid JWE: Protected header must be an object with \"alg\" and \"enc\" properties.", "ERR_JWE_INVALID");
|
|
1926
1915
|
const alg = protectedHeader.alg;
|
|
1927
1916
|
const enc = protectedHeader.enc;
|
|
1928
|
-
if (options?.algorithms && !options.algorithms.includes(alg)) throw new
|
|
1929
|
-
if (options?.encryptionAlgorithms && !options.encryptionAlgorithms.includes(enc)) throw new
|
|
1930
|
-
const
|
|
1917
|
+
if (options?.algorithms && !options.algorithms.includes(alg)) throw new JWTError(`Key management algorithm not allowed: ${alg}`, "ERR_JWE_ALG_NOT_ALLOWED");
|
|
1918
|
+
if (options?.encryptionAlgorithms && !options.encryptionAlgorithms.includes(enc)) throw new JWTError(`Content encryption algorithm not allowed: ${enc}`, "ERR_JWE_ALG_NOT_ALLOWED");
|
|
1919
|
+
const rawKeyMaterial = typeof key === "function" ? await key(protectedHeader, jwe) : key;
|
|
1931
1920
|
const encryptedKeyBytes = base64UrlDecode(encryptedKeyEncoded, false);
|
|
1932
1921
|
const contentIVBytes = base64UrlDecode(ivEncoded, false);
|
|
1933
1922
|
const contentAuthTagBytes = base64UrlDecode(authTagEncoded, false);
|
|
1934
1923
|
const ciphertextBytes = base64UrlDecode(ciphertextEncoded, false);
|
|
1935
1924
|
const unwrapKeyOpts = {
|
|
1936
1925
|
enc,
|
|
1937
|
-
|
|
1926
|
+
format: enc.includes("GCM") ? "cryptokey" : "raw",
|
|
1938
1927
|
keyUsage: options?.keyUsage || ["decrypt"]
|
|
1939
1928
|
};
|
|
1940
1929
|
if (protectedHeader.iv) unwrapKeyOpts.iv = protectedHeader.iv;
|
|
@@ -1946,11 +1935,34 @@ async function decrypt(jwe, key, options = {}) {
|
|
|
1946
1935
|
if (protectedHeader.apv) unwrapKeyOpts.apv = protectedHeader.apv;
|
|
1947
1936
|
if (options?.unwrappedKeyAlgorithm) unwrapKeyOpts.unwrappedKeyAlgorithm = options.unwrappedKeyAlgorithm;
|
|
1948
1937
|
if (options?.extractable) unwrapKeyOpts.extractable = options.extractable;
|
|
1949
|
-
const cek = await unwrapKey(alg, encryptedKeyBytes, unwrappingKey, unwrapKeyOpts);
|
|
1950
1938
|
const aadBytes = textEncoder.encode(protectedHeaderEncoded);
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
if (
|
|
1939
|
+
let cek;
|
|
1940
|
+
let plaintextBytes;
|
|
1941
|
+
if (isJWKSet(rawKeyMaterial)) {
|
|
1942
|
+
const candidates = getJWKsFromSet(rawKeyMaterial, _buildJWESetFilter(protectedHeader));
|
|
1943
|
+
if (candidates.length === 0) throw new JWTError(`No key found in JWK Set${protectedHeader.kid ? ` with kid "${protectedHeader.kid}"` : ""}.`, "ERR_JWK_KEY_NOT_FOUND");
|
|
1944
|
+
let decrypted = false;
|
|
1945
|
+
for (const candidate of candidates) try {
|
|
1946
|
+
const candidateCek = await unwrapKey(alg, encryptedKeyBytes, await importKey(candidate, alg), unwrapKeyOpts);
|
|
1947
|
+
plaintextBytes = await decrypt$1(enc, candidateCek, ciphertextBytes, contentIVBytes, contentAuthTagBytes, aadBytes);
|
|
1948
|
+
cek = candidateCek;
|
|
1949
|
+
decrypted = true;
|
|
1950
|
+
break;
|
|
1951
|
+
} catch {}
|
|
1952
|
+
if (!decrypted) throw new JWTError("JWE decryption failed.", "ERR_JWE_DECRYPTION_FAILED");
|
|
1953
|
+
} else {
|
|
1954
|
+
const unwrappingKey = await importKey(rawKeyMaterial, alg);
|
|
1955
|
+
try {
|
|
1956
|
+
cek = await unwrapKey(alg, encryptedKeyBytes, unwrappingKey, unwrapKeyOpts);
|
|
1957
|
+
plaintextBytes = await decrypt$1(enc, cek, ciphertextBytes, contentIVBytes, contentAuthTagBytes, aadBytes);
|
|
1958
|
+
} catch (error_) {
|
|
1959
|
+
if (error_ instanceof JWTError) throw error_;
|
|
1960
|
+
throw new JWTError("JWE decryption failed.", "ERR_JWE_DECRYPTION_FAILED", error_);
|
|
1961
|
+
}
|
|
1962
|
+
}
|
|
1963
|
+
const payload = decodePayloadFromBytes(plaintextBytes, protectedHeader, options?.forceUint8Array);
|
|
1964
|
+
if (protectedHeader.crit || options?.recognizedHeaders?.length) validateCriticalHeadersJWE(protectedHeader, options?.recognizedHeaders);
|
|
1965
|
+
if (payload && typeof payload === "object" && options.validateClaims !== false && (options.validateClaims === true || protectedHeader.typ?.toLowerCase().includes("jwt")) && !options.forceUint8Array && !(payload instanceof Uint8Array)) validateJwtClaims(payload, options);
|
|
1954
1966
|
const result = {
|
|
1955
1967
|
payload,
|
|
1956
1968
|
protectedHeader
|
|
@@ -1986,24 +1998,30 @@ function parseEphemeralKey(ephemeralKey) {
|
|
|
1986
1998
|
epkPrivateKey
|
|
1987
1999
|
};
|
|
1988
2000
|
}
|
|
2001
|
+
function _buildJWESetFilter(header) {
|
|
2002
|
+
const { kid, alg } = header;
|
|
2003
|
+
return (k) => (!kid || k.kid === kid) && (!k.alg || k.alg === alg);
|
|
2004
|
+
}
|
|
2005
|
+
function _buildJWEHeader(alg, enc, key, userHeader, keyManagementParams, payload) {
|
|
2006
|
+
const safeHeader = sanitizeObject(userHeader);
|
|
2007
|
+
return applyTypCtyDefaults({
|
|
2008
|
+
...isJWK(key) && key.kid ? { kid: key.kid } : {},
|
|
2009
|
+
...safeHeader,
|
|
2010
|
+
...keyManagementParams,
|
|
2011
|
+
alg,
|
|
2012
|
+
enc
|
|
2013
|
+
}, payload);
|
|
2014
|
+
}
|
|
1989
2015
|
//#endregion
|
|
1990
2016
|
//#region src/core/jws.ts
|
|
1991
2017
|
async function sign(payload, key, options = {}) {
|
|
1992
|
-
const { protectedHeader:
|
|
1993
|
-
const safeAdditionalHeader = sanitizeObject(additionalProtectedHeader);
|
|
2018
|
+
const { protectedHeader: userHeader } = options;
|
|
1994
2019
|
let { alg } = options;
|
|
1995
2020
|
if (!alg) if (isJWK(key) && key.alg) alg = key.alg;
|
|
1996
2021
|
else throw new TypeError("JWS \"alg\" (Algorithm) must be provided in options");
|
|
1997
|
-
|
|
1998
|
-
const signingKey = await importKey(key, alg);
|
|
1999
|
-
const protectedHeader =
|
|
2000
|
-
...safeAdditionalHeader,
|
|
2001
|
-
...isJWK(key) && key.kid ? { kid: key.kid } : {},
|
|
2002
|
-
alg,
|
|
2003
|
-
typ: safeAdditionalHeader?.typ
|
|
2004
|
-
};
|
|
2005
|
-
applyTypCtyDefaults(protectedHeader, payload);
|
|
2006
|
-
if (protectedHeader.b64 === true) delete protectedHeader.b64;
|
|
2022
|
+
if (alg === "none") throw new JWTError("\"none\" is not a valid signing algorithm", "ERR_JWS_ALG_NOT_ALLOWED");
|
|
2023
|
+
const signingKey = await _resolveSigningKey(alg, await importKey(key, alg), "sign");
|
|
2024
|
+
const protectedHeader = _buildJWSHeader(alg, key, userHeader, payload);
|
|
2007
2025
|
const computedPayload = computeJwtTimeClaims(payload, protectedHeader.typ, options.expiresIn, options.currentDate);
|
|
2008
2026
|
const protectedHeaderEncoded = base64UrlEncode(JSON.stringify(protectedHeader));
|
|
2009
2027
|
let payloadBytes;
|
|
@@ -2017,53 +2035,82 @@ async function sign(payload, key, options = {}) {
|
|
|
2017
2035
|
}
|
|
2018
2036
|
async function verify(jws, key, options = {}) {
|
|
2019
2037
|
const parts = jws.split(".");
|
|
2020
|
-
if (parts.length !== 3) throw new
|
|
2038
|
+
if (parts.length !== 3) throw new JWTError("Invalid JWS: Must contain three parts separated by dots.", "ERR_JWS_INVALID");
|
|
2021
2039
|
const [protectedHeaderEncoded, payloadEncoded, signatureEncoded] = parts;
|
|
2022
2040
|
let protectedHeader;
|
|
2023
2041
|
try {
|
|
2024
2042
|
const protectedHeaderString = base64UrlDecode(protectedHeaderEncoded);
|
|
2025
2043
|
protectedHeader = sanitizeObject(JSON.parse(protectedHeaderString));
|
|
2026
2044
|
} catch {
|
|
2027
|
-
throw new
|
|
2045
|
+
throw new JWTError("Invalid JWS: Protected header could not be decoded.", "ERR_JWS_INVALID");
|
|
2028
2046
|
}
|
|
2029
|
-
if (!protectedHeader || typeof protectedHeader !== "object" || !protectedHeader.alg) throw new
|
|
2047
|
+
if (!protectedHeader || typeof protectedHeader !== "object" || !protectedHeader.alg) throw new JWTError("Invalid JWS: Protected header must be an object with an \"alg\" property.", "ERR_JWS_INVALID");
|
|
2030
2048
|
const alg = protectedHeader.alg;
|
|
2031
|
-
if (options.algorithms && !options.algorithms.includes(alg)) throw new
|
|
2032
|
-
if (options.typ && protectedHeader.typ !== options.typ) throw new
|
|
2049
|
+
if (options.algorithms && !options.algorithms.includes(alg)) throw new JWTError(`Algorithm not allowed: ${alg}`, "ERR_JWS_ALG_NOT_ALLOWED");
|
|
2050
|
+
if (options.typ && protectedHeader.typ !== options.typ) throw new JWTError(`Invalid JWS: "typ" (Type) Header Parameter mismatch. Expected "${options.typ}", got "${protectedHeader.typ}".`, "ERR_JWS_INVALID");
|
|
2033
2051
|
let signatureBytes;
|
|
2034
2052
|
try {
|
|
2035
2053
|
signatureBytes = base64UrlDecode(signatureEncoded, false);
|
|
2036
2054
|
} catch {
|
|
2037
|
-
throw new
|
|
2055
|
+
throw new JWTError("Invalid JWS: Signature could not be decoded.", "ERR_JWS_INVALID");
|
|
2038
2056
|
}
|
|
2039
2057
|
const keyInput = typeof key === "function" ? await key(protectedHeader, jws) : key;
|
|
2040
|
-
const
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2058
|
+
const signingInputBytes = textEncoder.encode(`${protectedHeaderEncoded}.${payloadEncoded}`);
|
|
2059
|
+
if (isJWKSet(keyInput)) {
|
|
2060
|
+
const candidates = getJWKsFromSet(keyInput, _buildJWSSetFilter(protectedHeader));
|
|
2061
|
+
if (candidates.length === 0) throw new JWTError(`No key found in JWK Set${protectedHeader.kid ? ` with kid "${protectedHeader.kid}"` : ""}.`, "ERR_JWK_KEY_NOT_FOUND");
|
|
2062
|
+
let verified = false;
|
|
2063
|
+
for (const candidate of candidates) try {
|
|
2064
|
+
if (await verify$1(alg, await _resolveSigningKey(alg, await importKey(candidate, alg), "verify"), signatureBytes, signingInputBytes)) {
|
|
2065
|
+
verified = true;
|
|
2066
|
+
break;
|
|
2067
|
+
}
|
|
2068
|
+
} catch {}
|
|
2069
|
+
if (!verified) throw new JWTError("JWS signature verification failed.", "ERR_JWS_SIGNATURE_INVALID");
|
|
2070
|
+
} else if (!await verify$1(alg, await _resolveSigningKey(alg, await importKey(keyInput, alg), "verify"), signatureBytes, signingInputBytes)) throw new JWTError("JWS signature verification failed.", "ERR_JWS_SIGNATURE_INVALID");
|
|
2044
2071
|
const useB64 = protectedHeader.b64 !== false;
|
|
2045
2072
|
let payload;
|
|
2046
2073
|
try {
|
|
2047
2074
|
payload = useB64 ? decodePayloadFromB64UrlSegment(payloadEncoded, protectedHeader, options.forceUint8Array) : options.forceUint8Array ? textEncoder.encode(payloadEncoded) : payloadEncoded;
|
|
2048
2075
|
} catch (error_) {
|
|
2049
|
-
throw new
|
|
2076
|
+
throw new JWTError(`Invalid JWS: Payload decoding failed (${error_ instanceof Error ? error_.message : error_})`, "ERR_JWS_INVALID");
|
|
2050
2077
|
}
|
|
2051
|
-
validateCriticalHeadersJWS(protectedHeader,
|
|
2052
|
-
if (payload && typeof payload === "object" && options.
|
|
2078
|
+
validateCriticalHeadersJWS(protectedHeader, options.recognizedHeaders);
|
|
2079
|
+
if (payload && typeof payload === "object" && options.validateClaims !== false && (options.validateClaims === true || protectedHeader.typ?.toLowerCase().includes("jwt")) && !options.forceUint8Array && !(payload instanceof Uint8Array)) validateJwtClaims(payload, options);
|
|
2053
2080
|
return {
|
|
2054
2081
|
payload,
|
|
2055
2082
|
protectedHeader
|
|
2056
2083
|
};
|
|
2057
2084
|
}
|
|
2058
|
-
function
|
|
2059
|
-
if (
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2085
|
+
async function _resolveSigningKey(alg, key, usage) {
|
|
2086
|
+
if (key instanceof Uint8Array) {
|
|
2087
|
+
const minBytes = Number.parseInt(alg.slice(2), 10) / 8;
|
|
2088
|
+
if (key.length < minBytes) throw new JWTError(`${alg} requires a key of at least ${minBytes} bytes`, "ERR_JWK_INVALID");
|
|
2089
|
+
return crypto.subtle.importKey("raw", key, {
|
|
2090
|
+
name: "HMAC",
|
|
2091
|
+
hash: `SHA-${alg.slice(-3)}`
|
|
2092
|
+
}, false, [usage]);
|
|
2093
|
+
}
|
|
2094
|
+
if (alg.startsWith("RS") || alg.startsWith("PS")) {
|
|
2064
2095
|
const { modulusLength } = key.algorithm;
|
|
2065
|
-
if (typeof modulusLength !== "number" || modulusLength < 2048) throw new
|
|
2096
|
+
if (typeof modulusLength !== "number" || modulusLength < 2048) throw new JWTError(`${alg} requires a key modulusLength of at least 2048 bits`, "ERR_JWK_INVALID");
|
|
2066
2097
|
}
|
|
2098
|
+
return key;
|
|
2099
|
+
}
|
|
2100
|
+
function _buildJWSSetFilter(header) {
|
|
2101
|
+
const { kid, alg } = header;
|
|
2102
|
+
return (k) => (!kid || k.kid === kid) && (!k.alg || k.alg === alg);
|
|
2103
|
+
}
|
|
2104
|
+
function _buildJWSHeader(alg, key, userHeader, payload) {
|
|
2105
|
+
const safeHeader = sanitizeObject(userHeader);
|
|
2106
|
+
const header = {
|
|
2107
|
+
...isJWK(key) && key.kid ? { kid: key.kid } : {},
|
|
2108
|
+
...safeHeader,
|
|
2109
|
+
alg
|
|
2110
|
+
};
|
|
2111
|
+
applyTypCtyDefaults(header, payload);
|
|
2112
|
+
if (header.b64 === true) delete header.b64;
|
|
2113
|
+
return header;
|
|
2067
2114
|
}
|
|
2068
2115
|
//#endregion
|
|
2069
|
-
export { deriveJWKFromPassword as a,
|
|
2116
|
+
export { deriveJWKFromPassword as a, importFromPEM as c, isSymmetricJWK as d, computeExpiresInSeconds as f, encrypt as i, isPrivateJWK as l, isJWTError as m, verify as n, exportToPEM as o, JWTError as p, decrypt as r, generateJWK as s, sign as t, isPublicJWK as u };
|