import { FlattenedEncrypt, unprotected } from '../flattened/encrypt.js'; import { JOSENotSupported, JWEInvalid } from '../../util/errors.js'; import generateCek from '../../lib/cek.js'; import isDisjoint from '../../lib/is_disjoint.js'; import encryptKeyManagement from '../../lib/encrypt_key_management.js'; import { encode as base64url } from '../../runtime/base64url.js'; import validateCrit from '../../lib/validate_crit.js'; class IndividualRecipient { parent; unprotectedHeader; key; options; constructor(enc, key, options) { this.parent = enc; this.key = key; this.options = options; } setUnprotectedHeader(unprotectedHeader) { if (this.unprotectedHeader) { throw new TypeError('setUnprotectedHeader can only be called once'); } this.unprotectedHeader = unprotectedHeader; return this; } addRecipient(...args) { return this.parent.addRecipient(...args); } encrypt(...args) { return this.parent.encrypt(...args); } done() { return this.parent; } } export class GeneralEncrypt { _plaintext; _recipients = []; _protectedHeader; _unprotectedHeader; _aad; constructor(plaintext) { this._plaintext = plaintext; } addRecipient(key, options) { const recipient = new IndividualRecipient(this, key, { crit: options?.crit }); this._recipients.push(recipient); return recipient; } setProtectedHeader(protectedHeader) { if (this._protectedHeader) { throw new TypeError('setProtectedHeader can only be called once'); } this._protectedHeader = protectedHeader; return this; } setSharedUnprotectedHeader(sharedUnprotectedHeader) { if (this._unprotectedHeader) { throw new TypeError('setSharedUnprotectedHeader can only be called once'); } this._unprotectedHeader = sharedUnprotectedHeader; return this; } setAdditionalAuthenticatedData(aad) { this._aad = aad; return this; } async encrypt() { if (!this._recipients.length) { throw new JWEInvalid('at least one recipient must be added'); } if (this._recipients.length === 1) { const [recipient] = this._recipients; const flattened = await new FlattenedEncrypt(this._plaintext) .setAdditionalAuthenticatedData(this._aad) .setProtectedHeader(this._protectedHeader) .setSharedUnprotectedHeader(this._unprotectedHeader) .setUnprotectedHeader(recipient.unprotectedHeader) .encrypt(recipient.key, { ...recipient.options }); const jwe = { ciphertext: flattened.ciphertext, iv: flattened.iv, recipients: [{}], tag: flattened.tag, }; if (flattened.aad) jwe.aad = flattened.aad; if (flattened.protected) jwe.protected = flattened.protected; if (flattened.unprotected) jwe.unprotected = flattened.unprotected; if (flattened.encrypted_key) jwe.recipients[0].encrypted_key = flattened.encrypted_key; if (flattened.header) jwe.recipients[0].header = flattened.header; return jwe; } let enc; for (let i = 0; i < this._recipients.length; i++) { const recipient = this._recipients[i]; if (!isDisjoint(this._protectedHeader, this._unprotectedHeader, recipient.unprotectedHeader)) { throw new JWEInvalid('JWE Protected, JWE Shared Unprotected and JWE Per-Recipient Header Parameter names must be disjoint'); } const joseHeader = { ...this._protectedHeader, ...this._unprotectedHeader, ...recipient.unprotectedHeader, }; const { alg } = joseHeader; if (typeof alg !== 'string' || !alg) { throw new JWEInvalid('JWE "alg" (Algorithm) Header Parameter missing or invalid'); } if (alg === 'dir' || alg === 'ECDH-ES') { throw new JWEInvalid('"dir" and "ECDH-ES" alg may only be used with a single recipient'); } if (typeof joseHeader.enc !== 'string' || !joseHeader.enc) { throw new JWEInvalid('JWE "enc" (Encryption Algorithm) Header Parameter missing or invalid'); } if (!enc) { enc = joseHeader.enc; } else if (enc !== joseHeader.enc) { throw new JWEInvalid('JWE "enc" (Encryption Algorithm) Header Parameter must be the same for all recipients'); } validateCrit(JWEInvalid, new Map(), recipient.options.crit, this._protectedHeader, joseHeader); if (joseHeader.zip !== undefined) { throw new JOSENotSupported('JWE "zip" (Compression Algorithm) Header Parameter is not supported.'); } } const cek = generateCek(enc); const jwe = { ciphertext: '', iv: '', recipients: [], tag: '', }; for (let i = 0; i < this._recipients.length; i++) { const recipient = this._recipients[i]; const target = {}; jwe.recipients.push(target); const joseHeader = { ...this._protectedHeader, ...this._unprotectedHeader, ...recipient.unprotectedHeader, }; const p2c = joseHeader.alg.startsWith('PBES2') ? 2048 + i : undefined; if (i === 0) { const flattened = await new FlattenedEncrypt(this._plaintext) .setAdditionalAuthenticatedData(this._aad) .setContentEncryptionKey(cek) .setProtectedHeader(this._protectedHeader) .setSharedUnprotectedHeader(this._unprotectedHeader) .setUnprotectedHeader(recipient.unprotectedHeader) .setKeyManagementParameters({ p2c }) .encrypt(recipient.key, { ...recipient.options, [unprotected]: true, }); jwe.ciphertext = flattened.ciphertext; jwe.iv = flattened.iv; jwe.tag = flattened.tag; if (flattened.aad) jwe.aad = flattened.aad; if (flattened.protected) jwe.protected = flattened.protected; if (flattened.unprotected) jwe.unprotected = flattened.unprotected; target.encrypted_key = flattened.encrypted_key; if (flattened.header) target.header = flattened.header; continue; } const { encryptedKey, parameters } = await encryptKeyManagement(recipient.unprotectedHeader?.alg || this._protectedHeader?.alg || this._unprotectedHeader?.alg, enc, recipient.key, cek, { p2c }); target.encrypted_key = base64url(encryptedKey); if (recipient.unprotectedHeader || parameters) target.header = { ...recipient.unprotectedHeader, ...parameters }; } return jwe; } }