| import { prepareDecrypt, decryptCompact } from '../../lib/jwe_decrypt.js'; | ||
| export async function compactDecrypt(jwe, key, options) { | ||
| const decrypted = await decryptCompact(jwe, prepareDecrypt(options), key); | ||
| const result = { plaintext: decrypted.plaintext, protectedHeader: decrypted.parsedProt }; | ||
| const result = { plaintext: decrypted[0], protectedHeader: decrypted[1] }; | ||
| if (typeof key === 'function') { | ||
| return { ...result, key: decrypted.key }; | ||
| return { ...result, key: decrypted[2] }; | ||
| } | ||
| return result; | ||
| } |
@@ -59,15 +59,15 @@ import { unprotected, assertNotSet } from '../../lib/helpers.js'; | ||
| validateCritDuplicates(JWEInvalid, this.#protectedHeader); | ||
| return createJWE({ | ||
| plaintext: this.#plaintext, | ||
| protectedHeader: this.#protectedHeader, | ||
| unprotectedHeader: this.#unprotectedHeader, | ||
| sharedUnprotectedHeader: this.#sharedUnprotectedHeader, | ||
| aad: this.#aad, | ||
| cek: this.#cek, | ||
| iv: this.#iv, | ||
| keyManagementParameters: this.#keyManagementParameters, | ||
| crit: options?.crit, | ||
| unprotectedParameters: options ? unprotected in options : false, | ||
| }, key); | ||
| return createJWE([ | ||
| this.#plaintext, | ||
| this.#protectedHeader, | ||
| this.#unprotectedHeader, | ||
| this.#sharedUnprotectedHeader, | ||
| this.#aad, | ||
| this.#cek, | ||
| this.#iv, | ||
| this.#keyManagementParameters, | ||
| options?.crit, | ||
| options ? unprotected in options : false, | ||
| ], key); | ||
| } | ||
| } |
@@ -13,19 +13,15 @@ import { FlattenedEncrypt } from '../flattened/encrypt.js'; | ||
| #parent; | ||
| unprotectedHeader; | ||
| keyManagementParameters; | ||
| key; | ||
| options; | ||
| constructor(enc, key, options) { | ||
| state; | ||
| constructor(enc, key, crit) { | ||
| this.#parent = enc; | ||
| this.key = key; | ||
| this.options = options; | ||
| this.state = [undefined, undefined, key, crit]; | ||
| } | ||
| setUnprotectedHeader(unprotectedHeader) { | ||
| assertNotSet(this.unprotectedHeader, 'setUnprotectedHeader'); | ||
| this.unprotectedHeader = unprotectedHeader; | ||
| assertNotSet(this.state[0], 'setUnprotectedHeader'); | ||
| this.state[0] = unprotectedHeader; | ||
| return this; | ||
| } | ||
| setKeyManagementParameters(parameters) { | ||
| assertNotSet(this.keyManagementParameters, 'setKeyManagementParameters'); | ||
| this.keyManagementParameters = parameters; | ||
| assertNotSet(this.state[1], 'setKeyManagementParameters'); | ||
| this.state[1] = parameters; | ||
| return this; | ||
@@ -43,2 +39,13 @@ } | ||
| } | ||
| function copyOptionalMembers(flattened, jwe, recipient) { | ||
| const { aad, protected: protectedHeader, unprotected, header } = flattened; | ||
| if (aad) | ||
| jwe.aad = aad; | ||
| if (protectedHeader) | ||
| jwe.protected = protectedHeader; | ||
| if (unprotected) | ||
| jwe.unprotected = unprotected; | ||
| if (header) | ||
| recipient.header = header; | ||
| } | ||
| export class GeneralEncrypt { | ||
@@ -54,3 +61,3 @@ #plaintext; | ||
| addRecipient(key, options) { | ||
| const recipient = new IndividualRecipient(this, key, { crit: options?.crit }); | ||
| const recipient = new IndividualRecipient(this, key, options?.crit); | ||
| this.#recipients.push(recipient); | ||
@@ -82,2 +89,3 @@ return recipient; | ||
| const [recipient] = this.#recipients; | ||
| const [unprotectedHeader, keyManagementParameters, key, crit] = recipient.state; | ||
| const flattened = await new FlattenedEncrypt(this.#plaintext) | ||
@@ -87,5 +95,5 @@ .setAdditionalAuthenticatedData(this.#aad) | ||
| .setSharedUnprotectedHeader(this.#unprotectedHeader) | ||
| .setUnprotectedHeader(recipient.unprotectedHeader) | ||
| .setKeyManagementParameters(recipient.keyManagementParameters) | ||
| .encrypt(recipient.key, { ...recipient.options }); | ||
| .setUnprotectedHeader(unprotectedHeader) | ||
| .setKeyManagementParameters(keyManagementParameters) | ||
| .encrypt(key, { crit }); | ||
| const jwe = { | ||
@@ -97,12 +105,5 @@ ciphertext: flattened.ciphertext, | ||
| }; | ||
| if (flattened.aad) | ||
| jwe.aad = flattened.aad; | ||
| if (flattened.protected) | ||
| jwe.protected = flattened.protected; | ||
| if (flattened.unprotected) | ||
| jwe.unprotected = flattened.unprotected; | ||
| if (flattened.encrypted_key) | ||
| jwe.recipients[0].encrypted_key = flattened.encrypted_key; | ||
| if (flattened.header) | ||
| jwe.recipients[0].header = flattened.header; | ||
| copyOptionalMembers(flattened, jwe, jwe.recipients[0]); | ||
| return jwe; | ||
@@ -116,26 +117,29 @@ } | ||
| const recipient = this.#recipients[i]; | ||
| const input = { | ||
| plaintext: this.#plaintext, | ||
| protectedHeader: this.#protectedHeader, | ||
| unprotectedHeader: recipient.unprotectedHeader, | ||
| sharedUnprotectedHeader: this.#unprotectedHeader, | ||
| aad: this.#aad, | ||
| keyManagementParameters: recipient.keyManagementParameters, | ||
| crit: recipient.options.crit, | ||
| unprotectedParameters: true, | ||
| }; | ||
| const [unprotectedHeader, keyManagementParameters, , crit] = recipient.state; | ||
| const input = [ | ||
| this.#plaintext, | ||
| this.#protectedHeader, | ||
| unprotectedHeader, | ||
| this.#unprotectedHeader, | ||
| this.#aad, | ||
| undefined, | ||
| undefined, | ||
| keyManagementParameters, | ||
| crit, | ||
| true, | ||
| ]; | ||
| const headers = checkEncryptHeaders(input); | ||
| inputs.push(input); | ||
| checked.push(headers); | ||
| if (headers.alg === 'dir' || headers.alg === 'ECDH-ES') { | ||
| if (headers[1] === 'dir' || headers[1] === 'ECDH-ES') { | ||
| throw new JWEInvalid('"dir" and "ECDH-ES" alg may only be used with a single recipient'); | ||
| } | ||
| if (!enc) { | ||
| enc = headers.enc; | ||
| enc = headers[2]; | ||
| } | ||
| else if (enc !== headers.enc) { | ||
| else if (enc !== headers[2]) { | ||
| throw new JWEInvalid('JWE "enc" (Encryption Algorithm) Header Parameter must be the same for all recipients'); | ||
| } | ||
| } | ||
| const cek = generateCek(checked[0].encEntry); | ||
| const cek = generateCek(checked[0][3]); | ||
| const jwe = { | ||
@@ -147,26 +151,21 @@ ciphertext: '', | ||
| const recipient = this.#recipients[i]; | ||
| const [unprotectedHeader, keyManagementParameters, key] = recipient.state; | ||
| const target = {}; | ||
| jwe.recipients.push(target); | ||
| if (i === 0) { | ||
| const flattened = await encryptJWE({ ...inputs[0], cek }, checked[0], recipient.key); | ||
| inputs[0][5] = cek; | ||
| const flattened = await encryptJWE(inputs[0], checked[0], key); | ||
| jwe.ciphertext = flattened.ciphertext; | ||
| jwe.iv = flattened.iv; | ||
| jwe.tag = flattened.tag; | ||
| if (flattened.aad) | ||
| jwe.aad = flattened.aad; | ||
| if (flattened.protected) | ||
| jwe.protected = flattened.protected; | ||
| if (flattened.unprotected) | ||
| jwe.unprotected = flattened.unprotected; | ||
| target.encrypted_key = flattened.encrypted_key; | ||
| if (flattened.header) | ||
| target.header = flattened.header; | ||
| copyOptionalMembers(flattened, jwe, target); | ||
| continue; | ||
| } | ||
| const { alg } = checked[i]; | ||
| const k = await prepareKey(jweAlgorithm(alg), recipient.key, 'encrypt'); | ||
| const { encryptedKey, parameters } = await encryptKeyManagement(alg, checked[i].encEntry, k, cek, recipient.keyManagementParameters); | ||
| const [, alg, , encEntry] = checked[i]; | ||
| const k = await prepareKey(jweAlgorithm(alg), key, 'encrypt'); | ||
| const [, encryptedKey, parameters] = await encryptKeyManagement(alg, encEntry, k, cek, keyManagementParameters); | ||
| target.encrypted_key = b64u(encryptedKey); | ||
| if (recipient.unprotectedHeader || parameters) | ||
| target.header = { ...recipient.unprotectedHeader, ...parameters }; | ||
| if (unprotectedHeader || parameters) | ||
| target.header = { ...unprotectedHeader, ...parameters }; | ||
| } | ||
@@ -173,0 +172,0 @@ return jwe; |
@@ -7,3 +7,3 @@ import { jwkToKey } from '../lib/jwk_to_key.js'; | ||
| const entry = typeof alg === 'string' ? maybeJWSAlgorithm(alg) : undefined; | ||
| if (!entry || entry.symmetric) { | ||
| if (!entry || entry.secret) { | ||
| throw new JOSENotSupported('Unsupported "alg" value for a JSON Web Key Set'); | ||
@@ -18,7 +18,4 @@ } | ||
| const { keys } = jwks; | ||
| return Array.isArray(keys) && keys.every(isJWKLike); | ||
| return Array.isArray(keys) && keys.every((isObject)); | ||
| } | ||
| function isJWKLike(key) { | ||
| return isObject(key); | ||
| } | ||
| class LocalJWKSetImpl { | ||
@@ -39,21 +36,8 @@ #jwks; | ||
| const entry = signatureAlgorithm(alg); | ||
| const candidates = this.#jwks.keys.filter((jwk) => { | ||
| let candidate = entry.kty.includes(jwk.kty); | ||
| if (candidate && typeof kid === 'string') { | ||
| candidate = kid === jwk.kid; | ||
| } | ||
| if (candidate && (typeof jwk.alg === 'string' || jwk.kty === 'AKP')) { | ||
| candidate = alg === jwk.alg; | ||
| } | ||
| if (candidate && typeof jwk.use === 'string') { | ||
| candidate = jwk.use === 'sig'; | ||
| } | ||
| if (candidate && Array.isArray(jwk.key_ops)) { | ||
| candidate = jwk.key_ops.includes('verify'); | ||
| } | ||
| if (candidate && entry.crv) { | ||
| candidate = jwk.crv === entry.crv; | ||
| } | ||
| return candidate; | ||
| }); | ||
| const candidates = this.#jwks.keys.filter((jwk) => entry.kty.includes(jwk.kty) && | ||
| (typeof kid !== 'string' || kid === jwk.kid) && | ||
| (!(typeof jwk.alg === 'string' || jwk.kty === 'AKP') || alg === jwk.alg) && | ||
| (typeof jwk.use !== 'string' || jwk.use === 'sig') && | ||
| (!Array.isArray(jwk.key_ops) || jwk.key_ops.includes('verify')) && | ||
| (!entry.crv || jwk.crv === entry.crv)); | ||
| const { 0: jwk, length } = candidates; | ||
@@ -93,11 +77,6 @@ if (length === 0) { | ||
| const localJWKSet = async (protectedHeader, token) => set.getKey(protectedHeader, token); | ||
| Object.defineProperties(localJWKSet, { | ||
| jwks: { | ||
| value: () => structuredClone(set.jwks()), | ||
| enumerable: false, | ||
| configurable: false, | ||
| writable: false, | ||
| }, | ||
| Object.defineProperty(localJWKSet, 'jwks', { | ||
| value: () => structuredClone(set.jwks()), | ||
| }); | ||
| return localJWKSet; | ||
| } |
@@ -12,3 +12,3 @@ import { JOSEError, JWKSNoMatchingKey, JWKSTimeout } from '../util/errors.js'; | ||
| const NAME = 'jose'; | ||
| const VERSION = 'v6.2.6'; | ||
| const VERSION = 'v6.2.7'; | ||
| USER_AGENT = `${NAME}/${VERSION}`; | ||
@@ -71,8 +71,8 @@ } | ||
| this.#url = new URL(url.href); | ||
| this.#timeoutDuration = | ||
| typeof options?.timeoutDuration === 'number' ? options?.timeoutDuration : 5000; | ||
| const opts = options ?? {}; | ||
| this.#timeoutDuration = typeof opts.timeoutDuration === 'number' ? opts.timeoutDuration : 5000; | ||
| this.#cooldownDuration = | ||
| typeof options?.cooldownDuration === 'number' ? options?.cooldownDuration : 30000; | ||
| this.#cacheMaxAge = typeof options?.cacheMaxAge === 'number' ? options?.cacheMaxAge : 600000; | ||
| this.#headers = new Headers(options?.headers); | ||
| typeof opts.cooldownDuration === 'number' ? opts.cooldownDuration : 30000; | ||
| this.#cacheMaxAge = typeof opts.cacheMaxAge === 'number' ? opts.cacheMaxAge : 600000; | ||
| this.#headers = new Headers(opts.headers); | ||
| if (USER_AGENT && !this.#headers.has('User-Agent')) { | ||
@@ -85,6 +85,7 @@ this.#headers.set('User-Agent', USER_AGENT); | ||
| } | ||
| this.#customFetch = options?.[customFetch]; | ||
| if (options?.[jwksCache] !== undefined) { | ||
| this.#cache = options?.[jwksCache]; | ||
| if (isFreshJwksCache(options?.[jwksCache], this.#cacheMaxAge)) { | ||
| this.#customFetch = opts[customFetch]; | ||
| const cache = opts[jwksCache]; | ||
| if (cache !== undefined) { | ||
| this.#cache = cache; | ||
| if (isFreshJwksCache(cache, this.#cacheMaxAge)) { | ||
| this.#jwksTimestamp = this.#cache.uat; | ||
@@ -98,11 +99,10 @@ this.#local = createLocalJWKSet(this.#cache.jwks); | ||
| } | ||
| #validFor(duration) { | ||
| return typeof this.#jwksTimestamp === 'number' && Date.now() < this.#jwksTimestamp + duration; | ||
| } | ||
| coolingDown() { | ||
| return typeof this.#jwksTimestamp === 'number' | ||
| ? Date.now() < this.#jwksTimestamp + this.#cooldownDuration | ||
| : false; | ||
| return this.#validFor(this.#cooldownDuration); | ||
| } | ||
| fresh() { | ||
| return typeof this.#jwksTimestamp === 'number' | ||
| ? Date.now() < this.#jwksTimestamp + this.#cacheMaxAge | ||
| : false; | ||
| return this.#validFor(this.#cacheMaxAge); | ||
| } | ||
@@ -141,7 +141,5 @@ jwks() { | ||
| this.#jwksTimestamp = Date.now(); | ||
| this.#pendingFetch = undefined; | ||
| }) | ||
| .catch((err) => { | ||
| .finally(() => { | ||
| this.#pendingFetch = undefined; | ||
| throw err; | ||
| }); | ||
@@ -158,3 +156,2 @@ await this.#pendingFetch; | ||
| enumerable: true, | ||
| configurable: false, | ||
| }, | ||
@@ -164,3 +161,2 @@ fresh: { | ||
| enumerable: true, | ||
| configurable: false, | ||
| }, | ||
@@ -170,4 +166,2 @@ reload: { | ||
| enumerable: true, | ||
| configurable: false, | ||
| writable: false, | ||
| }, | ||
@@ -177,3 +171,2 @@ reloading: { | ||
| enumerable: true, | ||
| configurable: false, | ||
| }, | ||
@@ -183,4 +176,2 @@ jwks: { | ||
| enumerable: true, | ||
| configurable: false, | ||
| writable: false, | ||
| }, | ||
@@ -187,0 +178,0 @@ }); |
| import { prepareVerify, verifyCompact } from '../../lib/jws_verify.js'; | ||
| export async function compactVerify(jws, key, options) { | ||
| const verified = await verifyCompact(jws, prepareVerify(options), key); | ||
| const result = { payload: verified.payload, protectedHeader: verified.parsedProt }; | ||
| const result = { payload: verified[0], protectedHeader: verified[1] }; | ||
| if (typeof key === 'function') { | ||
| return { ...result, key: verified.key }; | ||
| return { ...result, key: verified[3] }; | ||
| } | ||
| return result; | ||
| } |
@@ -1,2 +0,1 @@ | ||
| import { JWSInvalid } from '../../util/errors.js'; | ||
| import { createSignature } from '../../lib/jws_sign.js'; | ||
@@ -25,6 +24,3 @@ import { assertNotSet } from '../../lib/helpers.js'; | ||
| async sign(key, options) { | ||
| if (!this.#protectedHeader && !this.#unprotectedHeader) { | ||
| throw new JWSInvalid('either setProtectedHeader or setUnprotectedHeader must be called before #sign()'); | ||
| } | ||
| const jws = await createSignature({ | ||
| return createSignature({ | ||
| payload: this.#payload, | ||
@@ -35,7 +31,3 @@ protectedHeader: this.#protectedHeader, | ||
| }, key); | ||
| if (this.#unprotectedHeader) { | ||
| jws.header = this.#unprotectedHeader; | ||
| } | ||
| return jws; | ||
| } | ||
| } |
@@ -6,19 +6,15 @@ import { createSignature } from '../../lib/jws_sign.js'; | ||
| #parent; | ||
| protectedHeader; | ||
| unprotectedHeader; | ||
| options; | ||
| key; | ||
| state; | ||
| constructor(sig, key, options) { | ||
| this.#parent = sig; | ||
| this.key = key; | ||
| this.options = options; | ||
| this.state = [undefined, undefined, key, options?.crit]; | ||
| } | ||
| setProtectedHeader(protectedHeader) { | ||
| assertNotSet(this.protectedHeader, 'setProtectedHeader'); | ||
| this.protectedHeader = protectedHeader; | ||
| assertNotSet(this.state[0], 'setProtectedHeader'); | ||
| this.state[0] = protectedHeader; | ||
| return this; | ||
| } | ||
| setUnprotectedHeader(unprotectedHeader) { | ||
| assertNotSet(this.unprotectedHeader, 'setUnprotectedHeader'); | ||
| this.unprotectedHeader = unprotectedHeader; | ||
| assertNotSet(this.state[1], 'setUnprotectedHeader'); | ||
| this.state[1] = unprotectedHeader; | ||
| return this; | ||
@@ -58,18 +54,13 @@ } | ||
| }; | ||
| const encoded = {}; | ||
| const encoded = []; | ||
| for (let i = 0; i < this.#signatures.length; i++) { | ||
| const signature = this.#signatures[i]; | ||
| if (!signature.protectedHeader && !signature.unprotectedHeader) { | ||
| throw new JWSInvalid('either setProtectedHeader or setUnprotectedHeader must be called before #sign()'); | ||
| } | ||
| const [protectedHeader, unprotectedHeader, key, crit] = signature.state; | ||
| const { payload, ...rest } = await createSignature({ | ||
| payload: this.#payload, | ||
| protectedHeader: signature.protectedHeader, | ||
| unprotectedHeader: signature.unprotectedHeader, | ||
| crit: signature.options?.crit, | ||
| protectedHeader, | ||
| unprotectedHeader, | ||
| crit, | ||
| encoded, | ||
| }, signature.key); | ||
| if (signature.unprotectedHeader) { | ||
| rest.header = signature.unprotectedHeader; | ||
| } | ||
| }, key); | ||
| if (i === 0) { | ||
@@ -76,0 +67,0 @@ jws.payload = payload; |
@@ -8,3 +8,4 @@ import { prepareVerify, verifySignature, verifyResult } from '../../lib/jws_verify.js'; | ||
| } | ||
| if (!Array.isArray(jws.signatures) || !jws.signatures.every(isObject)) { | ||
| const { signatures, payload } = jws; | ||
| if (!Array.isArray(signatures) || !signatures.every(isObject)) { | ||
| throw new JWSInvalid('JWS Signatures missing or incorrect type'); | ||
@@ -14,3 +15,3 @@ } | ||
| try { | ||
| if (jws.payload === undefined) | ||
| if (payload === undefined) | ||
| throw new Error(); | ||
@@ -22,18 +23,19 @@ shared = prepareVerify(options); | ||
| } | ||
| for (const signature of jws.signatures) { | ||
| for (const signature of signatures) { | ||
| try { | ||
| if (signature.protected === undefined && signature.header === undefined) | ||
| const { protected: encodedProtected, header, signature: encodedSignature } = signature; | ||
| if (encodedProtected === undefined && header === undefined) | ||
| throw new Error(); | ||
| if (signature.protected !== undefined && typeof signature.protected !== 'string') { | ||
| if (encodedProtected !== undefined && typeof encodedProtected !== 'string') { | ||
| throw new Error(); | ||
| } | ||
| if (typeof signature.signature !== 'string') | ||
| if (typeof encodedSignature !== 'string') | ||
| throw new Error(); | ||
| if (signature.header !== undefined && !isObject(signature.header)) | ||
| if (header !== undefined && !isObject(header)) | ||
| throw new Error(); | ||
| return verifyResult(signature, await verifySignature({ | ||
| header: signature.header, | ||
| payload: jws.payload, | ||
| protected: signature.protected, | ||
| signature: signature.signature, | ||
| header, | ||
| payload, | ||
| protected: encodedProtected, | ||
| signature: encodedSignature, | ||
| }, shared, key)); | ||
@@ -40,0 +42,0 @@ } |
@@ -6,4 +6,4 @@ import { prepareDecrypt, decryptCompact } from '../lib/jwe_decrypt.js'; | ||
| const decrypted = await decryptCompact(jwt, prepareDecrypt(options), key); | ||
| const protectedHeader = decrypted.parsedProt; | ||
| const payload = validateClaimsSet(protectedHeader, decrypted.plaintext, options); | ||
| const protectedHeader = decrypted[1]; | ||
| const payload = validateClaimsSet(protectedHeader, decrypted[0], options); | ||
| if (protectedHeader.iss !== undefined && protectedHeader.iss !== payload.iss) { | ||
@@ -21,5 +21,5 @@ throw new JWTClaimValidationFailed('replicated "iss" claim header parameter mismatch', payload, 'iss', 'mismatch'); | ||
| if (typeof key === 'function') { | ||
| return { ...result, key: decrypted.key }; | ||
| return { ...result, key: decrypted[2] }; | ||
| } | ||
| return result; | ||
| } |
@@ -6,11 +6,11 @@ import { prepareVerify, verifyCompact } from '../lib/jws_verify.js'; | ||
| const verified = await verifyCompact(jwt, prepareVerify(options), key); | ||
| if (!verified.b64) { | ||
| if (!verified[2]) { | ||
| throw new JWTInvalid('JWTs MUST NOT use unencoded payload'); | ||
| } | ||
| const payload = validateClaimsSet(verified.parsedProt, verified.payload, options); | ||
| const result = { payload, protectedHeader: verified.parsedProt }; | ||
| const payload = validateClaimsSet(verified[1], verified[0], options); | ||
| const result = { payload, protectedHeader: verified[1] }; | ||
| if (typeof key === 'function') { | ||
| return { ...result, key: verified.key }; | ||
| return { ...result, key: verified[3] }; | ||
| } | ||
| return result; | ||
| } |
@@ -36,10 +36,10 @@ import { toSPKI as exportPublic, toPKCS8 as exportPrivate } from '../lib/asn1.js'; | ||
| } | ||
| export async function exportSPKI(key) { | ||
| export function exportSPKI(key) { | ||
| return exportPublic(key); | ||
| } | ||
| export async function exportPKCS8(key) { | ||
| export function exportPKCS8(key) { | ||
| return exportPrivate(key); | ||
| } | ||
| export async function exportJWK(key) { | ||
| export function exportJWK(key) { | ||
| return keyToJWK(key); | ||
| } |
@@ -12,12 +12,13 @@ import { JOSENotSupported } from '../util/errors.js'; | ||
| const entry = keyAlgorithm(alg); | ||
| if (entry.symmetric) { | ||
| if (entry.secret) { | ||
| throw new JOSENotSupported('Invalid or unsupported JWK "alg" (Algorithm) Parameter value'); | ||
| } | ||
| let algorithm; | ||
| if (entry.subtleFor) { | ||
| switch (options?.crv ?? 'P-256') { | ||
| if (entry.resolve) { | ||
| const crv = options?.crv ?? 'P-256'; | ||
| switch (crv) { | ||
| case 'P-256': | ||
| case 'P-384': | ||
| case 'P-521': | ||
| algorithm = { name: 'ECDH', namedCurve: options?.crv ?? 'P-256' }; | ||
| algorithm = { name: 'ECDH', namedCurve: crv }; | ||
| break; | ||
@@ -45,5 +46,5 @@ case 'X25519': | ||
| return crypto.subtle.generateKey(algorithm, options?.extractable ?? false, [ | ||
| ...entry.usages.private, | ||
| ...entry.usages.public, | ||
| ...entry.usages[1], | ||
| ...entry.usages[0], | ||
| ]); | ||
| } |
+29
-62
@@ -60,9 +60,6 @@ import { invalidKeyInput } from './invalid_key_input.js'; | ||
| const skipElement = (state, count = 1) => { | ||
| if (count <= 0) | ||
| return; | ||
| state.pos++; | ||
| const length = parseLength(state); | ||
| state.pos += length; | ||
| if (count > 1) { | ||
| skipElement(state, count - 1); | ||
| while (count-- > 0) { | ||
| state.pos++; | ||
| const length = parseLength(state); | ||
| state.pos += length; | ||
| } | ||
@@ -88,20 +85,12 @@ }; | ||
| }; | ||
| function parsePKCS8Header(state) { | ||
| expectTag(state, 0x30, 'Invalid PKCS#8 structure'); | ||
| function parseKeyHeader(state, keyFormat) { | ||
| expectTag(state, 0x30, `Invalid ${keyFormat === 'spki' ? 'SPKI' : 'PKCS#8'} structure`); | ||
| parseLength(state); | ||
| expectTag(state, 0x02, 'Expected version field'); | ||
| const verLen = parseLength(state); | ||
| state.pos += verLen; | ||
| if (keyFormat === 'pkcs8') { | ||
| expectTag(state, 0x02, 'Expected version field'); | ||
| const length = parseLength(state); | ||
| state.pos += length; | ||
| } | ||
| expectTag(state, 0x30, 'Expected algorithm identifier'); | ||
| const algIdLen = parseLength(state); | ||
| const algIdStart = state.pos; | ||
| return { algIdStart, algIdLength: algIdLen }; | ||
| } | ||
| function parseSPKIHeader(state) { | ||
| expectTag(state, 0x30, 'Invalid SPKI structure'); | ||
| parseLength(state); | ||
| expectTag(state, 0x30, 'Expected algorithm identifier'); | ||
| const algIdLen = parseLength(state); | ||
| const algIdStart = state.pos; | ||
| return { algIdStart, algIdLength: algIdLen }; | ||
| } | ||
@@ -119,11 +108,8 @@ const parseECAlgorithmIdentifier = (state) => { | ||
| const curveOid = getSubarray(state, curveOidLen); | ||
| for (const { name, oid } of [ | ||
| { name: 'P-256', oid: [0x2a, 0x86, 0x48, 0xce, 0x3d, 0x03, 0x01, 0x07] }, | ||
| { name: 'P-384', oid: [0x2b, 0x81, 0x04, 0x00, 0x22] }, | ||
| { name: 'P-521', oid: [0x2b, 0x81, 0x04, 0x00, 0x23] }, | ||
| ]) { | ||
| if (bytesEqual(curveOid, oid)) { | ||
| return name; | ||
| } | ||
| } | ||
| if (bytesEqual(curveOid, [0x2a, 0x86, 0x48, 0xce, 0x3d, 0x03, 0x01, 0x07])) | ||
| return 'P-256'; | ||
| if (bytesEqual(curveOid, [0x2b, 0x81, 0x04, 0x00, 0x22])) | ||
| return 'P-384'; | ||
| if (bytesEqual(curveOid, [0x2b, 0x81, 0x04, 0x00, 0x23])) | ||
| return 'P-521'; | ||
| throw new Error('Unsupported named curve'); | ||
@@ -133,3 +119,3 @@ }; | ||
| const entry = keyAlgorithm(alg); | ||
| if (entry.symmetric) { | ||
| if (entry.secret) { | ||
| throw new JOSENotSupported('Invalid or unsupported "alg" (Algorithm) value'); | ||
@@ -139,7 +125,9 @@ } | ||
| let algorithm; | ||
| if (entry.subtleFor) { | ||
| if (entry.resolve) { | ||
| try { | ||
| algorithm = entry.subtleFor({ crv: options.getNamedCurve(keyData) }); | ||
| const state = createASN1State(keyData); | ||
| parseKeyHeader(state, keyFormat); | ||
| algorithm = entry.resolve({ crv: parseECAlgorithmIdentifier(state) }); | ||
| } | ||
| catch (cause) { | ||
| catch { | ||
| throw new JOSENotSupported('Invalid or unsupported key format'); | ||
@@ -151,3 +139,3 @@ } | ||
| } | ||
| return crypto.subtle.importKey(keyFormat, keyData, algorithm, options?.extractable ?? (isPublic ? true : false), isPublic ? entry.usages.public : entry.usages.private); | ||
| return crypto.subtle.importKey(keyFormat, keyData, algorithm, options?.extractable ?? isPublic, entry.usages[isPublic ? 0 : 1]); | ||
| }; | ||
@@ -159,25 +147,7 @@ const processPEMData = (pem, pattern) => { | ||
| const keyData = processPEMData(pem, /(?:-----(?:BEGIN|END) PRIVATE KEY-----|\s)/g); | ||
| let opts = options; | ||
| if (alg?.startsWith?.('ECDH-ES')) { | ||
| opts ||= {}; | ||
| opts.getNamedCurve = (keyData) => { | ||
| const state = createASN1State(keyData); | ||
| parsePKCS8Header(state); | ||
| return parseECAlgorithmIdentifier(state); | ||
| }; | ||
| } | ||
| return genericImport('pkcs8', keyData, alg, opts); | ||
| return genericImport('pkcs8', keyData, alg, options); | ||
| }; | ||
| export const fromSPKI = (pem, alg, options) => { | ||
| const keyData = processPEMData(pem, /(?:-----(?:BEGIN|END) PUBLIC KEY-----|\s)/g); | ||
| let opts = options; | ||
| if (alg?.startsWith?.('ECDH-ES')) { | ||
| opts ||= {}; | ||
| opts.getNamedCurve = (keyData) => { | ||
| const state = createASN1State(keyData); | ||
| parseSPKIHeader(state); | ||
| return parseECAlgorithmIdentifier(state); | ||
| }; | ||
| } | ||
| return genericImport('spki', keyData, alg, opts); | ||
| return genericImport('spki', keyData, alg, options); | ||
| }; | ||
@@ -201,10 +171,7 @@ function spkiFromX509(buf) { | ||
| } | ||
| function extractX509SPKI(x509) { | ||
| const derBytes = processPEMData(x509, /(?:-----(?:BEGIN|END) CERTIFICATE-----|\s)/g); | ||
| return spkiFromX509(derBytes); | ||
| } | ||
| export const fromX509 = (pem, alg, options) => { | ||
| let spki; | ||
| try { | ||
| spki = extractX509SPKI(pem); | ||
| const certificate = processPEMData(pem, /(?:-----(?:BEGIN|END) CERTIFICATE-----|\s)/g); | ||
| spki = spkiFromX509(certificate); | ||
| } | ||
@@ -214,3 +181,3 @@ catch (cause) { | ||
| } | ||
| return fromSPKI(formatPEM(encodeBase64(spki), 'PUBLIC KEY'), alg, options); | ||
| return genericImport('spki', spki, alg, options); | ||
| }; |
@@ -29,3 +29,3 @@ import { concat, uint64be } from './buffer_utils.js'; | ||
| }, false, ['sign']); | ||
| return { encKey, macKey, keySize }; | ||
| return [encKey, macKey, keySize]; | ||
| } | ||
@@ -36,3 +36,3 @@ async function cbcHmacTag(macKey, macData, keySize) { | ||
| async function cbcEncrypt(enc, plaintext, cek, iv, aad) { | ||
| const { encKey, macKey, keySize } = await cbcKeySetup(enc, cek, 'encrypt'); | ||
| const [encKey, macKey, keySize] = await cbcKeySetup(enc, cek, 'encrypt'); | ||
| const ciphertext = new Uint8Array(await crypto.subtle.encrypt({ | ||
@@ -47,21 +47,9 @@ iv: iv, | ||
| async function timingSafeEqual(a, b) { | ||
| if (!(a instanceof Uint8Array)) { | ||
| throw new TypeError('First argument must be a buffer'); | ||
| } | ||
| if (!(b instanceof Uint8Array)) { | ||
| throw new TypeError('Second argument must be a buffer'); | ||
| } | ||
| const algorithm = { name: 'HMAC', hash: 'SHA-256' }; | ||
| const key = (await crypto.subtle.generateKey(algorithm, false, ['sign'])); | ||
| const aHmac = new Uint8Array(await crypto.subtle.sign(algorithm, key, a)); | ||
| const bHmac = new Uint8Array(await crypto.subtle.sign(algorithm, key, b)); | ||
| let out = 0; | ||
| let i = -1; | ||
| while (++i < 32) { | ||
| out |= aHmac[i] ^ bHmac[i]; | ||
| } | ||
| return out === 0; | ||
| const key = (await crypto.subtle.generateKey(algorithm, false, ['sign', 'verify'])); | ||
| const aHmac = await crypto.subtle.sign(algorithm, key, a); | ||
| return crypto.subtle.verify(algorithm, key, aHmac, b); | ||
| } | ||
| async function cbcDecrypt(enc, cek, ciphertext, iv, tag, aad) { | ||
| const { encKey, macKey, keySize } = await cbcKeySetup(enc, cek, 'decrypt'); | ||
| const [encKey, macKey, keySize] = await cbcKeySetup(enc, cek, 'decrypt'); | ||
| const macData = concat(aad, iv, ciphertext, uint64be(aad.length * 8)); | ||
@@ -90,10 +78,7 @@ const expectedTag = await cbcHmacTag(macKey, macData, keySize); | ||
| async function gcmEncrypt(enc, plaintext, cek, iv, aad) { | ||
| let encKey; | ||
| if (cek instanceof Uint8Array) { | ||
| encKey = await crypto.subtle.importKey('raw', cek, 'AES-GCM', false, ['encrypt']); | ||
| } | ||
| else { | ||
| checkCryptoKey(cek, enc.subtle, 'encrypt'); | ||
| encKey = cek; | ||
| } | ||
| const encKey = cek instanceof Uint8Array | ||
| ? await crypto.subtle.importKey('raw', cek, 'AES-GCM', false, [ | ||
| 'encrypt', | ||
| ]) | ||
| : (checkCryptoKey(cek, enc.subtle, 'encrypt'), cek); | ||
| const encrypted = new Uint8Array(await crypto.subtle.encrypt({ | ||
@@ -110,10 +95,7 @@ additionalData: aad, | ||
| async function gcmDecrypt(enc, cek, ciphertext, iv, tag, aad) { | ||
| let encKey; | ||
| if (cek instanceof Uint8Array) { | ||
| encKey = await crypto.subtle.importKey('raw', cek, 'AES-GCM', false, ['decrypt']); | ||
| } | ||
| else { | ||
| checkCryptoKey(cek, enc.subtle, 'decrypt'); | ||
| encKey = cek; | ||
| } | ||
| const encKey = cek instanceof Uint8Array | ||
| ? await crypto.subtle.importKey('raw', cek, 'AES-GCM', false, [ | ||
| 'decrypt', | ||
| ]) | ||
| : (checkCryptoKey(cek, enc.subtle, 'decrypt'), cek); | ||
| try { | ||
@@ -120,0 +102,0 @@ return new Uint8Array(await crypto.subtle.decrypt({ |
@@ -7,2 +7,8 @@ const unusable = (name, prop = 'algorithm.name') => new TypeError(`CryptoKey does not support this operation, its ${prop} must be ${name}`); | ||
| } | ||
| export function checkModulusLength(alg, key) { | ||
| const { modulusLength } = key.algorithm; | ||
| if (typeof modulusLength !== 'number' || modulusLength < 2048) { | ||
| throw new TypeError(`${alg} requires key modulusLength to be 2048 bits or larger`); | ||
| } | ||
| } | ||
| export function checkCryptoKey(key, expected, usage) { | ||
@@ -9,0 +15,0 @@ const algorithm = key.algorithm; |
| function message(msg, actual, ...types) { | ||
| types = types.filter(Boolean); | ||
| if (types.length > 2) { | ||
@@ -4,0 +3,0 @@ const last = types.pop(); |
| import { JOSENotSupported } from '../util/errors.js'; | ||
| import { table } from './key_descriptor.js'; | ||
| const wrap = { | ||
| public: ['encrypt', 'wrapKey'], | ||
| private: ['decrypt', 'unwrapKey'], | ||
| }; | ||
| const derive = { public: [], private: ['deriveBits'] }; | ||
| const none = { public: [], private: [] }; | ||
| const wrap = [ | ||
| ['encrypt', 'wrapKey'], | ||
| ['decrypt', 'unwrapKey'], | ||
| ]; | ||
| const derive = [[], ['deriveBits']]; | ||
| const none = [[], []]; | ||
| function rsaes(bits) { | ||
@@ -14,11 +14,10 @@ return { | ||
| usages: wrap, | ||
| minModulusLength: 2048, | ||
| keyOps: { encrypt: 'wrapKey', decrypt: 'unwrapKey' }, | ||
| ops: ['wrapKey', 'unwrapKey'], | ||
| }; | ||
| } | ||
| function ecdh(kwBits) { | ||
| function ecdh() { | ||
| return { | ||
| kty: ['EC', 'OKP'], | ||
| subtle: { name: 'ECDH' }, | ||
| subtleFor: ({ kty, crv, asymmetricKeyType }) => { | ||
| resolve: ({ kty, crv, asymmetricKeyType }) => { | ||
| if (crv === 'X25519' || asymmetricKeyType === 'x25519') { | ||
@@ -33,34 +32,21 @@ return { name: 'X25519' }; | ||
| usages: derive, | ||
| kwBits, | ||
| keyOps: { decrypt: 'deriveBits' }, | ||
| ops: [undefined, 'deriveBits'], | ||
| }; | ||
| } | ||
| function aeskw(bits) { | ||
| function aeskw(bits, gcm = false) { | ||
| return { | ||
| kty: ['oct'], | ||
| symmetric: true, | ||
| subtle: { name: 'AES-KW', length: bits }, | ||
| secret: true, | ||
| subtle: { name: gcm ? 'AES-GCM' : 'AES-KW', length: bits }, | ||
| usages: none, | ||
| keyOps: { encrypt: 'wrapKey', decrypt: 'unwrapKey' }, | ||
| ops: gcm ? ['encrypt', 'decrypt'] : ['wrapKey', 'unwrapKey'], | ||
| }; | ||
| } | ||
| function aesgcmkw(bits) { | ||
| function pbes2() { | ||
| return { | ||
| kty: ['oct'], | ||
| symmetric: true, | ||
| subtle: { name: 'AES-GCM', length: bits }, | ||
| usages: none, | ||
| gcmkw: `A${bits}GCM`, | ||
| keyOps: { encrypt: 'encrypt', decrypt: 'decrypt' }, | ||
| }; | ||
| } | ||
| function pbes2(bits, kwBits) { | ||
| return { | ||
| kty: ['oct'], | ||
| symmetric: true, | ||
| secret: true, | ||
| subtle: { name: 'PBKDF2' }, | ||
| usages: none, | ||
| pbes2Hash: `SHA-${bits}`, | ||
| kwBits, | ||
| keyOps: { encrypt: 'deriveBits', decrypt: 'deriveBits' }, | ||
| ops: ['deriveBits', 'deriveBits'], | ||
| }; | ||
@@ -71,6 +57,6 @@ } | ||
| kty: ['oct'], | ||
| symmetric: true, | ||
| secret: true, | ||
| subtle: { name: 'AES-GCM' }, | ||
| usages: none, | ||
| keyOps: { encrypt: 'encrypt', decrypt: 'decrypt' }, | ||
| ops: ['encrypt', 'decrypt'], | ||
| }, | ||
@@ -82,54 +68,41 @@ 'RSA-OAEP': rsaes(1), | ||
| 'ECDH-ES': ecdh(), | ||
| 'ECDH-ES+A128KW': ecdh(128), | ||
| 'ECDH-ES+A192KW': ecdh(192), | ||
| 'ECDH-ES+A256KW': ecdh(256), | ||
| 'ECDH-ES+A128KW': ecdh(), | ||
| 'ECDH-ES+A192KW': ecdh(), | ||
| 'ECDH-ES+A256KW': ecdh(), | ||
| A128KW: aeskw(128), | ||
| A192KW: aeskw(192), | ||
| A256KW: aeskw(256), | ||
| A128GCMKW: aesgcmkw(128), | ||
| A192GCMKW: aesgcmkw(192), | ||
| A256GCMKW: aesgcmkw(256), | ||
| 'PBES2-HS256+A128KW': pbes2(256, 128), | ||
| 'PBES2-HS384+A192KW': pbes2(384, 192), | ||
| 'PBES2-HS512+A256KW': pbes2(512, 256), | ||
| A128GCMKW: aeskw(128, true), | ||
| A192GCMKW: aeskw(192, true), | ||
| A256GCMKW: aeskw(256, true), | ||
| 'PBES2-HS256+A128KW': pbes2(), | ||
| 'PBES2-HS384+A192KW': pbes2(), | ||
| 'PBES2-HS512+A256KW': pbes2(), | ||
| }); | ||
| const content = { public: [], private: [] }; | ||
| const contentOps = { encrypt: 'encrypt', decrypt: 'decrypt' }; | ||
| function gcm(bits) { | ||
| const contentOps = ['encrypt', 'decrypt']; | ||
| function contentEncryption(bits, cbc = false) { | ||
| return { | ||
| kty: ['oct'], | ||
| symmetric: true, | ||
| subtle: { name: 'AES-GCM', length: bits }, | ||
| usages: content, | ||
| keyOps: contentOps, | ||
| secret: true, | ||
| subtle: { name: cbc ? 'AES-CBC' : 'AES-GCM', length: bits }, | ||
| usages: none, | ||
| ops: contentOps, | ||
| cekBits: bits, | ||
| ivBits: 96, | ||
| cbc: false, | ||
| ivBits: cbc ? 128 : 96, | ||
| cbc, | ||
| }; | ||
| } | ||
| function cbc(bits) { | ||
| return { | ||
| kty: ['oct'], | ||
| symmetric: true, | ||
| subtle: { name: 'AES-CBC', length: bits }, | ||
| usages: content, | ||
| keyOps: contentOps, | ||
| cekBits: bits, | ||
| ivBits: 128, | ||
| cbc: true, | ||
| }; | ||
| } | ||
| const ENC = table({ | ||
| A128GCM: gcm(128), | ||
| A192GCM: gcm(192), | ||
| A256GCM: gcm(256), | ||
| 'A128CBC-HS256': cbc(256), | ||
| 'A192CBC-HS384': cbc(384), | ||
| 'A256CBC-HS512': cbc(512), | ||
| A128GCM: contentEncryption(128), | ||
| A192GCM: contentEncryption(192), | ||
| A256GCM: contentEncryption(256), | ||
| 'A128CBC-HS256': contentEncryption(256, true), | ||
| 'A192CBC-HS384': contentEncryption(384, true), | ||
| 'A256CBC-HS512': contentEncryption(512, true), | ||
| }); | ||
| const unsupportedAlgHeader = 'Invalid or unsupported "alg" (JWE Algorithm) header value'; | ||
| const unsupportedAlg = 'Invalid or unsupported "alg" (JWE Algorithm) header value'; | ||
| export function jweAlgorithm(alg) { | ||
| const entry = JWE[alg]; | ||
| if (!entry) { | ||
| throw new JOSENotSupported(unsupportedAlgHeader); | ||
| throw new JOSENotSupported(unsupportedAlg); | ||
| } | ||
@@ -136,0 +109,0 @@ return entry; |
@@ -12,6 +12,7 @@ import { decrypt, generateCek } from './content_encryption.js'; | ||
| export function checkShared(jwe) { | ||
| const { ciphertext, protected: encodedProtected, unprotected } = jwe; | ||
| if (jwe.iv !== undefined && typeof jwe.iv !== 'string') { | ||
| throw new JWEInvalid('JWE Initialization Vector incorrect type'); | ||
| } | ||
| if (typeof jwe.ciphertext !== 'string') { | ||
| if (typeof ciphertext !== 'string') { | ||
| throw new JWEInvalid('JWE Ciphertext missing or incorrect type'); | ||
@@ -22,3 +23,3 @@ } | ||
| } | ||
| if (jwe.protected !== undefined && typeof jwe.protected !== 'string') { | ||
| if (encodedProtected !== undefined && typeof encodedProtected !== 'string') { | ||
| throw new JWEInvalid('JWE Protected Header incorrect type'); | ||
@@ -29,3 +30,3 @@ } | ||
| } | ||
| if (jwe.unprotected !== undefined && !isObject(jwe.unprotected)) { | ||
| if (unprotected !== undefined && !isObject(unprotected)) { | ||
| throw new JWEInvalid('JWE Shared Unprotected Header incorrect type'); | ||
@@ -35,9 +36,10 @@ } | ||
| export function checkRecipient(jwe) { | ||
| if (jwe.encrypted_key !== undefined && typeof jwe.encrypted_key !== 'string') { | ||
| const { encrypted_key: encryptedKey, header } = jwe; | ||
| if (encryptedKey !== undefined && typeof encryptedKey !== 'string') { | ||
| throw new JWEInvalid('JWE Encrypted Key incorrect type'); | ||
| } | ||
| if (jwe.header !== undefined && !isObject(jwe.header)) { | ||
| if (header !== undefined && !isObject(header)) { | ||
| throw new JWEInvalid('JWE Per-Recipient Unprotected Header incorrect type'); | ||
| } | ||
| if (jwe.protected === undefined && jwe.header === undefined && jwe.unprotected === undefined) { | ||
| if (jwe.protected === undefined && header === undefined && jwe.unprotected === undefined) { | ||
| throw new JWEInvalid('JOSE Header missing'); | ||
@@ -47,33 +49,36 @@ } | ||
| export function shareJWE(jwe) { | ||
| const { protected: encodedProtected, ciphertext, iv, tag, aad } = jwe; | ||
| let parsedProt; | ||
| if (jwe.protected) { | ||
| parsedProt = parseJoseHeader(jwe.protected, JWEInvalid, 'JWE Protected Header is invalid'); | ||
| if (encodedProtected) { | ||
| parsedProt = parseJoseHeader(encodedProtected, JWEInvalid, 'JWE Protected Header is invalid'); | ||
| } | ||
| const protectedHeader = jwe.protected !== undefined ? encode(jwe.protected) : new Uint8Array(); | ||
| return { | ||
| const protectedHeader = encodedProtected !== undefined ? encode(encodedProtected) : new Uint8Array(); | ||
| return [ | ||
| parsedProt, | ||
| ciphertext: decodeBase64url(jwe.ciphertext, 'ciphertext', JWEInvalid), | ||
| iv: jwe.iv !== undefined ? decodeBase64url(jwe.iv, 'iv', JWEInvalid) : undefined, | ||
| tag: jwe.tag !== undefined ? decodeBase64url(jwe.tag, 'tag', JWEInvalid) : undefined, | ||
| additionalData: jwe.aad !== undefined | ||
| ? concat(protectedHeader, encode('.'), encodeBase64url(jwe.aad, 'aad', JWEInvalid)) | ||
| decodeBase64url(ciphertext, 'ciphertext', JWEInvalid), | ||
| iv !== undefined ? decodeBase64url(iv, 'iv', JWEInvalid) : undefined, | ||
| tag !== undefined ? decodeBase64url(tag, 'tag', JWEInvalid) : undefined, | ||
| aad !== undefined | ||
| ? concat(protectedHeader, encode('.'), encodeBase64url(aad, 'aad', JWEInvalid)) | ||
| : protectedHeader, | ||
| }; | ||
| ]; | ||
| } | ||
| export function decryptResult(jwe, decrypted) { | ||
| const result = { plaintext: decrypted.plaintext }; | ||
| if (jwe.protected !== undefined) { | ||
| result.protectedHeader = decrypted.parsedProt; | ||
| const [plaintext, parsedProt, key, resolvedKey] = decrypted; | ||
| const { protected: encodedProtected, aad, unprotected, header } = jwe; | ||
| const result = { plaintext }; | ||
| if (encodedProtected !== undefined) { | ||
| result.protectedHeader = parsedProt; | ||
| } | ||
| if (jwe.aad !== undefined) { | ||
| result.additionalAuthenticatedData = decodeBase64url(jwe.aad, 'aad', JWEInvalid); | ||
| if (aad !== undefined) { | ||
| result.additionalAuthenticatedData = decodeBase64url(aad, 'aad', JWEInvalid); | ||
| } | ||
| if (jwe.unprotected !== undefined) { | ||
| result.sharedUnprotectedHeader = jwe.unprotected; | ||
| if (unprotected !== undefined) { | ||
| result.sharedUnprotectedHeader = unprotected; | ||
| } | ||
| if (jwe.header !== undefined) { | ||
| result.unprotectedHeader = jwe.header; | ||
| if (header !== undefined) { | ||
| result.unprotectedHeader = header; | ||
| } | ||
| if (decrypted.resolvedKey) { | ||
| return { ...result, key: decrypted.key }; | ||
| if (resolvedKey) { | ||
| return { ...result, key }; | ||
| } | ||
@@ -83,18 +88,19 @@ return result; | ||
| export function prepareDecrypt(options) { | ||
| return { | ||
| keyManagementAlgorithms: options && validateAlgorithms('keyManagementAlgorithms', options.keyManagementAlgorithms), | ||
| contentEncryptionAlgorithms: options && | ||
| return [ | ||
| options && validateAlgorithms('keyManagementAlgorithms', options.keyManagementAlgorithms), | ||
| options && | ||
| validateAlgorithms('contentEncryptionAlgorithms', options.contentEncryptionAlgorithms), | ||
| options, | ||
| }; | ||
| ]; | ||
| } | ||
| export async function decryptRecipient(jwe, token, shared, key) { | ||
| const { options } = shared; | ||
| const { parsedProt } = token; | ||
| const [keyManagementAlgorithms, contentEncryptionAlgorithms, options] = shared; | ||
| const [parsedProt, ciphertext, iv, tag, additionalData] = token; | ||
| const { encrypted_key: encodedKey, header, unprotected } = jwe; | ||
| let joseHeader; | ||
| if (jwe.header !== undefined || jwe.unprotected !== undefined) { | ||
| if (!isDisjoint(parsedProt, jwe.header, jwe.unprotected)) { | ||
| if (header !== undefined || unprotected !== undefined) { | ||
| if (!isDisjoint(parsedProt, header, unprotected)) { | ||
| throw new JWEInvalid('JWE Protected, JWE Unprotected Header, and JWE Per-Recipient Unprotected Header Parameter names must be disjoint'); | ||
| } | ||
| joseHeader = { ...parsedProt, ...jwe.header, ...jwe.unprotected }; | ||
| joseHeader = { ...parsedProt, ...header, ...unprotected }; | ||
| } | ||
@@ -118,3 +124,2 @@ else { | ||
| } | ||
| const { keyManagementAlgorithms, contentEncryptionAlgorithms } = shared; | ||
| if ((keyManagementAlgorithms && !keyManagementAlgorithms.has(alg)) || | ||
@@ -129,4 +134,4 @@ (!keyManagementAlgorithms && alg.startsWith('PBES2'))) { | ||
| let encryptedKey; | ||
| if (jwe.encrypted_key !== undefined) { | ||
| encryptedKey = decodeBase64url(jwe.encrypted_key, 'encrypted_key', JWEInvalid); | ||
| if (encodedKey !== undefined) { | ||
| encryptedKey = decodeBase64url(encodedKey, 'encrypted_key', JWEInvalid); | ||
| } | ||
@@ -150,3 +155,3 @@ let resolvedKey = false; | ||
| } | ||
| let plaintext = await decrypt(encEntry, cek, token.ciphertext, token.iv, token.tag, token.additionalData); | ||
| let plaintext = await decrypt(encEntry, cek, ciphertext, iv, tag, additionalData); | ||
| if (joseHeader.zip === 'DEF') { | ||
@@ -167,3 +172,3 @@ const maxDecompressedLength = options?.maxDecompressedLength ?? 250_000; | ||
| } | ||
| return { plaintext, parsedProt, key: k, resolvedKey }; | ||
| return [plaintext, parsedProt, k, resolvedKey]; | ||
| } | ||
@@ -170,0 +175,0 @@ export async function decryptJWE(jwe, shared, key) { |
@@ -12,3 +12,3 @@ import { encode as b64u } from '../util/base64url.js'; | ||
| export function checkEncryptHeaders(input) { | ||
| const { protectedHeader, unprotectedHeader, sharedUnprotectedHeader } = input; | ||
| const [, protectedHeader, unprotectedHeader, sharedUnprotectedHeader, , , , , crit] = input; | ||
| if (!isDisjoint(protectedHeader, unprotectedHeader, sharedUnprotectedHeader)) { | ||
@@ -22,3 +22,3 @@ throw new JWEInvalid('JWE Protected, JWE Shared Unprotected and JWE Per-Recipient Header Parameter names must be disjoint'); | ||
| }; | ||
| validateCrit(JWEInvalid, JWE_RECOGNIZED, input.crit, protectedHeader, joseHeader); | ||
| validateCrit(JWEInvalid, JWE_RECOGNIZED, crit, protectedHeader, joseHeader); | ||
| if (joseHeader.zip !== undefined && joseHeader.zip !== 'DEF') { | ||
@@ -37,9 +37,10 @@ throw new JOSENotSupported('Unsupported JWE "zip" (Compression Algorithm) Header Parameter value.'); | ||
| } | ||
| return { joseHeader, alg, enc, encEntry: jweEncryption(enc) }; | ||
| return [joseHeader, alg, enc, jweEncryption(enc)]; | ||
| } | ||
| export async function encryptJWE(input, checked, key) { | ||
| const { joseHeader, alg, encEntry } = checked; | ||
| let { protectedHeader, unprotectedHeader } = input; | ||
| const { sharedUnprotectedHeader } = input; | ||
| if (input.cek && (alg === 'dir' || alg === 'ECDH-ES')) { | ||
| const [joseHeader, alg, , encEntry] = checked; | ||
| const [inputPlaintext, inputProtectedHeader, inputUnprotectedHeader, sharedUnprotectedHeader, aad, providedCek, inputIv, keyManagementParameters, , unprotectedParameters,] = input; | ||
| let protectedHeader = inputProtectedHeader; | ||
| let unprotectedHeader = inputUnprotectedHeader; | ||
| if (providedCek && (alg === 'dir' || alg === 'ECDH-ES')) { | ||
| throw new TypeError(`setContentEncryptionKey cannot be called with JWE "alg" (Algorithm) Header ${alg}`); | ||
@@ -49,5 +50,5 @@ } | ||
| const k = await prepareKey(alg === 'dir' ? encEntry : algEntry, key, 'encrypt'); | ||
| const { cek, encryptedKey, parameters } = await encryptKeyManagement(alg, encEntry, k, input.cek, input.keyManagementParameters); | ||
| const [cek, encryptedKey, parameters] = await encryptKeyManagement(alg, encEntry, k, providedCek, keyManagementParameters); | ||
| if (parameters) { | ||
| if (input.unprotectedParameters) { | ||
| if (unprotectedParameters) { | ||
| unprotectedHeader = unprotectedHeader ? { ...unprotectedHeader, ...parameters } : parameters; | ||
@@ -71,4 +72,4 @@ } | ||
| let aadMember; | ||
| if (input.aad?.byteLength) { | ||
| aadMember = b64u(input.aad); | ||
| if (aad?.byteLength) { | ||
| aadMember = b64u(aad); | ||
| additionalData = concat(protectedHeaderB, encode('.'), encode(aadMember)); | ||
@@ -79,3 +80,3 @@ } | ||
| } | ||
| let plaintext = input.plaintext; | ||
| let plaintext = inputPlaintext; | ||
| if (joseHeader.zip === 'DEF') { | ||
@@ -86,3 +87,3 @@ plaintext = await compress(plaintext).catch((cause) => { | ||
| } | ||
| const { ciphertext, tag, iv } = await encrypt(encEntry, plaintext, cek, input.iv, additionalData); | ||
| const { ciphertext, tag, iv } = await encrypt(encEntry, plaintext, cek, inputIv, additionalData); | ||
| const jwe = { | ||
@@ -89,0 +90,0 @@ ciphertext: b64u(ciphertext), |
| import { JOSENotSupported } from '../util/errors.js'; | ||
| const unsupportedAlg = 'Invalid or unsupported JWK "alg" (Algorithm) Parameter value'; | ||
| function subtleParams(entry, jwk) { | ||
| if (!entry.kty.includes(jwk.kty)) { | ||
| throw new JOSENotSupported(unsupportedAlg); | ||
| } | ||
| return entry.subtleFor?.({ kty: jwk.kty, crv: jwk.crv }) ?? entry.subtle; | ||
| } | ||
| export async function jwkToKey(entry, jwk) { | ||
@@ -13,5 +6,7 @@ if (jwk.kty === 'RSA' && 'oth' in jwk && jwk.oth !== undefined) { | ||
| } | ||
| const algorithm = subtleParams(entry, jwk); | ||
| if (!entry.kty.includes(jwk.kty)) { | ||
| throw new JOSENotSupported('Invalid or unsupported JWK "alg" (Algorithm) Parameter value'); | ||
| } | ||
| const algorithm = entry.resolve?.({ kty: jwk.kty, crv: jwk.crv }) ?? entry.subtle; | ||
| const isPrivate = !!(jwk.d || jwk.priv); | ||
| const keyUsages = isPrivate ? entry.usages.private : entry.usages.public; | ||
| const keyData = { ...jwk }; | ||
@@ -22,3 +17,3 @@ if (keyData.kty !== 'AKP') { | ||
| delete keyData.use; | ||
| return crypto.subtle.importKey('jwk', keyData, algorithm, jwk.ext ?? (isPrivate ? false : true), jwk.key_ops ?? keyUsages); | ||
| return crypto.subtle.importKey('jwk', keyData, algorithm, jwk.ext ?? !isPrivate, jwk.key_ops ?? entry.usages[isPrivate ? 1 : 0]); | ||
| } |
| import { JOSENotSupported } from '../util/errors.js'; | ||
| import { table } from './key_descriptor.js'; | ||
| const sig = { public: ['verify'], private: ['sign'] }; | ||
| const sig = [['verify'], ['sign']]; | ||
| function hmac(bits) { | ||
| const subtle = { name: 'HMAC', hash: `SHA-${bits}` }; | ||
| return { kty: ['oct'], symmetric: true, subtle, operation: subtle, usages: sig }; | ||
| return { kty: ['oct'], secret: true, subtle, signing: subtle, usages: sig }; | ||
| } | ||
| function rsa(name, bits, saltLength) { | ||
| function rsa(bits, saltLength) { | ||
| const name = saltLength ? 'RSA-PSS' : 'RSASSA-PKCS1-v1_5'; | ||
| const subtle = { name, hash: `SHA-${bits}` }; | ||
@@ -13,5 +14,5 @@ return { | ||
| subtle, | ||
| operation: saltLength ? { ...subtle, saltLength } : subtle, | ||
| signing: saltLength ? { ...subtle, saltLength } : subtle, | ||
| usages: sig, | ||
| minModulusLength: 2048, | ||
| minRsaBits: 2048, | ||
| }; | ||
@@ -24,3 +25,3 @@ } | ||
| subtle: { name: 'ECDSA', namedCurve: crv }, | ||
| operation: { name: 'ECDSA', hash: `SHA-${bits}` }, | ||
| signing: { name: 'ECDSA', hash: `SHA-${bits}` }, | ||
| usages: sig, | ||
@@ -35,7 +36,8 @@ }; | ||
| subtle, | ||
| operation: subtle, | ||
| signing: subtle, | ||
| usages: sig, | ||
| }; | ||
| } | ||
| function mldsa(name) { | ||
| function mldsa(bits) { | ||
| const name = `ML-DSA-${bits}`; | ||
| const subtle = { name }; | ||
@@ -45,3 +47,3 @@ return { | ||
| subtle, | ||
| operation: subtle, | ||
| signing: subtle, | ||
| usages: sig, | ||
@@ -54,8 +56,8 @@ }; | ||
| HS512: hmac(512), | ||
| RS256: rsa('RSASSA-PKCS1-v1_5', 256), | ||
| RS384: rsa('RSASSA-PKCS1-v1_5', 384), | ||
| RS512: rsa('RSASSA-PKCS1-v1_5', 512), | ||
| PS256: rsa('RSA-PSS', 256, 32), | ||
| PS384: rsa('RSA-PSS', 384, 48), | ||
| PS512: rsa('RSA-PSS', 512, 64), | ||
| RS256: rsa(256), | ||
| RS384: rsa(384), | ||
| RS512: rsa(512), | ||
| PS256: rsa(256, 32), | ||
| PS384: rsa(384, 48), | ||
| PS512: rsa(512, 64), | ||
| ES256: ecdsa('P-256', 256), | ||
@@ -66,5 +68,5 @@ ES384: ecdsa('P-384', 384), | ||
| Ed25519: eddsa(), | ||
| 'ML-DSA-44': mldsa('ML-DSA-44'), | ||
| 'ML-DSA-65': mldsa('ML-DSA-65'), | ||
| 'ML-DSA-87': mldsa('ML-DSA-87'), | ||
| 'ML-DSA-44': mldsa(44), | ||
| 'ML-DSA-65': mldsa(65), | ||
| 'ML-DSA-87': mldsa(87), | ||
| }); | ||
@@ -71,0 +73,0 @@ export function jwsAlgorithm(alg) { |
@@ -16,2 +16,5 @@ import { encode as b64u } from '../util/base64url.js'; | ||
| const { protectedHeader, unprotectedHeader } = input; | ||
| if (!protectedHeader && !unprotectedHeader) { | ||
| throw new JWSInvalid('either setProtectedHeader or setUnprotectedHeader must be called before #sign()'); | ||
| } | ||
| if (!isDisjoint(protectedHeader, unprotectedHeader)) { | ||
@@ -24,3 +27,3 @@ throw new JWSInvalid('JWS Protected and JWS Unprotected Header Parameter names must be disjoint'); | ||
| let b64 = true; | ||
| if (extensions.has('b64')) { | ||
| if (extensions.includes('b64')) { | ||
| b64 = protectedHeader.b64; | ||
@@ -39,7 +42,7 @@ if (typeof b64 !== 'boolean') { | ||
| if (b64) { | ||
| const encoded = (input.encoded ??= {}); | ||
| encoded.b64 ??= b64u(input.payload); | ||
| encoded.raw ??= encode(encoded.b64); | ||
| payloadS = encoded.b64; | ||
| payloadB = encoded.raw; | ||
| const encoded = (input.encoded ??= []); | ||
| encoded[0] ??= b64u(input.payload); | ||
| encoded[1] ??= encode(encoded[0]); | ||
| payloadS = encoded[0]; | ||
| payloadB = encoded[1]; | ||
| } | ||
@@ -70,3 +73,6 @@ else { | ||
| } | ||
| if (unprotectedHeader) { | ||
| jws.header = unprotectedHeader; | ||
| } | ||
| return jws; | ||
| } |
@@ -10,5 +10,6 @@ import { verify } from './signing.js'; | ||
| export function verifyResult(jws, verified) { | ||
| const result = { payload: verified.payload }; | ||
| const [payload, parsedProt, , key, resolvedKey] = verified; | ||
| const result = { payload }; | ||
| if (jws.protected !== undefined) { | ||
| result.protectedHeader = verified.parsedProt; | ||
| result.protectedHeader = parsedProt; | ||
| } | ||
@@ -18,4 +19,4 @@ if (jws.header !== undefined) { | ||
| } | ||
| if (verified.resolvedKey) { | ||
| return { ...result, key: verified.key }; | ||
| if (resolvedKey) { | ||
| return { ...result, key }; | ||
| } | ||
@@ -25,18 +26,16 @@ return result; | ||
| export function prepareVerify(options) { | ||
| return { | ||
| algorithms: options && validateAlgorithms('algorithms', options.algorithms), | ||
| crit: options?.crit, | ||
| }; | ||
| return [options && validateAlgorithms('algorithms', options.algorithms), options?.crit]; | ||
| } | ||
| export async function verifySignature(jws, shared, key) { | ||
| const { protected: encodedProtected, header, payload: inputPayload } = jws; | ||
| let parsedProt = {}; | ||
| if (jws.protected) { | ||
| parsedProt = parseJoseHeader(jws.protected, JWSInvalid, 'JWS Protected Header is invalid'); | ||
| if (encodedProtected) { | ||
| parsedProt = parseJoseHeader(encodedProtected, JWSInvalid, 'JWS Protected Header is invalid'); | ||
| } | ||
| let joseHeader; | ||
| if (jws.header !== undefined) { | ||
| if (!isDisjoint(parsedProt, jws.header)) { | ||
| if (header !== undefined) { | ||
| if (!isDisjoint(parsedProt, header)) { | ||
| throw new JWSInvalid('JWS Protected and JWS Unprotected Header Parameter names must be disjoint'); | ||
| } | ||
| joseHeader = { ...parsedProt, ...jws.header }; | ||
| joseHeader = { ...parsedProt, ...header }; | ||
| } | ||
@@ -46,5 +45,5 @@ else { | ||
| } | ||
| const extensions = validateCrit(JWSInvalid, JWS_RECOGNIZED, shared.crit, parsedProt, joseHeader); | ||
| const extensions = validateCrit(JWSInvalid, JWS_RECOGNIZED, shared[1], parsedProt, joseHeader); | ||
| let b64 = true; | ||
| if (extensions.has('b64')) { | ||
| if (extensions.includes('b64')) { | ||
| b64 = parsedProt.b64; | ||
@@ -59,11 +58,11 @@ if (typeof b64 !== 'boolean') { | ||
| } | ||
| if (shared.algorithms && !shared.algorithms.has(alg)) { | ||
| if (shared[0] && !shared[0].has(alg)) { | ||
| throw new JOSEAlgNotAllowed('"alg" (Algorithm) Header Parameter value not allowed'); | ||
| } | ||
| if (b64) { | ||
| if (typeof jws.payload !== 'string') { | ||
| if (typeof inputPayload !== 'string') { | ||
| throw new JWSInvalid('JWS Payload must be a string'); | ||
| } | ||
| } | ||
| else if (typeof jws.payload !== 'string' && !(jws.payload instanceof Uint8Array)) { | ||
| else if (typeof inputPayload !== 'string' && !(inputPayload instanceof Uint8Array)) { | ||
| throw new JWSInvalid('JWS Payload must be a string or an Uint8Array instance'); | ||
@@ -77,8 +76,8 @@ } | ||
| const entry = jwsAlgorithm(alg); | ||
| const data = concat(jws.protected !== undefined ? encode(jws.protected) : new Uint8Array(), encode('.'), typeof jws.payload === 'string' | ||
| const data = concat(encodedProtected !== undefined ? encode(encodedProtected) : new Uint8Array(), encode('.'), typeof inputPayload === 'string' | ||
| ? b64 | ||
| ? | ||
| (shared.b64p ??= encodeBase64url(jws.payload, 'payload', JWSInvalid)) | ||
| : encoder.encode(jws.payload) | ||
| : jws.payload); | ||
| (shared[2] ??= encodeBase64url(inputPayload, 'payload', JWSInvalid)) | ||
| : encoder.encode(inputPayload) | ||
| : inputPayload); | ||
| const signature = decodeBase64url(jws.signature, 'signature', JWSInvalid); | ||
@@ -92,11 +91,11 @@ const k = await prepareKey(entry, key, 'verify'); | ||
| if (b64) { | ||
| payload = decodeBase64url(jws.payload, 'payload', JWSInvalid); | ||
| payload = decodeBase64url(inputPayload, 'payload', JWSInvalid); | ||
| } | ||
| else if (typeof jws.payload === 'string') { | ||
| payload = encoder.encode(jws.payload); | ||
| else if (typeof inputPayload === 'string') { | ||
| payload = encoder.encode(inputPayload); | ||
| } | ||
| else { | ||
| payload = jws.payload; | ||
| payload = inputPayload; | ||
| } | ||
| return { payload, parsedProt, b64, key: k, resolvedKey }; | ||
| return [payload, parsedProt, b64, k, resolvedKey]; | ||
| } | ||
@@ -103,0 +102,0 @@ export async function verifyCompact(jws, shared, key) { |
@@ -5,8 +5,12 @@ import { JWTClaimValidationFailed, JWTExpired, JWTInvalid } from '../util/errors.js'; | ||
| const epoch = (date) => Math.floor(date.getTime() / 1000); | ||
| const minute = 60; | ||
| const hour = minute * 60; | ||
| const day = hour * 24; | ||
| const week = day * 7; | ||
| const year = day * 365.25; | ||
| const multipliers = { | ||
| s: 1, | ||
| m: 60, | ||
| h: 3600, | ||
| d: 86400, | ||
| w: 604800, | ||
| y: 31557600, | ||
| }; | ||
| const REGEX = /^(\+|\-)? ?(\d+|\d+\.\d+) ?(seconds?|secs?|s|minutes?|mins?|m|hours?|hrs?|h|days?|d|weeks?|w|years?|yrs?|y)(?: (ago|from now))?$/i; | ||
| const checkFailed = 'check_failed'; | ||
| export function secs(str) { | ||
@@ -18,40 +22,3 @@ const matched = REGEX.exec(str); | ||
| const value = parseFloat(matched[2]); | ||
| const unit = matched[3].toLowerCase(); | ||
| let numericDate; | ||
| switch (unit) { | ||
| case 'sec': | ||
| case 'secs': | ||
| case 'second': | ||
| case 'seconds': | ||
| case 's': | ||
| numericDate = Math.round(value); | ||
| break; | ||
| case 'minute': | ||
| case 'minutes': | ||
| case 'min': | ||
| case 'mins': | ||
| case 'm': | ||
| numericDate = Math.round(value * minute); | ||
| break; | ||
| case 'hour': | ||
| case 'hours': | ||
| case 'hr': | ||
| case 'hrs': | ||
| case 'h': | ||
| numericDate = Math.round(value * hour); | ||
| break; | ||
| case 'day': | ||
| case 'days': | ||
| case 'd': | ||
| numericDate = Math.round(value * day); | ||
| break; | ||
| case 'week': | ||
| case 'weeks': | ||
| case 'w': | ||
| numericDate = Math.round(value * week); | ||
| break; | ||
| default: | ||
| numericDate = Math.round(value * year); | ||
| break; | ||
| } | ||
| const numericDate = Math.round(value * multipliers[matched[3][0].toLowerCase()]); | ||
| if (matched[1] === '-' || matched[4] === 'ago') { | ||
@@ -68,2 +35,9 @@ return -numericDate; | ||
| } | ||
| function numericDate(value, label) { | ||
| if (typeof value === 'number') | ||
| return validateInput(label, value); | ||
| if (value instanceof Date) | ||
| return validateInput(label, epoch(value)); | ||
| return epoch(new Date()) + secs(value); | ||
| } | ||
| const normalizeTyp = (value) => { | ||
@@ -80,6 +54,18 @@ if (value.includes('/')) { | ||
| if (Array.isArray(audPayload)) { | ||
| return audOption.some(Set.prototype.has.bind(new Set(audPayload))); | ||
| return audOption.some((aud) => audPayload.includes(aud)); | ||
| } | ||
| return false; | ||
| }; | ||
| function validateNumericDate(payload, claim, required = false) { | ||
| const value = payload[claim]; | ||
| if (value === undefined && !required) | ||
| return undefined; | ||
| if (typeof value !== 'number') { | ||
| throw new JWTClaimValidationFailed(`"${claim}" claim must be a number`, payload, claim, 'invalid'); | ||
| } | ||
| return value; | ||
| } | ||
| function unexpectedClaim(payload, claim) { | ||
| throw new JWTClaimValidationFailed(`unexpected "${claim}" claim value`, payload, claim, checkFailed); | ||
| } | ||
| export function validateClaimsSet(protectedHeader, encodedPayload, options = {}) { | ||
@@ -99,3 +85,3 @@ let payload; | ||
| normalizeTyp(protectedHeader.typ) !== normalizeTyp(typ))) { | ||
| throw new JWTClaimValidationFailed('unexpected "typ" JWT header value', payload, 'typ', 'check_failed'); | ||
| throw new JWTClaimValidationFailed('unexpected "typ" JWT header value', payload, 'typ', checkFailed); | ||
| } | ||
@@ -113,3 +99,3 @@ const { requiredClaims = [], issuer, subject, audience, maxTokenAge } = options; | ||
| for (const claim of new Set(presenceCheck.reverse())) { | ||
| if (!(claim in payload)) { | ||
| if (!Object.hasOwn(payload, claim)) { | ||
| throw new JWTClaimValidationFailed(`missing required "${claim}" claim`, payload, claim, 'missing'); | ||
@@ -120,24 +106,21 @@ } | ||
| !(Array.isArray(issuer) ? issuer : [issuer]).includes(payload.iss)) { | ||
| throw new JWTClaimValidationFailed('unexpected "iss" claim value', payload, 'iss', 'check_failed'); | ||
| unexpectedClaim(payload, 'iss'); | ||
| } | ||
| if (subject !== undefined && payload.sub !== subject) { | ||
| throw new JWTClaimValidationFailed('unexpected "sub" claim value', payload, 'sub', 'check_failed'); | ||
| unexpectedClaim(payload, 'sub'); | ||
| } | ||
| if (audience !== undefined && | ||
| !checkAudiencePresence(payload.aud, typeof audience === 'string' ? [audience] : audience)) { | ||
| throw new JWTClaimValidationFailed('unexpected "aud" claim value', payload, 'aud', 'check_failed'); | ||
| unexpectedClaim(payload, 'aud'); | ||
| } | ||
| let tolerance; | ||
| switch (typeof options.clockTolerance) { | ||
| case 'string': | ||
| tolerance = secs(options.clockTolerance); | ||
| break; | ||
| case 'number': | ||
| tolerance = options.clockTolerance; | ||
| break; | ||
| case 'undefined': | ||
| tolerance = 0; | ||
| break; | ||
| default: | ||
| const { clockTolerance } = options; | ||
| let tolerance = 0; | ||
| if (typeof clockTolerance === 'string') { | ||
| tolerance = secs(clockTolerance); | ||
| } | ||
| else if (clockTolerance !== undefined) { | ||
| if (typeof clockTolerance !== 'number') { | ||
| throw new TypeError('Invalid clockTolerance option type'); | ||
| } | ||
| tolerance = clockTolerance; | ||
| } | ||
@@ -147,29 +130,23 @@ validateInput('clockTolerance option', tolerance); | ||
| const now = validateInput('currentDate option', epoch(currentDate || new Date())); | ||
| if ((payload.iat !== undefined || maxTokenAge !== undefined) && typeof payload.iat !== 'number') { | ||
| throw new JWTClaimValidationFailed('"iat" claim must be a number', payload, 'iat', 'invalid'); | ||
| } | ||
| if (payload.nbf !== undefined) { | ||
| if (typeof payload.nbf !== 'number') { | ||
| throw new JWTClaimValidationFailed('"nbf" claim must be a number', payload, 'nbf', 'invalid'); | ||
| const iat = validateNumericDate(payload, 'iat', maxTokenAge !== undefined); | ||
| const nbf = validateNumericDate(payload, 'nbf'); | ||
| if (nbf !== undefined) { | ||
| if (nbf > now + tolerance) { | ||
| throw new JWTClaimValidationFailed('"nbf" claim timestamp check failed', payload, 'nbf', checkFailed); | ||
| } | ||
| if (payload.nbf > now + tolerance) { | ||
| throw new JWTClaimValidationFailed('"nbf" claim timestamp check failed', payload, 'nbf', 'check_failed'); | ||
| } | ||
| } | ||
| if (payload.exp !== undefined) { | ||
| if (typeof payload.exp !== 'number') { | ||
| throw new JWTClaimValidationFailed('"exp" claim must be a number', payload, 'exp', 'invalid'); | ||
| const exp = validateNumericDate(payload, 'exp'); | ||
| if (exp !== undefined) { | ||
| if (exp <= now - tolerance) { | ||
| throw new JWTExpired('"exp" claim timestamp check failed', payload, 'exp', checkFailed); | ||
| } | ||
| if (payload.exp <= now - tolerance) { | ||
| throw new JWTExpired('"exp" claim timestamp check failed', payload, 'exp', 'check_failed'); | ||
| } | ||
| } | ||
| if (maxTokenAge !== undefined) { | ||
| const age = now - payload.iat; | ||
| const age = now - iat; | ||
| const max = typeof maxTokenAge === 'number' ? maxTokenAge : secs(maxTokenAge); | ||
| if (age - tolerance > max) { | ||
| throw new JWTExpired('"iat" claim timestamp check failed (too far in the past)', payload, 'iat', 'check_failed'); | ||
| throw new JWTExpired('"iat" claim timestamp check failed (too far in the past)', payload, 'iat', checkFailed); | ||
| } | ||
| if (age < 0 - tolerance) { | ||
| throw new JWTClaimValidationFailed('"iat" claim timestamp check failed (it should be in the past)', payload, 'iat', 'check_failed'); | ||
| throw new JWTClaimValidationFailed('"iat" claim timestamp check failed (it should be in the past)', payload, 'iat', checkFailed); | ||
| } | ||
@@ -212,22 +189,6 @@ } | ||
| set nbf(value) { | ||
| if (typeof value === 'number') { | ||
| this.#payload.nbf = validateInput('setNotBefore', value); | ||
| } | ||
| else if (value instanceof Date) { | ||
| this.#payload.nbf = validateInput('setNotBefore', epoch(value)); | ||
| } | ||
| else { | ||
| this.#payload.nbf = epoch(new Date()) + secs(value); | ||
| } | ||
| this.#payload.nbf = numericDate(value, 'setNotBefore'); | ||
| } | ||
| set exp(value) { | ||
| if (typeof value === 'number') { | ||
| this.#payload.exp = validateInput('setExpirationTime', value); | ||
| } | ||
| else if (value instanceof Date) { | ||
| this.#payload.exp = validateInput('setExpirationTime', epoch(value)); | ||
| } | ||
| else { | ||
| this.#payload.exp = epoch(new Date()) + secs(value); | ||
| } | ||
| this.#payload.exp = numericDate(value, 'setExpirationTime'); | ||
| } | ||
@@ -238,5 +199,2 @@ set iat(value) { | ||
| } | ||
| else if (value instanceof Date) { | ||
| this.#payload.iat = validateInput('setIssuedAt', epoch(value)); | ||
| } | ||
| else if (typeof value === 'string') { | ||
@@ -246,5 +204,5 @@ this.#payload.iat = validateInput('setIssuedAt', epoch(new Date()) + secs(value)); | ||
| else { | ||
| this.#payload.iat = validateInput('setIssuedAt', value); | ||
| this.#payload.iat = numericDate(value, 'setIssuedAt'); | ||
| } | ||
| } | ||
| } |
| import { JOSENotSupported } from '../util/errors.js'; | ||
| import { maybeJWSAlgorithm } from './jws_algorithms.js'; | ||
| import { maybeJWEAlgorithm } from './jwe_algorithms.js'; | ||
| function unsupportedAlgorithm() { | ||
| return new JOSENotSupported('Invalid or unsupported JWK "alg" (Algorithm) Parameter value'); | ||
| } | ||
| export function keyAlgorithm(alg) { | ||
| if (typeof alg !== 'string') { | ||
| throw unsupportedAlgorithm(); | ||
| } | ||
| const entry = maybeJWSAlgorithm(alg) ?? maybeJWEAlgorithm(alg); | ||
| const entry = typeof alg === 'string' ? (maybeJWSAlgorithm(alg) ?? maybeJWEAlgorithm(alg)) : undefined; | ||
| if (!entry) { | ||
| throw unsupportedAlgorithm(); | ||
| throw new JOSENotSupported('Invalid or unsupported JWK "alg" (Algorithm) Parameter value'); | ||
| } | ||
| return entry; | ||
| } |
| export function table(entries) { | ||
| const out = { __proto__: null }; | ||
| for (const alg of Object.keys(entries)) { | ||
| for (const alg in entries) { | ||
| out[alg] = { ...entries[alg], alg }; | ||
@@ -5,0 +5,0 @@ } |
@@ -9,31 +9,23 @@ import { encode as b64u } from '../util/base64url.js'; | ||
| import { isObject } from './type_checks.js'; | ||
| import { checkCryptoKey, checkUsage } from './crypto_key.js'; | ||
| import { checkModulusLength } from './signing.js'; | ||
| import { checkCryptoKey, checkModulusLength, checkUsage } from './crypto_key.js'; | ||
| import { concat, encode, uint32be } from './buffer_utils.js'; | ||
| import { assertCryptoKey } from './is_key_like.js'; | ||
| function checkEcdhCryptoKey(key, usage) { | ||
| switch (key.algorithm.name) { | ||
| case 'ECDH': | ||
| case 'X25519': | ||
| break; | ||
| default: | ||
| throw new TypeError('CryptoKey does not support this operation, its algorithm.name must be ECDH or X25519'); | ||
| if (key.algorithm.name !== 'ECDH' && key.algorithm.name !== 'X25519') { | ||
| throw new TypeError('CryptoKey does not support this operation, its algorithm.name must be ECDH or X25519'); | ||
| } | ||
| checkUsage(key, usage); | ||
| } | ||
| function checkKeySize(key, alg) { | ||
| if (key.algorithm.length !== parseInt(alg.slice(1, 4), 10)) { | ||
| throw new TypeError(`Invalid key size for alg: ${alg}`); | ||
| } | ||
| async function aeskwCryptoKey(key, alg, usage) { | ||
| const expected = jweAlgorithm(alg).subtle; | ||
| const cryptoKey = key instanceof Uint8Array | ||
| ? await crypto.subtle.importKey('raw', key, 'AES-KW', true, [ | ||
| usage, | ||
| ]) | ||
| : key; | ||
| checkCryptoKey(cryptoKey, expected, usage); | ||
| return cryptoKey; | ||
| } | ||
| function aeskwCryptoKey(key, alg, usage) { | ||
| if (key instanceof Uint8Array) { | ||
| return crypto.subtle.importKey('raw', key, 'AES-KW', true, [usage]); | ||
| } | ||
| checkCryptoKey(key, jweAlgorithm(alg).subtle, usage); | ||
| return key; | ||
| } | ||
| async function aeskwWrap(alg, key, cek) { | ||
| const cryptoKey = await aeskwCryptoKey(key, alg, 'wrapKey'); | ||
| checkKeySize(cryptoKey, alg); | ||
| const cryptoKeyCek = await crypto.subtle.importKey('raw', cek, { hash: 'SHA-256', name: 'HMAC' }, true, ['sign']); | ||
@@ -44,38 +36,9 @@ return new Uint8Array(await crypto.subtle.wrapKey('raw', cryptoKeyCek, cryptoKey, 'AES-KW')); | ||
| const cryptoKey = await aeskwCryptoKey(key, alg, 'unwrapKey'); | ||
| checkKeySize(cryptoKey, alg); | ||
| const cryptoKeyCek = await crypto.subtle.unwrapKey('raw', encryptedKey, cryptoKey, 'AES-KW', { hash: 'SHA-256', name: 'HMAC' }, true, ['sign']); | ||
| return new Uint8Array(await crypto.subtle.exportKey('raw', cryptoKeyCek)); | ||
| } | ||
| async function aesGcmKwWrap(gcm, key, cek, iv) { | ||
| const wrapped = await encrypt(gcm, cek, key, iv, new Uint8Array()); | ||
| return { | ||
| encryptedKey: wrapped.ciphertext, | ||
| iv: b64u(wrapped.iv), | ||
| tag: b64u(wrapped.tag), | ||
| }; | ||
| } | ||
| async function aesGcmKwUnwrap(gcm, key, encryptedKey, iv, tag) { | ||
| return decrypt(gcm, key, encryptedKey, iv, tag, new Uint8Array()); | ||
| } | ||
| const subtleAlgorithm = (alg) => { | ||
| switch (alg) { | ||
| case 'RSA-OAEP': | ||
| case 'RSA-OAEP-256': | ||
| case 'RSA-OAEP-384': | ||
| case 'RSA-OAEP-512': | ||
| return 'RSA-OAEP'; | ||
| default: | ||
| throw new JOSENotSupported(`alg ${alg} is not supported either by JOSE or your javascript runtime`); | ||
| } | ||
| }; | ||
| async function rsaesEncrypt(alg, key, cek) { | ||
| checkCryptoKey(key, jweAlgorithm(alg).subtle, 'encrypt'); | ||
| function checkRsaKey(alg, key, usage) { | ||
| checkCryptoKey(key, jweAlgorithm(alg).subtle, usage); | ||
| checkModulusLength(alg, key); | ||
| return new Uint8Array(await crypto.subtle.encrypt(subtleAlgorithm(alg), key, cek)); | ||
| } | ||
| async function rsaesDecrypt(alg, key, encryptedKey) { | ||
| checkCryptoKey(key, jweAlgorithm(alg).subtle, 'decrypt'); | ||
| checkModulusLength(alg, key); | ||
| return new Uint8Array(await crypto.subtle.decrypt(subtleAlgorithm(alg), key, encryptedKey)); | ||
| } | ||
| function pbes2CryptoKey(key, alg) { | ||
@@ -90,3 +53,2 @@ if (key instanceof Uint8Array) { | ||
| } | ||
| const concatSalt = (alg, p2sInput) => concat(encode(alg), Uint8Array.of(0x00), p2sInput); | ||
| async function deriveKey(p2s, alg, p2c, key) { | ||
@@ -99,3 +61,3 @@ if (!(p2s instanceof Uint8Array) || p2s.length < 8) { | ||
| } | ||
| const salt = concatSalt(alg, p2s); | ||
| const salt = concat(encode(alg), Uint8Array.of(0), p2s); | ||
| const keylen = parseInt(alg.slice(13, 16), 10); | ||
@@ -111,11 +73,2 @@ const subtleAlg = { | ||
| } | ||
| async function pbes2kwWrap(alg, key, cek, p2c = 2048, p2s = crypto.getRandomValues(new Uint8Array(16))) { | ||
| const derived = await deriveKey(p2s, alg, p2c, key); | ||
| const encryptedKey = await aeskwWrap(alg.slice(-6), derived, cek); | ||
| return { encryptedKey, p2c, p2s: b64u(p2s) }; | ||
| } | ||
| async function pbes2kwUnwrap(alg, key, encryptedKey, p2c, p2s) { | ||
| const derived = await deriveKey(p2s, alg, p2c, key); | ||
| return aeskwUnwrap(alg.slice(-6), derived, encryptedKey); | ||
| } | ||
| function lengthAndInput(input) { | ||
@@ -130,7 +83,3 @@ return concat(uint32be(input.length), input); | ||
| for (let i = 1; i <= reps; i++) { | ||
| const hashInput = new Uint8Array(4 + Z.length + OtherInfo.length); | ||
| hashInput.set(uint32be(i), 0); | ||
| hashInput.set(Z, 4); | ||
| hashInput.set(OtherInfo, 4 + Z.length); | ||
| const hashResult = await digest('sha256', hashInput); | ||
| const hashResult = await digest('sha256', concat(uint32be(i), Z, OtherInfo)); | ||
| dk.set(hashResult, (i - 1) * hashLen); | ||
@@ -143,31 +92,21 @@ } | ||
| checkEcdhCryptoKey(privateKey, 'deriveBits'); | ||
| const algorithmID = lengthAndInput(encode(algorithm)); | ||
| const partyUInfo = lengthAndInput(apu); | ||
| const partyVInfo = lengthAndInput(apv); | ||
| const suppPubInfo = uint32be(keyLength); | ||
| const suppPrivInfo = new Uint8Array(); | ||
| const otherInfo = concat(algorithmID, partyUInfo, partyVInfo, suppPubInfo, suppPrivInfo); | ||
| const otherInfo = concat(lengthAndInput(encode(algorithm)), lengthAndInput(apu), lengthAndInput(apv), uint32be(keyLength)); | ||
| const Z = new Uint8Array(await crypto.subtle.deriveBits({ | ||
| name: publicKey.algorithm.name, | ||
| public: publicKey, | ||
| }, privateKey, getEcdhBitLength(publicKey))); | ||
| }, privateKey, publicKey.algorithm.name === 'X25519' | ||
| ? 256 | ||
| : Math.ceil(parseInt(publicKey.algorithm.namedCurve.slice(-3), 10) / 8) << 3)); | ||
| return concatKdf(Z, keyLength, otherInfo); | ||
| } | ||
| function getEcdhBitLength(publicKey) { | ||
| if (publicKey.algorithm.name === 'X25519') { | ||
| return 256; | ||
| function assertEcdhKey(key) { | ||
| assertCryptoKey(key); | ||
| const curve = key.algorithm.namedCurve; | ||
| if (curve !== 'P-256' && | ||
| curve !== 'P-384' && | ||
| curve !== 'P-521' && | ||
| key.algorithm.name !== 'X25519') { | ||
| throw new JOSENotSupported('ECDH with the provided key is not allowed or not supported by your javascript runtime'); | ||
| } | ||
| return (Math.ceil(parseInt(publicKey.algorithm.namedCurve.slice(-3), 10) / 8) << 3); | ||
| } | ||
| function ecdhesAllowed(key) { | ||
| switch (key.algorithm.namedCurve) { | ||
| case 'P-256': | ||
| case 'P-384': | ||
| case 'P-521': | ||
| return true; | ||
| default: | ||
| return key.algorithm.name === 'X25519'; | ||
| } | ||
| } | ||
| const unsupportedAlgHeader = 'Invalid or unsupported "alg" (JWE Algorithm) header value'; | ||
| function assertEncryptedKey(encryptedKey) { | ||
@@ -177,21 +116,20 @@ if (encryptedKey === undefined) | ||
| } | ||
| function assertNoEncryptedKey(encryptedKey) { | ||
| if (encryptedKey !== undefined) | ||
| throw new JWEInvalid('Encountered unexpected JWE Encrypted Key'); | ||
| } | ||
| export async function decryptKeyManagement(alg, enc, key, encryptedKey, joseHeader, options) { | ||
| switch (alg) { | ||
| case 'dir': { | ||
| if (encryptedKey !== undefined) | ||
| throw new JWEInvalid('Encountered unexpected JWE Encrypted Key'); | ||
| return key; | ||
| } | ||
| case 'ECDH-ES': | ||
| if (encryptedKey !== undefined) | ||
| throw new JWEInvalid('Encountered unexpected JWE Encrypted Key'); | ||
| case 'ECDH-ES+A128KW': | ||
| case 'ECDH-ES+A192KW': | ||
| case 'ECDH-ES+A256KW': { | ||
| const entry = jweAlgorithm(alg); | ||
| if (alg === 'dir') { | ||
| assertNoEncryptedKey(encryptedKey); | ||
| return key; | ||
| } | ||
| switch (entry.subtle.name) { | ||
| case 'ECDH': { | ||
| if (alg === 'ECDH-ES') | ||
| assertNoEncryptedKey(encryptedKey); | ||
| if (!isObject(joseHeader.epk)) | ||
| throw new JWEInvalid(`JOSE Header "epk" (Ephemeral Public Key) missing or invalid`); | ||
| assertCryptoKey(key); | ||
| if (!ecdhesAllowed(key)) | ||
| throw new JOSENotSupported('ECDH with the provided key is not allowed or not supported by your javascript runtime'); | ||
| const epk = await jwkToKey(jweAlgorithm(alg), joseHeader.epk); | ||
| assertEcdhKey(key); | ||
| const epk = await jwkToKey(entry, joseHeader.epk); | ||
| let partyUInfo; | ||
@@ -215,13 +153,9 @@ let partyVInfo; | ||
| } | ||
| case 'RSA-OAEP': | ||
| case 'RSA-OAEP-256': | ||
| case 'RSA-OAEP-384': | ||
| case 'RSA-OAEP-512': { | ||
| case 'RSA-OAEP': { | ||
| assertEncryptedKey(encryptedKey); | ||
| assertCryptoKey(key); | ||
| return rsaesDecrypt(alg, key, encryptedKey); | ||
| checkRsaKey(alg, key, 'decrypt'); | ||
| return new Uint8Array(await crypto.subtle.decrypt('RSA-OAEP', key, encryptedKey)); | ||
| } | ||
| case 'PBES2-HS256+A128KW': | ||
| case 'PBES2-HS384+A192KW': | ||
| case 'PBES2-HS512+A256KW': { | ||
| case 'PBKDF2': { | ||
| assertEncryptedKey(encryptedKey); | ||
@@ -235,15 +169,11 @@ if (typeof joseHeader.p2c !== 'number') | ||
| throw new JWEInvalid(`JOSE Header "p2s" (PBES2 Salt) missing or invalid`); | ||
| let p2s; | ||
| p2s = decodeBase64url(joseHeader.p2s, 'p2s', JWEInvalid); | ||
| return pbes2kwUnwrap(alg, key, encryptedKey, joseHeader.p2c, p2s); | ||
| const p2s = decodeBase64url(joseHeader.p2s, 'p2s', JWEInvalid); | ||
| const derived = await deriveKey(p2s, alg, joseHeader.p2c, key); | ||
| return aeskwUnwrap(alg.slice(-6), derived, encryptedKey); | ||
| } | ||
| case 'A128KW': | ||
| case 'A192KW': | ||
| case 'A256KW': { | ||
| case 'AES-KW': { | ||
| assertEncryptedKey(encryptedKey); | ||
| return aeskwUnwrap(alg, key, encryptedKey); | ||
| } | ||
| case 'A128GCMKW': | ||
| case 'A192GCMKW': | ||
| case 'A256GCMKW': { | ||
| case 'AES-GCM': { | ||
| assertEncryptedKey(encryptedKey); | ||
@@ -258,7 +188,4 @@ if (typeof joseHeader.iv !== 'string') | ||
| tag = decodeBase64url(joseHeader.tag, 'tag', JWEInvalid); | ||
| return aesGcmKwUnwrap(jweEncryption(jweAlgorithm(alg).gcmkw), key, encryptedKey, iv, tag); | ||
| return decrypt(jweEncryption(alg.slice(0, -2)), key, encryptedKey, iv, tag, new Uint8Array()); | ||
| } | ||
| default: { | ||
| throw new JOSENotSupported(unsupportedAlgHeader); | ||
| } | ||
| } | ||
@@ -270,19 +197,12 @@ } | ||
| let cek; | ||
| switch (alg) { | ||
| case 'dir': { | ||
| cek = key; | ||
| break; | ||
| } | ||
| case 'ECDH-ES': | ||
| case 'ECDH-ES+A128KW': | ||
| case 'ECDH-ES+A192KW': | ||
| case 'ECDH-ES+A256KW': { | ||
| assertCryptoKey(key); | ||
| if (!ecdhesAllowed(key)) { | ||
| throw new JOSENotSupported('ECDH with the provided key is not allowed or not supported by your javascript runtime'); | ||
| } | ||
| const entry = jweAlgorithm(alg); | ||
| if (alg === 'dir') | ||
| return [key, undefined, undefined]; | ||
| switch (entry.subtle.name) { | ||
| case 'ECDH': { | ||
| assertEcdhKey(key); | ||
| const { apu, apv } = providedParameters; | ||
| let ephemeralKey; | ||
| if (providedParameters.epk) { | ||
| ephemeralKey = (await prepareKey(jweAlgorithm(alg), providedParameters.epk, 'decrypt')); | ||
| ephemeralKey = (await prepareKey(entry, providedParameters.epk, 'decrypt')); | ||
| } | ||
@@ -318,22 +238,18 @@ else { | ||
| } | ||
| case 'RSA-OAEP': | ||
| case 'RSA-OAEP-256': | ||
| case 'RSA-OAEP-384': | ||
| case 'RSA-OAEP-512': { | ||
| case 'RSA-OAEP': { | ||
| cek = providedCek || generateCek(enc); | ||
| assertCryptoKey(key); | ||
| encryptedKey = await rsaesEncrypt(alg, key, cek); | ||
| checkRsaKey(alg, key, 'encrypt'); | ||
| encryptedKey = new Uint8Array(await crypto.subtle.encrypt('RSA-OAEP', key, cek)); | ||
| break; | ||
| } | ||
| case 'PBES2-HS256+A128KW': | ||
| case 'PBES2-HS384+A192KW': | ||
| case 'PBES2-HS512+A256KW': { | ||
| case 'PBKDF2': { | ||
| cek = providedCek || generateCek(enc); | ||
| const { p2c, p2s } = providedParameters; | ||
| ({ encryptedKey, ...parameters } = await pbes2kwWrap(alg, key, cek, p2c, p2s)); | ||
| const { p2c = 2048, p2s = crypto.getRandomValues(new Uint8Array(16)) } = providedParameters; | ||
| const derived = await deriveKey(p2s, alg, p2c, key); | ||
| encryptedKey = await aeskwWrap(alg.slice(-6), derived, cek); | ||
| parameters = { p2c, p2s: b64u(p2s) }; | ||
| break; | ||
| } | ||
| case 'A128KW': | ||
| case 'A192KW': | ||
| case 'A256KW': { | ||
| case 'AES-KW': { | ||
| cek = providedCek || generateCek(enc); | ||
@@ -343,15 +259,12 @@ encryptedKey = await aeskwWrap(alg, key, cek); | ||
| } | ||
| case 'A128GCMKW': | ||
| case 'A192GCMKW': | ||
| case 'A256GCMKW': { | ||
| case 'AES-GCM': { | ||
| cek = providedCek || generateCek(enc); | ||
| const { iv } = providedParameters; | ||
| ({ encryptedKey, ...parameters } = await aesGcmKwWrap(jweEncryption(jweAlgorithm(alg).gcmkw), key, cek, iv)); | ||
| const wrapped = await encrypt(jweEncryption(alg.slice(0, -2)), cek, key, iv, new Uint8Array()); | ||
| encryptedKey = wrapped.ciphertext; | ||
| parameters = { iv: b64u(wrapped.iv), tag: b64u(wrapped.tag) }; | ||
| break; | ||
| } | ||
| default: { | ||
| throw new JOSENotSupported(unsupportedAlgHeader); | ||
| } | ||
| } | ||
| return { cek, encryptedKey, parameters }; | ||
| return [cek, encryptedKey, parameters]; | ||
| } |
+61
-99
@@ -10,13 +10,3 @@ import { withAlg as invalidKeyInput } from './invalid_key_input.js'; | ||
| if (key.use !== undefined) { | ||
| let expected; | ||
| switch (usage) { | ||
| case 'sign': | ||
| case 'verify': | ||
| expected = 'sig'; | ||
| break; | ||
| case 'encrypt': | ||
| case 'decrypt': | ||
| expected = 'enc'; | ||
| break; | ||
| } | ||
| const expected = usage === 'sign' || usage === 'verify' ? 'sig' : 'enc'; | ||
| if (key.use !== expected) { | ||
@@ -30,75 +20,52 @@ throw new TypeError(`Invalid key for this operation, its "use" must be "${expected}" when present`); | ||
| if (Array.isArray(key.key_ops)) { | ||
| const expectedKeyOp = usage === 'encrypt' || usage === 'decrypt' ? entry.keyOps?.[usage] : usage; | ||
| if (expectedKeyOp && key.key_ops?.includes?.(expectedKeyOp) === false) { | ||
| const expectedKeyOp = usage === 'encrypt' || usage === 'decrypt' ? entry.ops?.[usage === 'encrypt' ? 0 : 1] : usage; | ||
| if (expectedKeyOp && !key.key_ops.includes(expectedKeyOp)) { | ||
| throw new TypeError(`Invalid key for this operation, its "key_ops" must include "${expectedKeyOp}" when present`); | ||
| } | ||
| } | ||
| return true; | ||
| }; | ||
| const symmetricTypeCheck = (entry, key, usage) => { | ||
| const { alg } = entry; | ||
| if (key instanceof Uint8Array) | ||
| return { kind: BYTES, key }; | ||
| export function checkKeyType(entry, key, usage) { | ||
| const { alg, secret } = entry; | ||
| const privateKey = usage === 'decrypt' || usage === 'sign'; | ||
| if (secret && key instanceof Uint8Array) | ||
| return [BYTES, key]; | ||
| if (jwk.isJWK(key)) { | ||
| if (jwk.isSecretJWK(key) && jwkMatchesOp(entry, key, usage)) | ||
| return { kind: JWK, key }; | ||
| throw new TypeError(`JSON Web Key for symmetric algorithms must have JWK "kty" (Key Type) equal to "oct" and the JWK "k" (Key Value) present`); | ||
| } | ||
| if (!isKeyLike(key)) { | ||
| throw new TypeError(invalidKeyInput(alg, key, 'CryptoKey', 'KeyObject', 'JSON Web Key', 'Uint8Array')); | ||
| } | ||
| if (key.type !== 'secret') { | ||
| throw new TypeError(`${tag(key)} instances for symmetric algorithms must be of type "secret"`); | ||
| } | ||
| return isCryptoKey(key) ? { kind: CRYPTO, key } : { kind: KEYOBJECT, key }; | ||
| }; | ||
| const asymmetricTypeCheck = (entry, key, usage) => { | ||
| const { alg } = entry; | ||
| if (jwk.isJWK(key)) { | ||
| switch (usage) { | ||
| case 'decrypt': | ||
| case 'sign': | ||
| if (jwk.isPrivateJWK(key) && jwkMatchesOp(entry, key, usage)) | ||
| return { kind: JWK, key }; | ||
| throw new TypeError(`JSON Web Key for this operation must be a private JWK`); | ||
| case 'encrypt': | ||
| case 'verify': | ||
| if (jwk.isPublicJWK(key) && jwkMatchesOp(entry, key, usage)) | ||
| return { kind: JWK, key }; | ||
| throw new TypeError(`JSON Web Key for this operation must be a public JWK`); | ||
| if (secret ? !jwk.isSecretJWK(key) : !(privateKey ? jwk.isPrivateJWK(key) : jwk.isPublicJWK(key))) { | ||
| throw new TypeError(secret | ||
| ? `JSON Web Key for symmetric algorithms must have JWK "kty" (Key Type) equal to "oct" and the JWK "k" (Key Value) present` | ||
| : `JSON Web Key for this operation must be a ${privateKey ? 'private' : 'public'} JWK`); | ||
| } | ||
| jwkMatchesOp(entry, key, usage); | ||
| return [JWK, key]; | ||
| } | ||
| if (!isKeyLike(key)) { | ||
| throw new TypeError(invalidKeyInput(alg, key, 'CryptoKey', 'KeyObject', 'JSON Web Key')); | ||
| throw new TypeError(secret | ||
| ? invalidKeyInput(alg, key, 'CryptoKey', 'KeyObject', 'JSON Web Key', 'Uint8Array') | ||
| : invalidKeyInput(alg, key, 'CryptoKey', 'KeyObject', 'JSON Web Key')); | ||
| } | ||
| if (key.type === 'secret') { | ||
| throw new TypeError(`${tag(key)} instances for asymmetric algorithms must not be of type "secret"`); | ||
| } | ||
| if (key.type === 'public') { | ||
| switch (usage) { | ||
| case 'sign': | ||
| throw new TypeError(`${tag(key)} instances for asymmetric algorithm signing must be of type "private"`); | ||
| case 'decrypt': | ||
| throw new TypeError(`${tag(key)} instances for asymmetric algorithm decryption must be of type "private"`); | ||
| if (secret) { | ||
| if (key.type !== 'secret') { | ||
| throw new TypeError(`${tag(key)} instances for symmetric algorithms must be of type "secret"`); | ||
| } | ||
| } | ||
| if (key.type === 'private') { | ||
| switch (usage) { | ||
| case 'verify': | ||
| throw new TypeError(`${tag(key)} instances for asymmetric algorithm verifying must be of type "public"`); | ||
| case 'encrypt': | ||
| throw new TypeError(`${tag(key)} instances for asymmetric algorithm encryption must be of type "public"`); | ||
| else { | ||
| if (key.type === 'secret') { | ||
| throw new TypeError(`${tag(key)} instances for asymmetric algorithms must not be of type "secret"`); | ||
| } | ||
| const expectedType = privateKey ? 'private' : 'public'; | ||
| if ((key.type === 'public' || key.type === 'private') && key.type !== expectedType) { | ||
| const operation = usage === 'sign' | ||
| ? 'signing' | ||
| : usage === 'verify' | ||
| ? 'verifying' | ||
| : `${usage.slice(0, -1)}tion`; | ||
| throw new TypeError(`${tag(key)} instances for asymmetric algorithm ${operation} must be of type "${expectedType}"`); | ||
| } | ||
| } | ||
| return isCryptoKey(key) ? { kind: CRYPTO, key } : { kind: KEYOBJECT, key }; | ||
| }; | ||
| const BYTES = Symbol(); | ||
| const CRYPTO = Symbol(); | ||
| const KEYOBJECT = Symbol(); | ||
| const JWK = Symbol(); | ||
| export function checkKeyType(entry, key, usage) { | ||
| return entry.symmetric | ||
| ? symmetricTypeCheck(entry, key, usage) | ||
| : asymmetricTypeCheck(entry, key, usage); | ||
| return isCryptoKey(key) ? [CRYPTO, key] : [KEYOBJECT, key]; | ||
| } | ||
| const BYTES = 0; | ||
| const CRYPTO = 1; | ||
| const KEYOBJECT = 2; | ||
| const JWK = 3; | ||
| let cache; | ||
@@ -111,23 +78,17 @@ const nist = { | ||
| }; | ||
| function cached(key, alg) { | ||
| function cached(key, alg, value) { | ||
| cache ||= new WeakMap(); | ||
| return cache.get(key)?.[alg]; | ||
| } | ||
| function store(key, alg, cryptoKey) { | ||
| const entry = cache.get(key); | ||
| if (entry) { | ||
| entry[alg] = cryptoKey; | ||
| if (value) { | ||
| if (entry) { | ||
| entry[alg] = value; | ||
| } | ||
| else { | ||
| cache.set(key, { [alg]: value }); | ||
| } | ||
| } | ||
| else { | ||
| cache.set(key, { [alg]: cryptoKey }); | ||
| } | ||
| return cryptoKey; | ||
| return value ?? entry?.[alg]; | ||
| } | ||
| const handleJWK = async (key, jwk, entry) => { | ||
| const hit = cached(key, entry.alg); | ||
| if (hit) | ||
| return hit; | ||
| const cryptoKey = await jwkToKey(entry, { ...jwk, alg: entry.alg }); | ||
| return store(key, entry.alg, cryptoKey); | ||
| }; | ||
| const handleJWK = async (key, jwk, entry) => cached(key, entry.alg) ?? | ||
| cached(key, entry.alg, await jwkToKey(entry, { ...jwk, alg: entry.alg })); | ||
| const handleKeyObject = (keyObject, entry) => { | ||
@@ -138,28 +99,29 @@ const hit = cached(keyObject, entry.alg); | ||
| const isPublic = keyObject.type === 'public'; | ||
| const usages = isPublic ? entry.usages.public : entry.usages.private; | ||
| const usages = entry.usages[isPublic ? 0 : 1]; | ||
| const { asymmetricKeyType } = keyObject; | ||
| const crv = nist[keyObject.asymmetricKeyDetails?.namedCurve]; | ||
| const params = entry.subtleFor?.({ crv, asymmetricKeyType }) ?? entry.subtle; | ||
| return store(keyObject, entry.alg, keyObject.toCryptoKey(params, isPublic, usages)); | ||
| const params = entry.resolve?.({ crv, asymmetricKeyType }) ?? entry.subtle; | ||
| return cached(keyObject, entry.alg, keyObject.toCryptoKey(params, isPublic, usages)); | ||
| }; | ||
| export async function prepareKey(entry, key, usage) { | ||
| const tagged = checkKeyType(entry, key, usage); | ||
| switch (tagged.kind) { | ||
| switch (tagged[0]) { | ||
| case BYTES: | ||
| case CRYPTO: | ||
| return tagged.key; | ||
| return tagged[1]; | ||
| case JWK: { | ||
| if (tagged.key.k) { | ||
| return decode(tagged.key.k); | ||
| const key = tagged[1]; | ||
| if (key.k) { | ||
| return decode(key.k); | ||
| } | ||
| if (!Object.isFrozen(tagged.key)) { | ||
| const { key_ops } = tagged.key; | ||
| if (!Object.isFrozen(key)) { | ||
| const { key_ops } = key; | ||
| if (Array.isArray(key_ops)) | ||
| Object.freeze(key_ops); | ||
| Object.freeze(tagged.key); | ||
| Object.freeze(key); | ||
| } | ||
| return handleJWK(tagged.key, tagged.key, entry); | ||
| return handleJWK(key, key, entry); | ||
| } | ||
| case KEYOBJECT: { | ||
| const keyObject = tagged.key; | ||
| const keyObject = tagged[1]; | ||
| if (keyObject.type === 'secret') { | ||
@@ -166,0 +128,0 @@ return keyObject.export(); |
| import { JOSENotSupported, JWEInvalid, JWSInvalid } from '../util/errors.js'; | ||
| export const JWS_RECOGNIZED = new Map([['b64', true]]); | ||
| export const JWE_RECOGNIZED = new Map(); | ||
| export const JWS_RECOGNIZED = { __proto__: null, b64: true }; | ||
| export const JWE_RECOGNIZED = { __proto__: null }; | ||
| export function validateAlgorithms(option, algorithms) { | ||
@@ -25,3 +25,3 @@ if (algorithms !== undefined && | ||
| if (!protectedHeader || protectedHeader.crit === undefined) { | ||
| return new Set(); | ||
| return []; | ||
| } | ||
@@ -33,21 +33,18 @@ if (!Array.isArray(protectedHeader.crit) || | ||
| } | ||
| let recognized; | ||
| if (recognizedOption !== undefined) { | ||
| recognized = new Map([...Object.entries(recognizedOption), ...recognizedDefault.entries()]); | ||
| } | ||
| else { | ||
| recognized = recognizedDefault; | ||
| } | ||
| const recognized = recognizedOption === undefined | ||
| ? recognizedDefault | ||
| : { __proto__: null, ...recognizedOption, ...recognizedDefault }; | ||
| for (const parameter of protectedHeader.crit) { | ||
| if (!recognized.has(parameter)) { | ||
| if (!(parameter in recognized)) { | ||
| throw new JOSENotSupported(`Extension Header Parameter "${parameter}" is not recognized`); | ||
| } | ||
| if (joseHeader[parameter] === undefined) { | ||
| if (!Object.hasOwn(joseHeader, parameter) || joseHeader[parameter] === undefined) { | ||
| throw new Err(`Extension Header Parameter "${parameter}" is missing`); | ||
| } | ||
| if (recognized.get(parameter) && protectedHeader[parameter] === undefined) { | ||
| if (recognized[parameter] && | ||
| (!Object.hasOwn(protectedHeader, parameter) || protectedHeader[parameter] === undefined)) { | ||
| throw new Err(`Extension Header Parameter "${parameter}" MUST be integrity protected`); | ||
| } | ||
| } | ||
| return new Set(protectedHeader.crit); | ||
| return protectedHeader.crit; | ||
| } |
@@ -1,14 +0,2 @@ | ||
| import { checkCryptoKey } from './crypto_key.js'; | ||
| export function checkModulusLength(alg, key) { | ||
| const { modulusLength } = key.algorithm; | ||
| if (typeof modulusLength !== 'number' || modulusLength < 2048) { | ||
| throw new TypeError(`${alg} requires key modulusLength to be 2048 bits or larger`); | ||
| } | ||
| } | ||
| function checkSigCryptoKey(entry, key, usage) { | ||
| checkCryptoKey(key, entry.subtle, usage); | ||
| if (entry.minModulusLength) { | ||
| checkModulusLength(entry.alg, key); | ||
| } | ||
| } | ||
| import { checkCryptoKey, checkModulusLength } from './crypto_key.js'; | ||
| async function getSigKey(entry, key, usage) { | ||
@@ -20,3 +8,5 @@ if (key instanceof Uint8Array) { | ||
| } | ||
| checkSigCryptoKey(entry, key, usage); | ||
| checkCryptoKey(key, entry.subtle, usage); | ||
| if (entry.minRsaBits) | ||
| checkModulusLength(entry.alg, key); | ||
| return key; | ||
@@ -26,3 +16,3 @@ } | ||
| const cryptoKey = await getSigKey(entry, key, 'sign'); | ||
| const signature = await crypto.subtle.sign(entry.operation, cryptoKey, data); | ||
| const signature = await crypto.subtle.sign(entry.signing, cryptoKey, data); | ||
| return new Uint8Array(signature); | ||
@@ -33,3 +23,3 @@ } | ||
| try { | ||
| return await crypto.subtle.verify(entry.operation, cryptoKey, signature, data); | ||
| return await crypto.subtle.verify(entry.signing, cryptoKey, signature, data); | ||
| } | ||
@@ -36,0 +26,0 @@ catch { |
@@ -1,32 +0,27 @@ | ||
| const isObjectLike = (value) => typeof value === 'object' && value !== null; | ||
| export function isObject(input) { | ||
| if (!isObjectLike(input) || Object.prototype.toString.call(input) !== '[object Object]') { | ||
| if (typeof input !== 'object' || | ||
| input === null || | ||
| Object.prototype.toString.call(input) !== '[object Object]') { | ||
| return false; | ||
| } | ||
| if (Object.getPrototypeOf(input) === null) { | ||
| const prototype = Object.getPrototypeOf(input); | ||
| if (prototype === null) { | ||
| return true; | ||
| } | ||
| let proto = input; | ||
| let proto = prototype; | ||
| while (Object.getPrototypeOf(proto) !== null) { | ||
| proto = Object.getPrototypeOf(proto); | ||
| } | ||
| return Object.getPrototypeOf(input) === proto; | ||
| return prototype === proto; | ||
| } | ||
| export function isDisjoint(...headers) { | ||
| const sources = headers.filter(Boolean); | ||
| if (sources.length === 0 || sources.length === 1) { | ||
| return true; | ||
| } | ||
| let acc; | ||
| for (const header of sources) { | ||
| const parameters = Object.keys(header); | ||
| if (!acc || acc.size === 0) { | ||
| acc = new Set(parameters); | ||
| const parameters = new Set(); | ||
| for (const header of headers) { | ||
| if (!header) | ||
| continue; | ||
| } | ||
| for (const parameter of parameters) { | ||
| if (acc.has(parameter)) { | ||
| for (const parameter of Object.keys(header)) { | ||
| if (parameters.has(parameter)) { | ||
| return false; | ||
| } | ||
| acc.add(parameter); | ||
| parameters.add(parameter); | ||
| } | ||
@@ -33,0 +28,0 @@ } |
| import { encoder, decoder } from '../lib/buffer_utils.js'; | ||
| import { encodeBase64, decodeBase64 } from '../lib/base64.js'; | ||
| const invalid = 'The input to be decoded is not correctly encoded.'; | ||
| export function decode(input) { | ||
@@ -11,3 +12,3 @@ if (Uint8Array.fromBase64) { | ||
| catch (cause) { | ||
| throw new TypeError('The input to be decoded is not correctly encoded.', { cause }); | ||
| throw new TypeError(invalid, { cause }); | ||
| } | ||
@@ -20,3 +21,3 @@ } | ||
| if (encoded.includes('+') || encoded.includes('/')) { | ||
| throw new TypeError('The input to be decoded is not correctly encoded.'); | ||
| throw new TypeError(invalid); | ||
| } | ||
@@ -28,3 +29,3 @@ encoded = encoded.replace(/-/g, '+').replace(/_/g, '/'); | ||
| catch { | ||
| throw new TypeError('The input to be decoded is not correctly encoded.'); | ||
| throw new TypeError(invalid); | ||
| } | ||
@@ -31,0 +32,0 @@ } |
+1
-1
| { | ||
| "name": "jose", | ||
| "version": "6.2.6", | ||
| "version": "6.2.7", | ||
| "description": "JWA, JWS, JWE, JWT, JWK, JWKS for Node.js, Browser, Cloudflare Workers, Deno, Bun, and other Web-interoperable runtimes", | ||
@@ -5,0 +5,0 @@ "keywords": [ |
246664
-3.96%5629
-5.06%