UNPKG

jose

Version:

JWA, JWS, JWE, JWT, JWK, JWKS for Node.js, Browser, Cloudflare Workers, Deno, Bun, and other Web-interoperable runtimes

244 lines (243 loc) 10.1 kB
import { decrypt, generateCek } from './content_encryption.js'; import { decodeBase64url, encodeBase64url, parseJoseHeader } from './helpers.js'; import { JOSEAlgNotAllowed, JOSENotSupported, JWEInvalid } from '../util/errors.js'; import { isDisjoint, isObject } from './type_checks.js'; import { decryptKeyManagement } from './key_management.js'; import { concat, decoder, encode } from './buffer_utils.js'; import { validateCrit, validateAlgorithms, JWE_RECOGNIZED } from './options.js'; import { prepareKey } from './key.js'; import { jweAlgorithm, jweEncryption } from './jwe_algorithms.js'; 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'); } if (typeof ciphertext !== 'string') { throw new JWEInvalid('JWE Ciphertext missing or incorrect type'); } if (tag !== undefined && typeof tag !== 'string') { throw new JWEInvalid('JWE Authentication Tag incorrect type'); } if (encodedProtected !== undefined && typeof encodedProtected !== 'string') { throw new JWEInvalid('JWE Protected Header incorrect type'); } if (aad !== undefined && (typeof aad !== 'string' || !aad)) { throw new JWEInvalid('JWE AAD incorrect type'); } if (unprotected !== undefined && !isObject(unprotected)) { throw new JWEInvalid('JWE Shared Unprotected Header incorrect type'); } 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) { const { encrypted_key: encryptedKey, header } = jwe; if (encryptedKey !== undefined && typeof encryptedKey !== 'string') { throw new JWEInvalid('JWE Encrypted Key incorrect type'); } if (header !== undefined) { if (!isObject(header)) { throw new JWEInvalid('JWE Per-Recipient Unprotected Header incorrect type'); } } if (jwe.protected === undefined && header === undefined && jwe.unprotected === undefined) { throw new JWEInvalid('JOSE Header missing'); } } export function shareJWE(jwe) { const { protected: encodedProtected, ciphertext, iv, tag, aad } = jwe; let parsedProt; if (encodedProtected !== undefined) { parsedProt = parseJoseHeader(encodedProtected, JWEInvalid, 'JWE Protected Header is invalid'); } const protectedHeader = encodedProtected !== undefined ? encode(encodedProtected) : new Uint8Array(); return [ parsedProt, decodeBase64url(ciphertext, 'ciphertext', JWEInvalid), iv !== undefined ? decodeBase64url(iv, 'iv', JWEInvalid) : undefined, tag !== undefined ? decodeBase64url(tag, 'tag', JWEInvalid) : undefined, aad !== undefined ? concat(protectedHeader, encode('.'), encodeBase64url(aad, 'aad', JWEInvalid)) : protectedHeader, ]; } export function decryptResult(jwe, decrypted) { const [plaintext, parsedProt, key, resolvedKey] = decrypted; const { protected: encodedProtected, aad, unprotected, header } = jwe; const result = { plaintext }; if (encodedProtected !== undefined) { result.protectedHeader = parsedProt; } if (aad !== undefined) { result.additionalAuthenticatedData = decodeBase64url(aad, 'aad', JWEInvalid); } if (unprotected !== undefined) { result.sharedUnprotectedHeader = unprotected; } if (header !== undefined) { result.unprotectedHeader = header; } if (resolvedKey) { return { ...result, key }; } return result; } export function prepareDecrypt(options) { return [ options && validateAlgorithms('keyManagementAlgorithms', options.keyManagementAlgorithms), options && validateAlgorithms('contentEncryptionAlgorithms', options.contentEncryptionAlgorithms), options?.crit, options?.maxPBES2Count, options?.maxDecompressedLength, ]; } export async function decryptRecipient(jwe, token, shared, key) { const [parsedProt] = token; const { header, unprotected } = jwe; let joseHeader; if (header !== undefined || unprotected !== undefined) { if (!isDisjoint(parsedProt, header, unprotected)) { throw new JWEInvalid('JWE Protected, JWE Unprotected Header, and JWE Per-Recipient Unprotected Header Parameter names must be disjoint'); } joseHeader = { ...parsedProt, ...header, ...unprotected }; } else { joseHeader = parsedProt ?? {}; } 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; if (typeof alg !== 'string' || !alg) { throw new JWEInvalid('missing JWE Algorithm (alg) in JWE Header'); } if (typeof enc !== 'string' || !enc) { throw new JWEInvalid('missing JWE Encryption Algorithm (enc) in JWE Header'); } if ((keyManagementAlgorithms && !keyManagementAlgorithms.has(alg)) || (!keyManagementAlgorithms && alg.startsWith('PBES2'))) { throw new JOSEAlgNotAllowed('"alg" (Algorithm) Header Parameter value not allowed'); } if (contentEncryptionAlgorithms && !contentEncryptionAlgorithms.has(enc)) { throw new JOSEAlgNotAllowed('"enc" (Encryption Algorithm) Header Parameter value not allowed'); } const encEntry = jweEncryption(enc); let encryptedKey; if (encodedKey !== undefined) { encryptedKey = decodeBase64url(encodedKey, 'encrypted_key', JWEInvalid); } let resolvedKey = false; if (typeof key === 'function') { key = await key(parsedProt, jwe); resolvedKey = true; } const algEntry = jweAlgorithm(alg); const k = await prepareKey(alg === 'dir' ? encEntry : algEntry, key, 'decrypt'); let cek; try { cek = await decryptKeyManagement(alg, encEntry, k, encryptedKey, joseHeader, maxPBES2Count); if (encodedKey !== undefined && cek instanceof Uint8Array && cek.byteLength << 3 !== encEntry.cekBits) { cek = generateCek(encEntry); } } catch (err) { if (err instanceof TypeError || err instanceof JWEInvalid || err instanceof JOSENotSupported) { throw err; } cek = generateCek(encEntry); } let plaintext = await decrypt(encEntry, cek, ciphertext, iv, tag, additionalData); if (joseHeader.zip === 'DEF') { const decompressionLimit = maxDecompressedLength ?? 250_000; if (decompressionLimit === 0) { throw new JOSENotSupported('JWE "zip" (Compression Algorithm) Header Parameter is not supported.'); } 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, decompressionLimit).catch((cause) => { if (cause instanceof JWEInvalid) throw cause; throw new JWEInvalid('Failed to decompress plaintext', { cause }); }); } return [plaintext, parsedProt, k, resolvedKey]; } export async function decryptJWE(jwe, shared, key) { return decryptRecipient(jwe, shareJWE(jwe), shared, key); } export async function decryptCompact(jwe, shared, key) { if (jwe instanceof Uint8Array) { jwe = decoder.decode(jwe); } if (typeof jwe !== 'string') { throw new JWEInvalid('Compact JWE must be a string or Uint8Array'); } const { 0: protectedHeader, 1: encryptedKey, 2: iv, 3: ciphertext, 4: tag, length, } = jwe.split('.'); if (length !== 5) { throw new JWEInvalid('Invalid Compact JWE'); } const flattened = { ciphertext, iv: iv || undefined, protected: protectedHeader, tag: tag || undefined, encrypted_key: encryptedKey || undefined, }; 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); }