"use strict";
// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
Object.defineProperty(exports, "__esModule", { value: true });
exports.nodeKdf = exports.curryCryptoStream = exports.getDecryptionHelper = exports.getEncryptHelper = void 0;
const material_management_1 = require("@aws-crypto/material-management");
const crypto_1 = require("crypto");
const hkdf_node_1 = require("@aws-crypto/hkdf-node");
const serialize_1 = require("@aws-crypto/serialize");
const kdfIndex = Object.freeze({
    sha256: (0, hkdf_node_1.HKDF)('sha256'),
    sha384: (0, hkdf_node_1.HKDF)('sha384'),
    sha512: (0, hkdf_node_1.HKDF)('sha512'),
});
const getEncryptHelper = (material) => {
    /* Precondition: NodeEncryptionMaterial must have a valid data key. */
    (0, material_management_1.needs)(material.hasValidKey(), 'Material has no unencrypted data key.');
    const { signatureHash } = material.suite;
    /* Conditional types can not narrow the return type :(
     * Function overloads "works" but then I can not export
     * the function and have eslint be happy (Multiple exports of name)
     */
    const getCipherInfo = curryCryptoStream(material, crypto_1.createCipheriv);
    return Object.freeze({
        getCipherInfo,
        getSigner: signatureHash ? getSigner : undefined,
        dispose,
    });
    function getSigner() {
        /* Precondition: The NodeEncryptionMaterial must have not been zeroed.
         * hasUnencryptedDataKey will check that the unencrypted data key has been set
         * *and* that it has not been zeroed.  At this point it must have been set
         * because the KDF function operated on it.  So at this point
         * we are protecting that someone has zeroed out the material
         * because the Encrypt process has been complete.
         */
        (0, material_management_1.needs)(material.hasUnencryptedDataKey, 'Unencrypted data key has been zeroed.');
        if (!signatureHash)
            throw new Error('Material does not support signature.');
        const { signatureKey } = material;
        if (!signatureKey)
            throw new Error('Material does not support signature.');
        const { privateKey } = signatureKey;
        if (typeof privateKey !== 'string')
            throw new Error('Material does not support signature.');
        const signer = Object.assign((0, crypto_1.createSign)(signatureHash), 
        // don't export the private key if we don't have to
        { awsCryptoSign: () => signer.sign(privateKey) });
        return signer;
    }
    function dispose() {
        material.zeroUnencryptedDataKey();
    }
};
exports.getEncryptHelper = getEncryptHelper;
const getDecryptionHelper = (material) => {
    /* Precondition: NodeDecryptionMaterial must have a valid data key. */
    (0, material_management_1.needs)(material.hasValidKey(), 'Material has no unencrypted data key.');
    const { signatureHash } = material.suite;
    /* Conditional types can not narrow the return type :(
     * Function overloads "works" but then I can not export
     * the function and have eslint be happy (Multiple exports of name)
     */
    const getDecipherInfo = curryCryptoStream(material, crypto_1.createDecipheriv);
    return Object.freeze({
        getDecipherInfo,
        getVerify: signatureHash ? getVerify : undefined,
        dispose,
    });
    function getVerify() {
        if (!signatureHash)
            throw new Error('Material does not support signature.');
        const { verificationKey } = material;
        if (!verificationKey)
            throw new Error('Material does not support signature.');
        const verify = Object.assign((0, crypto_1.createVerify)(signatureHash), 
        // explicitly bind the public key for this material
        {
            awsCryptoVerify: (signature) => 
            // As typescript gets better typing
            // We should consider either generics or
            // 2 different verificationKeys for Node and WebCrypto
            verify.verify(verificationKey.publicKey, signature),
        });
        return verify;
    }
    function dispose() {
        material.zeroUnencryptedDataKey();
    }
};
exports.getDecryptionHelper = getDecryptionHelper;
function curryCryptoStream(material, createCryptoIvStream) {
    const { encryption: cipherName, ivLength } = material.suite;
    const isEncrypt = material instanceof material_management_1.NodeEncryptionMaterial;
    /* Precondition: material must be either NodeEncryptionMaterial or NodeDecryptionMaterial.
     *
     */
    (0, material_management_1.needs)(isEncrypt
        ? crypto_1.createCipheriv === createCryptoIvStream
        : material instanceof material_management_1.NodeDecryptionMaterial
            ? crypto_1.createDecipheriv === createCryptoIvStream
            : false, 'Unsupported cryptographic material.');
    return (messageId, commitKey) => {
        const { derivedKey, keyCommitment } = nodeKdf(material, messageId, commitKey);
        return (isEncrypt
            ? { getCipher: createCryptoStream, keyCommitment }
            : createCryptoStream);
        function createCryptoStream(iv) {
            /* Precondition: The length of the IV must match the NodeAlgorithmSuite specification. */
            (0, material_management_1.needs)(iv.byteLength === ivLength, 'Iv length does not match algorithm suite specification');
            /* Precondition: The material must have not been zeroed.
             * hasUnencryptedDataKey will check that the unencrypted data key has been set
             * *and* that it has not been zeroed.  At this point it must have been set
             * because the KDF function operated on it.  So at this point
             * we are protecting that someone has zeroed out the material
             * because the Encrypt process has been complete.
             */
            (0, material_management_1.needs)(material.hasUnencryptedDataKey, 'Unencrypted data key has been zeroed.');
            /* createDecipheriv is incorrectly typed in @types/node. It should take key: CipherKey, not key: BinaryLike.
             * Also, the check above ensures
             * that _createCryptoStream is not false.
             * But TypeScript does not believe me.
             * For any complicated code,
             * you should defer to the checker,
             * but here I'm going to assert
             * it is simple enough.
             */
            return createCryptoIvStream(cipherName, derivedKey, iv);
        }
    };
}
exports.curryCryptoStream = curryCryptoStream;
function nodeKdf(material, nonce, commitKey) {
    const dataKey = material.getUnencryptedDataKey();
    const { kdf, kdfHash, keyLengthBytes, commitmentLength, saltLengthBytes, commitment, id: suiteId, } = material.suite;
    /* Check for early return (Postcondition): No Node.js KDF, just return the unencrypted data key. */
    if (!kdf && !kdfHash) {
        /* Postcondition: Non-KDF algorithm suites *must* not have a commitment. */
        (0, material_management_1.needs)(!commitKey, 'Commitment not supported.');
        return { derivedKey: dataKey };
    }
    /* Precondition: Valid HKDF values must exist for Node.js. */
    (0, material_management_1.needs)(kdf === 'HKDF' &&
        kdfHash &&
        kdfIndex[kdfHash] &&
        nonce instanceof Uint8Array, 'Invalid HKDF values.');
    /* The unwrap is done once we *know* that a KDF is required.
     * If we unwrapped before everything will work,
     * but we may be creating new copies of the unencrypted data key (export).
     */
    const { buffer: dkBuffer, byteOffset: dkByteOffset, byteLength: dkByteLength, } = (0, material_management_1.unwrapDataKey)(dataKey);
    if (commitment === 'NONE') {
        /* Postcondition: Non-committing Node algorithm suites *must* not have a commitment. */
        (0, material_management_1.needs)(!commitKey, 'Commitment not supported.');
        const toExtract = Buffer.from(dkBuffer, dkByteOffset, dkByteLength);
        const { buffer, byteOffset, byteLength } = (0, serialize_1.kdfInfo)(suiteId, nonce);
        const infoBuff = Buffer.from(buffer, byteOffset, byteLength);
        const derivedBytes = kdfIndex[kdfHash](toExtract)(keyLengthBytes, infoBuff);
        const derivedKey = (0, material_management_1.wrapWithKeyObjectIfSupported)(derivedBytes);
        return { derivedKey };
    }
    /* Precondition UNTESTED: Committing suites must define expected values. */
    (0, material_management_1.needs)(commitment === 'KEY' && commitmentLength && saltLengthBytes, 'Malformed suite data.');
    /* Precondition: For committing algorithms, the nonce *must* be 256 bit.
     * i.e. It must target a V2 message format.
     */
    (0, material_management_1.needs)(nonce.byteLength === saltLengthBytes, 'Nonce is not the correct length for committed algorithm suite.');
    const toExtract = Buffer.from(dkBuffer, dkByteOffset, dkByteLength);
    const expand = kdfIndex[kdfHash](toExtract, nonce);
    const { keyLabel, commitLabel } = (0, serialize_1.kdfCommitKeyInfo)(material.suite);
    const keyCommitment = expand(commitmentLength / 8, commitLabel);
    const isDecrypt = material instanceof material_management_1.NodeDecryptionMaterial;
    /* Precondition: If material is NodeDecryptionMaterial the key commitments *must* match.
     * This is also the preferred location to check,
     * because then the decryption key is never even derived.
     */
    (0, material_management_1.needs)((isDecrypt && commitKey && (0, crypto_1.timingSafeEqual)(keyCommitment, commitKey)) ||
        (!isDecrypt && !commitKey), isDecrypt ? 'Commitment does not match.' : 'Invalid arguments.');
    const derivedBytes = expand(keyLengthBytes, keyLabel);
    const derivedKey = (0, material_management_1.wrapWithKeyObjectIfSupported)(derivedBytes);
    return { derivedKey, keyCommitment };
}
exports.nodeKdf = nodeKdf;
//# sourceMappingURL=data:application/json;base64, 