unjwt 0.5.40 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +147 -6
- package/dist/_chunks/_crypto.mjs +1014 -0
- package/dist/_chunks/error.d.mts +67 -0
- package/dist/_chunks/index.d.mts +149 -45
- package/dist/_chunks/jwk.d.mts +154 -57
- package/dist/_chunks/jws.mjs +729 -683
- package/dist/_chunks/utils.mjs +74 -70
- package/dist/{_chunks/jws.d.mts → adapters/h3v1/index.d.mts} +98 -58
- package/dist/{_chunks/jws2.mjs → adapters/h3v1/index.mjs} +198 -85
- package/dist/adapters/h3v2/index.d.mts +213 -0
- package/dist/adapters/{h3v2.mjs → h3v2/index.mjs} +195 -89
- package/dist/core/_crypto/index.d.mts +71 -0
- package/dist/core/_crypto/index.mjs +2 -0
- package/dist/core/jwe.d.mts +40 -0
- package/dist/{_chunks → core}/jwe.mjs +61 -32
- package/dist/core/jwk.d.mts +231 -0
- package/dist/core/jwk.mjs +686 -0
- package/dist/core/jws.d.mts +31 -0
- package/dist/core/jws.mjs +104 -0
- package/dist/{_chunks/index2.d.mts → core/utils/index.d.mts} +13 -12
- package/dist/core/utils/index.mjs +2 -0
- package/dist/index.d.mts +7 -6
- package/dist/index.mjs +5 -5
- package/package.json +48 -40
- package/dist/_chunks/chunk.mjs +0 -13
- package/dist/_chunks/jwe.d.mts +0 -43
- package/dist/_chunks/jwk.mjs +0 -1592
- package/dist/adapters/h3.d.mts +0 -3
- package/dist/adapters/h3.mjs +0 -3
- package/dist/adapters/h3v1.d.mts +0 -3
- package/dist/adapters/h3v1.mjs +0 -3
- package/dist/adapters/h3v2.d.mts +0 -183
- package/dist/jwe.d.mts +0 -3
- package/dist/jwe.mjs +0 -2
- package/dist/jwk.d.mts +0 -3
- package/dist/jwk.mjs +0 -2
- package/dist/jws.d.mts +0 -3
- package/dist/jws.mjs +0 -2
- package/dist/utils.d.mts +0 -3
- package/dist/utils.mjs +0 -2
package/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,41 +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
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
const items = Array.isArray(current) ? current : Object.values(current);
|
|
200
|
-
for (const v of items) if (v && typeof v === "object") {
|
|
201
|
-
if (!seen.has(v)) {
|
|
202
|
-
seen.add(v);
|
|
203
|
-
sanitizeKeys(v, seen);
|
|
204
|
-
}
|
|
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;
|
|
205
242
|
}
|
|
243
|
+
return result;
|
|
206
244
|
}
|
|
207
245
|
//#endregion
|
|
208
246
|
//#region src/core/utils/index.ts
|
|
@@ -338,157 +376,61 @@ function isPublicJWK(key) {
|
|
|
338
376
|
return false;
|
|
339
377
|
}
|
|
340
378
|
//#endregion
|
|
341
|
-
//#region src/core/
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
*/
|
|
347
|
-
function unusable(name, prop = "algorithm.name") {
|
|
348
|
-
return /* @__PURE__ */ new TypeError(`CryptoKey does not support this operation, its ${prop} must be ${name}`);
|
|
349
|
-
}
|
|
350
|
-
function isAlgorithm(algorithm, name) {
|
|
351
|
-
return algorithm.name === name;
|
|
352
|
-
}
|
|
353
|
-
function getHashLength(hash) {
|
|
354
|
-
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);
|
|
355
384
|
}
|
|
356
|
-
function
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
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;
|
|
362
391
|
}
|
|
363
392
|
}
|
|
364
|
-
function checkUsage(key, usage) {
|
|
365
|
-
if (usage && !key.usages.includes(usage)) throw new TypeError(`CryptoKey does not support this operation, its usages must include ${usage}.`);
|
|
366
|
-
}
|
|
367
393
|
function checkSigCryptoKey(key, alg, usage) {
|
|
368
394
|
switch (alg) {
|
|
369
395
|
case "HS256":
|
|
370
396
|
case "HS384":
|
|
371
397
|
case "HS512": {
|
|
372
|
-
if (!isAlgorithm(key.algorithm, "HMAC")) throw unusable("HMAC");
|
|
398
|
+
if (!isAlgorithm$3(key.algorithm, "HMAC")) throw unusable$4("HMAC");
|
|
373
399
|
const expected = Number.parseInt(alg.slice(2), 10);
|
|
374
|
-
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");
|
|
375
401
|
break;
|
|
376
402
|
}
|
|
377
403
|
case "RS256":
|
|
378
404
|
case "RS384":
|
|
379
405
|
case "RS512": {
|
|
380
|
-
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");
|
|
381
407
|
const expected = Number.parseInt(alg.slice(2), 10);
|
|
382
|
-
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");
|
|
383
409
|
break;
|
|
384
410
|
}
|
|
385
411
|
case "PS256":
|
|
386
412
|
case "PS384":
|
|
387
413
|
case "PS512": {
|
|
388
|
-
if (!isAlgorithm(key.algorithm, "RSA-PSS")) throw unusable("RSA-PSS");
|
|
414
|
+
if (!isAlgorithm$3(key.algorithm, "RSA-PSS")) throw unusable$4("RSA-PSS");
|
|
389
415
|
const expected = Number.parseInt(alg.slice(2), 10);
|
|
390
|
-
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");
|
|
391
417
|
break;
|
|
392
418
|
}
|
|
393
419
|
case "Ed25519":
|
|
394
420
|
case "EdDSA":
|
|
395
|
-
if (!isAlgorithm(key.algorithm, "Ed25519")) throw unusable("Ed25519");
|
|
421
|
+
if (!isAlgorithm$3(key.algorithm, "Ed25519")) throw unusable$4("Ed25519");
|
|
396
422
|
break;
|
|
397
423
|
case "ES256":
|
|
398
424
|
case "ES384":
|
|
399
425
|
case "ES512": {
|
|
400
|
-
if (!isAlgorithm(key.algorithm, "ECDSA")) throw unusable("ECDSA");
|
|
426
|
+
if (!isAlgorithm$3(key.algorithm, "ECDSA")) throw unusable$4("ECDSA");
|
|
401
427
|
const expected = getNamedCurve$1(alg);
|
|
402
|
-
if (key.algorithm.namedCurve !== expected) throw unusable(expected, "algorithm.namedCurve");
|
|
403
|
-
break;
|
|
404
|
-
}
|
|
405
|
-
default: throw new TypeError("CryptoKey does not support this operation");
|
|
406
|
-
}
|
|
407
|
-
checkUsage(key, usage);
|
|
408
|
-
}
|
|
409
|
-
function checkEncCryptoKey(key, alg, usage) {
|
|
410
|
-
switch (alg) {
|
|
411
|
-
case "A128GCM":
|
|
412
|
-
case "A192GCM":
|
|
413
|
-
case "A256GCM": {
|
|
414
|
-
if (!isAlgorithm(key.algorithm, "AES-GCM")) throw unusable("AES-GCM");
|
|
415
|
-
const expected = Number.parseInt(alg.slice(1, 4), 10);
|
|
416
|
-
if (key.algorithm.length !== expected) throw unusable(expected, "algorithm.length");
|
|
417
|
-
break;
|
|
418
|
-
}
|
|
419
|
-
case "A128KW":
|
|
420
|
-
case "A192KW":
|
|
421
|
-
case "A256KW": {
|
|
422
|
-
if (!isAlgorithm(key.algorithm, "AES-KW")) throw unusable("AES-KW");
|
|
423
|
-
const expected = Number.parseInt(alg.slice(1, 4), 10);
|
|
424
|
-
if (key.algorithm.length !== expected) throw unusable(expected, "algorithm.length");
|
|
425
|
-
break;
|
|
426
|
-
}
|
|
427
|
-
case "ECDH":
|
|
428
|
-
switch (key.algorithm.name) {
|
|
429
|
-
case "ECDH":
|
|
430
|
-
case "X25519": break;
|
|
431
|
-
default: throw unusable("ECDH or X25519");
|
|
432
|
-
}
|
|
433
|
-
break;
|
|
434
|
-
case "PBES2-HS256+A128KW":
|
|
435
|
-
case "PBES2-HS384+A192KW":
|
|
436
|
-
case "PBES2-HS512+A256KW":
|
|
437
|
-
if (!isAlgorithm(key.algorithm, "PBKDF2")) throw unusable("PBKDF2");
|
|
438
|
-
break;
|
|
439
|
-
case "RSA-OAEP":
|
|
440
|
-
case "RSA-OAEP-256":
|
|
441
|
-
case "RSA-OAEP-384":
|
|
442
|
-
case "RSA-OAEP-512": {
|
|
443
|
-
if (!isAlgorithm(key.algorithm, "RSA-OAEP")) throw unusable("RSA-OAEP");
|
|
444
|
-
const expected = Number.parseInt(alg.slice(9), 10) || 1;
|
|
445
|
-
if (getHashLength(key.algorithm.hash) !== expected) throw unusable(`SHA-${expected}`, "algorithm.hash");
|
|
428
|
+
if (key.algorithm.namedCurve !== expected) throw unusable$4(expected, "algorithm.namedCurve");
|
|
446
429
|
break;
|
|
447
430
|
}
|
|
448
431
|
default: throw new TypeError("CryptoKey does not support this operation");
|
|
449
432
|
}
|
|
450
|
-
checkUsage(key, usage);
|
|
451
|
-
}
|
|
452
|
-
//#endregion
|
|
453
|
-
//#region src/core/jose/sign-verify.ts
|
|
454
|
-
/**
|
|
455
|
-
* This is a fork of Jose library's utility functions.
|
|
456
|
-
* @source https://github.com/panva/jose/tree/69b7960c67e05be55fa2ec31c74b987696c20c60/src/lib/sign.ts
|
|
457
|
-
* @source https://github.com/panva/jose/tree/69b7960c67e05be55fa2ec31c74b987696c20c60/src/lib/verify.ts
|
|
458
|
-
* @license MIT https://github.com/panva/jose/blob/69b7960c67e05be55fa2ec31c74b987696c20c60/LICENSE.md
|
|
459
|
-
*/
|
|
460
|
-
async function sign$1(alg, key, data) {
|
|
461
|
-
const cryptoKey = await getSignVerifyKey(alg, key, "sign");
|
|
462
|
-
checkKeyLength(alg, cryptoKey);
|
|
463
|
-
const signature = await crypto.subtle.sign(subtleAlgorithm(alg, cryptoKey.algorithm), cryptoKey, data);
|
|
464
|
-
return new Uint8Array(signature);
|
|
465
|
-
}
|
|
466
|
-
async function verify$1(alg, key, signature, data) {
|
|
467
|
-
const cryptoKey = await getSignVerifyKey(alg, key, "verify");
|
|
468
|
-
checkKeyLength(alg, cryptoKey);
|
|
469
|
-
const algorithm = subtleAlgorithm(alg, cryptoKey.algorithm);
|
|
470
|
-
try {
|
|
471
|
-
return await crypto.subtle.verify(algorithm, cryptoKey, signature, data);
|
|
472
|
-
} catch {
|
|
473
|
-
return false;
|
|
474
|
-
}
|
|
475
|
-
}
|
|
476
|
-
function checkKeyLength(alg, key) {
|
|
477
|
-
if (alg.startsWith("RS") || alg.startsWith("PS")) {
|
|
478
|
-
const { modulusLength } = key.algorithm;
|
|
479
|
-
if (typeof modulusLength !== "number" || modulusLength < 2048) throw new TypeError(`${alg} requires key modulusLength to be 2048 bits or larger`);
|
|
480
|
-
}
|
|
481
|
-
}
|
|
482
|
-
async function getSignVerifyKey(alg, key, usage) {
|
|
483
|
-
if (key instanceof Uint8Array) {
|
|
484
|
-
if (!alg.startsWith("HS")) throw new TypeError("Key must be of type CryptoKey or JSON Web Key for non-HMAC algorithms");
|
|
485
|
-
return crypto.subtle.importKey("raw", key, {
|
|
486
|
-
hash: `SHA-${alg.slice(-3)}`,
|
|
487
|
-
name: "HMAC"
|
|
488
|
-
}, false, [usage]);
|
|
489
|
-
}
|
|
490
|
-
checkSigCryptoKey(key, alg, usage);
|
|
491
|
-
return key;
|
|
433
|
+
checkUsage$4(key, usage);
|
|
492
434
|
}
|
|
493
435
|
function subtleAlgorithm(alg, algorithm) {
|
|
494
436
|
const hash = `SHA-${alg.slice(-3)}`;
|
|
@@ -524,44 +466,28 @@ function subtleAlgorithm(alg, algorithm) {
|
|
|
524
466
|
default: throw new Error(`alg ${alg} is not supported either by JOSE or your javascript runtime`);
|
|
525
467
|
}
|
|
526
468
|
}
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
/**
|
|
530
|
-
* This is a fork of Jose library's utility functions.
|
|
531
|
-
* @source https://github.com/panva/jose/tree/69b7960c67e05be55fa2ec31c74b987696c20c60/src/lib/buffer_utils.ts
|
|
532
|
-
* @license MIT https://github.com/panva/jose/blob/69b7960c67e05be55fa2ec31c74b987696c20c60/LICENSE.md
|
|
533
|
-
*/
|
|
534
|
-
const MAX_INT32 = 2 ** 32;
|
|
535
|
-
function writeUInt32BE(buf, value, offset) {
|
|
536
|
-
if (value < 0 || value >= MAX_INT32) throw new RangeError(`value must be >= 0 and <= ${MAX_INT32 - 1}. Received ${value}`);
|
|
537
|
-
buf.set([
|
|
538
|
-
value >>> 24,
|
|
539
|
-
value >>> 16,
|
|
540
|
-
value >>> 8,
|
|
541
|
-
value & 255
|
|
542
|
-
], 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}`);
|
|
543
471
|
}
|
|
544
|
-
function
|
|
545
|
-
|
|
546
|
-
const low = value % MAX_INT32;
|
|
547
|
-
const buf = new Uint8Array(8);
|
|
548
|
-
writeUInt32BE(buf, high, 0);
|
|
549
|
-
writeUInt32BE(buf, low, 4);
|
|
550
|
-
return buf;
|
|
472
|
+
function isAlgorithm$3(algorithm, name) {
|
|
473
|
+
return algorithm.name === name;
|
|
551
474
|
}
|
|
552
|
-
function
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
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}.`);
|
|
556
488
|
}
|
|
557
489
|
//#endregion
|
|
558
|
-
//#region src/core/
|
|
559
|
-
/**
|
|
560
|
-
* This is a fork of Jose library's utility functions.
|
|
561
|
-
* @source https://github.com/panva/jose/tree/69b7960c67e05be55fa2ec31c74b987696c20c60/src/lib/cek.ts
|
|
562
|
-
* @source https://github.com/panva/jose/tree/69b7960c67e05be55fa2ec31c74b987696c20c60/src/lib/iv.ts
|
|
563
|
-
* @license MIT https://github.com/panva/jose/blob/69b7960c67e05be55fa2ec31c74b987696c20c60/LICENSE.md
|
|
564
|
-
*/
|
|
490
|
+
//#region src/core/_crypto/_aes.ts
|
|
565
491
|
const IV_BIT_LENGTHS = {
|
|
566
492
|
A128GCM: 96,
|
|
567
493
|
A128GCMKW: 96,
|
|
@@ -604,14 +530,112 @@ function checkCEKLength(cek, expected) {
|
|
|
604
530
|
const actual = cek.byteLength << 3;
|
|
605
531
|
if (actual !== expected) throw new Error(`Invalid Content Encryption Key length. Expected ${expected} bits, got ${actual} bits`);
|
|
606
532
|
}
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
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
|
+
}
|
|
615
639
|
async function importCBCKeys(enc, cek, usage) {
|
|
616
640
|
const keySize = Number.parseInt(enc.slice(1, 4), 10);
|
|
617
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), {
|
|
@@ -645,7 +669,7 @@ async function gcmEncrypt(enc, plaintext, cek, iv, aad) {
|
|
|
645
669
|
let encKey;
|
|
646
670
|
if (cek instanceof Uint8Array) encKey = await crypto.subtle.importKey("raw", cek, "AES-GCM", false, ["encrypt"]);
|
|
647
671
|
else {
|
|
648
|
-
|
|
672
|
+
checkAESCryptoKey(cek, enc, "encrypt");
|
|
649
673
|
encKey = cek;
|
|
650
674
|
}
|
|
651
675
|
const encrypted = new Uint8Array(await crypto.subtle.encrypt({
|
|
@@ -661,50 +685,6 @@ async function gcmEncrypt(enc, plaintext, cek, iv, aad) {
|
|
|
661
685
|
iv
|
|
662
686
|
};
|
|
663
687
|
}
|
|
664
|
-
async function encrypt$1(enc, plaintext, cek, iv, aad) {
|
|
665
|
-
if (!isCryptoKey(cek) && !(cek instanceof Uint8Array)) throw new TypeError("Key must be one of type: CryptoKey or Uint8Array");
|
|
666
|
-
if (iv) checkIvLength(enc, iv);
|
|
667
|
-
else iv = generateIV(enc);
|
|
668
|
-
switch (enc) {
|
|
669
|
-
case "A128CBC-HS256":
|
|
670
|
-
case "A192CBC-HS384":
|
|
671
|
-
case "A256CBC-HS512":
|
|
672
|
-
if (cek instanceof Uint8Array) checkCEKLength(cek, Number.parseInt(enc.slice(-3), 10));
|
|
673
|
-
return cbcEncrypt(enc, plaintext, cek, iv, aad);
|
|
674
|
-
case "A128GCM":
|
|
675
|
-
case "A192GCM":
|
|
676
|
-
case "A256GCM":
|
|
677
|
-
if (cek instanceof Uint8Array) checkCEKLength(cek, Number.parseInt(enc.slice(1, 4), 10));
|
|
678
|
-
return gcmEncrypt(enc, plaintext, cek, iv, aad);
|
|
679
|
-
default: throw new Error("Unsupported JWE Content Encryption Algorithm");
|
|
680
|
-
}
|
|
681
|
-
}
|
|
682
|
-
/**
|
|
683
|
-
* Decrypt fork
|
|
684
|
-
*/
|
|
685
|
-
let _timingSafeKey;
|
|
686
|
-
function getTimingSafeKey() {
|
|
687
|
-
if (!_timingSafeKey) _timingSafeKey = crypto.subtle.generateKey({
|
|
688
|
-
name: "HMAC",
|
|
689
|
-
hash: "SHA-256"
|
|
690
|
-
}, false, ["sign"]);
|
|
691
|
-
return _timingSafeKey;
|
|
692
|
-
}
|
|
693
|
-
async function timingSafeEqual(a, b) {
|
|
694
|
-
if (!(a instanceof Uint8Array)) throw new TypeError("First argument must be a buffer");
|
|
695
|
-
if (!(b instanceof Uint8Array)) throw new TypeError("Second argument must be a buffer");
|
|
696
|
-
const algorithm = {
|
|
697
|
-
name: "HMAC",
|
|
698
|
-
hash: "SHA-256"
|
|
699
|
-
};
|
|
700
|
-
const key = await getTimingSafeKey();
|
|
701
|
-
const aHmac = new Uint8Array(await crypto.subtle.sign(algorithm, key, a));
|
|
702
|
-
const bHmac = new Uint8Array(await crypto.subtle.sign(algorithm, key, b));
|
|
703
|
-
let out = 0;
|
|
704
|
-
let i = -1;
|
|
705
|
-
while (++i < 32) out |= aHmac[i] ^ bHmac[i];
|
|
706
|
-
return out === 0;
|
|
707
|
-
}
|
|
708
688
|
async function cbcDecrypt(enc, cek, ciphertext, iv, tag, aad) {
|
|
709
689
|
if (!(cek instanceof Uint8Array)) throw new TypeError("CBC key must be of type Uint8Array");
|
|
710
690
|
const { encKey, macKey, keySize } = await importCBCKeys(enc, cek, "decrypt");
|
|
@@ -729,7 +709,7 @@ async function gcmDecrypt(enc, cek, ciphertext, iv, tag, aad) {
|
|
|
729
709
|
let encKey;
|
|
730
710
|
if (cek instanceof Uint8Array) encKey = await crypto.subtle.importKey("raw", cek, "AES-GCM", false, ["decrypt"]);
|
|
731
711
|
else {
|
|
732
|
-
|
|
712
|
+
checkAESCryptoKey(cek, enc, "decrypt");
|
|
733
713
|
encKey = cek;
|
|
734
714
|
}
|
|
735
715
|
try {
|
|
@@ -743,27 +723,62 @@ async function gcmDecrypt(enc, cek, ciphertext, iv, tag, aad) {
|
|
|
743
723
|
throw new Error("JWE Decryption Failed");
|
|
744
724
|
}
|
|
745
725
|
}
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
if (!
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
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;
|
|
764
765
|
}
|
|
765
766
|
//#endregion
|
|
766
|
-
//#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
|
+
}
|
|
767
782
|
function subtleMapping(jwk) {
|
|
768
783
|
let algorithm;
|
|
769
784
|
let keyUsages;
|
|
@@ -861,72 +876,47 @@ function subtleMapping(jwk) {
|
|
|
861
876
|
keyUsages
|
|
862
877
|
};
|
|
863
878
|
}
|
|
864
|
-
async function jwkTokey(jwk) {
|
|
865
|
-
if (!jwk.alg) throw new TypeError("\"alg\" argument is required when \"jwk.alg\" is not present");
|
|
866
|
-
const { algorithm, keyUsages } = subtleMapping(jwk);
|
|
867
|
-
return crypto.subtle.importKey("jwk", jwk, algorithm, jwk.ext ?? (jwk.d ? false : true), jwk.key_ops ?? keyUsages);
|
|
868
|
-
}
|
|
869
|
-
//#endregion
|
|
870
|
-
//#region src/core/jose/key_to_jwk.ts
|
|
871
|
-
async function keyToJWK(key) {
|
|
872
|
-
if (key instanceof Uint8Array) return {
|
|
873
|
-
kty: "oct",
|
|
874
|
-
k: base64UrlEncode(key)
|
|
875
|
-
};
|
|
876
|
-
if (!isCryptoKey(key)) throw new TypeError("Key must be one of type: CryptoKey or Uint8Array");
|
|
877
|
-
if (!key.extractable) throw new TypeError("non-extractable CryptoKey cannot be exported as a JWK");
|
|
878
|
-
return await crypto.subtle.exportKey("jwk", key);
|
|
879
|
-
}
|
|
880
879
|
//#endregion
|
|
881
|
-
//#region src/core/
|
|
880
|
+
//#region src/core/_crypto/_rsa.ts
|
|
882
881
|
async function encryptRSAES(alg, key, cek) {
|
|
883
|
-
|
|
884
|
-
|
|
882
|
+
checkRSACryptoKey(key, alg, "encrypt");
|
|
883
|
+
checkRSAModulusLength(alg, key);
|
|
885
884
|
return new Uint8Array(await crypto.subtle.encrypt("RSA-OAEP", key, cek));
|
|
886
885
|
}
|
|
887
886
|
async function decryptRSAES(alg, key, encryptedKey) {
|
|
888
|
-
|
|
889
|
-
|
|
887
|
+
checkRSACryptoKey(key, alg, "decrypt");
|
|
888
|
+
checkRSAModulusLength(alg, key);
|
|
890
889
|
return new Uint8Array(await crypto.subtle.decrypt("RSA-OAEP", key, encryptedKey));
|
|
891
890
|
}
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
* @license MIT https://github.com/panva/jose/blob/69b7960c67e05be55fa2ec31c74b987696c20c60/LICENSE.md
|
|
898
|
-
*/
|
|
899
|
-
async function encryptIV(alg, key, plaintext, iv) {
|
|
900
|
-
const wrapped = await encrypt$1(alg.slice(0, 7), plaintext, key, iv, new Uint8Array(0));
|
|
901
|
-
return {
|
|
902
|
-
encryptedKey: wrapped.ciphertext,
|
|
903
|
-
iv: base64UrlEncode(wrapped.iv),
|
|
904
|
-
tag: base64UrlEncode(wrapped.tag)
|
|
905
|
-
};
|
|
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);
|
|
906
896
|
}
|
|
907
|
-
|
|
908
|
-
|
|
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
|
+
}
|
|
909
902
|
}
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
function lengthAndInput(input) {
|
|
913
|
-
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}`);
|
|
914
905
|
}
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
res.set(await digest("sha256", buf), iter * 32);
|
|
924
|
-
}
|
|
925
|
-
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}.`);
|
|
926
914
|
}
|
|
915
|
+
//#endregion
|
|
916
|
+
//#region src/core/_crypto/_ecdh.ts
|
|
927
917
|
async function deriveECDHESKey(publicKey, privateKey, algorithm, keyLength, apu = new Uint8Array(0), apv = new Uint8Array(0)) {
|
|
928
|
-
|
|
929
|
-
|
|
918
|
+
checkECDHCryptoKey(publicKey, "ECDH");
|
|
919
|
+
checkECDHCryptoKey(privateKey, "ECDH", "deriveBits");
|
|
930
920
|
const value = concatUint8Arrays(lengthAndInput(textEncoder.encode(algorithm)), lengthAndInput(apu), lengthAndInput(apv), uint32be(keyLength));
|
|
931
921
|
const length = publicKey.algorithm.name === "X25519" ? 256 : Math.ceil(Number.parseInt(publicKey.algorithm.namedCurve.slice(-3), 10) / 8) << 3;
|
|
932
922
|
return concatKdf(new Uint8Array(await crypto.subtle.deriveBits({
|
|
@@ -934,7 +924,7 @@ async function deriveECDHESKey(publicKey, privateKey, algorithm, keyLength, apu
|
|
|
934
924
|
public: publicKey
|
|
935
925
|
}, privateKey, length)), keyLength, value);
|
|
936
926
|
}
|
|
937
|
-
function
|
|
927
|
+
function isECDHKeyAllowed(key) {
|
|
938
928
|
switch (key.algorithm.namedCurve) {
|
|
939
929
|
case "P-256":
|
|
940
930
|
case "P-384":
|
|
@@ -942,38 +932,65 @@ function allowed(key) {
|
|
|
942
932
|
default: return key.algorithm.name === "X25519";
|
|
943
933
|
}
|
|
944
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
|
+
}
|
|
945
964
|
async function digest(algorithm, data) {
|
|
946
|
-
|
|
947
|
-
|
|
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;
|
|
948
980
|
}
|
|
949
981
|
//#endregion
|
|
950
|
-
//#region src/core/
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
p2s: base64UrlEncode(p2s)
|
|
963
|
-
};
|
|
964
|
-
} else return { encryptedKey: await _wrap(alg, key, cek) };
|
|
965
|
-
}
|
|
966
|
-
async function unwrap(alg, key, encryptedKey, p2c, p2s) {
|
|
967
|
-
if (p2c && p2s) {
|
|
968
|
-
const derived = await deriveKey(p2s, alg, p2c, key);
|
|
969
|
-
return _unwrap(alg.slice(-6), derived, encryptedKey);
|
|
970
|
-
} else return _unwrap(alg, key, encryptedKey);
|
|
971
|
-
}
|
|
972
|
-
function getCryptoKey(key, alg, options = {}) {
|
|
973
|
-
const { importAlg = "PBKDF2", extractable = false, usage = "deriveBits" } = options;
|
|
974
|
-
if (key instanceof Uint8Array) return crypto.subtle.importKey("raw", key, importAlg, extractable, [usage]);
|
|
975
|
-
checkEncCryptoKey(key, alg, usage);
|
|
976
|
-
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);
|
|
977
994
|
}
|
|
978
995
|
async function deriveKey(p2s, alg, p2c, key) {
|
|
979
996
|
if (!(p2s instanceof Uint8Array) || p2s.length < 8) throw new Error("PBES2 Salt Input must be 8 or more octets");
|
|
@@ -986,92 +1003,44 @@ async function deriveKey(p2s, alg, p2c, key) {
|
|
|
986
1003
|
name: "PBKDF2",
|
|
987
1004
|
salt
|
|
988
1005
|
};
|
|
989
|
-
const cryptoKey = await
|
|
1006
|
+
const cryptoKey = await getPBKDF2CryptoKey(key, alg);
|
|
990
1007
|
return new Uint8Array(await crypto.subtle.deriveBits(subtleAlg, cryptoKey, keylen));
|
|
991
1008
|
}
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
function checkKeySize(key, alg) {
|
|
996
|
-
if (key.algorithm.length !== Number.parseInt(alg.slice(1, 4), 10)) throw new TypeError(`Invalid key size for alg: ${alg}`);
|
|
997
|
-
}
|
|
998
|
-
async function _wrap(alg, key, cek) {
|
|
999
|
-
const cryptoKey = await getCryptoKey(key, alg, {
|
|
1000
|
-
importAlg: "AES-KW",
|
|
1001
|
-
extractable: true,
|
|
1002
|
-
usage: "wrapKey"
|
|
1003
|
-
});
|
|
1004
|
-
checkKeySize(cryptoKey, alg);
|
|
1005
|
-
const cryptoKeyCek = await crypto.subtle.importKey("raw", cek, {
|
|
1006
|
-
hash: "SHA-256",
|
|
1007
|
-
name: "HMAC"
|
|
1008
|
-
}, true, ["sign"]);
|
|
1009
|
-
return new Uint8Array(await crypto.subtle.wrapKey("raw", cryptoKeyCek, cryptoKey, "AES-KW"));
|
|
1010
|
-
}
|
|
1011
|
-
async function _unwrap(alg, key, encryptedKey) {
|
|
1012
|
-
const cryptoKey = await getCryptoKey(key, alg, {
|
|
1013
|
-
importAlg: "AES-KW",
|
|
1014
|
-
extractable: true,
|
|
1015
|
-
usage: "unwrapKey"
|
|
1016
|
-
});
|
|
1017
|
-
checkKeySize(cryptoKey, alg);
|
|
1018
|
-
const cryptoKeyCek = await crypto.subtle.unwrapKey("raw", encryptedKey, cryptoKey, "AES-KW", {
|
|
1019
|
-
hash: "SHA-256",
|
|
1020
|
-
name: "HMAC"
|
|
1021
|
-
}, true, ["sign"]);
|
|
1022
|
-
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);
|
|
1023
1012
|
}
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
const PUBLIC_EPK_FIELDS = new Set([
|
|
1027
|
-
"kty",
|
|
1028
|
-
"crv",
|
|
1029
|
-
"x",
|
|
1030
|
-
"y",
|
|
1031
|
-
"kid",
|
|
1032
|
-
"use",
|
|
1033
|
-
"key_ops",
|
|
1034
|
-
"alg",
|
|
1035
|
-
"x5c",
|
|
1036
|
-
"x5t",
|
|
1037
|
-
"x5t#S256",
|
|
1038
|
-
"x5u"
|
|
1039
|
-
]);
|
|
1040
|
-
function toPublicEpkJwk(jwk) {
|
|
1041
|
-
const filtered = {};
|
|
1042
|
-
for (const [key, value] of Object.entries(jwk)) if (value !== void 0 && PUBLIC_EPK_FIELDS.has(key)) filtered[key] = value;
|
|
1043
|
-
const { kty, crv, x, y } = filtered;
|
|
1044
|
-
if (typeof kty !== "string") throw new TypeError("ECDH-ES ephemeral key JWK must include the \"kty\" parameter.");
|
|
1045
|
-
if (typeof crv !== "string") throw new TypeError("ECDH-ES ephemeral key JWK must include the \"crv\" parameter.");
|
|
1046
|
-
if (typeof x !== "string") throw new TypeError("ECDH-ES ephemeral key JWK must include the \"x\" coordinate.");
|
|
1047
|
-
if (kty === "EC" && typeof y !== "string") throw new TypeError("ECDH-ES EC ephemeral key JWK must include both \"x\" and \"y\" coordinates.");
|
|
1048
|
-
const publicJwk = {
|
|
1049
|
-
...filtered,
|
|
1050
|
-
kty,
|
|
1051
|
-
crv,
|
|
1052
|
-
x
|
|
1053
|
-
};
|
|
1054
|
-
if (typeof y === "string") publicJwk.y = y;
|
|
1055
|
-
else delete publicJwk.y;
|
|
1056
|
-
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}`);
|
|
1057
1015
|
}
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
if (!(normalized instanceof CryptoKey)) throw new TypeError("ECDH-ES ephemeral private key must be a CryptoKey or convertible JWK.");
|
|
1061
|
-
if (normalized.type !== "private") throw new TypeError("ECDH-ES ephemeral private key must have type \"private\".");
|
|
1062
|
-
return normalized;
|
|
1016
|
+
function isAlgorithm(algorithm, name) {
|
|
1017
|
+
return algorithm.name === name;
|
|
1063
1018
|
}
|
|
1064
|
-
|
|
1065
|
-
if (
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
if (!(normalized instanceof CryptoKey)) throw new TypeError("ECDH-ES ephemeral public key must be a CryptoKey or convertible JWK.");
|
|
1073
|
-
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;
|
|
1074
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
|
+
]);
|
|
1075
1044
|
async function encryptKey(alg, enc, key, providedCek, providedParameters = {}) {
|
|
1076
1045
|
let encryptedKey;
|
|
1077
1046
|
let parameters = {};
|
|
@@ -1085,7 +1054,7 @@ async function encryptKey(alg, enc, key, providedCek, providedParameters = {}) {
|
|
|
1085
1054
|
case "ECDH-ES+A192KW":
|
|
1086
1055
|
case "ECDH-ES+A256KW": {
|
|
1087
1056
|
assertCryptoKey(key);
|
|
1088
|
-
if (!
|
|
1057
|
+
if (!isECDHKeyAllowed(key)) throw new Error("ECDH with the provided key is not allowed or not supported by your javascript runtime");
|
|
1089
1058
|
const { apu, apv, epk: providedEpk, epkPrivateKey } = providedParameters;
|
|
1090
1059
|
if (providedEpk && !epkPrivateKey) throw new TypeError("ECDH-ES custom ephemeral key material must include the matching private key.");
|
|
1091
1060
|
let ephemeralPrivateKey;
|
|
@@ -1108,7 +1077,7 @@ async function encryptKey(alg, enc, key, providedCek, providedParameters = {}) {
|
|
|
1108
1077
|
break;
|
|
1109
1078
|
}
|
|
1110
1079
|
cek = providedCek || generateCEK(enc);
|
|
1111
|
-
encryptedKey =
|
|
1080
|
+
encryptedKey = await aesKwWrap(alg.slice(-6), sharedSecret, cek);
|
|
1112
1081
|
break;
|
|
1113
1082
|
}
|
|
1114
1083
|
case "RSA-OAEP":
|
|
@@ -1124,21 +1093,21 @@ async function encryptKey(alg, enc, key, providedCek, providedParameters = {}) {
|
|
|
1124
1093
|
case "PBES2-HS512+A256KW": {
|
|
1125
1094
|
cek = providedCek || generateCEK(enc);
|
|
1126
1095
|
const { p2c, p2s } = providedParameters;
|
|
1127
|
-
({encryptedKey, ...parameters} = await
|
|
1096
|
+
({encryptedKey, ...parameters} = await pbes2Wrap(alg, key, cek, p2c, p2s));
|
|
1128
1097
|
break;
|
|
1129
1098
|
}
|
|
1130
1099
|
case "A128KW":
|
|
1131
1100
|
case "A192KW":
|
|
1132
1101
|
case "A256KW":
|
|
1133
1102
|
cek = providedCek || generateCEK(enc);
|
|
1134
|
-
encryptedKey =
|
|
1103
|
+
encryptedKey = await aesKwWrap(alg, key, cek);
|
|
1135
1104
|
break;
|
|
1136
1105
|
case "A128GCMKW":
|
|
1137
1106
|
case "A192GCMKW":
|
|
1138
1107
|
case "A256GCMKW": {
|
|
1139
1108
|
cek = providedCek || generateCEK(enc);
|
|
1140
1109
|
const { iv } = providedParameters;
|
|
1141
|
-
({encryptedKey, ...parameters} = await
|
|
1110
|
+
({encryptedKey, ...parameters} = await gcmkwEncrypt(alg, key, cek, iv));
|
|
1142
1111
|
break;
|
|
1143
1112
|
}
|
|
1144
1113
|
default: throw new Error("Invalid or unsupported \"alg\" (JWE Algorithm) header value");
|
|
@@ -1161,67 +1130,72 @@ async function normalizeKey(key, alg) {
|
|
|
1161
1130
|
}
|
|
1162
1131
|
throw new Error("unreachable");
|
|
1163
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
|
+
}
|
|
1164
1168
|
//#endregion
|
|
1165
|
-
//#region src/core/
|
|
1166
|
-
/**
|
|
1167
|
-
* This is a fork of Jose library's utility functions.
|
|
1168
|
-
* @source https://github.com/panva/jose/tree/69b7960c67e05be55fa2ec31c74b987696c20c60/src/lib/asn1.ts
|
|
1169
|
-
* @license MIT https://github.com/panva/jose/blob/69b7960c67e05be55fa2ec31c74b987696c20c60/LICENSE.md
|
|
1170
|
-
*/
|
|
1171
|
-
const formatPEM = (b64, descriptor) => {
|
|
1172
|
-
return `-----BEGIN ${descriptor}-----\n${(b64.match(/.{1,64}/g) || []).join("\n")}\n-----END ${descriptor}-----`;
|
|
1173
|
-
};
|
|
1174
|
-
const genericExport = async (keyType, keyFormat, key) => {
|
|
1175
|
-
if (!isCryptoKey(key)) throw new TypeError(`Key must be ${key} of type CryptoKey`);
|
|
1176
|
-
if (!key.extractable) throw new TypeError("CryptoKey is not extractable");
|
|
1177
|
-
if (key.type !== keyType) throw new TypeError(`key is not a ${keyType} key`);
|
|
1178
|
-
return formatPEM(base64Encode(new Uint8Array(await crypto.subtle.exportKey(keyFormat, key))), `${keyType.toUpperCase()} KEY`);
|
|
1179
|
-
};
|
|
1169
|
+
//#region src/core/_crypto/_pem.ts
|
|
1180
1170
|
const toSPKI = (key) => {
|
|
1181
1171
|
return genericExport("public", "spki", key);
|
|
1182
1172
|
};
|
|
1183
1173
|
const toPKCS8 = (key) => {
|
|
1184
1174
|
return genericExport("private", "pkcs8", key);
|
|
1185
1175
|
};
|
|
1186
|
-
const
|
|
1187
|
-
|
|
1188
|
-
oid.unshift(oid.length);
|
|
1189
|
-
oid.unshift(6);
|
|
1190
|
-
}
|
|
1191
|
-
const i = keyData.indexOf(oid[0], from);
|
|
1192
|
-
if (i === -1) return false;
|
|
1193
|
-
const sub = keyData.subarray(i, i + oid.length);
|
|
1194
|
-
if (sub.length !== oid.length) return false;
|
|
1195
|
-
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);
|
|
1196
1178
|
};
|
|
1197
|
-
const
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
1,
|
|
1207
|
-
7
|
|
1208
|
-
]): return "P-256";
|
|
1209
|
-
case findOid(keyData, [
|
|
1210
|
-
43,
|
|
1211
|
-
129,
|
|
1212
|
-
4,
|
|
1213
|
-
0,
|
|
1214
|
-
34
|
|
1215
|
-
]): return "P-384";
|
|
1216
|
-
case findOid(keyData, [
|
|
1217
|
-
43,
|
|
1218
|
-
129,
|
|
1219
|
-
4,
|
|
1220
|
-
0,
|
|
1221
|
-
35
|
|
1222
|
-
]): return "P-521";
|
|
1223
|
-
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_ });
|
|
1224
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`);
|
|
1225
1199
|
};
|
|
1226
1200
|
const genericImport = async (replace, keyFormat, pem, alg, options) => {
|
|
1227
1201
|
let algorithm;
|
|
@@ -1299,13 +1273,47 @@ const genericImport = async (replace, keyFormat, pem, alg, options) => {
|
|
|
1299
1273
|
}
|
|
1300
1274
|
return crypto.subtle.importKey(keyFormat, keyData, algorithm, options?.extractable ?? (isPublic ? true : false), keyUsages);
|
|
1301
1275
|
};
|
|
1302
|
-
const
|
|
1303
|
-
|
|
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);
|
|
1304
1286
|
};
|
|
1305
|
-
const
|
|
1306
|
-
|
|
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
|
+
}
|
|
1307
1315
|
};
|
|
1308
|
-
|
|
1316
|
+
const getElement = (seq) => {
|
|
1309
1317
|
const result = [];
|
|
1310
1318
|
let next = 0;
|
|
1311
1319
|
while (next < seq.length) {
|
|
@@ -1314,8 +1322,8 @@ function getElement(seq) {
|
|
|
1314
1322
|
next += nextPart.byteLength;
|
|
1315
1323
|
}
|
|
1316
1324
|
return result;
|
|
1317
|
-
}
|
|
1318
|
-
|
|
1325
|
+
};
|
|
1326
|
+
const parseElement = (bytes) => {
|
|
1319
1327
|
let position = 0;
|
|
1320
1328
|
let tag = bytes[0] & 31;
|
|
1321
1329
|
position++;
|
|
@@ -1359,13 +1367,13 @@ function parseElement(bytes) {
|
|
|
1359
1367
|
contents: bytes.subarray(position, byteLength),
|
|
1360
1368
|
raw: bytes.subarray(0, byteLength)
|
|
1361
1369
|
};
|
|
1362
|
-
}
|
|
1363
|
-
|
|
1370
|
+
};
|
|
1371
|
+
const spkiFromX509 = (buf) => {
|
|
1364
1372
|
const tbsCertificate = getElement(getElement(parseElement(buf).contents)[0].contents);
|
|
1365
1373
|
return base64Encode(tbsCertificate[tbsCertificate[0].raw[0] === 160 ? 6 : 5].raw);
|
|
1366
|
-
}
|
|
1374
|
+
};
|
|
1367
1375
|
let createPublicKey;
|
|
1368
|
-
|
|
1376
|
+
const getSPKI = (x509) => {
|
|
1369
1377
|
try {
|
|
1370
1378
|
createPublicKey ??= globalThis.process?.getBuiltinModule?.("node:crypto")?.createPublicKey;
|
|
1371
1379
|
} catch {
|
|
@@ -1378,15 +1386,6 @@ function getSPKI(x509) {
|
|
|
1378
1386
|
});
|
|
1379
1387
|
} catch {}
|
|
1380
1388
|
return formatPEM(spkiFromX509(base64Decode(x509.replace(/(?:-----(?:BEGIN|END) CERTIFICATE-----|\s)/g, ""), false)), "PUBLIC KEY");
|
|
1381
|
-
}
|
|
1382
|
-
const fromX509 = (pem, alg, options) => {
|
|
1383
|
-
let spki;
|
|
1384
|
-
try {
|
|
1385
|
-
spki = getSPKI(pem);
|
|
1386
|
-
} catch (error_) {
|
|
1387
|
-
throw new TypeError("Failed to parse the X.509 certificate", { cause: error_ });
|
|
1388
|
-
}
|
|
1389
|
-
return fromSPKI(spki, alg, options);
|
|
1390
1389
|
};
|
|
1391
1390
|
//#endregion
|
|
1392
1391
|
//#region src/core/jwk.ts
|
|
@@ -1400,25 +1399,13 @@ async function generateKey(alg, options = {}) {
|
|
|
1400
1399
|
}
|
|
1401
1400
|
const { algorithm, keyUsages } = getGenerateKeyParams(alg, options);
|
|
1402
1401
|
const key = await crypto.subtle.generateKey(algorithm, defaultExtractable, keyUsages);
|
|
1403
|
-
if (exportToJWK) {
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
const [publicKey, privateKey] = await Promise.all([exportKey(key.publicKey, {
|
|
1411
|
-
...additionalKeyParams,
|
|
1412
|
-
alg
|
|
1413
|
-
}), exportKey(key.privateKey, {
|
|
1414
|
-
...additionalKeyParams,
|
|
1415
|
-
alg
|
|
1416
|
-
})]);
|
|
1417
|
-
return {
|
|
1418
|
-
privateKey,
|
|
1419
|
-
publicKey
|
|
1420
|
-
};
|
|
1421
|
-
}
|
|
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
|
+
};
|
|
1422
1409
|
}
|
|
1423
1410
|
return key;
|
|
1424
1411
|
}
|
|
@@ -1431,14 +1418,31 @@ async function generateKey(alg, options = {}) {
|
|
|
1431
1418
|
* @returns A Promise resolving to the generated JWK or JWK pair representation.
|
|
1432
1419
|
*/
|
|
1433
1420
|
async function generateJWK(alg, jwkParams, options = {}) {
|
|
1434
|
-
const
|
|
1435
|
-
|
|
1436
|
-
...
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
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
|
|
1441
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
|
+
};
|
|
1442
1446
|
}
|
|
1443
1447
|
async function deriveKeyFromPassword(password, alg, options) {
|
|
1444
1448
|
const { salt, iterations, extractable, keyUsage } = options;
|
|
@@ -1450,16 +1454,11 @@ async function deriveKeyFromPassword(password, alg, options) {
|
|
|
1450
1454
|
const finalUsages = keyUsage ?? ["wrapKey", "unwrapKey"];
|
|
1451
1455
|
const finalExtractable = exportToJWK ? true : extractable === true;
|
|
1452
1456
|
const derivedKey = await crypto.subtle.importKey("raw", derivedBytes, { name: "AES-KW" }, finalExtractable, finalUsages);
|
|
1453
|
-
if (exportToJWK) {
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
...jwk,
|
|
1459
|
-
alg: wrappingAlg,
|
|
1460
|
-
kty: "oct"
|
|
1461
|
-
};
|
|
1462
|
-
}
|
|
1457
|
+
if (exportToJWK) return {
|
|
1458
|
+
...await keyToJWK(derivedKey),
|
|
1459
|
+
alg: wrappingAlg,
|
|
1460
|
+
kty: "oct"
|
|
1461
|
+
};
|
|
1463
1462
|
return derivedKey;
|
|
1464
1463
|
}
|
|
1465
1464
|
/**
|
|
@@ -1472,36 +1471,67 @@ async function deriveKeyFromPassword(password, alg, options) {
|
|
|
1472
1471
|
* @returns A Promise resolving to the derived JWK (JWK_oct).
|
|
1473
1472
|
*/
|
|
1474
1473
|
async function deriveJWKFromPassword(password, alg, options, jwkParams) {
|
|
1475
|
-
const
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
toJWK: jwkParams || true
|
|
1474
|
+
const result = await deriveKeyFromPassword(password, alg, {
|
|
1475
|
+
...options,
|
|
1476
|
+
toJWK: true
|
|
1479
1477
|
});
|
|
1478
|
+
return jwkParams ? {
|
|
1479
|
+
...jwkParams,
|
|
1480
|
+
...result
|
|
1481
|
+
} : result;
|
|
1480
1482
|
}
|
|
1481
1483
|
/**
|
|
1482
|
-
*
|
|
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.
|
|
1483
1495
|
*/
|
|
1484
|
-
|
|
1485
|
-
|
|
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;
|
|
1486
1510
|
if (typeof key === "string") key = textEncoder.encode(key);
|
|
1487
1511
|
if (key instanceof Uint8Array) return key;
|
|
1488
1512
|
if (isCryptoKey(key)) return key;
|
|
1489
|
-
if (isJWK(key)) if ("k" in key && typeof key.k === "string")
|
|
1490
|
-
|
|
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 {
|
|
1491
1521
|
if (!key.alg && !alg) throw new TypeError("Algorithm must be provided when importing non-oct JWK");
|
|
1492
1522
|
const effectiveAlg = key.alg || alg;
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1523
|
+
if (_activeCache) {
|
|
1524
|
+
const cached = _activeCache.get(key, effectiveAlg);
|
|
1525
|
+
if (cached) return cached;
|
|
1526
|
+
}
|
|
1496
1527
|
const cryptoKey = await jwkTokey(key.alg ? key : {
|
|
1497
1528
|
...key,
|
|
1498
1529
|
alg
|
|
1499
1530
|
});
|
|
1500
|
-
if (
|
|
1501
|
-
else cached[effectiveAlg] = cryptoKey;
|
|
1531
|
+
if (_activeCache) _activeCache.set(key, effectiveAlg, cryptoKey);
|
|
1502
1532
|
return cryptoKey;
|
|
1503
1533
|
}
|
|
1504
|
-
throw new
|
|
1534
|
+
throw new JWTError("Invalid key type provided to importKey", "ERR_JWK_INVALID");
|
|
1505
1535
|
}
|
|
1506
1536
|
/**
|
|
1507
1537
|
* Exports a CryptoKey to a JWK (JSON Web Key) format.
|
|
@@ -1523,41 +1553,37 @@ async function resolveWrappingKey(alg, key) {
|
|
|
1523
1553
|
if (typeof key === "string") throw new TypeError(`Key must be CryptoKey, JWK, or Uint8Array for ${alg}`);
|
|
1524
1554
|
return importKey(key, alg);
|
|
1525
1555
|
}
|
|
1526
|
-
/**
|
|
1527
|
-
* Unwraps a Content Encryption Key (CEK) using the specified algorithm and unwrapping key.
|
|
1528
|
-
*
|
|
1529
|
-
* @param alg The JWA key management algorithm (e.g., "A128KW", "RSA-OAEP", "PBES2-HS256+A128KW").
|
|
1530
|
-
* @param wrappedKey The wrapped key (ciphertext) as Uint8Array.
|
|
1531
|
-
* @param unwrappingKey The key used to unwrap the CEK (CryptoKey, JWK, or password string/Uint8Array for PBES2).
|
|
1532
|
-
* @param options Additional options required by certain algorithms (e.g., iv, tag, p2s, p2c, epk).
|
|
1533
|
-
* @returns A Promise resolving to the unwrapped key (CEK) as a CryptoKey or Uint8Array.
|
|
1534
|
-
*/
|
|
1535
1556
|
async function unwrapKey(alg, wrappedKey, unwrappingKey, options = {}) {
|
|
1536
|
-
const
|
|
1557
|
+
const returnRaw = options.format === "raw";
|
|
1537
1558
|
const defaultExtractable = options.extractable !== false;
|
|
1538
1559
|
const importedUnwrappingKey = isCryptoKey(unwrappingKey) ? unwrappingKey : await resolveWrappingKey(alg, unwrappingKey);
|
|
1539
1560
|
let unwrappedCekBytes;
|
|
1540
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;
|
|
1541
1569
|
case "A128KW":
|
|
1542
1570
|
case "A192KW":
|
|
1543
1571
|
case "A256KW":
|
|
1572
|
+
unwrappedCekBytes = await aesKwUnwrap(alg, importedUnwrappingKey, wrappedKey);
|
|
1573
|
+
break;
|
|
1544
1574
|
case "PBES2-HS256+A128KW":
|
|
1545
1575
|
case "PBES2-HS384+A192KW":
|
|
1546
1576
|
case "PBES2-HS512+A256KW": {
|
|
1547
1577
|
const { p2s, p2c } = options;
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
if (!p2s || typeof p2c !== "number") throw new Error("PBES2 requires 'p2s' (salt) and 'p2c' (count) options");
|
|
1551
|
-
p2sBytes = typeof p2s === "string" ? base64UrlDecode(p2s, false) : p2s;
|
|
1552
|
-
}
|
|
1553
|
-
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);
|
|
1554
1580
|
break;
|
|
1555
1581
|
}
|
|
1556
1582
|
case "A128GCMKW":
|
|
1557
1583
|
case "A192GCMKW":
|
|
1558
1584
|
case "A256GCMKW":
|
|
1559
1585
|
if (!options.iv || !options.tag) throw new Error("AES-GCMKW requires 'iv' and 'tag' options for unwrapping");
|
|
1560
|
-
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);
|
|
1561
1587
|
break;
|
|
1562
1588
|
case "RSA-OAEP":
|
|
1563
1589
|
case "RSA-OAEP-256":
|
|
@@ -1565,7 +1591,7 @@ async function unwrapKey(alg, wrappedKey, unwrappingKey, options = {}) {
|
|
|
1565
1591
|
case "RSA-OAEP-512": {
|
|
1566
1592
|
if (!(importedUnwrappingKey instanceof CryptoKey)) throw new TypeError("RSA-OAEP requires the unwrapping key to be provided as a CryptoKey");
|
|
1567
1593
|
unwrappedCekBytes = await decryptRSAES(alg, importedUnwrappingKey, wrappedKey);
|
|
1568
|
-
if (
|
|
1594
|
+
if (returnRaw) break;
|
|
1569
1595
|
const inferredAlgorithm = options.unwrappedKeyAlgorithm || inferAesImportAlgorithm(options.enc, unwrappedCekBytes.length);
|
|
1570
1596
|
if (!inferredAlgorithm) throw new Error("Unable to infer algorithm for RSA-OAEP unwrapped key. Provide \"unwrappedKeyAlgorithm\" in options.");
|
|
1571
1597
|
const keyUsages = options.keyUsage || ["encrypt", "decrypt"];
|
|
@@ -1577,7 +1603,7 @@ async function unwrapKey(alg, wrappedKey, unwrappingKey, options = {}) {
|
|
|
1577
1603
|
case "ECDH-ES+A256KW": {
|
|
1578
1604
|
if (!options.epk) throw new Error("ECDH-ES requires 'epk' (Ephemeral Public Key) option");
|
|
1579
1605
|
if (!(importedUnwrappingKey instanceof CryptoKey)) throw new TypeError("ECDH-ES requires the unwrapping key to be a CryptoKey");
|
|
1580
|
-
if (!
|
|
1606
|
+
if (!isECDHKeyAllowed(importedUnwrappingKey)) throw new Error("ECDH with the provided key is not allowed or not supported");
|
|
1581
1607
|
const epkCandidate = options.epk instanceof CryptoKey ? options.epk : await normalizeKey(options.epk, alg);
|
|
1582
1608
|
if (!(epkCandidate instanceof CryptoKey)) throw new TypeError("Failed to normalize ECDH ephemeral public key");
|
|
1583
1609
|
const apuBytes = typeof options.apu === "string" ? base64UrlDecode(options.apu, false) : options.apu ?? new Uint8Array(0);
|
|
@@ -1589,13 +1615,13 @@ async function unwrapKey(alg, wrappedKey, unwrappingKey, options = {}) {
|
|
|
1589
1615
|
else {
|
|
1590
1616
|
const kwAlg = alg.slice(-6);
|
|
1591
1617
|
if (!(wrappedKey instanceof Uint8Array) || wrappedKey.length === 0) throw new Error("ECDH-ES key agreement with key wrapping requires an encrypted key");
|
|
1592
|
-
unwrappedCekBytes = await
|
|
1618
|
+
unwrappedCekBytes = await aesKwUnwrap(kwAlg, sharedSecret, wrappedKey);
|
|
1593
1619
|
}
|
|
1594
1620
|
break;
|
|
1595
1621
|
}
|
|
1596
1622
|
default: throw new Error(`Unsupported key unwrapping algorithm: ${alg}`);
|
|
1597
1623
|
}
|
|
1598
|
-
if (
|
|
1624
|
+
if (returnRaw) return unwrappedCekBytes;
|
|
1599
1625
|
return await crypto.subtle.importKey("raw", unwrappedCekBytes, options.unwrappedKeyAlgorithm || { name: "AES-GCM" }, defaultExtractable, options.keyUsage || ["encrypt", "decrypt"]);
|
|
1600
1626
|
}
|
|
1601
1627
|
/**
|
|
@@ -1609,35 +1635,25 @@ async function unwrapKey(alg, wrappedKey, unwrappingKey, options = {}) {
|
|
|
1609
1635
|
* @param jwkExtras Additional properties to merge into the resulting JWK.
|
|
1610
1636
|
* @returns A Promise resolving to the imported key as a JWK.
|
|
1611
1637
|
*/
|
|
1612
|
-
async function
|
|
1638
|
+
async function importFromPEM(pem, pemType, alg, options) {
|
|
1639
|
+
const extractable = options?.extractable !== false;
|
|
1613
1640
|
let cryptoKey;
|
|
1614
|
-
const defaultExtractable = importOptions?.extractable !== false;
|
|
1615
1641
|
switch (pemType) {
|
|
1616
1642
|
case "pkcs8":
|
|
1617
|
-
cryptoKey = await fromPKCS8(pem, alg, {
|
|
1618
|
-
...importOptions,
|
|
1619
|
-
extractable: defaultExtractable
|
|
1620
|
-
});
|
|
1643
|
+
cryptoKey = await fromPKCS8(pem, alg, { extractable });
|
|
1621
1644
|
break;
|
|
1622
1645
|
case "spki":
|
|
1623
|
-
cryptoKey = await fromSPKI(pem, alg, {
|
|
1624
|
-
...importOptions,
|
|
1625
|
-
extractable: defaultExtractable
|
|
1626
|
-
});
|
|
1646
|
+
cryptoKey = await fromSPKI(pem, alg, { extractable });
|
|
1627
1647
|
break;
|
|
1628
1648
|
case "x509":
|
|
1629
|
-
cryptoKey = await fromX509(pem, alg, {
|
|
1630
|
-
...importOptions,
|
|
1631
|
-
extractable: defaultExtractable
|
|
1632
|
-
});
|
|
1649
|
+
cryptoKey = await fromX509(pem, alg, { extractable });
|
|
1633
1650
|
break;
|
|
1634
1651
|
default: throw new TypeError(`Unsupported PEM type: ${pemType}`);
|
|
1635
1652
|
}
|
|
1636
|
-
|
|
1653
|
+
return exportKey(cryptoKey, {
|
|
1637
1654
|
alg,
|
|
1638
|
-
...
|
|
1639
|
-
};
|
|
1640
|
-
return exportKey(cryptoKey, finalJWKExtras);
|
|
1655
|
+
...options?.jwkParams
|
|
1656
|
+
});
|
|
1641
1657
|
}
|
|
1642
1658
|
/**
|
|
1643
1659
|
* Exports a JWK to a PEM-encoded string.
|
|
@@ -1648,7 +1664,7 @@ async function importJWKFromPEM(pem, pemType, alg, importOptions, jwkExtras) {
|
|
|
1648
1664
|
* required to correctly convert it to a CryptoKey first.
|
|
1649
1665
|
* @returns A Promise resolving to the PEM-encoded key string.
|
|
1650
1666
|
*/
|
|
1651
|
-
async function
|
|
1667
|
+
async function exportToPEM(jwk, pemFormat, algForCryptoKeyImport) {
|
|
1652
1668
|
if (jwk.kty === "oct") throw new TypeError("Octet (symmetric) JWKs (kty: 'oct') cannot be exported to PKCS8 or SPKI PEM formats.");
|
|
1653
1669
|
const effectiveAlg = jwk.alg || algForCryptoKeyImport;
|
|
1654
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.");
|
|
@@ -1669,43 +1685,19 @@ async function exportJWKToPEM(jwk, pemFormat, algForCryptoKeyImport) {
|
|
|
1669
1685
|
}
|
|
1670
1686
|
}
|
|
1671
1687
|
/**
|
|
1672
|
-
*
|
|
1673
|
-
*
|
|
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.
|
|
1674
1692
|
*
|
|
1675
|
-
* @param jwkSet The
|
|
1676
|
-
* @param
|
|
1677
|
-
*
|
|
1678
|
-
* provided, it must contain a 'kid' property. It can optionally contain
|
|
1679
|
-
* 'alg' (Algorithm) and 'kty' (Key Type) to further refine the search.
|
|
1680
|
-
* @returns The matching JWK from the set.
|
|
1681
|
-
* @throws TypeError if `jwkSet` is invalid or `kidOrProtectedHeader` is not a
|
|
1682
|
-
* string or a valid header object.
|
|
1683
|
-
* @throws Error if no key is found matching the provided criteria, or if the
|
|
1684
|
-
* 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.
|
|
1685
1696
|
*/
|
|
1686
|
-
function
|
|
1697
|
+
function getJWKsFromSet(jwkSet, filter) {
|
|
1687
1698
|
if (!jwkSet || !isJWKSet(jwkSet)) throw new TypeError("Invalid JWK Set provided");
|
|
1688
|
-
if (
|
|
1689
|
-
|
|
1690
|
-
const selectedKey = jwkSet.keys.find((k) => k.kid === kid);
|
|
1691
|
-
if (!selectedKey) throw new Error(`No key found in JWK Set with kid "${kid}".`);
|
|
1692
|
-
return selectedKey;
|
|
1693
|
-
} else if (typeof kidOrProtectedHeader === "object") {
|
|
1694
|
-
const { kid, alg, kty } = kidOrProtectedHeader;
|
|
1695
|
-
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.");
|
|
1696
|
-
const selectedKey = jwkSet.keys.find((k) => {
|
|
1697
|
-
return k.kid === kid && (!alg || k.alg === alg) && (!kty || k.kty === kty);
|
|
1698
|
-
});
|
|
1699
|
-
if (!selectedKey) {
|
|
1700
|
-
let errorMessage = `No key found in JWK Set with kid "${kid}"`;
|
|
1701
|
-
if (alg) errorMessage += ` and alg "${alg}"`;
|
|
1702
|
-
if (kty) errorMessage += ` and kty "${kty}"`;
|
|
1703
|
-
errorMessage += ".";
|
|
1704
|
-
throw new Error(errorMessage);
|
|
1705
|
-
}
|
|
1706
|
-
return selectedKey;
|
|
1707
|
-
}
|
|
1708
|
-
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);
|
|
1709
1701
|
}
|
|
1710
1702
|
function getGenerateKeyParams(alg, options) {
|
|
1711
1703
|
let algorithm;
|
|
@@ -1866,20 +1858,23 @@ function inferAesImportAlgorithm(enc, cekLengthBytes) {
|
|
|
1866
1858
|
//#endregion
|
|
1867
1859
|
//#region src/core/jwe.ts
|
|
1868
1860
|
async function encrypt(payload, key, options = {}) {
|
|
1869
|
-
const { protectedHeader:
|
|
1861
|
+
const { protectedHeader: userHeader, cek: providedCek, contentEncryptionIV: providedContentIV, keyManagementIV, p2s, p2c, ecdh } = options;
|
|
1870
1862
|
let { alg, enc } = options;
|
|
1871
1863
|
if (!alg) {
|
|
1872
1864
|
if (typeof key === "string" || key instanceof Uint8Array) alg = "PBES2-HS256+A128KW";
|
|
1873
1865
|
else if (isJWK(key)) alg = key.alg === "A128GCM" || key.alg === "A192GCM" || key.alg === "A256GCM" ? `${key.alg}KW` : key.alg;
|
|
1874
1866
|
}
|
|
1875
1867
|
if (!alg) throw new TypeError("JWE \"alg\" (Key Management Algorithm) must be provided in options or inferable from the key");
|
|
1876
|
-
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";
|
|
1877
1872
|
const jweKeyManagementParams = {};
|
|
1878
1873
|
if (keyManagementIV) jweKeyManagementParams.iv = keyManagementIV;
|
|
1879
1874
|
if (p2s) jweKeyManagementParams.p2s = p2s;
|
|
1880
1875
|
else if (alg?.startsWith("PBES2")) jweKeyManagementParams.p2s = randomBytes(16);
|
|
1881
1876
|
if (p2c) jweKeyManagementParams.p2c = p2c;
|
|
1882
|
-
else if (alg?.startsWith("PBES2")) jweKeyManagementParams.p2c =
|
|
1877
|
+
else if (alg?.startsWith("PBES2")) jweKeyManagementParams.p2c = 6e5;
|
|
1883
1878
|
if (ecdh?.partyUInfo) jweKeyManagementParams.apu = ecdh.partyUInfo;
|
|
1884
1879
|
if (ecdh?.partyVInfo) jweKeyManagementParams.apv = ecdh.partyVInfo;
|
|
1885
1880
|
if (ecdh?.ephemeralKey) {
|
|
@@ -1889,21 +1884,14 @@ async function encrypt(payload, key, options = {}) {
|
|
|
1889
1884
|
}
|
|
1890
1885
|
const wrappingKeyMaterial = await importKey(key, alg);
|
|
1891
1886
|
const { cek: finalCek, encryptedKey: jweEncryptedKey, parameters: keyManagementHeaderParams } = await encryptKey(alg, enc, wrappingKeyMaterial, providedCek, jweKeyManagementParams);
|
|
1892
|
-
const
|
|
1893
|
-
...additionalProtectedHeader,
|
|
1894
|
-
...keyManagementHeaderParams,
|
|
1895
|
-
alg,
|
|
1896
|
-
enc
|
|
1897
|
-
};
|
|
1898
|
-
if (isJWK(key) && key.kid) jweProtectedHeader.kid = key.kid;
|
|
1899
|
-
const protectedHeader = applyTypCtyDefaults(jweProtectedHeader, payload);
|
|
1887
|
+
const protectedHeader = _buildJWEHeader(alg, enc, key, userHeader, keyManagementHeaderParams, payload);
|
|
1900
1888
|
const plaintextBytes = getPlaintextBytes(computeJwtTimeClaims(payload, protectedHeader.typ, options.expiresIn, options.currentDate) || payload);
|
|
1901
1889
|
const protectedHeaderEncoded = base64UrlEncode(JSON.stringify(protectedHeader));
|
|
1902
1890
|
const aadBytes = textEncoder.encode(protectedHeaderEncoded);
|
|
1903
1891
|
const contentIVBytes = providedContentIV ?? generateIV(enc);
|
|
1904
1892
|
const { ciphertext: contentCiphertext, tag: contentAuthTag, iv: actualContentIV } = await encrypt$1(enc, plaintextBytes, finalCek, contentIVBytes, aadBytes);
|
|
1905
|
-
if (!actualContentIV) throw new
|
|
1906
|
-
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");
|
|
1907
1895
|
return [
|
|
1908
1896
|
protectedHeaderEncoded,
|
|
1909
1897
|
jweEncryptedKey ? base64UrlEncode(jweEncryptedKey) : "",
|
|
@@ -1914,28 +1902,28 @@ async function encrypt(payload, key, options = {}) {
|
|
|
1914
1902
|
}
|
|
1915
1903
|
async function decrypt(jwe, key, options = {}) {
|
|
1916
1904
|
const parts = jwe.split(".");
|
|
1917
|
-
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");
|
|
1918
1906
|
const [protectedHeaderEncoded, encryptedKeyEncoded, ivEncoded, ciphertextEncoded, authTagEncoded] = parts;
|
|
1919
1907
|
let protectedHeader;
|
|
1920
1908
|
try {
|
|
1921
1909
|
const protectedHeaderJson = base64UrlDecode(protectedHeaderEncoded);
|
|
1922
1910
|
protectedHeader = sanitizeObject(JSON.parse(protectedHeaderJson));
|
|
1923
1911
|
} catch {
|
|
1924
|
-
throw new
|
|
1912
|
+
throw new JWTError("Invalid JWE: Protected header could not be decoded.", "ERR_JWE_INVALID");
|
|
1925
1913
|
}
|
|
1926
|
-
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");
|
|
1927
1915
|
const alg = protectedHeader.alg;
|
|
1928
1916
|
const enc = protectedHeader.enc;
|
|
1929
|
-
if (options?.algorithms && !options.algorithms.includes(alg)) throw new
|
|
1930
|
-
if (options?.encryptionAlgorithms && !options.encryptionAlgorithms.includes(enc)) throw new
|
|
1931
|
-
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;
|
|
1932
1920
|
const encryptedKeyBytes = base64UrlDecode(encryptedKeyEncoded, false);
|
|
1933
1921
|
const contentIVBytes = base64UrlDecode(ivEncoded, false);
|
|
1934
1922
|
const contentAuthTagBytes = base64UrlDecode(authTagEncoded, false);
|
|
1935
1923
|
const ciphertextBytes = base64UrlDecode(ciphertextEncoded, false);
|
|
1936
1924
|
const unwrapKeyOpts = {
|
|
1937
1925
|
enc,
|
|
1938
|
-
|
|
1926
|
+
format: enc.includes("GCM") ? "cryptokey" : "raw",
|
|
1939
1927
|
keyUsage: options?.keyUsage || ["decrypt"]
|
|
1940
1928
|
};
|
|
1941
1929
|
if (protectedHeader.iv) unwrapKeyOpts.iv = protectedHeader.iv;
|
|
@@ -1947,11 +1935,34 @@ async function decrypt(jwe, key, options = {}) {
|
|
|
1947
1935
|
if (protectedHeader.apv) unwrapKeyOpts.apv = protectedHeader.apv;
|
|
1948
1936
|
if (options?.unwrappedKeyAlgorithm) unwrapKeyOpts.unwrappedKeyAlgorithm = options.unwrappedKeyAlgorithm;
|
|
1949
1937
|
if (options?.extractable) unwrapKeyOpts.extractable = options.extractable;
|
|
1950
|
-
const cek = await unwrapKey(alg, encryptedKeyBytes, unwrappingKey, unwrapKeyOpts);
|
|
1951
1938
|
const aadBytes = textEncoder.encode(protectedHeaderEncoded);
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
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);
|
|
1955
1966
|
const result = {
|
|
1956
1967
|
payload,
|
|
1957
1968
|
protectedHeader
|
|
@@ -1987,24 +1998,30 @@ function parseEphemeralKey(ephemeralKey) {
|
|
|
1987
1998
|
epkPrivateKey
|
|
1988
1999
|
};
|
|
1989
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
|
+
}
|
|
1990
2015
|
//#endregion
|
|
1991
2016
|
//#region src/core/jws.ts
|
|
1992
2017
|
async function sign(payload, key, options = {}) {
|
|
1993
|
-
const { protectedHeader:
|
|
1994
|
-
const safeAdditionalHeader = sanitizeObject(additionalProtectedHeader);
|
|
2018
|
+
const { protectedHeader: userHeader } = options;
|
|
1995
2019
|
let { alg } = options;
|
|
1996
2020
|
if (!alg) if (isJWK(key) && key.alg) alg = key.alg;
|
|
1997
2021
|
else throw new TypeError("JWS \"alg\" (Algorithm) must be provided in options");
|
|
1998
|
-
|
|
1999
|
-
const signingKey = await importKey(key, alg);
|
|
2000
|
-
const protectedHeader =
|
|
2001
|
-
...safeAdditionalHeader,
|
|
2002
|
-
...isJWK(key) && key.kid ? { kid: key.kid } : {},
|
|
2003
|
-
alg,
|
|
2004
|
-
typ: safeAdditionalHeader?.typ
|
|
2005
|
-
};
|
|
2006
|
-
applyTypCtyDefaults(protectedHeader, payload);
|
|
2007
|
-
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);
|
|
2008
2025
|
const computedPayload = computeJwtTimeClaims(payload, protectedHeader.typ, options.expiresIn, options.currentDate);
|
|
2009
2026
|
const protectedHeaderEncoded = base64UrlEncode(JSON.stringify(protectedHeader));
|
|
2010
2027
|
let payloadBytes;
|
|
@@ -2018,53 +2035,82 @@ async function sign(payload, key, options = {}) {
|
|
|
2018
2035
|
}
|
|
2019
2036
|
async function verify(jws, key, options = {}) {
|
|
2020
2037
|
const parts = jws.split(".");
|
|
2021
|
-
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");
|
|
2022
2039
|
const [protectedHeaderEncoded, payloadEncoded, signatureEncoded] = parts;
|
|
2023
2040
|
let protectedHeader;
|
|
2024
2041
|
try {
|
|
2025
2042
|
const protectedHeaderString = base64UrlDecode(protectedHeaderEncoded);
|
|
2026
2043
|
protectedHeader = sanitizeObject(JSON.parse(protectedHeaderString));
|
|
2027
2044
|
} catch {
|
|
2028
|
-
throw new
|
|
2045
|
+
throw new JWTError("Invalid JWS: Protected header could not be decoded.", "ERR_JWS_INVALID");
|
|
2029
2046
|
}
|
|
2030
|
-
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");
|
|
2031
2048
|
const alg = protectedHeader.alg;
|
|
2032
|
-
if (options.algorithms && !options.algorithms.includes(alg)) throw new
|
|
2033
|
-
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");
|
|
2034
2051
|
let signatureBytes;
|
|
2035
2052
|
try {
|
|
2036
2053
|
signatureBytes = base64UrlDecode(signatureEncoded, false);
|
|
2037
2054
|
} catch {
|
|
2038
|
-
throw new
|
|
2055
|
+
throw new JWTError("Invalid JWS: Signature could not be decoded.", "ERR_JWS_INVALID");
|
|
2039
2056
|
}
|
|
2040
2057
|
const keyInput = typeof key === "function" ? await key(protectedHeader, jws) : key;
|
|
2041
|
-
const
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
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");
|
|
2045
2071
|
const useB64 = protectedHeader.b64 !== false;
|
|
2046
2072
|
let payload;
|
|
2047
2073
|
try {
|
|
2048
2074
|
payload = useB64 ? decodePayloadFromB64UrlSegment(payloadEncoded, protectedHeader, options.forceUint8Array) : options.forceUint8Array ? textEncoder.encode(payloadEncoded) : payloadEncoded;
|
|
2049
2075
|
} catch (error_) {
|
|
2050
|
-
throw new
|
|
2076
|
+
throw new JWTError(`Invalid JWS: Payload decoding failed (${error_ instanceof Error ? error_.message : error_})`, "ERR_JWS_INVALID");
|
|
2051
2077
|
}
|
|
2052
|
-
validateCriticalHeadersJWS(protectedHeader,
|
|
2053
|
-
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);
|
|
2054
2080
|
return {
|
|
2055
2081
|
payload,
|
|
2056
2082
|
protectedHeader
|
|
2057
2083
|
};
|
|
2058
2084
|
}
|
|
2059
|
-
function
|
|
2060
|
-
if (
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
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")) {
|
|
2065
2095
|
const { modulusLength } = key.algorithm;
|
|
2066
|
-
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");
|
|
2067
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;
|
|
2068
2114
|
}
|
|
2069
2115
|
//#endregion
|
|
2070
|
-
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 };
|