// Crypto support functions for static authentication (i.e. client secret-key encoding the auth-data with InteropService server-key decoding it)
import Logger from "../../../clients/logger";
const CRYPTO_ALGORITHM = "RSASSA-PKCS1-v1_5";
const CRYPTO_HASH = "SHA-256";
const CRYPTO_MODULUS_LENGTH = 2048;
const SALT_LENGTH = 32;
const EXPORTABLE = true;
const NO_CRYPTO_ERROR_MSG = "AuthCrypto: window crypto functions are not available on insecure sites (e.g. sites loaded with http); any standalone apps will not be authenticated.";
/*
   This module must properly handle running in four contexts:
     1) running in a secure window when window's crypto functions are available;
     2) running in an insecure window when crypto functions are not available, where authorization will fail;
     3) running unit tests under Node v15+, when Node's webcrypto functions are available.
     4) running unit tests under Node v14 or below, when Node's webcrypto functions are not available.
*/
const getCrypto = () => {
    var _a;
    try {
        return ((_a = window === null || window === void 0 ? void 0 : window.crypto) === null || _a === void 0 ? void 0 : _a.subtle) || require("crypto").webcrypto.subtle;
    }
    catch (err) {
        const error = "AuthSignVerify: Crypto functions could not be found. Key generation is available only in Electron, a browser, or Node 17+.";
        Logger.console.warn(error);
        throw error;
    }
};
/**
 * Returns true if crypto API is available
 */
export const cryptoExists = () => getCrypto() !== undefined;
/**
 * Utility function to convert a string to an ArrayBuffer
 *
 * @private
 * @param {*} arrayBuf data to convert
 * @returns the array buffer
 */
function stringToArrayBuffer(str) {
    var buf = new ArrayBuffer(str.length * 2); // 2 bytes for each char
    var bufView = new Uint16Array(buf);
    for (var i = 0, strLen = str.length; i < strLen; i++) {
        bufView[i] = str.charCodeAt(i);
    }
    return buf;
}
/**
 * Utility Function to convert a base64 string to an ArrayBuffer.
 *
 * @private
 * @param {*} str data to convert
 * @returns the array buffer
 */
function base64ToArrayBuffer(str) {
    var binaryString = window.atob(str);
    var len = binaryString.length;
    var bytes = new Uint8Array(len);
    for (var i = 0; i < len; i++) {
        bytes[i] = binaryString.charCodeAt(i);
    }
    return bytes.buffer;
}
/**
 * Utility Function to convert ArrayBuffer to base64 string.
 *
 * @private
 * @param {*} arrayBuf data to convert
 * @returns the string
 */
function arrayBufferToBase64(arrayBuf) {
    var binaryString = "";
    var bytes = new Uint8Array(arrayBuf);
    var len = bytes.byteLength;
    for (var i = 0; i < len; i++) {
        binaryString = binaryString + String.fromCharCode(bytes[i]);
    }
    return window.btoa(binaryString);
}
/**
 * Generate a key pair -- a secret key for client and a server key for InteropService
 *
 * @returns {Promise<AuthCryptoKeyPair>} the key pair in JSON Web Key format
 */
export async function authGenerateKeys() {
    let keys = {
        privateJWKey: "undefined",
        publicJWKey: "undefined",
    };
    let err;
    try {
        if (!cryptoExists()) {
            throw NO_CRYPTO_ERROR_MSG;
        }
        let key = await getCrypto().generateKey({
            name: CRYPTO_ALGORITHM,
            modulusLength: CRYPTO_MODULUS_LENGTH,
            publicExponent: new Uint8Array([0x01, 0x00, 0x01]),
            hash: { name: CRYPTO_HASH },
        }, EXPORTABLE, ["sign", "verify"] //can be any combination of "sign" and "verify"
        );
        if (key.privateKey === undefined) {
            throw new Error("key.privateKey is undefined");
        }
        if (key.publicKey === undefined) {
            throw new Error("key.publicKey is undefined");
        }
        keys.privateJWKey = await getCrypto().exportKey("jwk", key.privateKey);
        keys.publicJWKey = await getCrypto().exportKey("jwk", key.publicKey);
        Logger.system.debug("AuthCrypto:authGenerateKeys", keys);
    }
    catch (generateKeysError) {
        err = String(generateKeysError);
        Logger.system.error("AuthCrypto:authGenerateKeys caught error generating keys.", err);
    }
    return { err, keys };
}
/**
 * Return a signature for data provided using provided key.
 *
 * @param jwKey JKT key, which was provided by generateKey in AuthCryptoKeyPair.appClientSecretKey
 * @param dataToEncrypt
 * @returns
 */
