var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
    function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
    return new (P || (P = Promise))(function (resolve, reject) {
        function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
        function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
        function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
        step((generator = generator.apply(thisArg, _arguments || [])).next());
    });
};
import { Convert } from '@enbox/common';
import { CryptoError, CryptoErrorCode, CryptoUtils, isCipher, LocalKeyManager } from '@enbox/crypto';
import { AgentCryptoApi } from '../../../crypto-api.js';
import { isValidJweHeader, JweKeyManagement } from './jwe.js';
/**
 * A helper utility function used internally to decode a JWE header parameter from a Base64 URL
 * encoded string to a Uint8Array. It's designed to process individual JWE header parameter values,
 * ensuring they are correctly formatted and decoded.
 *
 * @param param - The name of the JWE header parameter being decoded; used for error messaging.
 * @param value - The Base64 URL encoded string value of the header parameter to decode.
 * @returns The decoded parameter as a Uint8Array, or undefined if the input value is undefined.
 * @throws {@link CryptoError} if the value is not a properly encoded Base64 URL string or if it's
 *         not a string.
 */
function decodeHeaderParam(param, value) {
    // If the parameter value is not present, return undefined.
    if (value === undefined) {
        return undefined;
    }
    try {
        if (typeof value !== 'string') {
            throw new Error();
        }
        return Convert.base64Url(value).toUint8Array();
    }
    catch (_a) {
        throw new CryptoError(CryptoErrorCode.InvalidJwe, `Failed to decode the JWE Header parameter '${param}' from Base64 URL format to ` +
            'Uint8Array. Ensure the value is properly encoded in Base64 URL format without padding.');
    }
}
/**
 * The `FlattenedJwe` class handles the encryption and decryption of JSON Web Encryption (JWE)
 * objects in the flattened serialization format. This format is a compact, URL-safe means of
 * representing encrypted content, typically used when dealing with a single recipient or when
 * bandwidth efficiency is important.
 *
 * This class provides methods to encrypt plaintext to a flattened JWE and decrypt a flattened JWE
 * back to plaintext, utilizing a variety of supported cryptographic algorithms as specified in the
 * JWE header parameters.
 *
 * @example
 * ```ts
 *  // Example usage of encrypt method
 * const plaintext = new TextEncoder().encode("Secret Message");
 * const key = { kty: "oct", k: "your-secret-key" }; // Example symmetric key
 * const protectedHeader = { alg: "dir", enc: "A256GCM" };
 * const encryptedJwe = await FlattenedJwe.encrypt({
 *   plaintext,
 *   protectedHeader,
 *   key,
 * });
 * ```
 *
 * @example
 * // Decryption example
 * const { plaintext, protectedHeader } = await FlattenedJwe.decrypt({
 *   jwe: yourFlattenedJweObject,
 *   key: yourDecryptionKey,
 *   crypto: new YourCryptoApi(),
 * });
 */
