| export function snapshotJwk(jwk) { | ||
| return { __proto__: null, ...jwk }; | ||
| } | ||
| export function normalizeJwk(jwk) { | ||
| const normalized = snapshotJwk(jwk); | ||
| if (normalized.ext !== undefined && typeof normalized.ext !== 'boolean') { | ||
| throw new TypeError('"ext" (Extractable) Parameter must be a boolean'); | ||
| } | ||
| if (normalized.key_ops !== undefined) { | ||
| const value = normalized.key_ops; | ||
| const keyOps = Array.isArray(value) ? [...value] : undefined; | ||
| if (!keyOps || | ||
| keyOps.some((operation) => typeof operation !== 'string') || | ||
| new Set(keyOps).size !== keyOps.length) { | ||
| throw new TypeError('"key_ops" (Key Operations) Parameter must be an array of unique strings'); | ||
| } | ||
| normalized.key_ops = keyOps; | ||
| } | ||
| return normalized; | ||
| } |
| export function validateExtractableOption(extractable) { | ||
| if (extractable !== undefined && typeof extractable !== 'boolean') { | ||
| throw new TypeError('"extractable" option must be a boolean'); | ||
| } | ||
| return extractable; | ||
| } |
@@ -39,3 +39,3 @@ import type * as types from '../types.d.ts'; | ||
| * Timeout (in milliseconds) for the HTTP request. When reached the request will be aborted and | ||
| * the verification will fail. Default is 5000 (5 seconds). | ||
| * the verification will fail. Must be a non-negative integer. Default is 5000 (5 seconds). | ||
| */ | ||
@@ -45,3 +45,3 @@ timeoutDuration?: number; | ||
| * Duration (in milliseconds) for which no more HTTP requests will be triggered after a previous | ||
| * successful fetch. Default is 30000 (30 seconds). | ||
| * successful fetch. Must not be `NaN`. Default is 30000 (30 seconds). | ||
| */ | ||
@@ -51,3 +51,3 @@ cooldownDuration?: number; | ||
| * Maximum time (in milliseconds) between successful HTTP requests. Default is 600000 (10 | ||
| * minutes). | ||
| * minutes). Must not be `NaN`. | ||
| */ | ||
@@ -66,3 +66,3 @@ cacheMaxAge?: number | typeof Infinity; | ||
| jwks: types.JSONWebKeySet; | ||
| /** Last updated at timestamp (seconds since epoch) */ | ||
| /** Last updated at timestamp (milliseconds since epoch) */ | ||
| uat: number; | ||
@@ -69,0 +69,0 @@ } |
@@ -14,5 +14,5 @@ import type * as types from '../../types.d.ts'; | ||
| * > the payload, protected header, and unprotected header of that successfully verified signature | ||
| * > entry. Other signature entries in the General JWS are not validated, and their headers are not | ||
| * > included in the returned result. Recipients of a General JWS should only rely on the returned | ||
| * > (verified) data. | ||
| * > entry. Other signature entries' headers may be inspected solely to reject inconsistent use of the | ||
| * > JWS Unencoded Payload Option, and their headers are not included in the returned result. | ||
| * > Recipients of a General JWS should only rely on the returned (verified) data. | ||
| * | ||
@@ -19,0 +19,0 @@ * @param jws General JWS. |
| import type * as types from '../types.d.ts'; | ||
| /** | ||
| * EncryptJWT constructor | ||
| * | ||
| * @param payload The JWT Claims Set object. Defaults to an empty object. | ||
| */ | ||
| declare const EncryptJWT_base: new (payload?: types.JWTPayload) => types.ProduceJWT; | ||
| /** The EncryptJWT class is used to build and encrypt Compact JWE formatted JSON Web Tokens. */ | ||
| export declare class EncryptJWT implements types.ProduceJWT { | ||
| export declare class EncryptJWT extends EncryptJWT_base { | ||
| #private; | ||
| /** | ||
| * {@link EncryptJWT} constructor | ||
| * | ||
| * @param payload The JWT Claims Set object. Defaults to an empty object. | ||
| */ | ||
| constructor(payload?: types.JWTPayload); | ||
| setIssuer(issuer: string): this; | ||
| setSubject(subject: string): this; | ||
| setAudience(audience: string | string[]): this; | ||
| setJti(jwtId: string): this; | ||
| setNotBefore(input: number | string | Date): this; | ||
| setExpirationTime(input: number | string | Date): this; | ||
| setIssuedAt(input?: number | string | Date): this; | ||
| /** | ||
| * Sets the JWE Protected Header on the EncryptJWT object. | ||
@@ -69,1 +62,2 @@ * | ||
| } | ||
| export {}; |
| import type * as types from '../types.d.ts'; | ||
| /** | ||
| * SignJWT constructor | ||
| * | ||
| * @param payload The JWT Claims Set object. Defaults to an empty object. | ||
| */ | ||
| declare const SignJWT_base: new (payload?: types.JWTPayload) => types.ProduceJWT; | ||
| /** The SignJWT class is used to build and sign Compact JWS formatted JSON Web Tokens. */ | ||
| export declare class SignJWT implements types.ProduceJWT { | ||
| export declare class SignJWT extends SignJWT_base { | ||
| #private; | ||
| /** | ||
| * {@link SignJWT} constructor | ||
| * | ||
| * @param payload The JWT Claims Set object. Defaults to an empty object. | ||
| */ | ||
| constructor(payload?: types.JWTPayload); | ||
| setIssuer(issuer: string): this; | ||
| setSubject(subject: string): this; | ||
| setAudience(audience: string | string[]): this; | ||
| setJti(jwtId: string): this; | ||
| setNotBefore(input: number | string | Date): this; | ||
| setExpirationTime(input: number | string | Date): this; | ||
| setIssuedAt(input?: number | string | Date): this; | ||
| /** | ||
| * Sets the JWS Protected Header on the SignJWT object. | ||
@@ -33,1 +26,2 @@ * | ||
| } | ||
| export {}; |
@@ -9,20 +9,13 @@ import type * as types from '../types.d.ts'; | ||
| } | ||
| /** | ||
| * UnsecuredJWT constructor | ||
| * | ||
| * @param payload The JWT Claims Set object. Defaults to an empty object. | ||
| */ | ||
| declare const UnsecuredJWT_base: new (payload?: types.JWTPayload) => types.ProduceJWT; | ||
| /** The UnsecuredJWT class is a utility for dealing with `{ "alg": "none" }` Unsecured JWTs. */ | ||
| export declare class UnsecuredJWT implements types.ProduceJWT { | ||
| #private; | ||
| /** | ||
| * {@link UnsecuredJWT} constructor | ||
| * | ||
| * @param payload The JWT Claims Set object. Defaults to an empty object. | ||
| */ | ||
| constructor(payload?: types.JWTPayload); | ||
| export declare class UnsecuredJWT extends UnsecuredJWT_base { | ||
| private jwt; | ||
| /** Encodes the Unsecured JWT. */ | ||
| encode(): string; | ||
| setIssuer(issuer: string): this; | ||
| setSubject(subject: string): this; | ||
| setAudience(audience: string | string[]): this; | ||
| setJti(jwtId: string): this; | ||
| setNotBefore(input: number | string | Date): this; | ||
| setExpirationTime(input: number | string | Date): this; | ||
| setIssuedAt(input?: number | string | Date): this; | ||
| /** | ||
@@ -36,1 +29,2 @@ * Decodes an unsecured JWT. | ||
| } | ||
| export {}; |
@@ -1,27 +0,49 @@ | ||
| import { FlattenedEncrypt } from '../flattened/encrypt.js'; | ||
| import { assertNotSet } from '../../lib/helpers.js'; | ||
| import { assertUint8Array } from '../../lib/type_checks.js'; | ||
| import { createJWE } from '../../lib/jwe_encrypt.js'; | ||
| export class CompactEncrypt { | ||
| #flattened; | ||
| #plaintext; | ||
| #protectedHeader; | ||
| #cek; | ||
| #iv; | ||
| #keyManagementParameters; | ||
| constructor(plaintext) { | ||
| this.#flattened = new FlattenedEncrypt(plaintext); | ||
| assertUint8Array(plaintext, 'plaintext'); | ||
| this.#plaintext = plaintext; | ||
| } | ||
| setContentEncryptionKey(cek) { | ||
| this.#flattened.setContentEncryptionKey(cek); | ||
| assertNotSet(this.#cek, 'setContentEncryptionKey'); | ||
| this.#cek = cek; | ||
| return this; | ||
| } | ||
| setInitializationVector(iv) { | ||
| this.#flattened.setInitializationVector(iv); | ||
| assertNotSet(this.#iv, 'setInitializationVector'); | ||
| this.#iv = iv; | ||
| return this; | ||
| } | ||
| setProtectedHeader(protectedHeader) { | ||
| this.#flattened.setProtectedHeader(protectedHeader); | ||
| assertNotSet(this.#protectedHeader, 'setProtectedHeader'); | ||
| this.#protectedHeader = protectedHeader; | ||
| return this; | ||
| } | ||
| setKeyManagementParameters(parameters) { | ||
| this.#flattened.setKeyManagementParameters(parameters); | ||
| assertNotSet(this.#keyManagementParameters, 'setKeyManagementParameters'); | ||
| this.#keyManagementParameters = parameters; | ||
| return this; | ||
| } | ||
| async encrypt(key, options) { | ||
| const jwe = await this.#flattened.encrypt(key, options); | ||
| const jwe = await createJWE([ | ||
| this.#plaintext, | ||
| this.#protectedHeader, | ||
| undefined, | ||
| undefined, | ||
| undefined, | ||
| this.#cek, | ||
| this.#iv, | ||
| this.#keyManagementParameters, | ||
| undefined, | ||
| false, | ||
| ], key, options); | ||
| return [jwe.protected, jwe.encrypted_key, jwe.iv, jwe.ciphertext, jwe.tag].join('.'); | ||
| } | ||
| } |
| import { JWEInvalid } from '../../util/errors.js'; | ||
| import { isObject } from '../../lib/type_checks.js'; | ||
| import { prepareDecrypt, decryptJWE, decryptResult, checkShared, checkRecipient, } from '../../lib/jwe_decrypt.js'; | ||
| import { prepareDecrypt, decryptJWE, decryptResult, checkRecipient, snapshotSharedJWE, snapshotRecipientJWE, } from '../../lib/jwe_decrypt.js'; | ||
| export async function flattenedDecrypt(jwe, key, options) { | ||
@@ -8,5 +8,9 @@ if (!isObject(jwe)) { | ||
| } | ||
| checkShared(jwe); | ||
| checkRecipient(jwe); | ||
| return decryptResult(jwe, await decryptJWE(jwe, prepareDecrypt(options), key)); | ||
| const shared = snapshotSharedJWE(jwe); | ||
| const [recipient, , error] = snapshotRecipientJWE(jwe); | ||
| if (!recipient) | ||
| throw error; | ||
| const snapshot = { ...shared, ...recipient }; | ||
| checkRecipient(snapshot); | ||
| return decryptResult(snapshot, await decryptJWE(snapshot, prepareDecrypt(options), key)); | ||
| } |
@@ -1,5 +0,4 @@ | ||
| import { unprotected, assertNotSet } from '../../lib/helpers.js'; | ||
| import { JWEInvalid } from '../../util/errors.js'; | ||
| import { assertNotSet } from '../../lib/helpers.js'; | ||
| import { createJWE } from '../../lib/jwe_encrypt.js'; | ||
| import { validateCritDuplicates } from '../../lib/options.js'; | ||
| import { assertUint8Array } from '../../lib/type_checks.js'; | ||
| export class FlattenedEncrypt { | ||
@@ -15,5 +14,3 @@ #plaintext; | ||
| constructor(plaintext) { | ||
| if (!(plaintext instanceof Uint8Array)) { | ||
| throw new TypeError('plaintext must be an instance of Uint8Array'); | ||
| } | ||
| assertUint8Array(plaintext, 'plaintext'); | ||
| this.#plaintext = plaintext; | ||
@@ -56,6 +53,2 @@ } | ||
| async encrypt(key, options) { | ||
| if (!this.#protectedHeader && !this.#unprotectedHeader && !this.#sharedUnprotectedHeader) { | ||
| throw new JWEInvalid('either setProtectedHeader, setUnprotectedHeader, or sharedUnprotectedHeader must be called before #encrypt()'); | ||
| } | ||
| validateCritDuplicates(JWEInvalid, this.#protectedHeader); | ||
| return createJWE([ | ||
@@ -70,6 +63,6 @@ this.#plaintext, | ||
| this.#keyManagementParameters, | ||
| options?.crit, | ||
| options ? unprotected in options : false, | ||
| ], key); | ||
| undefined, | ||
| false, | ||
| ], key, options); | ||
| } | ||
| } |
@@ -1,2 +0,2 @@ | ||
| import { prepareDecrypt, shareJWE, decryptRecipient, decryptResult, checkShared, checkRecipient, } from '../../lib/jwe_decrypt.js'; | ||
| import { prepareDecrypt, shareJWE, decryptRecipient, decryptResult, checkRecipient, snapshotSharedJWE, snapshotRecipientJWE, } from '../../lib/jwe_decrypt.js'; | ||
| import { JWEDecryptionFailed, JWEInvalid } from '../../util/errors.js'; | ||
@@ -8,14 +8,20 @@ import { isObject } from '../../lib/type_checks.js'; | ||
| } | ||
| if (!Array.isArray(jwe.recipients) || !jwe.recipients.every(isObject)) { | ||
| const inputRecipients = jwe.recipients; | ||
| if (!Array.isArray(inputRecipients)) { | ||
| throw new JWEInvalid('JWE Recipients missing or incorrect type'); | ||
| } | ||
| if (!jwe.recipients.length) { | ||
| const recipients = Array.from(inputRecipients); | ||
| if (!recipients.every(isObject)) { | ||
| throw new JWEInvalid('JWE Recipients missing or incorrect type'); | ||
| } | ||
| if (!recipients.length) { | ||
| throw new JWEInvalid('JWE Recipients has no members'); | ||
| } | ||
| let shared; | ||
| let sharedJwe; | ||
| let token; | ||
| try { | ||
| checkShared(jwe); | ||
| shared = prepareDecrypt(options); | ||
| token = shareJWE(jwe); | ||
| sharedJwe = snapshotSharedJWE(jwe); | ||
| token = shareJWE(sharedJwe); | ||
| } | ||
@@ -25,5 +31,6 @@ catch { | ||
| } | ||
| if (jwe.recipients.length > 1) { | ||
| for (const { header } of jwe.recipients) { | ||
| const alg = token[0]?.alg ?? header?.alg ?? jwe.unprotected?.alg; | ||
| const recipientSnapshots = recipients.map((recipient) => snapshotRecipientJWE(recipient)); | ||
| if (recipients.length > 1) { | ||
| for (const [, headerAlg] of recipientSnapshots) { | ||
| const alg = token[0]?.alg ?? headerAlg ?? sharedJwe.unprotected?.alg; | ||
| if (alg === 'dir' || alg === 'ECDH-ES') { | ||
@@ -34,14 +41,7 @@ throw new JWEInvalid(`"${alg}" alg may only have a single recipient`); | ||
| } | ||
| for (const recipient of jwe.recipients) { | ||
| for (const [recipient] of recipientSnapshots) { | ||
| if (!recipient) | ||
| continue; | ||
| try { | ||
| const flattened = { | ||
| aad: jwe.aad, | ||
| ciphertext: jwe.ciphertext, | ||
| encrypted_key: recipient.encrypted_key, | ||
| header: recipient.header, | ||
| iv: jwe.iv, | ||
| protected: jwe.protected, | ||
| tag: jwe.tag, | ||
| unprotected: jwe.unprotected, | ||
| }; | ||
| const flattened = { ...sharedJwe, ...recipient }; | ||
| checkRecipient(flattened); | ||
@@ -48,0 +48,0 @@ return decryptResult(flattened, await decryptRecipient(flattened, token, shared, key)); |
@@ -1,2 +0,1 @@ | ||
| import { FlattenedEncrypt } from '../flattened/encrypt.js'; | ||
| import { assertNotSet } from '../../lib/helpers.js'; | ||
@@ -7,6 +6,6 @@ import { JWEInvalid } from '../../util/errors.js'; | ||
| import { encode as b64u } from '../../util/base64url.js'; | ||
| import { validateCritDuplicates } from '../../lib/options.js'; | ||
| import { checkDisjoint, checkEncryptHeaders, encryptJWE } from '../../lib/jwe_encrypt.js'; | ||
| import { checkDisjoint, checkEncryptHeaders, createJWE, encryptJWE } from '../../lib/jwe_encrypt.js'; | ||
| import { prepareKey } from '../../lib/key.js'; | ||
| import { jweAlgorithm } from '../../lib/jwe_algorithms.js'; | ||
| import { assertUint8Array } from '../../lib/type_checks.js'; | ||
| class IndividualRecipient { | ||
@@ -82,15 +81,17 @@ #parent; | ||
| } | ||
| if (!(this.#plaintext instanceof Uint8Array)) { | ||
| throw new TypeError('plaintext must be an instance of Uint8Array'); | ||
| } | ||
| assertUint8Array(this.#plaintext, 'plaintext'); | ||
| if (this.#recipients.length === 1) { | ||
| const [recipient] = this.#recipients; | ||
| const [unprotectedHeader, keyManagementParameters, key, crit] = recipient.state; | ||
| const flattened = await new FlattenedEncrypt(this.#plaintext) | ||
| .setAdditionalAuthenticatedData(this.#aad) | ||
| .setProtectedHeader(this.#protectedHeader) | ||
| .setSharedUnprotectedHeader(this.#unprotectedHeader) | ||
| .setUnprotectedHeader(unprotectedHeader) | ||
| .setKeyManagementParameters(keyManagementParameters) | ||
| .encrypt(key, { crit }); | ||
| const [unprotectedHeader, keyManagementParameters, key, crit] = this.#recipients[0].state; | ||
| const flattened = await createJWE([ | ||
| this.#plaintext, | ||
| this.#protectedHeader, | ||
| unprotectedHeader, | ||
| this.#unprotectedHeader, | ||
| this.#aad, | ||
| undefined, | ||
| undefined, | ||
| keyManagementParameters, | ||
| crit, | ||
| false, | ||
| ], key); | ||
| const jwe = { | ||
@@ -107,4 +108,5 @@ ciphertext: flattened.ciphertext, | ||
| } | ||
| validateCritDuplicates(JWEInvalid, this.#protectedHeader); | ||
| let enc; | ||
| let protectedHeader = this.#protectedHeader; | ||
| let sharedUnprotectedHeader = this.#unprotectedHeader; | ||
| const inputs = []; | ||
@@ -117,5 +119,5 @@ const checked = []; | ||
| this.#plaintext, | ||
| this.#protectedHeader, | ||
| protectedHeader, | ||
| unprotectedHeader, | ||
| this.#unprotectedHeader, | ||
| sharedUnprotectedHeader, | ||
| this.#aad, | ||
@@ -131,2 +133,6 @@ undefined, | ||
| checked.push(headers); | ||
| if (i === 0) { | ||
| protectedHeader = input[1]; | ||
| sharedUnprotectedHeader = input[3]; | ||
| } | ||
| if (headers[1] === 'dir' || headers[1] === 'ECDH-ES') { | ||
@@ -149,3 +155,3 @@ throw new JWEInvalid(`"${headers[1]}" alg may only have a single recipient`); | ||
| const recipient = this.#recipients[i]; | ||
| const [unprotectedHeader, keyManagementParameters, key] = recipient.state; | ||
| const [, keyManagementParameters, key] = recipient.state; | ||
| const target = {}; | ||
@@ -164,2 +170,3 @@ jwe.recipients.push(target); | ||
| const [, alg, , encEntry] = checked[i]; | ||
| const unprotectedHeader = inputs[i][2]; | ||
| const k = await prepareKey(jweAlgorithm(alg), key, 'encrypt'); | ||
@@ -171,3 +178,3 @@ const [, encryptedKey, parameters] = await encryptKeyManagement(alg, encEntry, k, cek, keyManagementParameters); | ||
| if (parameters) | ||
| checkDisjoint(this.#protectedHeader, header, this.#unprotectedHeader); | ||
| checkDisjoint(inputs[i][1], header, inputs[i][3]); | ||
| target.header = header; | ||
@@ -174,0 +181,0 @@ } |
@@ -5,2 +5,3 @@ import { jwkToKey } from '../lib/jwk_to_key.js'; | ||
| import { JWSInvalid } from '../util/errors.js'; | ||
| import { normalizeJwk } from '../lib/jwk_metadata.js'; | ||
| export async function EmbeddedJWK(protectedHeader, token) { | ||
@@ -14,4 +15,17 @@ const joseHeader = { | ||
| } | ||
| let jwk; | ||
| try { | ||
| jwk = normalizeJwk(joseHeader.jwk); | ||
| } | ||
| catch (cause) { | ||
| throw new JWSInvalid('Invalid Embedded JWK', { cause }); | ||
| } | ||
| const entry = jwsAlgorithm(joseHeader.alg); | ||
| const key = await jwkToKey(entry, { ...joseHeader.jwk, ext: true }); | ||
| if (jwk.use !== undefined && jwk.use !== 'sig') { | ||
| throw new JWSInvalid('Invalid Embedded JWK, its "use" must be "sig" when present'); | ||
| } | ||
| if (jwk.alg !== undefined && jwk.alg !== entry.alg) { | ||
| throw new JWSInvalid(`Invalid Embedded JWK, its "alg" must be "${entry.alg}" when present`); | ||
| } | ||
| const key = await jwkToKey(entry, { ...jwk, ext: true }); | ||
| if (key.type !== 'public') { | ||
@@ -18,0 +32,0 @@ throw new JWSInvalid('"jwk" (JSON Web Key) Header Parameter must be a public key'); |
@@ -6,5 +6,6 @@ import { digest } from '../lib/helpers.js'; | ||
| import { isKeyLike } from '../lib/is_key_like.js'; | ||
| import { isJWK } from '../lib/type_checks.js'; | ||
| import { isObject } from '../lib/type_checks.js'; | ||
| import { exportJWK } from '../key/export.js'; | ||
| import { invalidKeyInput } from '../lib/invalid_key_input.js'; | ||
| import { snapshotJwk } from '../lib/jwk_metadata.js'; | ||
| const check = (value, description) => { | ||
@@ -17,7 +18,10 @@ if (typeof value !== 'string' || !value) { | ||
| let jwk; | ||
| if (isJWK(key)) { | ||
| jwk = key; | ||
| if (isObject(key)) { | ||
| jwk = snapshotJwk(key); | ||
| if (typeof jwk.kty !== 'string') { | ||
| throw new TypeError(invalidKeyInput(key, 'CryptoKey', 'KeyObject', 'JSON Web Key')); | ||
| } | ||
| } | ||
| else if (isKeyLike(key)) { | ||
| jwk = await exportJWK(key); | ||
| jwk = snapshotJwk(await exportJWK(key)); | ||
| } | ||
@@ -57,3 +61,5 @@ else { | ||
| case 'oct': | ||
| check(jwk.k, '"k" (Key Value) Parameter'); | ||
| if (typeof jwk.k !== 'string') { | ||
| throw new JWKInvalid('"k" (Key Value) Parameter missing or invalid'); | ||
| } | ||
| components = { k: jwk.k, kty: jwk.kty }; | ||
@@ -60,0 +66,0 @@ break; |
| import { jwkToKey } from '../lib/jwk_to_key.js'; | ||
| import { JWS } from '../lib/jws_algorithms.js'; | ||
| import { JWKSInvalid, JOSENotSupported, JWKSNoMatchingKey, JWKSMultipleMatchingKeys, } from '../util/errors.js'; | ||
| import { isObject } from '../lib/type_checks.js'; | ||
| function signatureAlgorithm(alg) { | ||
| const entry = typeof alg === 'string' ? JWS[alg] : undefined; | ||
| if (!entry || entry.secret) { | ||
| throw new JOSENotSupported('Unsupported "alg" value for a JSON Web Key Set'); | ||
| } | ||
| return entry; | ||
| import { isJwkSet } from '../lib/type_checks.js'; | ||
| import { snapshotJwk } from '../lib/jwk_metadata.js'; | ||
| function isUsableJWK(jwk, entry, alg, kid) { | ||
| const { kty, key_ops, ext, kid: jwkKid, alg: jwkAlg, use, crv } = snapshotJwk(jwk); | ||
| const keyOps = Array.isArray(key_ops) ? [...key_ops] : key_ops; | ||
| return ((ext === undefined || typeof ext === 'boolean') && | ||
| (keyOps === undefined || | ||
| (Array.isArray(keyOps) && | ||
| keyOps.every((operation, index) => typeof operation === 'string' && keyOps.indexOf(operation) === index) && | ||
| keyOps.includes('verify'))) && | ||
| entry.kty.includes(kty) && | ||
| (kid === undefined || (typeof kid === 'string' && kid === jwkKid)) && | ||
| (jwkAlg === undefined ? kty !== 'AKP' : alg === jwkAlg) && | ||
| (use === undefined || use === 'sig') && | ||
| (!entry.crv || crv === entry.crv)); | ||
| } | ||
| function isJWKSLike(jwks) { | ||
| if (!jwks || typeof jwks !== 'object') { | ||
| return false; | ||
| async function importWithAlgCache(cache, jwk, entry) { | ||
| const cached = cache.get(jwk) || cache.set(jwk, {}).get(jwk); | ||
| const { alg } = entry; | ||
| if (cached[alg] === undefined) { | ||
| const key = await jwkToKey(entry, { ...jwk, alg, ext: true }); | ||
| if (key.type !== 'public') { | ||
| throw new JWKSInvalid('JSON Web Key Set members must be public keys'); | ||
| } | ||
| cached[alg] = key; | ||
| } | ||
| const { keys } = jwks; | ||
| return Array.isArray(keys) && keys.every((isObject)); | ||
| return cached[alg]; | ||
| } | ||
| class LocalJWKSetImpl { | ||
| #jwks; | ||
| #cached = new WeakMap(); | ||
| constructor(jwks) { | ||
| if (!isJWKSLike(jwks)) { | ||
| throw new JWKSInvalid('JSON Web Key Set malformed'); | ||
| } | ||
| this.#jwks = structuredClone(jwks); | ||
| export function createLocalJWKSet(jwks) { | ||
| let snapshot; | ||
| try { | ||
| snapshot = structuredClone(jwks); | ||
| } | ||
| jwks() { | ||
| return this.#jwks; | ||
| catch { } | ||
| if (!isJwkSet(snapshot)) { | ||
| throw new JWKSInvalid('JSON Web Key Set malformed'); | ||
| } | ||
| async getKey(protectedHeader, token) { | ||
| const cached = new WeakMap(); | ||
| const localJWKSet = async (protectedHeader, token) => { | ||
| const { alg, kid } = { ...protectedHeader, ...token?.header }; | ||
| const entry = signatureAlgorithm(alg); | ||
| 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 entry = typeof alg === 'string' ? JWS[alg] : undefined; | ||
| if (!entry || entry.secret) { | ||
| throw new JOSENotSupported('Unsupported "alg" value for a JSON Web Key Set'); | ||
| } | ||
| const candidates = snapshot.keys.filter((jwk) => isUsableJWK(jwk, entry, alg, kid)); | ||
| const { 0: jwk, length } = candidates; | ||
| if (length === 0) { | ||
| if (!length) { | ||
| throw new JWKSNoMatchingKey(); | ||
@@ -46,7 +55,6 @@ } | ||
| const error = new JWKSMultipleMatchingKeys(); | ||
| const _cached = this.#cached; | ||
| error[Symbol.asyncIterator] = async function* () { | ||
| for (const jwk of candidates) { | ||
| try { | ||
| yield await importWithAlgCache(_cached, jwk, entry); | ||
| yield await importWithAlgCache(cached, jwk, entry); | ||
| } | ||
@@ -58,23 +66,7 @@ catch { } | ||
| } | ||
| return importWithAlgCache(this.#cached, jwk, entry); | ||
| } | ||
| } | ||
| async function importWithAlgCache(cache, jwk, entry) { | ||
| const cached = cache.get(jwk) || cache.set(jwk, { __proto__: null }).get(jwk); | ||
| if (cached[entry.alg] === undefined) { | ||
| const key = await jwkToKey(entry, { ...jwk, alg: entry.alg, ext: true }); | ||
| if (key.type !== 'public') { | ||
| throw new JWKSInvalid('JSON Web Key Set members must be public keys'); | ||
| } | ||
| cached[entry.alg] = key; | ||
| } | ||
| return cached[entry.alg]; | ||
| } | ||
| export function createLocalJWKSet(jwks) { | ||
| const set = new LocalJWKSetImpl(jwks); | ||
| const localJWKSet = async (protectedHeader, token) => set.getKey(protectedHeader, token); | ||
| Object.defineProperty(localJWKSet, 'jwks', { | ||
| value: () => structuredClone(set.jwks()), | ||
| return importWithAlgCache(cached, jwk, entry); | ||
| }; | ||
| return Object.defineProperty(localJWKSet, 'jwks', { | ||
| value: () => structuredClone(snapshot), | ||
| }); | ||
| return localJWKSet; | ||
| } |
+82
-103
| import { JOSEError, JWKSNoMatchingKey, JWKSTimeout } from '../util/errors.js'; | ||
| import { createLocalJWKSet } from './local.js'; | ||
| import { isObject } from '../lib/type_checks.js'; | ||
| import { isJwkSet } from '../lib/type_checks.js'; | ||
| function isCloudflareWorkers() { | ||
@@ -12,3 +12,3 @@ return (typeof WebSocketPair !== 'undefined' || | ||
| const NAME = 'jose'; | ||
| const VERSION = 'v6.2.9'; | ||
| const VERSION = 'v6.2.10'; | ||
| USER_AGENT = `${NAME}/${VERSION}`; | ||
@@ -40,133 +40,112 @@ } | ||
| export const jwksCache = Symbol(); | ||
| function isFreshJwksCache(input, cacheMaxAge) { | ||
| if (typeof input !== 'object' || input === null) { | ||
| return false; | ||
| function isFreshFor(timestamp, duration) { | ||
| return Number.isFinite(timestamp) && Date.now() < timestamp + duration; | ||
| } | ||
| function validateDuration(value, fallback, option) { | ||
| if (Number.isNaN(value)) { | ||
| throw new TypeError(`"${option}" option must not be NaN`); | ||
| } | ||
| if (!('uat' in input) || typeof input.uat !== 'number' || Date.now() - input.uat >= cacheMaxAge) { | ||
| return false; | ||
| } | ||
| if (!('jwks' in input) || | ||
| !isObject(input.jwks) || | ||
| !Array.isArray(input.jwks.keys) || | ||
| !Array.prototype.every.call(input.jwks.keys, isObject)) { | ||
| return false; | ||
| } | ||
| return true; | ||
| return typeof value === 'number' ? value : fallback; | ||
| } | ||
| class RemoteJWKSetImpl { | ||
| #url; | ||
| #timeoutDuration; | ||
| #cooldownDuration; | ||
| #cacheMaxAge; | ||
| #jwksTimestamp; | ||
| #pendingFetch; | ||
| #headers; | ||
| #customFetch; | ||
| #local; | ||
| #cache; | ||
| constructor(url, options) { | ||
| if (!(url instanceof URL)) { | ||
| throw new TypeError('url must be an instance of URL'); | ||
| } | ||
| this.#url = new URL(url.href); | ||
| const opts = options ?? {}; | ||
| this.#timeoutDuration = typeof opts.timeoutDuration === 'number' ? opts.timeoutDuration : 5000; | ||
| this.#cooldownDuration = | ||
| 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')) { | ||
| this.#headers.set('User-Agent', USER_AGENT); | ||
| } | ||
| if (!this.#headers.has('accept')) { | ||
| this.#headers.set('accept', 'application/json'); | ||
| this.#headers.append('accept', 'application/jwk-set+json'); | ||
| } | ||
| this.#customFetch = opts[customFetch]; | ||
| const cache = opts[jwksCache]; | ||
| if (cache !== undefined) { | ||
| this.#cache = cache; | ||
| if (isFreshJwksCache(cache, this.#cacheMaxAge)) { | ||
| this.#jwksTimestamp = this.#cache.uat; | ||
| this.#local = createLocalJWKSet(this.#cache.jwks); | ||
| } | ||
| } | ||
| export function createRemoteJWKSet(url, options) { | ||
| if (!(url instanceof URL)) { | ||
| throw new TypeError('url must be an instance of URL'); | ||
| } | ||
| pendingFetch() { | ||
| return !!this.#pendingFetch; | ||
| const href = new URL(url.href).href; | ||
| const opts = options ?? {}; | ||
| const timeoutOption = opts.timeoutDuration; | ||
| if (typeof timeoutOption === 'number' && | ||
| (!Number.isInteger(timeoutOption) || timeoutOption < 0)) { | ||
| throw new TypeError('"timeoutDuration" option must be a non-negative integer'); | ||
| } | ||
| #validFor(duration) { | ||
| return typeof this.#jwksTimestamp === 'number' && Date.now() < this.#jwksTimestamp + duration; | ||
| const timeoutDuration = typeof timeoutOption === 'number' ? timeoutOption : 5000; | ||
| const cooldownDuration = validateDuration(opts.cooldownDuration, 30000, 'cooldownDuration'); | ||
| const cacheMaxAge = validateDuration(opts.cacheMaxAge, 600000, 'cacheMaxAge'); | ||
| const headers = new Headers(opts.headers); | ||
| if (USER_AGENT && !headers.has('User-Agent')) { | ||
| headers.set('User-Agent', USER_AGENT); | ||
| } | ||
| coolingDown() { | ||
| return this.#validFor(this.#cooldownDuration); | ||
| if (!headers.has('accept')) { | ||
| headers.set('accept', 'application/json, application/jwk-set+json'); | ||
| } | ||
| fresh() { | ||
| return this.#validFor(this.#cacheMaxAge); | ||
| const fetchImpl = opts[customFetch]; | ||
| const cache = opts[jwksCache]; | ||
| let jwksTimestamp; | ||
| let pendingFetch; | ||
| let reloadSequence = 0; | ||
| let appliedSequence = 0; | ||
| let local; | ||
| if (cache && typeof cache === 'object') { | ||
| const { uat, jwks } = cache; | ||
| if (isFreshFor(uat, cacheMaxAge) && isJwkSet(jwks)) { | ||
| jwksTimestamp = uat; | ||
| local = createLocalJWKSet(jwks); | ||
| } | ||
| } | ||
| jwks() { | ||
| return this.#local?.jwks(); | ||
| } | ||
| async getKey(protectedHeader, token) { | ||
| if (!this.#local || !this.fresh()) { | ||
| await this.reload(); | ||
| const reload = async () => { | ||
| if (pendingFetch && isCloudflareWorkers()) { | ||
| pendingFetch = undefined; | ||
| } | ||
| if (!pendingFetch) { | ||
| const sequence = ++reloadSequence; | ||
| const current = (pendingFetch = fetchJwks(href, headers, AbortSignal.timeout(timeoutDuration), fetchImpl) | ||
| .then((json) => { | ||
| const next = createLocalJWKSet(json); | ||
| if (sequence <= appliedSequence) { | ||
| return; | ||
| } | ||
| local = next; | ||
| const updatedAt = Date.now(); | ||
| if (cache) { | ||
| cache.uat = updatedAt; | ||
| cache.jwks = json; | ||
| } | ||
| jwksTimestamp = updatedAt; | ||
| appliedSequence = sequence; | ||
| }) | ||
| .finally(() => { | ||
| if (pendingFetch === current) { | ||
| pendingFetch = undefined; | ||
| } | ||
| })); | ||
| } | ||
| await pendingFetch; | ||
| }; | ||
| const remoteJWKSet = async (protectedHeader, token) => { | ||
| if (!local || !isFreshFor(jwksTimestamp, cacheMaxAge)) { | ||
| await reload(); | ||
| } | ||
| try { | ||
| return await this.#local(protectedHeader, token); | ||
| return await local(protectedHeader, token); | ||
| } | ||
| catch (err) { | ||
| if (err instanceof JWKSNoMatchingKey) { | ||
| if (this.coolingDown() === false) { | ||
| await this.reload(); | ||
| return this.#local(protectedHeader, token); | ||
| } | ||
| if (err instanceof JWKSNoMatchingKey && !isFreshFor(jwksTimestamp, cooldownDuration)) { | ||
| await reload(); | ||
| return local(protectedHeader, token); | ||
| } | ||
| throw err; | ||
| } | ||
| } | ||
| async reload() { | ||
| if (this.#pendingFetch && isCloudflareWorkers()) { | ||
| this.#pendingFetch = undefined; | ||
| } | ||
| this.#pendingFetch ||= fetchJwks(this.#url.href, this.#headers, AbortSignal.timeout(this.#timeoutDuration), this.#customFetch) | ||
| .then((json) => { | ||
| this.#local = createLocalJWKSet(json); | ||
| if (this.#cache) { | ||
| this.#cache.uat = Date.now(); | ||
| this.#cache.jwks = json; | ||
| } | ||
| this.#jwksTimestamp = Date.now(); | ||
| }) | ||
| .finally(() => { | ||
| this.#pendingFetch = undefined; | ||
| }); | ||
| await this.#pendingFetch; | ||
| } | ||
| } | ||
| export function createRemoteJWKSet(url, options) { | ||
| const set = new RemoteJWKSetImpl(url, options); | ||
| const remoteJWKSet = async (protectedHeader, token) => set.getKey(protectedHeader, token); | ||
| Object.defineProperties(remoteJWKSet, { | ||
| }; | ||
| return Object.defineProperties(remoteJWKSet, { | ||
| coolingDown: { | ||
| get: () => set.coolingDown(), | ||
| get: () => isFreshFor(jwksTimestamp, cooldownDuration), | ||
| enumerable: true, | ||
| }, | ||
| fresh: { | ||
| get: () => set.fresh(), | ||
| get: () => isFreshFor(jwksTimestamp, cacheMaxAge), | ||
| enumerable: true, | ||
| }, | ||
| reload: { | ||
| value: () => set.reload(), | ||
| value: reload, | ||
| enumerable: true, | ||
| }, | ||
| reloading: { | ||
| get: () => set.pendingFetch(), | ||
| get: () => !!pendingFetch, | ||
| enumerable: true, | ||
| }, | ||
| jwks: { | ||
| value: () => set.jwks(), | ||
| value: () => local?.jwks(), | ||
| enumerable: true, | ||
| }, | ||
| }); | ||
| return remoteJWKSet; | ||
| } |
@@ -1,11 +0,14 @@ | ||
| import { FlattenedSign } from '../flattened/sign.js'; | ||
| import { unencodedPayload } from '../../lib/jws_sign.js'; | ||
| import { createCompactSignature } from '../../lib/jws_sign.js'; | ||
| import { assertNotSet } from '../../lib/helpers.js'; | ||
| export class CompactSign { | ||
| #flattened; | ||
| #payload; | ||
| #protectedHeader; | ||
| constructor(payload) { | ||
| this.#flattened = new FlattenedSign(payload); | ||
| if (!(payload instanceof Uint8Array)) { | ||
| throw new TypeError('payload must be an instance of Uint8Array'); | ||
| } | ||
| this.#payload = payload; | ||
| } | ||
| setProtectedHeader(protectedHeader) { | ||
| this.#flattened.setProtectedHeader(protectedHeader); | ||
| assertNotSet(this.#protectedHeader, 'setProtectedHeader'); | ||
| this.#protectedHeader = protectedHeader; | ||
@@ -15,8 +18,6 @@ return this; | ||
| async sign(key, options) { | ||
| if (unencodedPayload(this.#protectedHeader)) { | ||
| return createCompactSignature(this.#payload, this.#protectedHeader, options?.crit, key, () => { | ||
| throw new TypeError('use the flattened module for creating JWS with b64: false'); | ||
| } | ||
| const jws = await this.#flattened.sign(key, options); | ||
| return `${jws.protected}.${jws.payload}.${jws.signature}`; | ||
| }); | ||
| } | ||
| } |
@@ -24,3 +24,3 @@ import { createSignature } from '../../lib/jws_sign.js'; | ||
| async sign(key, options) { | ||
| return createSignature({ | ||
| const [jws] = await createSignature({ | ||
| payload: this.#payload, | ||
@@ -31,3 +31,4 @@ protectedHeader: this.#protectedHeader, | ||
| }, key); | ||
| return jws; | ||
| } | ||
| } |
| import { JWSInvalid } from '../../util/errors.js'; | ||
| import { isObject } from '../../lib/type_checks.js'; | ||
| import { prepareVerify, verifySignature, verifyResult } from '../../lib/jws_verify.js'; | ||
| import { encodeJsonUnencodedPayload, prepareVerify, snapshotJws, verifySignature, verifyResult, } from '../../lib/jws_verify.js'; | ||
| export async function flattenedVerify(jws, key, options) { | ||
@@ -8,18 +8,19 @@ if (!isObject(jws)) { | ||
| } | ||
| if (jws.protected === undefined && jws.header === undefined) { | ||
| const snapshot = snapshotJws(jws); | ||
| if (snapshot.protected === undefined && snapshot.header === undefined) { | ||
| throw new JWSInvalid('Flattened JWS must have either of the "protected" or "header" members'); | ||
| } | ||
| if (jws.protected !== undefined && typeof jws.protected !== 'string') { | ||
| if (snapshot.protected !== undefined && typeof snapshot.protected !== 'string') { | ||
| throw new JWSInvalid('JWS Protected Header incorrect type'); | ||
| } | ||
| if (jws.payload === undefined) { | ||
| if (snapshot.payload === undefined) { | ||
| throw new JWSInvalid('JWS Payload missing'); | ||
| } | ||
| if (typeof jws.signature !== 'string') { | ||
| if (typeof snapshot.signature !== 'string') { | ||
| throw new JWSInvalid('JWS Signature missing or incorrect type'); | ||
| } | ||
| if (jws.header !== undefined && !isObject(jws.header)) { | ||
| if (snapshot.header !== undefined && !isObject(snapshot.header)) { | ||
| throw new JWSInvalid('JWS Unprotected Header incorrect type'); | ||
| } | ||
| return verifyResult(jws, await verifySignature(jws, prepareVerify(options), key)); | ||
| return verifyResult(snapshot, await verifySignature(snapshot, prepareVerify(options), key, encodeJsonUnencodedPayload)); | ||
| } |
@@ -54,6 +54,7 @@ import { createSignature } from '../../lib/jws_sign.js'; | ||
| const encoded = []; | ||
| let b64; | ||
| for (let i = 0; i < this.#signatures.length; i++) { | ||
| const signature = this.#signatures[i]; | ||
| const [protectedHeader, unprotectedHeader, key, crit] = signature.state; | ||
| const { payload, ...rest } = await createSignature({ | ||
| const [{ payload, ...rest }, signatureB64] = await createSignature({ | ||
| payload: this.#payload, | ||
@@ -65,6 +66,7 @@ protectedHeader, | ||
| }, key); | ||
| if (i === 0) { | ||
| if (b64 === undefined) { | ||
| b64 = signatureB64; | ||
| jws.payload = payload; | ||
| } | ||
| else if (jws.payload !== payload) { | ||
| else if (b64 !== signatureB64) { | ||
| throw new JWSInvalid('inconsistent use of JWS Unencoded Payload (RFC7797)'); | ||
@@ -71,0 +73,0 @@ } |
@@ -1,4 +0,34 @@ | ||
| import { prepareVerify, verifySignature, verifyResult } from '../../lib/jws_verify.js'; | ||
| import { encodeJsonUnencodedPayload, parseProtectedHeader, prepareVerify, snapshotJws, verifySignature, verifyResult, } from '../../lib/jws_verify.js'; | ||
| import { JWSInvalid, JWSSignatureVerificationFailed } from '../../util/errors.js'; | ||
| import { isObject } from '../../lib/type_checks.js'; | ||
| function snapshotSignature(signature, payload) { | ||
| try { | ||
| const jws = snapshotJws(signature, [payload]); | ||
| const { protected: encodedProtected, header, signature: encodedSignature } = jws; | ||
| if (encodedProtected === undefined && header === undefined) | ||
| return undefined; | ||
| if (encodedProtected !== undefined && typeof encodedProtected !== 'string') | ||
| return undefined; | ||
| if (typeof encodedSignature !== 'string') | ||
| return undefined; | ||
| if (header !== undefined && !isObject(header)) | ||
| return undefined; | ||
| const protectedHeader = parseProtectedHeader(encodedProtected); | ||
| const { b64, crit } = protectedHeader; | ||
| return [ | ||
| jws, | ||
| protectedHeader, | ||
| Array.isArray(crit) && crit.includes('b64') | ||
| ? typeof b64 === 'boolean' | ||
| ? b64 | ||
| ? 1 | ||
| : 2 | ||
| : 0 | ||
| : 1, | ||
| ]; | ||
| } | ||
| catch { | ||
| return undefined; | ||
| } | ||
| } | ||
| export async function generalVerify(jws, key, options) { | ||
@@ -8,9 +38,13 @@ if (!isObject(jws)) { | ||
| } | ||
| const { signatures, payload } = jws; | ||
| if (!Array.isArray(signatures) || !signatures.every(isObject)) { | ||
| const { signatures, payload: inputPayload } = jws; | ||
| if (!Array.isArray(signatures)) { | ||
| throw new JWSInvalid('JWS Signatures missing or incorrect type'); | ||
| } | ||
| const signatureEntries = Array.from(signatures); | ||
| if (!signatureEntries.every(isObject)) { | ||
| throw new JWSInvalid('JWS Signatures missing or incorrect type'); | ||
| } | ||
| let shared; | ||
| try { | ||
| if (payload === undefined) | ||
| if (inputPayload === undefined) | ||
| throw new Error(); | ||
@@ -22,20 +56,16 @@ shared = prepareVerify(options); | ||
| } | ||
| for (const signature of signatures) { | ||
| const payload = inputPayload instanceof Uint8Array ? new Uint8Array(inputPayload) : inputPayload; | ||
| const candidates = signatureEntries | ||
| .map((signature) => snapshotSignature(signature, payload)) | ||
| .filter((candidate) => candidate !== undefined); | ||
| let modes = 0; | ||
| for (const [, , mode] of candidates) { | ||
| modes |= mode; | ||
| if (modes === 3) { | ||
| throw new JWSInvalid('inconsistent use of JWS Unencoded Payload (RFC7797)'); | ||
| } | ||
| } | ||
| for (const candidate of candidates) { | ||
| try { | ||
| const { protected: encodedProtected, header, signature: encodedSignature } = signature; | ||
| if (encodedProtected === undefined && header === undefined) | ||
| throw new Error(); | ||
| if (encodedProtected !== undefined && typeof encodedProtected !== 'string') { | ||
| throw new Error(); | ||
| } | ||
| if (typeof encodedSignature !== 'string') | ||
| throw new Error(); | ||
| if (header !== undefined && !isObject(header)) | ||
| throw new Error(); | ||
| return verifyResult(signature, await verifySignature({ | ||
| header, | ||
| payload, | ||
| protected: encodedProtected, | ||
| signature: encodedSignature, | ||
| }, shared, key)); | ||
| return verifyResult(candidate[0], await verifySignature(candidate[0], shared, key, encodeJsonUnencodedPayload, candidate[1])); | ||
| } | ||
@@ -42,0 +72,0 @@ catch { |
@@ -8,12 +8,10 @@ import { prepareDecrypt, decryptCompact } from '../lib/jwe_decrypt.js'; | ||
| const payload = validateClaimsSet(protectedHeader, decrypted[0], options); | ||
| if (protectedHeader.iss !== undefined && protectedHeader.iss !== payload.iss) { | ||
| throw new JWTClaimValidationFailed('replicated "iss" claim header parameter mismatch', payload, 'iss', 'mismatch'); | ||
| for (const claim of ['iss', 'sub', 'aud']) { | ||
| if (protectedHeader[claim] !== undefined && | ||
| (claim === 'aud' | ||
| ? JSON.stringify(protectedHeader.aud) !== JSON.stringify(payload.aud) | ||
| : protectedHeader[claim] !== payload[claim])) { | ||
| throw new JWTClaimValidationFailed(`replicated "${claim}" claim header parameter mismatch`, payload, claim, 'mismatch'); | ||
| } | ||
| } | ||
| if (protectedHeader.sub !== undefined && protectedHeader.sub !== payload.sub) { | ||
| throw new JWTClaimValidationFailed('replicated "sub" claim header parameter mismatch', payload, 'sub', 'mismatch'); | ||
| } | ||
| if (protectedHeader.aud !== undefined && | ||
| JSON.stringify(protectedHeader.aud) !== JSON.stringify(payload.aud)) { | ||
| throw new JWTClaimValidationFailed('replicated "aud" claim header parameter mismatch', payload, 'aud', 'mismatch'); | ||
| } | ||
| const result = { payload, protectedHeader }; | ||
@@ -20,0 +18,0 @@ if (typeof key === 'function') { |
@@ -1,5 +0,6 @@ | ||
| import { CompactEncrypt } from '../jwe/compact/encrypt.js'; | ||
| import { JWTClaimsBuilder } from '../lib/jwt_claims_set.js'; | ||
| import { createJWE } from '../lib/jwe_encrypt.js'; | ||
| import { JWTClaimsBuilder, jwtClaim, jwtData } from '../lib/jwt_claims_set.js'; | ||
| import { assertNotSet } from '../lib/helpers.js'; | ||
| export class EncryptJWT { | ||
| const EncryptJWT_base = JWTClaimsBuilder; | ||
| export class EncryptJWT extends EncryptJWT_base { | ||
| #cek; | ||
@@ -12,34 +13,2 @@ #iv; | ||
| #replicateAudienceAsHeader; | ||
| #jwt; | ||
| constructor(payload = {}) { | ||
| this.#jwt = new JWTClaimsBuilder(payload); | ||
| } | ||
| setIssuer(issuer) { | ||
| this.#jwt.iss = issuer; | ||
| return this; | ||
| } | ||
| setSubject(subject) { | ||
| this.#jwt.sub = subject; | ||
| return this; | ||
| } | ||
| setAudience(audience) { | ||
| this.#jwt.aud = audience; | ||
| return this; | ||
| } | ||
| setJti(jwtId) { | ||
| this.#jwt.jti = jwtId; | ||
| return this; | ||
| } | ||
| setNotBefore(input) { | ||
| this.#jwt.nbf = input; | ||
| return this; | ||
| } | ||
| setExpirationTime(input) { | ||
| this.#jwt.exp = input; | ||
| return this; | ||
| } | ||
| setIssuedAt(input) { | ||
| this.#jwt.iat = input; | ||
| return this; | ||
| } | ||
| setProtectedHeader(protectedHeader) { | ||
@@ -78,3 +47,3 @@ assertNotSet(this.#protectedHeader, 'setProtectedHeader'); | ||
| async encrypt(key, options) { | ||
| const enc = new CompactEncrypt(this.#jwt.data()); | ||
| const plaintext = jwtData(this); | ||
| if (this.#protectedHeader && | ||
@@ -86,19 +55,21 @@ (this.#replicateIssuerAsHeader || | ||
| ...this.#protectedHeader, | ||
| iss: this.#replicateIssuerAsHeader ? this.#jwt.iss : undefined, | ||
| sub: this.#replicateSubjectAsHeader ? this.#jwt.sub : undefined, | ||
| aud: this.#replicateAudienceAsHeader ? this.#jwt.aud : undefined, | ||
| iss: this.#replicateIssuerAsHeader ? jwtClaim(this, 'iss') : undefined, | ||
| sub: this.#replicateSubjectAsHeader ? jwtClaim(this, 'sub') : undefined, | ||
| aud: this.#replicateAudienceAsHeader ? jwtClaim(this, 'aud') : undefined, | ||
| }; | ||
| } | ||
| enc.setProtectedHeader(this.#protectedHeader); | ||
| if (this.#iv) { | ||
| enc.setInitializationVector(this.#iv); | ||
| } | ||
| if (this.#cek) { | ||
| enc.setContentEncryptionKey(this.#cek); | ||
| } | ||
| if (this.#keyManagementParameters) { | ||
| enc.setKeyManagementParameters(this.#keyManagementParameters); | ||
| } | ||
| return enc.encrypt(key, options); | ||
| const jwe = await createJWE([ | ||
| plaintext, | ||
| this.#protectedHeader, | ||
| undefined, | ||
| undefined, | ||
| undefined, | ||
| this.#cek, | ||
| this.#iv, | ||
| this.#keyManagementParameters, | ||
| undefined, | ||
| false, | ||
| ], key, options); | ||
| return [jwe.protected, jwe.encrypted_key, jwe.iv, jwe.ciphertext, jwe.tag].join('.'); | ||
| } | ||
| } |
@@ -1,40 +0,10 @@ | ||
| import { CompactSign } from '../jws/compact/sign.js'; | ||
| import { unencodedPayload } from '../lib/jws_sign.js'; | ||
| import { createCompactSignature } from '../lib/jws_sign.js'; | ||
| import { JWTInvalid } from '../util/errors.js'; | ||
| import { JWTClaimsBuilder } from '../lib/jwt_claims_set.js'; | ||
| export class SignJWT { | ||
| import { JWTClaimsBuilder, jwtData } from '../lib/jwt_claims_set.js'; | ||
| import { assertNotSet } from '../lib/helpers.js'; | ||
| const SignJWT_base = JWTClaimsBuilder; | ||
| export class SignJWT extends SignJWT_base { | ||
| #protectedHeader; | ||
| #jwt; | ||
| constructor(payload = {}) { | ||
| this.#jwt = new JWTClaimsBuilder(payload); | ||
| } | ||
| setIssuer(issuer) { | ||
| this.#jwt.iss = issuer; | ||
| return this; | ||
| } | ||
| setSubject(subject) { | ||
| this.#jwt.sub = subject; | ||
| return this; | ||
| } | ||
| setAudience(audience) { | ||
| this.#jwt.aud = audience; | ||
| return this; | ||
| } | ||
| setJti(jwtId) { | ||
| this.#jwt.jti = jwtId; | ||
| return this; | ||
| } | ||
| setNotBefore(input) { | ||
| this.#jwt.nbf = input; | ||
| return this; | ||
| } | ||
| setExpirationTime(input) { | ||
| this.#jwt.exp = input; | ||
| return this; | ||
| } | ||
| setIssuedAt(input) { | ||
| this.#jwt.iat = input; | ||
| return this; | ||
| } | ||
| setProtectedHeader(protectedHeader) { | ||
| assertNotSet(this.#protectedHeader, 'setProtectedHeader'); | ||
| this.#protectedHeader = protectedHeader; | ||
@@ -44,9 +14,6 @@ return this; | ||
| async sign(key, options) { | ||
| const sig = new CompactSign(this.#jwt.data()); | ||
| sig.setProtectedHeader(this.#protectedHeader); | ||
| if (unencodedPayload(this.#protectedHeader)) { | ||
| return createCompactSignature(jwtData(this), this.#protectedHeader, options?.crit, key, () => { | ||
| throw new JWTInvalid('JWTs MUST NOT use unencoded payload'); | ||
| } | ||
| return sig.sign(key, options); | ||
| }); | ||
| } | ||
| } |
| import * as b64u from '../util/base64url.js'; | ||
| import { strictDecoder } from '../lib/buffer_utils.js'; | ||
| import { decodeBase64url } from '../lib/helpers.js'; | ||
| import { JWTInvalid } from '../util/errors.js'; | ||
| import { validateClaimsSet, JWTClaimsBuilder } from '../lib/jwt_claims_set.js'; | ||
| export class UnsecuredJWT { | ||
| #jwt; | ||
| constructor(payload = {}) { | ||
| this.#jwt = new JWTClaimsBuilder(payload); | ||
| } | ||
| import { decodeBase64url, parseJoseHeader } from '../lib/helpers.js'; | ||
| import { JWSInvalid, JWTInvalid } from '../util/errors.js'; | ||
| import { validateClaimsSet, JWTClaimsBuilder, jwtData } from '../lib/jwt_claims_set.js'; | ||
| import { JWS_RECOGNIZED, validateB64, validateCrit } from '../lib/options.js'; | ||
| const UnsecuredJWT_base = JWTClaimsBuilder; | ||
| export class UnsecuredJWT extends UnsecuredJWT_base { | ||
| encode() { | ||
| const header = b64u.encode(JSON.stringify({ alg: 'none' })); | ||
| const payload = b64u.encode(this.#jwt.data()); | ||
| const payload = b64u.encode(jwtData(this)); | ||
| return `${header}.${payload}.`; | ||
| } | ||
| setIssuer(issuer) { | ||
| this.#jwt.iss = issuer; | ||
| return this; | ||
| } | ||
| setSubject(subject) { | ||
| this.#jwt.sub = subject; | ||
| return this; | ||
| } | ||
| setAudience(audience) { | ||
| this.#jwt.aud = audience; | ||
| return this; | ||
| } | ||
| setJti(jwtId) { | ||
| this.#jwt.jti = jwtId; | ||
| return this; | ||
| } | ||
| setNotBefore(input) { | ||
| this.#jwt.nbf = input; | ||
| return this; | ||
| } | ||
| setExpirationTime(input) { | ||
| this.#jwt.exp = input; | ||
| return this; | ||
| } | ||
| setIssuedAt(input) { | ||
| this.#jwt.iat = input; | ||
| return this; | ||
| } | ||
| static decode(jwt, options) { | ||
@@ -53,10 +22,20 @@ if (typeof jwt !== 'string') { | ||
| let header; | ||
| let b64; | ||
| try { | ||
| header = JSON.parse(strictDecoder.decode(b64u.decode(encodedHeader))); | ||
| if (header.alg !== 'none') | ||
| throw new Error(); | ||
| header = parseJoseHeader(encodedHeader, JWSInvalid, 'JWS Protected Header is invalid'); | ||
| const extensions = validateCrit(JWSInvalid, JWS_RECOGNIZED, undefined, header, header); | ||
| b64 = validateB64(header, extensions); | ||
| } | ||
| catch { | ||
| catch (cause) { | ||
| if (!(cause instanceof JWSInvalid)) { | ||
| throw cause; | ||
| } | ||
| throw new JWTInvalid('Invalid Unsecured JWT', { cause }); | ||
| } | ||
| if (header.alg !== 'none') { | ||
| throw new JWTInvalid('Invalid Unsecured JWT'); | ||
| } | ||
| if (!b64) { | ||
| throw new JWTInvalid('JWTs MUST NOT use unencoded payload'); | ||
| } | ||
| const payload = validateClaimsSet(header, decodeBase64url(encodedPayload, 'payload', JWTInvalid), options); | ||
@@ -63,0 +42,0 @@ return { payload, header }; |
| import { JOSENotSupported } from '../util/errors.js'; | ||
| import { validateExtractableOption } from '../lib/key_options.js'; | ||
| import { keyAlgorithm, unsupportedAlg, algArgument } from '../lib/key_algorithm.js'; | ||
| function getModulusLengthOption(options) { | ||
| const modulusLength = options?.modulusLength ?? 2048; | ||
| if (typeof modulusLength !== 'number' || modulusLength < 2048) { | ||
| if (typeof modulusLength !== 'number' || | ||
| !Number.isInteger(modulusLength) || | ||
| modulusLength < 2048) { | ||
| throw new JOSENotSupported('Invalid or unsupported modulusLength option provided, 2048 bits or larger keys must be used'); | ||
@@ -11,2 +14,3 @@ } | ||
| export async function generateKeyPair(alg, options) { | ||
| const extractable = validateExtractableOption(options?.extractable); | ||
| const entry = keyAlgorithm(alg, algArgument); | ||
@@ -45,3 +49,3 @@ if (entry.secret) { | ||
| } | ||
| return crypto.subtle.generateKey(algorithm, options?.extractable ?? false, [ | ||
| return crypto.subtle.generateKey(algorithm, extractable ?? false, [ | ||
| ...entry.usages[1], | ||
@@ -48,0 +52,0 @@ ...entry.usages[0], |
| import { unsupportedAlg, algArgument } from '../lib/key_algorithm.js'; | ||
| import { validateExtractableOption } from '../lib/key_options.js'; | ||
| export async function generateSecret(alg, options) { | ||
| const extractable = validateExtractableOption(options?.extractable); | ||
| let length; | ||
@@ -10,3 +12,3 @@ let algorithm; | ||
| case 'HS512': | ||
| length = parseInt(alg.slice(-3), 10); | ||
| length = +alg.slice(-3); | ||
| algorithm = { name: 'HMAC', hash: `SHA-${length}`, length }; | ||
@@ -18,8 +20,7 @@ keyUsages = ['sign', 'verify']; | ||
| case 'A256CBC-HS512': | ||
| length = parseInt(alg.slice(-3), 10); | ||
| return crypto.getRandomValues(new Uint8Array(length >> 3)); | ||
| return crypto.getRandomValues(new Uint8Array(+alg.slice(-3) >> 3)); | ||
| case 'A128KW': | ||
| case 'A192KW': | ||
| case 'A256KW': | ||
| length = parseInt(alg.slice(1, 4), 10); | ||
| length = +alg.slice(1, 4); | ||
| algorithm = { name: 'AES-KW', length }; | ||
@@ -34,3 +35,3 @@ keyUsages = ['wrapKey', 'unwrapKey']; | ||
| case 'A256GCM': | ||
| length = parseInt(alg.slice(1, 4), 10); | ||
| length = +alg.slice(1, 4); | ||
| algorithm = { name: 'AES-GCM', length }; | ||
@@ -42,3 +43,3 @@ keyUsages = ['encrypt', 'decrypt']; | ||
| } | ||
| return crypto.subtle.generateKey(algorithm, options?.extractable ?? false, keyUsages); | ||
| return crypto.subtle.generateKey(algorithm, extractable ?? false, keyUsages); | ||
| } |
@@ -7,2 +7,4 @@ import { decode as decodeBase64URL } from '../util/base64url.js'; | ||
| import { isObject } from '../lib/type_checks.js'; | ||
| import { validateExtractableOption } from '../lib/key_options.js'; | ||
| import { normalizeJwk } from '../lib/jwk_metadata.js'; | ||
| export async function importSPKI(spki, alg, options) { | ||
@@ -30,27 +32,29 @@ if (typeof spki !== 'string' || spki.indexOf('-----BEGIN PUBLIC KEY-----') !== 0) { | ||
| } | ||
| alg ??= jwk.alg; | ||
| const ext = options?.extractable ?? jwk.ext; | ||
| if (jwk.kty !== 'oct' && !alg) { | ||
| const normalized = normalizeJwk(jwk); | ||
| const extractable = validateExtractableOption(options?.extractable); | ||
| const { alg: jwkAlg } = normalized; | ||
| alg ??= jwkAlg; | ||
| const ext = extractable ?? normalized.ext; | ||
| if (normalized.kty !== 'oct' && !alg) { | ||
| throw new TypeError('"alg" argument is required when "jwk.alg" is not present'); | ||
| } | ||
| switch (jwk.kty) { | ||
| switch (normalized.kty) { | ||
| case 'oct': | ||
| if (typeof jwk.k !== 'string' || !jwk.k) { | ||
| if (typeof normalized.k !== 'string') { | ||
| throw new TypeError('missing "k" (Key Value) Parameter value'); | ||
| } | ||
| return decodeBase64URL(jwk.k); | ||
| case 'RSA': | ||
| return jwkToKey(keyAlgorithm(alg), { ...jwk, alg, ext }); | ||
| return decodeBase64URL(normalized.k); | ||
| case 'AKP': { | ||
| if (typeof jwk.alg !== 'string' || !jwk.alg) { | ||
| if (typeof jwkAlg !== 'string' || !jwkAlg) { | ||
| throw new TypeError('missing "alg" (Algorithm) Parameter value'); | ||
| } | ||
| if (alg !== undefined && alg !== jwk.alg) { | ||
| if (alg !== jwkAlg) { | ||
| throw new TypeError('JWK alg and alg option value mismatch'); | ||
| } | ||
| return jwkToKey(keyAlgorithm(jwk.alg), { ...jwk, ext }); | ||
| return jwkToKey(keyAlgorithm(alg), { ...normalized, ext }); | ||
| } | ||
| case 'RSA': | ||
| case 'EC': | ||
| case 'OKP': | ||
| return jwkToKey(keyAlgorithm(alg), { ...jwk, alg, ext }); | ||
| return jwkToKey(keyAlgorithm(alg), { ...normalized, alg, ext }); | ||
| default: | ||
@@ -57,0 +61,0 @@ throw new JOSENotSupported('Unsupported "kty" (Key Type) Parameter value'); |
@@ -6,2 +6,3 @@ import { invalidKeyInput } from './invalid_key_input.js'; | ||
| import { isCryptoKey, isKeyObject } from './is_key_like.js'; | ||
| import { validateExtractableOption } from './key_options.js'; | ||
| const formatPEM = (b64, descriptor) => { | ||
@@ -116,2 +117,3 @@ const newlined = (b64.match(/.{1,64}/g) || []).join('\n'); | ||
| const genericImport = async (keyFormat, keyData, alg, options) => { | ||
| const extractable = validateExtractableOption(options?.extractable); | ||
| const entry = keyAlgorithm(alg, algArgument); | ||
@@ -136,3 +138,3 @@ if (entry.secret) { | ||
| } | ||
| return crypto.subtle.importKey(keyFormat, keyData, algorithm, options?.extractable ?? isPublic, entry.usages[isPublic ? 0 : 1]); | ||
| return crypto.subtle.importKey(keyFormat, keyData, algorithm, extractable ?? isPublic, entry.usages[isPublic ? 0 : 1]); | ||
| }; | ||
@@ -153,3 +155,6 @@ const processPEMData = (pem, pattern) => { | ||
| expectTag(state, 0x30, 'Invalid certificate structure'); | ||
| parseLength(state); | ||
| const certificateLength = parseLength(state); | ||
| if (certificateLength < 0 || state.pos + certificateLength > state.data.length) { | ||
| throw new Error('Unexpected end of ASN.1 input'); | ||
| } | ||
| expectTag(state, 0x30, 'Invalid tbsCertificate structure'); | ||
@@ -156,0 +161,0 @@ parseLength(state); |
@@ -54,21 +54,10 @@ import { concat, uint64be } from './buffer_utils.js'; | ||
| const expectedTag = await cbcHmacTag(macKey, macData, keySize); | ||
| let macCheckPassed; | ||
| try { | ||
| macCheckPassed = await timingSafeEqual(tag, expectedTag); | ||
| if (await timingSafeEqual(tag, expectedTag)) { | ||
| return new Uint8Array(await crypto.subtle.decrypt({ iv: iv, name: 'AES-CBC' }, encKey, ciphertext)); | ||
| } | ||
| } | ||
| catch { | ||
| } | ||
| if (!macCheckPassed) { | ||
| throw new JWEDecryptionFailed(); | ||
| } | ||
| let plaintext; | ||
| try { | ||
| plaintext = new Uint8Array(await crypto.subtle.decrypt({ iv: iv, name: 'AES-CBC' }, encKey, ciphertext)); | ||
| } | ||
| catch { | ||
| } | ||
| if (!plaintext) { | ||
| throw new JWEDecryptionFailed(); | ||
| } | ||
| return plaintext; | ||
| throw new JWEDecryptionFailed(); | ||
| } | ||
@@ -136,2 +125,5 @@ async function gcmEncrypt(enc, plaintext, cek, iv, aad) { | ||
| } | ||
| if (!enc.cbc && tag.length !== 16) { | ||
| throw new JWEInvalid('Invalid Authentication Tag length'); | ||
| } | ||
| checkIvLength(enc, iv); | ||
@@ -138,0 +130,0 @@ if (cek instanceof Uint8Array) { |
| import { JOSENotSupported, JWEInvalid } from '../util/errors.js'; | ||
| import { concat } from './buffer_utils.js'; | ||
| export function validateZip(joseHeader, protectedHeader) { | ||
| if (joseHeader.zip !== undefined && joseHeader.zip !== 'DEF') { | ||
| throw new JOSENotSupported('Unsupported JWE "zip" (Compression Algorithm) Header Parameter value.'); | ||
| } | ||
| if (joseHeader.zip !== undefined && !protectedHeader?.zip) { | ||
| throw new JWEInvalid('JWE "zip" (Compression Algorithm) Header Parameter MUST be in a protected header.'); | ||
| } | ||
| } | ||
| function supported(name) { | ||
@@ -4,0 +12,0 @@ if (typeof globalThis[name] === 'undefined') { |
@@ -6,3 +6,3 @@ import { decode } from '../util/base64url.js'; | ||
| export function assertNotSet(value, name) { | ||
| if (value) { | ||
| if (value !== undefined) { | ||
| throw new TypeError(`${name} can only be called once`); | ||
@@ -9,0 +9,0 @@ } |
@@ -10,6 +10,6 @@ import { decrypt, generateCek } from './content_encryption.js'; | ||
| import { jweAlgorithm, jweEncryption } from './jwe_algorithms.js'; | ||
| import { decompress } from './deflate.js'; | ||
| export function checkShared(jwe) { | ||
| const { ciphertext, protected: encodedProtected, unprotected } = jwe; | ||
| if (jwe.iv !== undefined && typeof jwe.iv !== 'string') { | ||
| import { decompress, validateZip } from './deflate.js'; | ||
| export function snapshotSharedJWE(jwe) { | ||
| const { aad, ciphertext, iv, protected: encodedProtected, tag, unprotected } = jwe; | ||
| if (iv !== undefined && typeof iv !== 'string') { | ||
| throw new JWEInvalid('JWE Initialization Vector incorrect type'); | ||
@@ -20,3 +20,3 @@ } | ||
| } | ||
| if (jwe.tag !== undefined && typeof jwe.tag !== 'string') { | ||
| if (tag !== undefined && typeof tag !== 'string') { | ||
| throw new JWEInvalid('JWE Authentication Tag incorrect type'); | ||
@@ -27,3 +27,3 @@ } | ||
| } | ||
| if (jwe.aad !== undefined && typeof jwe.aad !== 'string') { | ||
| if (aad !== undefined && (typeof aad !== 'string' || !aad)) { | ||
| throw new JWEInvalid('JWE AAD incorrect type'); | ||
@@ -34,3 +34,41 @@ } | ||
| } | ||
| return { | ||
| aad, | ||
| ciphertext, | ||
| iv, | ||
| protected: encodedProtected, | ||
| tag, | ||
| unprotected: unprotected === undefined ? undefined : { ...unprotected }, | ||
| }; | ||
| } | ||
| export function snapshotRecipientJWE(recipient) { | ||
| let header; | ||
| let headerAlg; | ||
| try { | ||
| const { header: inputHeader } = recipient; | ||
| if (isObject(inputHeader)) { | ||
| headerAlg = inputHeader.alg; | ||
| const parameters = Object.keys(inputHeader); | ||
| if (!parameters.includes('alg')) | ||
| headerAlg = undefined; | ||
| header = Object.fromEntries(parameters.map((parameter) => [ | ||
| parameter, | ||
| parameter === 'alg' ? headerAlg : inputHeader[parameter], | ||
| ])); | ||
| } | ||
| else { | ||
| header = inputHeader; | ||
| } | ||
| } | ||
| catch (error) { | ||
| return [undefined, headerAlg, error]; | ||
| } | ||
| try { | ||
| const { encrypted_key: encryptedKey } = recipient; | ||
| return [{ encrypted_key: encryptedKey, header }, headerAlg]; | ||
| } | ||
| catch (error) { | ||
| return [undefined, headerAlg, error]; | ||
| } | ||
| } | ||
| export function checkRecipient(jwe) { | ||
@@ -41,4 +79,6 @@ const { encrypted_key: encryptedKey, header } = jwe; | ||
| } | ||
| if (header !== undefined && !isObject(header)) { | ||
| throw new JWEInvalid('JWE Per-Recipient Unprotected Header incorrect type'); | ||
| if (header !== undefined) { | ||
| if (!isObject(header)) { | ||
| throw new JWEInvalid('JWE Per-Recipient Unprotected Header incorrect type'); | ||
| } | ||
| } | ||
@@ -52,3 +92,3 @@ if (jwe.protected === undefined && header === undefined && jwe.unprotected === undefined) { | ||
| let parsedProt; | ||
| if (encodedProtected) { | ||
| if (encodedProtected !== undefined) { | ||
| parsedProt = parseJoseHeader(encodedProtected, JWEInvalid, 'JWE Protected Header is invalid'); | ||
@@ -93,9 +133,10 @@ } | ||
| validateAlgorithms('contentEncryptionAlgorithms', options.contentEncryptionAlgorithms), | ||
| options, | ||
| options?.crit, | ||
| options?.maxPBES2Count, | ||
| options?.maxDecompressedLength, | ||
| ]; | ||
| } | ||
| export async function decryptRecipient(jwe, token, shared, key) { | ||
| const [keyManagementAlgorithms, contentEncryptionAlgorithms, options] = shared; | ||
| const [parsedProt, ciphertext, iv, tag, additionalData] = token; | ||
| const { encrypted_key: encodedKey, header, unprotected } = jwe; | ||
| const [parsedProt] = token; | ||
| const { header, unprotected } = jwe; | ||
| let joseHeader; | ||
@@ -111,9 +152,10 @@ if (header !== undefined || unprotected !== undefined) { | ||
| } | ||
| validateCrit(JWEInvalid, JWE_RECOGNIZED, options?.crit, parsedProt, joseHeader); | ||
| if (joseHeader.zip !== undefined && joseHeader.zip !== 'DEF') { | ||
| throw new JOSENotSupported('Unsupported JWE "zip" (Compression Algorithm) Header Parameter value.'); | ||
| } | ||
| if (joseHeader.zip !== undefined && !parsedProt?.zip) { | ||
| throw new JWEInvalid('JWE "zip" (Compression Algorithm) Header Parameter MUST be in a protected header.'); | ||
| } | ||
| return decryptRecipientCore(jwe, token, shared, key, joseHeader); | ||
| } | ||
| async function decryptRecipientCore(jwe, token, shared, key, joseHeader) { | ||
| const [keyManagementAlgorithms, contentEncryptionAlgorithms, crit, maxPBES2Count, maxDecompressedLength,] = shared; | ||
| const [parsedProt, ciphertext, iv, tag, additionalData] = token; | ||
| const { encrypted_key: encodedKey } = jwe; | ||
| validateCrit(JWEInvalid, JWE_RECOGNIZED, crit, parsedProt, joseHeader); | ||
| validateZip(joseHeader, parsedProt); | ||
| const { alg, enc } = joseHeader; | ||
@@ -147,3 +189,8 @@ if (typeof alg !== 'string' || !alg) { | ||
| try { | ||
| cek = await decryptKeyManagement(alg, encEntry, k, encryptedKey, joseHeader, options); | ||
| cek = await decryptKeyManagement(alg, encEntry, k, encryptedKey, joseHeader, maxPBES2Count); | ||
| if (encodedKey !== undefined && | ||
| cek instanceof Uint8Array && | ||
| cek.byteLength << 3 !== encEntry.cekBits) { | ||
| cek = generateCek(encEntry); | ||
| } | ||
| } | ||
@@ -158,11 +205,11 @@ catch (err) { | ||
| if (joseHeader.zip === 'DEF') { | ||
| const maxDecompressedLength = options?.maxDecompressedLength ?? 250_000; | ||
| if (maxDecompressedLength === 0) { | ||
| const decompressionLimit = maxDecompressedLength ?? 250_000; | ||
| if (decompressionLimit === 0) { | ||
| throw new JOSENotSupported('JWE "zip" (Compression Algorithm) Header Parameter is not supported.'); | ||
| } | ||
| if (maxDecompressedLength !== Infinity && | ||
| (!Number.isSafeInteger(maxDecompressedLength) || maxDecompressedLength < 1)) { | ||
| if (decompressionLimit !== Infinity && | ||
| (!Number.isSafeInteger(decompressionLimit) || decompressionLimit < 1)) { | ||
| throw new TypeError('maxDecompressedLength must be 0, a positive safe integer, or Infinity'); | ||
| } | ||
| plaintext = await decompress(plaintext, maxDecompressedLength).catch((cause) => { | ||
| plaintext = await decompress(plaintext, decompressionLimit).catch((cause) => { | ||
| if (cause instanceof JWEInvalid) | ||
@@ -189,3 +236,3 @@ throw cause; | ||
| } | ||
| return decryptJWE({ | ||
| const flattened = { | ||
| ciphertext, | ||
@@ -196,3 +243,13 @@ iv: iv || undefined, | ||
| encrypted_key: encryptedKey || undefined, | ||
| }, shared, key); | ||
| }; | ||
| const parsedProt = parseJoseHeader(protectedHeader, JWEInvalid, 'JWE Protected Header is invalid'); | ||
| const protectedBytes = encode(protectedHeader); | ||
| const token = [ | ||
| parsedProt, | ||
| decodeBase64url(ciphertext, 'ciphertext', JWEInvalid), | ||
| iv ? decodeBase64url(iv, 'iv', JWEInvalid) : undefined, | ||
| tag ? decodeBase64url(tag, 'tag', JWEInvalid) : undefined, | ||
| protectedBytes, | ||
| ]; | ||
| return decryptRecipientCore(flattened, token, shared, key, parsedProt); | ||
| } |
| import { encode as b64u } from '../util/base64url.js'; | ||
| import { encrypt } from './content_encryption.js'; | ||
| import { encryptKeyManagement } from './key_management.js'; | ||
| import { JOSENotSupported, JWEInvalid } from '../util/errors.js'; | ||
| import { isDisjoint } from './type_checks.js'; | ||
| import { JWEInvalid } from '../util/errors.js'; | ||
| import { assertUint8Array, isDisjoint, isObject } from './type_checks.js'; | ||
| import { concat, encode } from './buffer_utils.js'; | ||
| import { validateCrit, JWE_RECOGNIZED } from './options.js'; | ||
| import { serializeJoseHeader, validateCrit, validateCritDuplicates, JWE_RECOGNIZED, } from './options.js'; | ||
| import { prepareKey } from './key.js'; | ||
| import { jweAlgorithm, jweEncryption } from './jwe_algorithms.js'; | ||
| import { compress } from './deflate.js'; | ||
| import { compress, validateZip } from './deflate.js'; | ||
| import { unprotected } from './helpers.js'; | ||
| export function checkDisjoint(protectedHeader, unprotectedHeader, sharedUnprotectedHeader) { | ||
@@ -17,3 +18,27 @@ if (!isDisjoint(protectedHeader, unprotectedHeader, sharedUnprotectedHeader)) { | ||
| export function checkEncryptHeaders(input) { | ||
| const [, protectedHeader, unprotectedHeader, sharedUnprotectedHeader, , , , , crit] = input; | ||
| let [, protectedHeader, unprotectedHeader, sharedUnprotectedHeader, aad, cek, iv, keyManagementParameters, crit,] = input; | ||
| if (aad !== undefined) { | ||
| assertUint8Array(aad, 'JWE Additional Authenticated Data'); | ||
| } | ||
| if (cek !== undefined) { | ||
| assertUint8Array(cek, 'JWE Content Encryption Key'); | ||
| } | ||
| if (iv !== undefined) { | ||
| assertUint8Array(iv, 'JWE Initialization Vector'); | ||
| } | ||
| if (protectedHeader !== undefined) { | ||
| protectedHeader = serializeJoseHeader(JWEInvalid, protectedHeader)[0]; | ||
| input[1] = protectedHeader; | ||
| } | ||
| if (unprotectedHeader !== undefined) { | ||
| unprotectedHeader = serializeJoseHeader(JWEInvalid, unprotectedHeader)[0]; | ||
| input[2] = unprotectedHeader; | ||
| } | ||
| if (sharedUnprotectedHeader !== undefined) { | ||
| sharedUnprotectedHeader = serializeJoseHeader(JWEInvalid, sharedUnprotectedHeader)[0]; | ||
| input[3] = sharedUnprotectedHeader; | ||
| } | ||
| if (keyManagementParameters !== undefined && !isObject(keyManagementParameters)) { | ||
| throw new TypeError('JWE Key Management Parameters must be an object'); | ||
| } | ||
| checkDisjoint(protectedHeader, unprotectedHeader, sharedUnprotectedHeader); | ||
@@ -25,9 +50,5 @@ const joseHeader = { | ||
| }; | ||
| validateCritDuplicates(JWEInvalid, protectedHeader); | ||
| validateCrit(JWEInvalid, JWE_RECOGNIZED, crit, protectedHeader, joseHeader); | ||
| if (joseHeader.zip !== undefined && joseHeader.zip !== 'DEF') { | ||
| throw new JOSENotSupported('Unsupported JWE "zip" (Compression Algorithm) Header Parameter value.'); | ||
| } | ||
| if (joseHeader.zip !== undefined && !protectedHeader?.zip) { | ||
| throw new JWEInvalid('JWE "zip" (Compression Algorithm) Header Parameter MUST be in a protected header.'); | ||
| } | ||
| validateZip(joseHeader, protectedHeader); | ||
| const { alg, enc } = joseHeader; | ||
@@ -114,4 +135,11 @@ if (typeof alg !== 'string' || !alg) { | ||
| } | ||
| export async function createJWE(input, key) { | ||
| export async function createJWE(input, key, options) { | ||
| if (!input[1] && !input[2] && !input[3]) { | ||
| throw new JWEInvalid('either setProtectedHeader, setUnprotectedHeader, or sharedUnprotectedHeader must be called before #encrypt()'); | ||
| } | ||
| if (options !== undefined) { | ||
| input[8] = options?.crit; | ||
| input[9] = options ? unprotected in options : false; | ||
| } | ||
| return encryptJWE(input, checkEncryptHeaders(input), key); | ||
| } |
@@ -7,11 +7,33 @@ import { encode as b64u } from '../util/base64url.js'; | ||
| import { concat, encode } from './buffer_utils.js'; | ||
| import { validateCrit, validateCritDuplicates, JWS_RECOGNIZED } from './options.js'; | ||
| import { serializeJoseHeader, validateB64, validateCrit, validateCritDuplicates, JWS_RECOGNIZED, } from './options.js'; | ||
| import { prepareKey } from './key.js'; | ||
| export function unencodedPayload(protectedHeader) { | ||
| return (protectedHeader?.b64 === false && | ||
| Array.isArray(protectedHeader.crit) && | ||
| protectedHeader.crit.includes('b64')); | ||
| function serializeProtectedHeader(protectedHeader) { | ||
| if (protectedHeader === undefined) | ||
| return [undefined, '']; | ||
| const normalized = serializeJoseHeader(JWSInvalid, protectedHeader); | ||
| return [normalized[0], b64u(normalized[1])]; | ||
| } | ||
| export async function createSignature(input, key) { | ||
| const { protectedHeader, unprotectedHeader } = input; | ||
| function validateSignatureHeader(protectedHeader, joseHeader, crit) { | ||
| validateCritDuplicates(JWSInvalid, protectedHeader); | ||
| return validateB64(protectedHeader, validateCrit(JWSInvalid, JWS_RECOGNIZED, crit, protectedHeader, joseHeader)); | ||
| } | ||
| function signatureAlgorithm(joseHeader) { | ||
| const alg = joseHeader.alg; | ||
| if (typeof alg !== 'string' || !alg) { | ||
| throw new JWSInvalid('JWS "alg" (Algorithm) Header Parameter missing or invalid'); | ||
| } | ||
| return jwsAlgorithm(alg); | ||
| } | ||
| async function signSignature(protectedHeader, payload, entry, key) { | ||
| const data = concat(encode(protectedHeader), encode('.'), payload); | ||
| const k = await prepareKey(entry, key, 'sign'); | ||
| return b64u(await sign(entry, k, data)); | ||
| } | ||
| export async function createSignature(input, key, assertB64) { | ||
| let { protectedHeader, unprotectedHeader } = input; | ||
| let protectedHeaderString; | ||
| [protectedHeader, protectedHeaderString] = serializeProtectedHeader(protectedHeader); | ||
| if (unprotectedHeader !== undefined) { | ||
| unprotectedHeader = serializeJoseHeader(JWSInvalid, unprotectedHeader)[0]; | ||
| } | ||
| if (!protectedHeader && !unprotectedHeader) { | ||
@@ -24,16 +46,5 @@ throw new JWSInvalid('either setProtectedHeader or setUnprotectedHeader must be called before #sign()'); | ||
| const joseHeader = { ...protectedHeader, ...unprotectedHeader }; | ||
| validateCritDuplicates(JWSInvalid, protectedHeader); | ||
| const extensions = validateCrit(JWSInvalid, JWS_RECOGNIZED, input.crit, protectedHeader, joseHeader); | ||
| let b64 = true; | ||
| if (extensions.includes('b64')) { | ||
| b64 = protectedHeader.b64; | ||
| if (typeof b64 !== 'boolean') { | ||
| throw new JWSInvalid('The "b64" (base64url-encode payload) Header Parameter must be a boolean'); | ||
| } | ||
| } | ||
| const { alg } = joseHeader; | ||
| if (typeof alg !== 'string' || !alg) { | ||
| throw new JWSInvalid('JWS "alg" (Algorithm) Header Parameter missing or invalid'); | ||
| } | ||
| const entry = jwsAlgorithm(alg); | ||
| const b64 = validateSignatureHeader(protectedHeader, joseHeader, input.crit); | ||
| assertB64?.(b64); | ||
| const entry = signatureAlgorithm(joseHeader); | ||
| let payloadS; | ||
@@ -52,17 +63,4 @@ let payloadB; | ||
| } | ||
| let protectedHeaderString; | ||
| let protectedHeaderBytes; | ||
| if (protectedHeader) { | ||
| protectedHeaderString = b64u(JSON.stringify(protectedHeader)); | ||
| protectedHeaderBytes = encode(protectedHeaderString); | ||
| } | ||
| else { | ||
| protectedHeaderString = ''; | ||
| protectedHeaderBytes = new Uint8Array(); | ||
| } | ||
| const data = concat(protectedHeaderBytes, encode('.'), payloadB); | ||
| const k = await prepareKey(entry, key, 'sign'); | ||
| const signature = await sign(entry, k, data); | ||
| const jws = { | ||
| signature: b64u(signature), | ||
| signature: await signSignature(protectedHeaderString, payloadB, entry, key), | ||
| payload: payloadS, | ||
@@ -76,3 +74,16 @@ }; | ||
| } | ||
| return jws; | ||
| return [jws, b64]; | ||
| } | ||
| export async function createCompactSignature(payload, inputProtectedHeader, inputCrit, key, rejectUnencoded) { | ||
| const [protectedHeader, protectedHeaderString] = serializeProtectedHeader(inputProtectedHeader); | ||
| if (!protectedHeader) { | ||
| throw new JWSInvalid('either setProtectedHeader or setUnprotectedHeader must be called before #sign()'); | ||
| } | ||
| const b64 = validateSignatureHeader(protectedHeader, protectedHeader, inputCrit); | ||
| if (!b64) | ||
| rejectUnencoded(); | ||
| const entry = signatureAlgorithm(protectedHeader); | ||
| const encodedPayload = b64u(payload); | ||
| const signature = await signSignature(protectedHeaderString, encode(encodedPayload), entry, key); | ||
| return `${protectedHeaderString}.${encodedPayload}.${signature}`; | ||
| } |
@@ -6,5 +6,21 @@ import { verify } from './signing.js'; | ||
| import { decodeBase64url, encodeBase64url, parseJoseHeader } from './helpers.js'; | ||
| import { isDisjoint } from './type_checks.js'; | ||
| import { validateCrit, validateAlgorithms, JWS_RECOGNIZED } from './options.js'; | ||
| import { isDisjoint, isObject } from './type_checks.js'; | ||
| import { validateB64, validateCrit, validateAlgorithms, JWS_RECOGNIZED } from './options.js'; | ||
| import { prepareKey } from './key.js'; | ||
| export function snapshotJws(jws, sharedPayload) { | ||
| const encodedProtected = jws.protected; | ||
| const inputHeader = jws.header; | ||
| const header = isObject(inputHeader) ? { ...inputHeader } : inputHeader; | ||
| let payload = sharedPayload ? sharedPayload[0] : jws.payload; | ||
| if (!sharedPayload && payload instanceof Uint8Array) { | ||
| payload = new Uint8Array(payload); | ||
| } | ||
| const signature = jws.signature; | ||
| const snapshot = { payload, signature }; | ||
| if (encodedProtected !== undefined) | ||
| snapshot.protected = encodedProtected; | ||
| if (inputHeader !== undefined) | ||
| snapshot.header = header; | ||
| return snapshot; | ||
| } | ||
| export function verifyResult(jws, verified) { | ||
@@ -27,8 +43,20 @@ const [payload, parsedProt, , key, resolvedKey] = verified; | ||
| } | ||
| export async function verifySignature(jws, shared, key) { | ||
| const { protected: encodedProtected, header, payload: inputPayload } = jws; | ||
| let parsedProt = {}; | ||
| if (encodedProtected) { | ||
| parsedProt = parseJoseHeader(encodedProtected, JWSInvalid, 'JWS Protected Header is invalid'); | ||
| export function parseProtectedHeader(encodedProtected, parsedProtected = encodedProtected === undefined | ||
| ? {} | ||
| : parseJoseHeader(encodedProtected, JWSInvalid, 'JWS Protected Header is invalid')) { | ||
| return parsedProtected; | ||
| } | ||
| function validateJwsHeaders(parsedProt, joseHeader, shared) { | ||
| const b64 = validateB64(parsedProt, validateCrit(JWSInvalid, JWS_RECOGNIZED, shared[1], parsedProt, joseHeader)); | ||
| const alg = joseHeader.alg; | ||
| if (typeof alg !== 'string' || !alg) { | ||
| throw new JWSInvalid('JWS "alg" (Algorithm) Header Parameter missing or invalid'); | ||
| } | ||
| if (shared[0] && !shared[0].has(alg)) { | ||
| throw new JOSEAlgNotAllowed('"alg" (Algorithm) Header Parameter value not allowed'); | ||
| } | ||
| return [b64, alg]; | ||
| } | ||
| export function parseJwsHeaders(encodedProtected, header, shared, parsedProtected) { | ||
| const parsedProt = parseProtectedHeader(encodedProtected, parsedProtected); | ||
| let joseHeader; | ||
@@ -44,25 +72,22 @@ if (header !== undefined) { | ||
| } | ||
| const extensions = validateCrit(JWSInvalid, JWS_RECOGNIZED, shared[1], parsedProt, joseHeader); | ||
| let b64 = true; | ||
| if (extensions.includes('b64')) { | ||
| b64 = parsedProt.b64; | ||
| if (typeof b64 !== 'boolean') { | ||
| throw new JWSInvalid('The "b64" (base64url-encode payload) Header Parameter must be a boolean'); | ||
| } | ||
| return [parsedProt, joseHeader, ...validateJwsHeaders(parsedProt, joseHeader, shared)]; | ||
| } | ||
| export function encodeJsonUnencodedPayload(payload) { | ||
| const invalid = /[\p{Cs}\p{Cn}]/u.exec(payload)?.[0]; | ||
| if (invalid !== undefined) { | ||
| throw new JWSInvalid(/\p{Cs}/u.test(invalid) | ||
| ? 'JWS Payload must be a well-formed Unicode string' | ||
| : 'JWS Payload must not contain unassigned Unicode code points'); | ||
| } | ||
| const { alg } = joseHeader; | ||
| if (typeof alg !== 'string' || !alg) { | ||
| throw new JWSInvalid('JWS "alg" (Algorithm) Header Parameter missing or invalid'); | ||
| return encoder.encode(payload); | ||
| } | ||
| function encodeCompactUnencodedPayload(payload) { | ||
| try { | ||
| return encode(payload); | ||
| } | ||
| if (shared[0] && !shared[0].has(alg)) { | ||
| throw new JOSEAlgNotAllowed('"alg" (Algorithm) Header Parameter value not allowed'); | ||
| catch { | ||
| throw new JWSInvalid('JWS Compact Serialization payload must use only ASCII characters'); | ||
| } | ||
| if (b64) { | ||
| if (typeof inputPayload !== 'string') { | ||
| throw new JWSInvalid('JWS Payload must be a string'); | ||
| } | ||
| } | ||
| else if (typeof inputPayload !== 'string' && !(inputPayload instanceof Uint8Array)) { | ||
| throw new JWSInvalid('JWS Payload must be a string or an Uint8Array instance'); | ||
| } | ||
| } | ||
| async function verifyPrepared(jws, shared, key, encodedProtected, parsedProt, alg, signingPayload) { | ||
| let resolvedKey = false; | ||
@@ -73,26 +98,29 @@ if (typeof key === 'function') { | ||
| } | ||
| const b64 = typeof signingPayload === 'string'; | ||
| const entry = jwsAlgorithm(alg); | ||
| const data = concat(encodedProtected !== undefined ? encode(encodedProtected) : new Uint8Array(), encode('.'), typeof inputPayload === 'string' | ||
| ? b64 | ||
| ? | ||
| (shared[2] ??= encodeBase64url(inputPayload, 'payload', JWSInvalid)) | ||
| : encoder.encode(inputPayload) | ||
| : inputPayload); | ||
| const data = concat(encodedProtected !== undefined ? encode(encodedProtected) : new Uint8Array(), encode('.'), b64 | ||
| ? | ||
| (shared[2] ??= encodeBase64url(signingPayload, 'payload', JWSInvalid)) | ||
| : signingPayload); | ||
| const signature = decodeBase64url(jws.signature, 'signature', JWSInvalid); | ||
| const k = await prepareKey(entry, key, 'verify'); | ||
| const verified = await verify(entry, k, signature, data); | ||
| if (!verified) { | ||
| if (!(await verify(entry, k, signature, data))) { | ||
| throw new JWSSignatureVerificationFailed(); | ||
| } | ||
| let payload; | ||
| const payload = b64 ? decodeBase64url(signingPayload, 'payload', JWSInvalid) : signingPayload; | ||
| return [payload, parsedProt, b64, k, resolvedKey]; | ||
| } | ||
| export async function verifySignature(jws, shared, key, encodeUnencodedPayload, parsedProtected) { | ||
| const { protected: encodedProtected, header, payload: inputPayload } = jws; | ||
| const [parsedProt, , b64, alg] = parseJwsHeaders(encodedProtected, header, shared, parsedProtected); | ||
| if (b64) { | ||
| payload = decodeBase64url(inputPayload, 'payload', JWSInvalid); | ||
| if (typeof inputPayload !== 'string') { | ||
| throw new JWSInvalid('JWS Payload must be a string'); | ||
| } | ||
| } | ||
| else if (typeof inputPayload === 'string') { | ||
| payload = encoder.encode(inputPayload); | ||
| else if (typeof inputPayload !== 'string' && !(inputPayload instanceof Uint8Array)) { | ||
| throw new JWSInvalid('JWS Payload must be a string or an Uint8Array instance'); | ||
| } | ||
| else { | ||
| payload = inputPayload; | ||
| } | ||
| return [payload, parsedProt, b64, k, resolvedKey]; | ||
| const signingPayload = b64 || typeof inputPayload !== 'string' ? inputPayload : encodeUnencodedPayload(inputPayload); | ||
| return verifyPrepared(jws, shared, key, encodedProtected, parsedProt, alg, signingPayload); | ||
| } | ||
@@ -110,3 +138,7 @@ export async function verifyCompact(jws, shared, key) { | ||
| } | ||
| return verifySignature({ payload, protected: protectedHeader, signature }, shared, key); | ||
| const compactJws = { payload, protected: protectedHeader, signature }; | ||
| const parsedProt = parseProtectedHeader(protectedHeader); | ||
| const [b64, alg] = validateJwsHeaders(parsedProt, parsedProt, shared); | ||
| const signingPayload = b64 ? payload : encodeCompactUnencodedPayload(payload); | ||
| return verifyPrepared(compactJws, shared, key, protectedHeader, parsedProt, alg, signingPayload); | ||
| } |
@@ -15,9 +15,18 @@ import { JWTClaimValidationFailed, JWTExpired, JWTInvalid } from '../util/errors.js'; | ||
| const checkFailed = 'check_failed'; | ||
| function invalidDuration() { | ||
| throw new TypeError('Invalid time period format'); | ||
| } | ||
| export function secs(str) { | ||
| if (typeof str !== 'string') { | ||
| invalidDuration(); | ||
| } | ||
| const matched = REGEX.exec(str); | ||
| if (!matched || (matched[4] && matched[1])) { | ||
| throw new TypeError('Invalid time period format'); | ||
| invalidDuration(); | ||
| } | ||
| const value = parseFloat(matched[2]); | ||
| const numericDate = Math.round(value * multipliers[matched[3][0].toLowerCase()]); | ||
| if (!Number.isFinite(numericDate)) { | ||
| invalidDuration(); | ||
| } | ||
| if (matched[1] === '-' || matched[4] === 'ago') { | ||
@@ -34,2 +43,13 @@ return -numericDate; | ||
| } | ||
| function validateStringClaim(claim, value) { | ||
| if (typeof value !== 'string') { | ||
| throw new TypeError(`"${claim}" claim must be a string`); | ||
| } | ||
| } | ||
| function validateAudienceClaim(value) { | ||
| if (typeof value !== 'string' && | ||
| (!Array.isArray(value) || Array.from(value).some((member) => typeof member !== 'string'))) { | ||
| throw new TypeError('"aud" claim must be a string or an array of strings'); | ||
| } | ||
| } | ||
| function numericDate(value, label) { | ||
@@ -43,6 +63,4 @@ if (typeof value === 'number') | ||
| const normalizeTyp = (value) => { | ||
| if (value.includes('/')) { | ||
| return value.toLowerCase(); | ||
| } | ||
| return `application/${value.toLowerCase()}`; | ||
| const normalized = value.toLowerCase(); | ||
| return value.includes('/') ? normalized : `application/${normalized}`; | ||
| }; | ||
@@ -81,3 +99,3 @@ const checkAudiencePresence = (audPayload, audOption) => { | ||
| const { typ } = options; | ||
| if (typ && | ||
| if (typ !== undefined && | ||
| (typeof protectedHeader.typ !== 'string' || | ||
@@ -126,3 +144,3 @@ normalizeTyp(protectedHeader.typ) !== normalizeTyp(typ))) { | ||
| const { currentDate } = options; | ||
| const now = validateInput('currentDate option', epoch(currentDate || new Date())); | ||
| const now = validateInput('currentDate option', epoch(currentDate === undefined ? new Date() : currentDate)); | ||
| const iat = validateNumericDate(payload, 'iat', maxTokenAge !== undefined); | ||
@@ -143,7 +161,7 @@ const nbf = validateNumericDate(payload, 'nbf'); | ||
| const age = now - iat; | ||
| const max = typeof maxTokenAge === 'number' ? maxTokenAge : secs(maxTokenAge); | ||
| const max = validateInput('maxTokenAge option', 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', checkFailed); | ||
| } | ||
| if (age < 0 - tolerance) { | ||
| if (age < -tolerance) { | ||
| throw new JWTClaimValidationFailed('"iat" claim timestamp check failed (it should be in the past)', payload, 'iat', checkFailed); | ||
@@ -154,51 +172,68 @@ } | ||
| } | ||
| let producerPayloads; | ||
| function producerPayload(producer) { | ||
| return producerPayloads.get(producer); | ||
| } | ||
| export function jwtData(producer) { | ||
| const payload = producerPayload(producer); | ||
| for (const claim of ['iat', 'nbf', 'exp']) { | ||
| const value = payload[claim]; | ||
| if (typeof value === 'number' && !Number.isFinite(value)) { | ||
| throw new TypeError(`"${claim}" claim must be a finite number`); | ||
| } | ||
| } | ||
| return encoder.encode(JSON.stringify(payload)); | ||
| } | ||
| export function jwtClaim(producer, claim) { | ||
| return producerPayload(producer)[claim]; | ||
| } | ||
| export class JWTClaimsBuilder { | ||
| #payload; | ||
| constructor(payload) { | ||
| constructor(payload = {}) { | ||
| if (!isObject(payload)) { | ||
| throw new TypeError('JWT Claims Set MUST be an object'); | ||
| } | ||
| this.#payload = structuredClone(payload); | ||
| ; | ||
| (producerPayloads ||= new WeakMap()).set(this, structuredClone(payload)); | ||
| } | ||
| data() { | ||
| return encoder.encode(JSON.stringify(this.#payload)); | ||
| setIssuer(value) { | ||
| validateStringClaim('iss', value); | ||
| producerPayload(this).iss = value; | ||
| return this; | ||
| } | ||
| get iss() { | ||
| return this.#payload.iss; | ||
| setSubject(value) { | ||
| validateStringClaim('sub', value); | ||
| producerPayload(this).sub = value; | ||
| return this; | ||
| } | ||
| set iss(value) { | ||
| this.#payload.iss = value; | ||
| setAudience(value) { | ||
| validateAudienceClaim(value); | ||
| producerPayload(this).aud = value; | ||
| return this; | ||
| } | ||
| get sub() { | ||
| return this.#payload.sub; | ||
| setJti(value) { | ||
| validateStringClaim('jti', value); | ||
| producerPayload(this).jti = value; | ||
| return this; | ||
| } | ||
| set sub(value) { | ||
| this.#payload.sub = value; | ||
| setNotBefore(value) { | ||
| producerPayload(this).nbf = numericDate(value, 'setNotBefore'); | ||
| return this; | ||
| } | ||
| get aud() { | ||
| return this.#payload.aud; | ||
| setExpirationTime(value) { | ||
| producerPayload(this).exp = numericDate(value, 'setExpirationTime'); | ||
| return this; | ||
| } | ||
| set aud(value) { | ||
| this.#payload.aud = value; | ||
| } | ||
| set jti(value) { | ||
| this.#payload.jti = value; | ||
| } | ||
| set nbf(value) { | ||
| this.#payload.nbf = numericDate(value, 'setNotBefore'); | ||
| } | ||
| set exp(value) { | ||
| this.#payload.exp = numericDate(value, 'setExpirationTime'); | ||
| } | ||
| set iat(value) { | ||
| setIssuedAt(value) { | ||
| const payload = producerPayload(this); | ||
| if (value === undefined) { | ||
| this.#payload.iat = epoch(new Date()); | ||
| payload.iat = epoch(new Date()); | ||
| } | ||
| else if (typeof value === 'string') { | ||
| this.#payload.iat = validateInput('setIssuedAt', epoch(new Date()) + secs(value)); | ||
| payload.iat = validateInput('setIssuedAt', epoch(new Date()) + secs(value)); | ||
| } | ||
| else { | ||
| this.#payload.iat = numericDate(value, 'setIssuedAt'); | ||
| payload.iat = numericDate(value, 'setIssuedAt'); | ||
| } | ||
| return this; | ||
| } | ||
| } |
@@ -8,3 +8,3 @@ import { encode as b64u } from '../util/base64url.js'; | ||
| import { generateCek, encrypt, decrypt } from './content_encryption.js'; | ||
| import { isObject } from './type_checks.js'; | ||
| import { assertUint8Array, isObject } from './type_checks.js'; | ||
| import { checkCryptoKey, checkModulusLength, checkUsage } from './crypto_key.js'; | ||
@@ -114,3 +114,3 @@ import { concat, encode, uint32be } from './buffer_utils.js'; | ||
| } | ||
| export async function decryptKeyManagement(alg, enc, key, encryptedKey, joseHeader, options) { | ||
| export async function decryptKeyManagement(alg, enc, key, encryptedKey, joseHeader, maxPBES2Count) { | ||
| const entry = jweAlgorithm(alg); | ||
@@ -157,3 +157,3 @@ if (alg === 'dir') { | ||
| throw new JWEInvalid(`JOSE Header "p2c" (PBES2 Count) missing or invalid`); | ||
| const p2cLimit = options?.maxPBES2Count || 10_000; | ||
| const p2cLimit = maxPBES2Count || 10_000; | ||
| if (joseHeader.p2c > p2cLimit) | ||
@@ -196,4 +196,10 @@ throw new JWEInvalid(`JOSE Header "p2c" (PBES2 Count) out is of acceptable bounds`); | ||
| const { apu, apv } = providedParameters; | ||
| if (apu !== undefined) { | ||
| assertUint8Array(apu, '"apu"'); | ||
| } | ||
| if (apv !== undefined) { | ||
| assertUint8Array(apv, '"apv"'); | ||
| } | ||
| let ephemeralKey; | ||
| if (providedParameters.epk) { | ||
| if (providedParameters.epk !== undefined) { | ||
| ephemeralKey = (await prepareKey(entry, providedParameters.epk, 'decrypt')); | ||
@@ -200,0 +206,0 @@ } |
| import { withAlg as invalidKeyInput } from './invalid_key_input.js'; | ||
| import { isKeyLike, isCryptoKey } from './is_key_like.js'; | ||
| import * as jwk from './type_checks.js'; | ||
| import { isObject } from './type_checks.js'; | ||
| import { decode } from '../util/base64url.js'; | ||
| import { jwkToKey } from './jwk_to_key.js'; | ||
| import { normalizeJwk } from './jwk_metadata.js'; | ||
| const tag = (key) => key[Symbol.toStringTag]; | ||
@@ -30,10 +31,23 @@ const jwkMatchesOp = (entry, key, usage) => { | ||
| return [BYTES, key]; | ||
| if (jwk.isJWK(key)) { | ||
| if (secret ? !jwk.isSecretJWK(key) : !(privateKey ? jwk.isPrivateJWK(key) : jwk.isPublicJWK(key))) { | ||
| if (isObject(key)) { | ||
| const normalized = normalizeJwk(key); | ||
| if (typeof normalized.kty !== 'string') { | ||
| throw new TypeError(secret | ||
| ? invalidKeyInput(alg, key, 'CryptoKey', 'KeyObject', 'JSON Web Key', 'Uint8Array') | ||
| : invalidKeyInput(alg, key, 'CryptoKey', 'KeyObject', 'JSON Web Key')); | ||
| } | ||
| const valid = secret | ||
| ? normalized.kty === 'oct' && typeof normalized.k === 'string' | ||
| : normalized.kty !== 'oct' && | ||
| (privateKey | ||
| ? (normalized.kty === 'AKP' && typeof normalized.priv === 'string') || | ||
| typeof normalized.d === 'string' | ||
| : normalized.d === undefined && normalized.priv === undefined); | ||
| if (!valid) { | ||
| 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]; | ||
| jwkMatchesOp(entry, normalized, usage); | ||
| return [JWK, key, normalized]; | ||
| } | ||
@@ -85,3 +99,3 @@ if (!isKeyLike(key)) { | ||
| else { | ||
| cache.set(key, { __proto__: null, [alg]: value }); | ||
| cache.set(key, { [alg]: value }); | ||
| } | ||
@@ -112,4 +126,5 @@ } | ||
| const key = tagged[1]; | ||
| if (key.k) { | ||
| return decode(key.k); | ||
| const normalized = tagged[2]; | ||
| if (normalized.kty === 'oct') { | ||
| return decode(normalized.k); | ||
| } | ||
@@ -122,3 +137,3 @@ if (!Object.isFrozen(key)) { | ||
| } | ||
| return handleJWK(key, key, entry); | ||
| return handleJWK(key, normalized, entry); | ||
| } | ||
@@ -125,0 +140,0 @@ case KEYOBJECT: { |
| import { JOSENotSupported, JWEInvalid, JWSInvalid } from '../util/errors.js'; | ||
| import { isObject } from './type_checks.js'; | ||
| export const JWS_RECOGNIZED = { __proto__: null, b64: true }; | ||
@@ -49,1 +50,26 @@ export const JWE_RECOGNIZED = { __proto__: null }; | ||
| } | ||
| export function validateB64(protectedHeader, extensions) { | ||
| if (extensions.includes('b64')) { | ||
| const b64 = protectedHeader.b64; | ||
| if (typeof b64 !== 'boolean') { | ||
| throw new JWSInvalid('The "b64" (base64url-encode payload) Header Parameter must be a boolean'); | ||
| } | ||
| return b64; | ||
| } | ||
| return true; | ||
| } | ||
| export function serializeJoseHeader(Err, header) { | ||
| let serialized; | ||
| let parsed; | ||
| try { | ||
| serialized = JSON.stringify(header); | ||
| parsed = JSON.parse(serialized); | ||
| } | ||
| catch (cause) { | ||
| throw new Err('JOSE Header is not valid JSON', { cause }); | ||
| } | ||
| if (!isObject(parsed)) { | ||
| throw new Err('JOSE Header is not a JSON object'); | ||
| } | ||
| return [parsed, serialized]; | ||
| } |
@@ -0,1 +1,6 @@ | ||
| export function assertUint8Array(input, label) { | ||
| if (!(input instanceof Uint8Array)) { | ||
| throw new TypeError(`${label} must be an instance of Uint8Array`); | ||
| } | ||
| } | ||
| export function isObject(input) { | ||
@@ -8,11 +13,9 @@ if (typeof input !== 'object' || | ||
| const prototype = Object.getPrototypeOf(input); | ||
| if (prototype === null) { | ||
| return true; | ||
| } | ||
| let proto = prototype; | ||
| while (Object.getPrototypeOf(proto) !== null) { | ||
| proto = Object.getPrototypeOf(proto); | ||
| } | ||
| return prototype === proto; | ||
| return prototype === null || Object.getPrototypeOf(prototype) === null; | ||
| } | ||
| export function isJwkSet(input) { | ||
| return (isObject(input) && | ||
| Array.isArray(input.keys) && | ||
| Array.from(input.keys).every(isObject)); | ||
| } | ||
| export function isDisjoint(...headers) { | ||
@@ -32,6 +35,1 @@ const parameters = new Set(); | ||
| } | ||
| export const isJWK = (key) => isObject(key) && typeof key.kty === 'string'; | ||
| export const isPrivateJWK = (key) => key.kty !== 'oct' && | ||
| ((key.kty === 'AKP' && typeof key.priv === 'string') || typeof key.d === 'string'); | ||
| export const isPublicJWK = (key) => key.kty !== 'oct' && key.d === undefined && key.priv === undefined; | ||
| export const isSecretJWK = (key) => key.kty === 'oct' && typeof key.k === 'string'; |
+1
-1
| { | ||
| "name": "jose", | ||
| "version": "6.2.9", | ||
| "version": "6.2.10", | ||
| "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": [ |
258772
4.42%90
2.27%5834
3.49%