export async function authSignature(jwKey, dataToSign) {
    let signatureString = "";
    let err;
    try {
        if (!cryptoExists()) {
            throw NO_CRYPTO_ERROR_MSG;
        }
        const importedPrivateKey = await getCrypto().importKey("jwk", jwKey, {
            name: CRYPTO_ALGORITHM,
            hash: CRYPTO_HASH,
        }, true, ["sign"]);
        const signature = await getCrypto().sign({
            name: CRYPTO_ALGORITHM,
            saltLength: SALT_LENGTH,
        }, importedPrivateKey, stringToArrayBuffer(JSON.stringify(dataToSign)));
        signatureString = arrayBufferToBase64(signature);
    }
    catch (signatureError) {
        err = String(signatureError);
        Logger.system.error(`AuthCrypto:authSignature caught error creating signature:${err}.`, err);
    }
    return { err, signature: signatureString };
}
/**
 * Return verify result of signed data.
 *
 * @param serverKey JKT key, which was provided by generateKey in AuthCryptoKeyPair.appServerKey
 * @param dataToEncrypt
 * @returns
 */
export async function authVerify(publicKey, signatureString, signedData) {
    let verificationResult = false;
    let err;
    try {
        if (!cryptoExists()) {
            throw NO_CRYPTO_ERROR_MSG;
        }
        const importedPublicKey = await getCrypto().importKey("jwk", publicKey, {
            name: CRYPTO_ALGORITHM,
            hash: CRYPTO_HASH,
        }, true, ["verify"]);
        let signature = base64ToArrayBuffer(signatureString);
        verificationResult = await getCrypto().verify({
            name: CRYPTO_ALGORITHM,
            saltLength: SALT_LENGTH,
        }, importedPublicKey, signature, stringToArrayBuffer(JSON.stringify(signedData)));
    }
    catch (verifyError) {
        err = String(verifyError);
        Logger.system.error(`AuthCrypto:authVerify caught error verifying signature: ${err}.  Could be due to illegal signatureKey in AppD, or bad AuthenticationToken within app register message.`);
    }
    return { err, verificationResult };
}
/**
 * Return true if the JWT is valid.
 *
 * @param jwsString the JWT, which was provided by generateKey in AuthCryptoKeyPair.appServerKey
 * @param publicJWK the public key in a JWK form
 * @returns
 */
async function verifyJWT(jwsString, publicJWK) {
    const key = await getCrypto().importKey("jwk", publicJWK, {
        name: CRYPTO_ALGORITHM,
        hash: CRYPTO_HASH,
    }, true, ["verify"]);
    // The JWT is a Base64URL string that consists of three parts separated by dots: header, payload, and signature.
    // The signing input is based on the first two parts: the header and the payload
    const jwsSigningInput = jwsString.split(".").slice(0, 2).join(".");
    // the third part is the auth signature
    const jwsSignature = jwsString.split(".")[2];
    const parsedSigningInput = new TextEncoder().encode(jwsSigningInput);
    // JWT uses a base64URL encoding, which is different than the normal base64 key encoding, so replace the URL characters here so base64ToArrayBuffer can be reused.
    // See https://stackoverflow.com/questions/55389211/string-based-data-encoding-base64-vs-base64url
    var jwsSignatureWithoutUrlFormat = jwsSignature.replace(/-/g, "+").replace(/_/g, "/");
    const signatureBase64 = base64ToArrayBuffer(jwsSignatureWithoutUrlFormat);
    const result = await getCrypto().verify({ name: CRYPTO_ALGORITHM }, key, signatureBase64, parsedSigningInput);
    Logger.system.debug(`AuthCrypto:authVerifyJWT: Result=${result}`, jwsString, jwsSigningInput, jwsSignature);
    return result;
}
/**
 * Return verify result of signed JSON Web Token.
 *
 * @param serverKey JKT key, which was provided by generateKey in AuthCryptoKeyPair.appServerKey
 * @param dataToEncrypt
 * @returns
 */