export class FlattenedJwe {
    constructor(params) {
        /** Base64URL encoded ciphertext. */
        this.ciphertext = '';
        Object.assign(this, params);
    }
    static decrypt(_a) {
        return __awaiter(this, arguments, void 0, function* ({ jwe, key, keyManager = new LocalKeyManager(), crypto = new AgentCryptoApi(), options = {} }) {
            var _b, _c;
            // Verify that the provided Crypto API supports the decrypt operation before proceeding.
            if (!isCipher(crypto)) {
                throw new CryptoError(CryptoErrorCode.OperationNotSupported, 'Crypto API does not support the "encrypt" operation.');
            }
            // Verify that the provided Key Manager supports the decrypt operation before proceeding.
            if (!isCipher(keyManager)) {
                throw new CryptoError(CryptoErrorCode.OperationNotSupported, 'Key Manager does not support the "decrypt" operation.');
            }
            // Verify that at least one of the JOSE header objects is present.
            if (!jwe.protected && !jwe.header && !jwe.unprotected) {
                throw new CryptoError(CryptoErrorCode.InvalidJwe, 'JWE is missing the required JOSE header parameters. ' +
                    'Please provide at least one of the following: "protected", "header", or "unprotected"');
            }
            // Verify that the JWE Ciphertext is present.
            if (typeof jwe.ciphertext !== 'string') {
                throw new CryptoError(CryptoErrorCode.InvalidJwe, 'JWE Ciphertext is missing or not a string.');
            }
            // Parse the JWE Protected Header, if present.
            let parsedProtectedHeader;
            if (jwe.protected) {
                try {
                    parsedProtectedHeader = Convert.base64Url(jwe.protected).toObject();
                }
                catch (_d) {
                    throw new Error('JWE Protected Header is invalid');
                }
            }
            // Per {@link https://www.rfc-editor.org/rfc/rfc7516#section-5.2 | RFC7516 Section 5.2}
            // the resulting JOSE Header MUST NOT contain duplicate Header Parameter names. In other words,
            // the same Header Parameter name MUST NOT occur in the `header`, `protected`, and
            // `unprotected` JSON object values that together comprise the JOSE Header.
            if (hasDuplicateProperties(parsedProtectedHeader, jwe.header, jwe.unprotected)) {
                throw new Error('Duplicate properties detected. Please ensure that each parameter is defined only once ' +
                    'across the JWE "header", "protected", and "unprotected" objects.');
            }
            // The JOSE Header is the union of the members of the JWE Protected Header (`protected`), the
            // JWE Shared Unprotected Header (`unprotected`), and the corresponding JWE Per-Recipient
            // Unprotected Header (`header`).
            const joseHeader = Object.assign(Object.assign(Object.assign({}, parsedProtectedHeader), jwe.header), jwe.unprotected);
            if (!isValidJweHeader(joseHeader)) {
                throw new Error('JWE Header is missing required "alg" (Algorithm) and/or "enc" (Encryption) Header Parameters');
            }
            if (Array.isArray(options.allowedAlgValues)
                && !options.allowedAlgValues.includes(joseHeader.alg)) {
                throw new Error(`"alg" (Algorithm) Header Parameter value not allowed: ${joseHeader.alg}`);
            }
            if (Array.isArray(options.allowedEncValues)
                && !options.allowedEncValues.includes(joseHeader.enc)) {
                throw new Error(`"enc" (Encryption Algorithm) Header Parameter value not allowed: ${joseHeader.enc}`);
            }
            let cek;
            try {
                const encryptedKey = jwe.encrypted_key
                    ? Convert.base64Url(jwe.encrypted_key).toUint8Array()
                    : undefined;
                cek = yield JweKeyManagement.decrypt({ key, encryptedKey, joseHeader, keyManager, crypto }, { minP2cCount: options.minP2cCount });
            }
            catch (error) {
                // If the error is a CryptoError with code "InvalidJwe" or "AlgorithmNotSupported", re-throw.
                if (error instanceof CryptoError
                    && (error.code === CryptoErrorCode.InvalidJwe || error.code === CryptoErrorCode.AlgorithmNotSupported)) {
                    throw error;
                }
                // Otherwise, generate a random CEK and proceed to the next step.
                // As noted in
                // {@link https://datatracker.ietf.org/doc/html/rfc7516#section-11.5 | RFC 7516 Section 11.5},
                // to mitigate the attacks described in
                // {@link https://datatracker.ietf.org/doc/html/rfc3218 | RFC 3218}, the recipient MUST NOT
                // distinguish between format, padding, and length errors of encrypted keys. It is strongly
                // recommended, in the event of receiving an improperly formatted key, that the recipient
                // substitute a randomly generated CEK and proceed to the next step, to mitigate timing
                // attacks.
                cek = typeof key === 'string'
                    ? yield keyManager.generateKey({ algorithm: joseHeader.enc })
                    : yield crypto.generateKey({ algorithm: joseHeader.enc });
            }
            // If present, decode the JWE Initialization Vector (IV) and Authentication Tag.
            const iv = decodeHeaderParam('iv', jwe.iv);
            const tag = decodeHeaderParam('tag', jwe.tag);
            // Decode the JWE Ciphertext to a byte array, and if present, append the Authentication Tag.
            const ciphertext = tag !== undefined
                ? new Uint8Array([
                    ...Convert.base64Url(jwe.ciphertext).toUint8Array(),
                    ...(tag !== null && tag !== void 0 ? tag : [])
                ])
                : Convert.base64Url(jwe.ciphertext).toUint8Array();
            // If the JWE Additional Authenticated Data (AAD) is present, the Additional Authenticated Data
            // input to the Content Encryption Algorithm is
            // ASCII(Encoded Protected Header || '.' || BASE64URL(JWE AAD)). If the JWE AAD is absent, the
            // Additional Authenticated Data is ASCII(BASE64URL(UTF8(JWE Protected Header))).
            const additionalData = jwe.aad !== undefined
                ? new Uint8Array([
                    ...Convert.string((_b = jwe.protected) !== null && _b !== void 0 ? _b : '').toUint8Array(),
                    ...Convert.string('.').toUint8Array(),
                    ...Convert.string(jwe.aad).toUint8Array()
                ])
                : Convert.string((_c = jwe.protected) !== null && _c !== void 0 ? _c : '').toUint8Array();
            // Decrypt the JWE using the Content Encryption Key (CEK) with:
            // - Key Manager: If the CEK is a Key Identifier.
            // - Crypto API: If the CEK is a JWK.
            const plaintext = typeof cek === 'string'
                ? yield keyManager.decrypt({ keyUri: cek, data: ciphertext, iv, additionalData })
                : yield crypto.decrypt({ key: cek, data: ciphertext, iv, additionalData });
            return {
                plaintext,
                protectedHeader: parsedProtectedHeader,
                additionalAuthenticatedData: decodeHeaderParam('aad', jwe.aad),
                sharedUnprotectedHeader: jwe.unprotected,
                unprotectedHeader: jwe.header
            };
        });
    }
    static encrypt(_a) {
        return __awaiter(this, arguments, void 0, function* ({ key, plaintext, additionalAuthenticatedData, protectedHeader, sharedUnprotectedHeader, unprotectedHeader, keyManager = new LocalKeyManager(), crypto = new AgentCryptoApi(), }) {
            // Verify that the provided Crypto API supports the decrypt operation before proceeding.
            if (!isCipher(crypto)) {
                throw new CryptoError(CryptoErrorCode.OperationNotSupported, 'Crypto API does not support the "encrypt" operation.');
            }
            // Verify that the provided Key Manager supports the decrypt operation before proceeding.
            if (!isCipher(keyManager)) {
                throw new CryptoError(CryptoErrorCode.OperationNotSupported, 'Key Manager does not support the "decrypt" operation.');
            }
            // Verify that at least one of the JOSE header objects is present.
            if (!protectedHeader && !sharedUnprotectedHeader && !unprotectedHeader) {
                throw new CryptoError(CryptoErrorCode.InvalidJwe, 'JWE is missing the required JOSE header parameters. ' +
                    'Please provide at least one of the following: "protectedHeader", "sharedUnprotectedHeader", or "unprotectedHeader"');
            }
            // Verify that the Plaintext is present.
            if (!(plaintext instanceof Uint8Array)) {
                throw new CryptoError(CryptoErrorCode.InvalidJwe, 'Plaintext is missing or not a byte array.');
            }
            // Per {@link https://www.rfc-editor.org/rfc/rfc7516#section-5.2 | RFC7516 Section 5.2}
            // the resulting JOSE Header MUST NOT contain duplicate Header Parameter names. In other words,
            // the same Header Parameter name MUST NOT occur in the `header`, `protected`, and
            // `unprotected` JSON object values that together comprise the JOSE Header.
            if (hasDuplicateProperties(protectedHeader, sharedUnprotectedHeader, unprotectedHeader)) {
                throw new Error('Duplicate properties detected. Please ensure that each parameter is defined only once ' +
                    'across the JWE "protectedHeader", "sharedUnprotectedHeader", and "unprotectedHeader" objects.');
            }
            // The JOSE Header is the union of the members of the JWE Protected Header (`protectedHeader`),
            // the JWE Shared Unprotected Header (`sharedUnprotectedHeader`), and the corresponding JWE
            // Per-Recipient Unprotected Header (`unprotectedHeader`).
            const joseHeader = Object.assign(Object.assign(Object.assign({}, protectedHeader), sharedUnprotectedHeader), unprotectedHeader);
            if (!isValidJweHeader(joseHeader)) {
                throw new Error('JWE Header is missing required "alg" (Algorithm) and/or "enc" (Encryption) Header Parameters');
            }
            const { cek, encryptedKey } = yield JweKeyManagement.encrypt({ key, joseHeader, keyManager, crypto });
            // If required for the Content Encryption Algorithm, generate a random JWE Initialization
            // Vector (IV) of the correct size; otherwise, let the JWE Initialization Vector be the empty
            // octet sequence.
            let iv;
            switch (joseHeader.enc) {
                case 'A128GCM':
                case 'A192GCM':
                case 'A256GCM':
                    iv = CryptoUtils.randomBytes(12);
                    break;
                default:
                    iv = new Uint8Array(0);
            }
            // Compute the Encoded Protected Header value BASE64URL(UTF8(JWE Protected Header)).  If the JWE
            // Protected Header is not present, let this value be the empty string.
            const encodedProtectedHeader = protectedHeader
                ? Convert.object(protectedHeader).toBase64Url()
                : '';
            // If the JWE Additional Authenticated Data (AAD) is present, the Additional Authenticated Data
            // input to the Content Encryption Algorithm is
            // ASCII(Encoded Protected Header || '.' || BASE64URL(JWE AAD)). If the JWE AAD is absent, the
            // Additional Authenticated Data is ASCII(BASE64URL(UTF8(JWE Protected Header))).
            let additionalData;
            let encodedAad;
            if (additionalAuthenticatedData) {
                encodedAad = Convert.uint8Array(additionalAuthenticatedData).toBase64Url();
                additionalData = Convert.string(encodedProtectedHeader + '.' + encodedAad).toUint8Array();
            }
            else {
                additionalData = Convert.string(encodedProtectedHeader).toUint8Array();
            }
            // Encrypt the plaintext using the CEK, the JWE Initialization Vector, and the Additional
            // Authenticated Data value using the specified content encryption algorithm to create the JWE
            // Ciphertext value and the JWE Authentication Tag.
            const ciphertextWithTag = typeof cek === 'string'
                ? yield keyManager.encrypt({ keyUri: cek, data: plaintext, iv, additionalData })
                : yield crypto.encrypt({ key: cek, data: plaintext, iv, additionalData });
            const ciphertext = ciphertextWithTag.slice(0, -16);
            const authenticationTag = ciphertextWithTag.slice(-16);
            // Create the Flattened JWE JSON Serialization output, which is based upon the General syntax,
            // but flattens it, optimizing it for the single-recipient case. It flattens it by removing the
            // "recipients" member and instead placing those members defined for use in the "recipients"
            // array (the "header" and "encrypted_key" members) in the top-level JSON object (at the same
            // level as the "ciphertext" member).
            const jwe = new FlattenedJwe({
                ciphertext: Convert.uint8Array(ciphertext).toBase64Url(),
            });
            if (encryptedKey) {
                jwe.encrypted_key = Convert.uint8Array(encryptedKey).toBase64Url();
            }
            if (protectedHeader) {
                jwe.protected = encodedProtectedHeader;
            }
            if (sharedUnprotectedHeader) {
                jwe.unprotected = sharedUnprotectedHeader;
            }
            if (unprotectedHeader) {
                jwe.header = unprotectedHeader;
            }
            if (iv) {
                jwe.iv = Convert.uint8Array(iv).toBase64Url();
            }
            if (encodedAad) {
                jwe.aad = encodedAad;
            }
            if (authenticationTag) {
                jwe.tag = Convert.uint8Array(authenticationTag).toBase64Url();
            }
            return jwe;
        });
    }
}
/** Check whether any two of the given objects share the same property name. */
function hasDuplicateProperties(...objects) {
    const propertySet = new Set();
    const objectsWithoutUndefined = objects.filter(Boolean);
    for (const obj of objectsWithoutUndefined) {
        for (const key in obj) {
            if (propertySet.has(key)) {
                return true;
            }
            propertySet.add(key);
        }
    }
    return false;
}
//# sourceMappingURL=jwe-flattened.js.map