"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.EncryptionUtil = exports.INITIALIZATION_VECTOR_LENGTH = void 0;
const crypto_1 = __importDefault(require("crypto"));
const ml_kem_1 = require("@noble/post-quantum/ml-kem");
const utils_1 = require("@noble/post-quantum/utils");
const StreamrClientError_1 = require("../StreamrClientError");
const trackerless_network_1 = require("@streamr/trackerless-network");
const utils_2 = require("@streamr/utils");
exports.INITIALIZATION_VECTOR_LENGTH = 16;
const INFO = Buffer.from('streamr-key-exchange');
const KEM_CIPHER_LENGTH_BYTES = 1568;
const KDF_SALT_LENGTH_BYTES = 64;
// eslint-disable-next-line @typescript-eslint/no-extraneous-class
class EncryptionUtil {
    /**
     * Public API for asymmetric encryption, unified interface across the different AsymmetricEncryptionTypes
     */
    static async encryptForPublicKey(plaintext, publicKey, type) {
        if (type === trackerless_network_1.AsymmetricEncryptionType.ML_KEM) {
            return this.encryptWithMLKEMPublicKey(plaintext, publicKey);
        }
        if (type === trackerless_network_1.AsymmetricEncryptionType.RSA) {
            return this.encryptWithRSAPublicKey(plaintext, publicKey);
        }
        throw new Error(`Unexpected encryption type: ${type}`);
    }
    static async decryptWithPrivateKey(cipher, privateKey, type) {
        if (type === trackerless_network_1.AsymmetricEncryptionType.ML_KEM) {
            return this.decryptWithMLKEMPrivateKey(cipher, privateKey);
        }
        if (type === trackerless_network_1.AsymmetricEncryptionType.RSA) {
            return this.decryptWithRSAPrivateKey(cipher, privateKey);
        }
        throw new Error(`Unexpected encryption type: ${type}`);
    }
    /**
     * RSA
     */
    static toRSAPublicKeyString(publicKey) {
        // RSA publicKey passed around in string format for legacy reasons
        const keyString = (0, utils_2.binaryToUtf8)(publicKey);
        if (!keyString.startsWith('-----BEGIN PUBLIC KEY-----')
            || !keyString.endsWith('-----END PUBLIC KEY-----\n')) {
            throw new Error('"publicKey" must be an RSA public key (SPKI) in PEM format, encoded in UTF-8');
        }
        return keyString;
    }
    static toRSAPrivateKeyString(privateKey) {
        // RSA privateKey passed around in string format for legacy reasons
        const keyString = (0, utils_2.binaryToUtf8)(privateKey);
        if (!keyString.startsWith('-----BEGIN PRIVATE KEY-----')
            || !keyString.endsWith('-----END PRIVATE KEY-----\n')) {
            throw new Error('"privateKey" must be a PKCS#8 RSA private key in PEM format, encoded in UTF-8');
        }
        return keyString;
    }
    static encryptWithRSAPublicKey(plaintextBuffer, publicKey) {
        const keyString = this.toRSAPublicKeyString(publicKey);
        const ciphertextBuffer = crypto_1.default.publicEncrypt(keyString, plaintextBuffer);
        return ciphertextBuffer;
    }
    static decryptWithRSAPrivateKey(ciphertext, privateKey) {
        const keyString = this.toRSAPrivateKeyString(privateKey);
        return crypto_1.default.privateDecrypt(keyString, ciphertext);
    }
    /**
     * ML-KEM
     */
    static async deriveAESWrapperKey(sharedSecret, kdfSalt) {
        const subtle = (0, utils_2.getSubtle)();
        const keyMaterial = await subtle.importKey('raw', sharedSecret, { name: 'HKDF' }, false, ['deriveKey']);
        const derivedKey = await subtle.deriveKey({
            name: 'HKDF',
            hash: 'SHA-512',
            salt: kdfSalt,
            info: INFO
        }, keyMaterial, { name: 'AES-CTR', length: 256 }, true, ['encrypt', 'decrypt']);
        const exportedKey = await subtle.exportKey('raw', derivedKey);
        return new Uint8Array(exportedKey);
    }
    static async encryptWithMLKEMPublicKey(plaintextBuffer, publicKey) {
        // Encapsulate to get kemCipher and shared secret
        // The recipient will be able to derive sharedSecret using privateKey and kemCipher
        const { cipherText: kemCipher, sharedSecret } = ml_kem_1.ml_kem1024.encapsulate(publicKey);
        if (kemCipher.length !== KEM_CIPHER_LENGTH_BYTES) {
            throw new Error(`Expected KEM cipher to be ${KEM_CIPHER_LENGTH_BYTES}, but it was ${kemCipher.length} bytes`);
        }
        // Derive an AES wrapping key from the shared secret using HKDF
        // The recipient will be able to repeat this computation to derive the same key
        const kdfSalt = (0, utils_1.randomBytes)(KDF_SALT_LENGTH_BYTES);
        const wrappingAESKey = await this.deriveAESWrapperKey(sharedSecret, kdfSalt);
        // Encrypt plaintext with the AES wrapping key
        const aesEncryptedPlaintext = this.encryptWithAES(plaintextBuffer, Buffer.from(wrappingAESKey));
        // Concatenate the deliverables into a binary package
        return Buffer.concat([kemCipher, kdfSalt, aesEncryptedPlaintext]);
    }
    static async decryptWithMLKEMPrivateKey(cipherPackage, privateKey) {
        // Split the cipherPackage, see encryptWithMLKEMPublicKey how it's constructed
        let pos = 0;
        const kemCipher = cipherPackage.slice(0, KEM_CIPHER_LENGTH_BYTES);
        pos += KEM_CIPHER_LENGTH_BYTES;
        const kdfSalt = cipherPackage.slice(pos, pos + KDF_SALT_LENGTH_BYTES);
        pos += KDF_SALT_LENGTH_BYTES;
        const aesEncryptedPlaintext = cipherPackage.slice(pos);
        // Derive the shared secret using the private key and kemCipher
        const sharedSecret = ml_kem_1.ml_kem1024.decapsulate(kemCipher, privateKey);
        // Derive the wrappingAESKey
        const wrappingAESKey = await this.deriveAESWrapperKey(sharedSecret, kdfSalt);
        // Decrypt the aesEncryptedPlaintext
        return this.decryptWithAES(aesEncryptedPlaintext, Buffer.from(wrappingAESKey));
    }
    /*
     * Returns a hex string without the '0x' prefix.
     */
    static encryptWithAES(data, cipherKey) {
        const iv = crypto_1.default.randomBytes(exports.INITIALIZATION_VECTOR_LENGTH); // always need a fresh IV when using CTR mode
        const cipher = crypto_1.default.createCipheriv('aes-256-ctr', cipherKey, iv);
        return Buffer.concat([iv, cipher.update(data), cipher.final()]);
    }
    /*
     * 'ciphertext' must be a hex string (without '0x' prefix), 'groupKey' must be a GroupKey. Returns a Buffer.
     */
    static decryptWithAES(cipher, cipherKey) {
        const iv = cipher.slice(0, exports.INITIALIZATION_VECTOR_LENGTH);
        const decipher = crypto_1.default.createDecipheriv('aes-256-ctr', cipherKey, iv);
        return Buffer.concat([decipher.update(cipher.slice(exports.INITIALIZATION_VECTOR_LENGTH)), decipher.final()]);
    }
    static decryptStreamMessage(streamMessage, groupKey) {
        let content;
        try {
            content = this.decryptWithAES(streamMessage.content, groupKey.data);
        }
        catch {
            throw new StreamrClientError_1.StreamrClientError('AES decryption failed', 'DECRYPT_ERROR', streamMessage);
        }
        let newGroupKey = undefined;
        if (streamMessage.newGroupKey) {
            try {
                newGroupKey = groupKey.decryptNextGroupKey(streamMessage.newGroupKey);
            }
            catch {
                throw new StreamrClientError_1.StreamrClientError('Could not decrypt new encryption key', 'DECRYPT_ERROR', streamMessage);
            }
        }
        return [content, newGroupKey];
    }
}
exports.EncryptionUtil = EncryptionUtil;
//# sourceMappingURL=EncryptionUtil.js.map