export async function authVerifyJWT(publicKey, jwtString) {
    let verificationResult = false;
    let err;
    try {
        if (!cryptoExists()) {
            throw NO_CRYPTO_ERROR_MSG;
        }
        verificationResult = await verifyJWT(jwtString, publicKey);
        if (!verificationResult) {
            Logger.system.error(`AuthCrypto:authVerifyJWT failed to verify JSON Web Token. Could be due to illegal signatureKey in AppD, or bad token within app register message.`);
        }
    }
    catch (verifyError) {
        err = String(verifyError);
        Logger.system.error(`AuthCrypto:authVerify caught error verifying signature: ${err}.  Could be due to illegal signatureKey in AppD, or bad AuthenticationToken within app register message.`);
    }
    return { err, verificationResult };
}
// Convenience function exposed on window generate keys to copy and paste from and app's dev console.
export async function createKeys() {
    const { keys } = await authGenerateKeys();
    window.FSBLPrivateKey = keys.privateJWKey;
    window.FSBLPublicKey = keys.publicJWKey;
    console.log("---> FSBLPrivateKey defined");
    console.log("---> FSBLPublicKey defined");
}
// test function which will eventually be removed in favor of authCrypto.spec.ts.
export async function authExamples() {
    // Mirroring expected DPE functionality
    const { keys } = await authGenerateKeys();
    const timestamp = Date.now();
    // Mirroring expected FDC3 Client Functionality
    const signatureData = { timestamp };
    const { signature } = await authSignature(keys.privateJWKey, signatureData);
    // Mirroring expected InteropService functionality
    let { verificationResult } = await authVerify(keys.publicJWKey, signature, signatureData);
    Logger.system.log("authExamples output ==>", verificationResult, verificationResult ? "successful" : "failed");
}
// test function which will eventually be removed in favor of authCrypto.spec.ts.
export async function authRawExamples() {
    let testData = "012345ABCDEabcde";
    let key = await getCrypto().generateKey({
        name: "RSA-PSS",
        modulusLength: 2048, //can be 1024, 2048, or 4096
        publicExponent: new Uint8Array([0x01, 0x00, 0x01]),
        hash: { name: "SHA-256" }, //can be "SHA-1", "SHA-256", "SHA-384", or "SHA-512"
    }, true, //whether the key is extractable (i.e. can be used in exportKey)
    ["sign", "verify"] //can be any combination of "sign" and "verify"
    );
    if (key.privateKey === undefined) {
        throw new Error("key.privateKey is undefined");
    }
    if (key.publicKey === undefined) {
        throw new Error("key.publicKey is undefined");
    }
    let exportedPrivateKey = await getCrypto().exportKey("jwk", key.privateKey);
    let exportedPublicKey = await getCrypto().exportKey("jwk", key.publicKey);
    let importedPrivateKey = await getCrypto().importKey("jwk", exportedPrivateKey, {
        name: "RSA-PSS",
        hash: { name: "SHA-256" }, //can be "SHA-1", "SHA-256", "SHA-384", or "SHA-512"
    }, true, ["sign"]);
    let importedPublicKey = await getCrypto().importKey("jwk", exportedPublicKey, {
        name: "RSA-PSS",
        hash: { name: "SHA-256" }, //can be "SHA-1", "SHA-256", "SHA-384", or "SHA-512"
    }, true, ["verify"]);
    let signature = await getCrypto().sign({
        name: "RSA-PSS",
        saltLength: 128,
    }, importedPrivateKey, stringToArrayBuffer(testData));
    let verificationResult = await getCrypto().verify({
        name: "RSA-PSS",
        saltLength: 128,
    }, importedPublicKey, signature, stringToArrayBuffer(testData));
    Logger.system.log("authRawExamples output", verificationResult, verificationResult ? "successful" : "failed");
}
//# sourceMappingURL=AuthSignVerify.js.map