Source: crypto.js

import crypto from "crypto";
import { KEYUTIL, KJUR, X509 } from "jsrsasign";
import moment from "moment";
import CoinString from "coinstring";
import _ from "lodash";
import GMCryptoUtils from "./gmCryptoUtils";
import SHA1withECDSA from "./algorithmNames";

const gmCUs = new GMCryptoUtils("wss://localhost:9003");

/**
 * @callback gmGetHashCallback 
 * @param {string} sm3HashVal hex格式的国密算法哈希值
 */
/**
 * 根据指定的密码学哈希算法为给定数据计算哈希值
 * 
 * @param {!Object} getHashParams 计算哈希值所需参数
 * @param {Buffer | string} getHashParams.data 待对其计算哈希值的原数据
 * @param {string} [getHashParams.alg] 待使用的密码学哈希算法,默认为sha256
 * @param {string} [getHashParams.provider] 密码学哈希算法的提供者,支持nodecrypto、jsrsasign以及gm
 * <li>nodecrypto,NodeJS内建的crypto工具,默认使用该provider,支持openssl提供的hash算法,
 *   可在终端使用命令`openssl list-message-digest-algorithms`(1.0.2版)
 *   或`openssl list -digest-algorithms`(1.1.0版)查看支持的哈希算法
 * </li>
 * <li>jsrsasign,开源js加密工具(https://kjur.github.io/jsrsasign),
 *   支持的哈希算法如<a href=https://kjur.github.io/jsrsasign/api/symbols/KJUR.crypto.MessageDigest.html>
 * https://kjur.github.io/jsrsasign/api/symbols/KJUR.crypto.MessageDigest.html<a/>所示
 * </li>
 * <li>gm,国密算法加密工具,支持sm3哈希算法
 * </li>
 * @param {gmGetHashCallback} [getHashParams.cb] sm3计算服务为异步实现,当使用sm3时需提供回调方法
 * @returns {Buffer} digest 结果哈希值,若使用sm3将返回undefined
 */
const GetHashVal = ({ 
    data, alg = "sha256", provider = "nodecrypto", cb, 
}) => {
    if (!Buffer.isBuffer(data) && !_.isString(data)) {
        throw new TypeError("The data field should be a Buffer or string");
    }
    if (!_.isString(alg)) throw new TypeError("The alg field should be a string");
    const providerEnumVals = ["nodecrypto", "jsrsasign", "gm"];
    if (_.indexOf(providerEnumVals, provider) === -1) {
        throw new RangeError(`The provider field should be one of ${providerEnumVals}`);
    }

    let hash;
    let digest;
    if (alg === "sm3" && provider !== "gm") throw new Error("当使用sm3时,必须指定provider为gm");
    if (alg !== "sm3" && provider === "gm") throw new Error("gm provider只支持sm3算法");

    switch (provider) {
        case "nodecrypto":
            // 使用NodeJS自带crypto工具计算哈希值
            // 支持的哈希算法更多
            hash = crypto.createHash(alg);
            hash.update(data);
            digest = hash.digest();
            break;
        case "jsrsasign":
            hash = new KJUR.crypto.MessageDigest({ alg, prov: "cryptojs" });
            // data = Buffer.isBuffer(data) ? data.toString() : data;
            // 兼容经browserify或webpack处理后,Buffer在Browser (Uint8Array)端的相关操作
            // data = data.constructor.name !== 'String' ? Buffer.from(data).toString() : data;
            hash.updateString(Buffer.from(data).toString());
            digest = Buffer.from(hash.digest(), "hex");
            break;
        case "gm":
            // data = Buffer.isBuffer(data) ? data.toString('hex') :
            //     Buffer.from(data).toString('hex');
            gmCUs.getGMHashVal(Buffer.from(data).toString("hex"), cb);
            break;
        default:
            throw new Error("Not supported hash algorithm provider");
    }
    return digest;
};

/**
 * 创建非对称密钥对,支持RSA与EC密钥对生成
 * 
 * @param {string} [alg] 密钥对生成算法,RSA或EC,默认使用EC
 * @param {string | number} [keyLenOrCurve] 指定密钥长度(针对RSA)或曲线名(针对EC),
 * 默认使用EC secp256k1曲线
 * @returns {Object} keypair 含有jsrsasign提供的prvKeyObj与pubKeyObj对象
 */
const CreateKeypair = (alg = "EC", keyLenOrCurve = "secp256k1") => {
    const algEnumTypes = ["EC", "RSA"];
    if (_.indexOf(algEnumTypes, alg) === -1) {
        throw new RangeError(`The alg param should be one of ${algEnumTypes}`);
    }
    if (alg === "EC" && !_.isString(keyLenOrCurve)) {
        throw new TypeError("The keyLenOrCurve param should be a string when use EC alg");
    }
    if (alg === "RSA" && !_.isInteger(keyLenOrCurve)) {
        throw new TypeError("The keyLenOrCurve param should be an integer when use RSA alg");
    }

    const keypair = KEYUTIL.generateKeypair(alg, keyLenOrCurve);
    return keypair;
};

/**
 * 导入已有非对称密钥
 * 
 * @param {string} keyPEM 使用符合PKCS#8标准的pem格式私钥信息获取私钥对象,支持导入使用PBKDF2_HmacSHA1_3DES加密的pem格式私钥信息
 * 使用符合PKCS#8标准的pem格式公钥信息或符合X.509标准的pem格式证书信息获取公钥对象
 * @param {string} [passWord] 私钥保护密码, 当导入已加密私钥时,需要提供该参数
 * @returns {Object} keyObj密钥对象,prvkeyObj或pubKeyObj
 */
const ImportKey = (keyPEM, passWord) => {
    if (!_.isString(keyPEM)) throw new TypeError("The keyPEM param should be a string");
    if (passWord && !_.isString(keyPEM)) throw new TypeError("The passWord param should be a string");

    let keyObj;
    try {
        keyObj = KEYUTIL.getKey(keyPEM, passWord);
    } catch (e) {
        if (e === "malformed plain PKCS8 private key(code:001)"
            || e.message === "Cannot read property 'sigBytes' of undefined"
            || e.message === "Cannot read property 'sigBytes' of null") {
            throw new Error("提供的私钥信息无效或解密密码无效");
        } else throw e;
    }
    return keyObj;
};

/**
 * 获得PEM格式的私钥或公钥信息
 * 
 * @param {Object} keyObj prvKeyObj或pubKeyObj
 * @param {string} [passWord] 私钥保护密码,当需要生成加密私钥信息时需提供该参数, 
 * 目前是由jsrsasign使用PBKDF2_HmacSHA1_3DES算法对私钥进行加密
 * @returns {string} keyPEM 符合PKCS#8标准的密钥信息
 */
const GetKeyPEM = (keyObj, passWord) => {
    if (passWord && !_.isString(passWord)) throw new TypeError("The passWord param should be a string");

    let keyPEM;
    const key = keyObj;
    if (key.isPrivate) {
        keyPEM = KEYUTIL.getPEM(keyObj, "PKCS8PRV", passWord);
    } else { 
        keyPEM = KEYUTIL.getPEM(keyObj); 
    }
    return keyPEM;
};

/**
 * 
 * @callback gmSignCallback
 * @param {string} signature hex格式签名信息
 */
/**
 * 根据指定私钥和签名算法,对给定数据进行签名
 * 
 * @param {Object} signParams 签名所需参数
 * @param {Object | string} signParams.prvKey 私钥信息,支持使用jsrsasign提供的私钥对象,
 * 或直接使用符合PKCS#5的未加密pem格式DSA/RSA私钥,符合PKCS#8的未加密pem格式RSA/ECDSA私钥,当使用gm签名算法时该参数应为null
 * @param {string | Buffer} signParams.data 待被签名的数据
 * @param {string} [signParams.alg] 签名算法,默认使用"SHA1"(NodeJS)或"ecdsa-with-SHA1"(Browser),
 * 国密签名算法为sm2-with-SM3
 * @param {string} [signParams.provider] 签名算法的提供者,支持使用jsrsasign或node内建的crypto(默认使用),以及gm国密签名算法工具
 * @param {string} [signParams.gmUserID] 国密签名算法需要的用户标识,该标识是到gm websocket server查找到其对应国密私钥的唯一标识
 * @param {gmSignCallback} [signParams.cb] 国密签名算法支持为异步实现,当使用国密签名算法时,需要使用该回调方法
 * @returns {Buffer} signature 签名结果值,当使用非国密签名时,会返回该结果
 */
const Sign = ({ 
    prvKey, data, alg = SHA1withECDSA, provider = "nodecrypto", gmUserID, cb, 
}) => {
    if (!Buffer.isBuffer(data) && !_.isString(data)) {
        throw new TypeError("The data field should be a Buffer or string");
    }
    if (!_.isString(alg)) throw new TypeError("The alg field should be a string");
    const providerEnumVals = ["nodecrypto", "jsrsasign", "gm"];
    if (_.indexOf(providerEnumVals, provider) === -1) {
        throw new RangeError(`The provider field should be one of ${providerEnumVals}`);
    }
    if (gmUserID && !_.isString(gmUserID)) throw new TypeError("The gmUserID should be a string");


    let sig;
    let signature;
    switch (provider) {
        case "nodecrypto":
            // 使用Node自带的crypto包进行签名
            // ps: 发现jsrsasign提供的签名工具包有bug:
            // 对普通数据签名时,在本地和RepChain端验签都ok,
            // 但是,当对一个哈希值进行签名时,在本地验签成功,但在RepChain端验签失败
            sig = crypto.createSign(alg);
            sig.update(data);
            signature = sig.sign((typeof prvKey === "object" ? GetKeyPEM(prvKey) : prvKey));
            break;
        case "jsrsasign":
            sig = new KJUR.crypto.Signature({ alg }); // alg = <hash>wth<crypto> like: SHA1withECDSA
            sig.init(prvKey);
            // let dataTBS = Buffer.isBuffer(data) ? data.toString() : data;
            // 兼容经browserify或webpack处理后,Buffer在Browser (Uint8Array)端的相关操作
            sig.updateString(Buffer.from(data).toString());
            signature = Buffer.from(sig.sign(), "hex");
            break;
        case "gm":
            // data = Buffer.isBuffer(data) ? data.toString('base64') :
            //     Buffer.from(data).toString('base64');
            // 兼容经browserify或webpack处理后,Buffer在Browser (Uint8Array)端的相关操作
            gmCUs.getGMSignature(gmUserID, Buffer.from(data).toString("base64"), cb);
            break;
        default:
            throw new Error("Not supported sign provider");
    }
    return signature;
};

/**
 * 验证签名
 * 
 * @param {Object} verifySignatureParams 验证签名所需参数
 * @param {Object | string} verifySignatureParams.pubKey 公钥信息,支持使用jsrsasign提供的公钥对象,
 * 或直接使用符合PKCS#8的pem格式DSA/RSA/ECDSA公钥,符合X.509的PEM格式包含公钥信息的证书
 * @param {Buffer} verifySignatureParams.sigValue 签名结果
 * @param {string | Buffer} verifySignatureParams.data 被签名的原数据
 * @param {string} [verifySignatureParams.alg] 签名算法,默认使用"SHA1"(NodeJS)或"ecdsa-with-SHA1"(Browser)
 * @param {string} [verifySignatureParams.provider] 签名算法的提供者,
 * 支持使用jsrsasign或node内建的crypto(默认使用),待支持使用国密算法提供者
 * @returns {boolean} isValid 签名真实性鉴定结果
 */
const VerifySign = ({ 
    pubKey, sigValue, data, alg = SHA1withECDSA, provider = "nodecrypto", 
}) => {
    if (!Buffer.isBuffer(sigValue)) {
        throw new TypeError("The sigValue field should be a Buffer");
    }
    if (!Buffer.isBuffer(data) && !_.isString(data)) {
        throw new TypeError("The data field should be a Buffer or string");
    }
    if (!_.isString(alg)) throw new TypeError("The alg field should be a string");
    const providerEnumVals = ["nodecrypto", "jsrsasign"];
    if (_.indexOf(providerEnumVals, provider) === -1) {
        throw new RangeError(`The provider field should be one of ${providerEnumVals}`);
    }

    let isValid;
    switch (provider) {
        case "nodecrypto":
            isValid = crypto.createVerify(alg)
                .update(data)
                .verify((typeof pubKey === "object") ? GetKeyPEM(pubKey) : pubKey, sigValue);
            break;
        case "jsrsasign":
            isValid = new KJUR.crypto.Signature({ alg });
            isValid.init(pubKey);
            isValid.updateString(Buffer.from(data).toString());
            isValid = isValid.verify(sigValue.toString("hex"));
            break;
        // Todo: case '国密'
        default:
            throw new Error("Not supported sign provider");
    }
    return isValid;
};

/**
 * 根据公钥信息计算其bitcoin地址
 * 
 * @param {!string} pubKeyPEM 符合PKCS#8标准的pem格式公钥信息,或符合X.509标准的公钥证书信息,目前只支持EC-secp256k1曲线
 * @returns {Buffer} bitcoin地址的Buffer数据
 */
const CalculateAddr = (pubKeyPEM) => {
    // TODO: 支持其它非对称算法公钥的地址计算

    if (!_.isString(pubKeyPEM)) throw new TypeError("The pubKeyPEM param should be a string");

    const pubKeySha256 = GetHashVal({ data: Buffer.from(ImportKey(pubKeyPEM).pubKeyHex, "hex"), alg: "sha256" });
    const pubKeyRmd160 = GetHashVal({ data: pubKeySha256, alg: "rmd160" });
    const addr = CoinString.encode(pubKeyRmd160, 0x00);
    return Buffer.from(addr);
};

/**
 * 生成PEM格式的证书签名请求(Certificate Signing Request, CSR)信息
 * 
 * @param {Object} params 生成csr所需参数
 * @param {string} params.subject 证书拥有者唯一标识名
 * @param {string} params.subjectPubKey 证书拥有者PEM格式的公钥信息
 * @param {string} params.signAlg 生成csr的签名算法 
 * @param {string} params.subjectPrvKey 证书拥有者PEM格式的私钥信息(无加密)
 * @returns {string} PEM格式的csr信息
 */
const CreateCSR = ({ 
    subject, subjectPubKey, signAlg, subjectPrvKey, 
}) => KJUR.asn1.csr.CSRUtil.newCSRPEM({
    subject: { str: subject },
    sbjpubkey: subjectPubKey,
    sigalg: signAlg,
    sbjprvkey: subjectPrvKey,
});

/**
 * 从PEM格式的csr中获取csr信息 
 * 
 * @param {string} csrPEM PEM格式的csr信息
 * @returns {Object} csrInfo 解析后的csr信息(JSON)
 * csrInfo.pubkey.obj - 证书拥有者公钥对象(jsrsasign提供的RSAKey, KJUR.crypto.{ECDSA,DSA}) 
 * csrInfo.pubkey.hex - 证书拥有者公钥的hex格式
 * csrInfo.subject.name - 证书拥有者唯一标识名
 * csrInfo.subject.hex - 证书拥有者唯一标识名的hex格式
 */
const GetCSRInfoFromPEM = csrPEM => KJUR.asn1.csr.CSRUtil.getInfo(csrPEM);

/**
 * @callback gmCertificateCallback
 * @param {string} certPEM pem格式证书
 */
/**
 * 生成符合X.509标准的证书信息
 * 
 * @param {Object} certFields 证书的具体信息,
 * @param {number} certFields.serialNumber 证书序列号
 * @param {string} certFields.sigAlg 签发证书时使用的签名算法
 * @param {string} certFields.issuerDN 符合X500标准的代表证书发行方身份标识的Distinguished Name
 * @param {string} certFields.subjectDN 符合X500标准的代表证书拥有方标识的Distinguished Name
 * @param {number} certFields.notBefore 代表证书有效性起始时间的unix时间戳(秒)
 * @param {number} certFields.notAfter 代表证书有效性终止时间的unix时间戳(秒)
 * @param {Object} certFields.subjectPubKey 证书拥有方的公钥对象,使用jsrsasign提供的pubKeyObj
 * @param {Object} certFields.issuerPrvKey 证书发行方的私钥对象,使用jsrsasign提供的prvKeyObj
 * @param {Object} [certFields.extensions] 证书扩展,jsrsasign支持的扩展属性(https://kjur.github.io/jsrsasign/api/symbols/KJUR.asn1.x509.TBSCertificate.html#appendExtension),
 * 如:
 * {
 *     BasicConstraints: { cA: true, critical: true }, 
 *     KeyUsage: { names: ["digitalSignature", "keyCertSign"] },
 *     AuthorityKeyIdentifier: {kid: "1234ab..."},
 *     SubjectAltName: {critical: true, array: [{uri: "http://aaa.com"}, {uri: "http://bbb.com"}]}
 * } 
 * @param {string} [certFields.gmUserID] 证书拥有者的id标识, 当creator为gm时,需要该属性,并且不需要其他属性;
 * 当creator为jsrsasign时,不需要该属性
 * @param {string} [creator] 证书生成者,支持使用jsrsasign或gm(即国密),默认使用jsrsasign
 * @param {gmCertificateCallback} [cb] 回调函数,gm证书生成实现是异步的,所以当creator为gm时,需要提供该参数
 * @returns {string} certPEM pem格式的证书信息,当creator为jsrsasign时,将返回该信息
 */
const CreateCertificate = (certFields, creator = "jsrsasign", cb) => {
    const tbs = new KJUR.asn1.x509.TBSCertificate();
    let cert;
    switch (creator) {
        case "jsrsasign":
            tbs.setSerialNumberByParam({ int: certFields.serialNumber });
            tbs.setSignatureAlgByParam({ name: certFields.sigAlg });
            tbs.setIssuerByParam({ str: certFields.issuerDN });
            tbs.setSubjectByParam({ str: certFields.subjectDN });
            tbs.setNotBeforeByParam({ str: `${moment.unix(certFields.notBefore).utc().format("YYYYMMDDHHmmss")}Z` });
            tbs.setNotAfterByParam({ str: `${moment.unix(certFields.notAfter).utc().format("YYYYMMDDHHmmss")}Z` });
            tbs.setSubjectPublicKey(certFields.subjectPubKey);
            for (const extField in certFields.extensions) {
                if (Object.prototype.hasOwnProperty.call(certFields.extensions, extField)) {
                    tbs.appendExtensionByName(extField, certFields.extensions[extField]);
                }
            }
            cert = new KJUR.asn1.x509.Certificate({
                tbscertobj: tbs,
                prvkeyobj: certFields.issuerPrvKey,
            });
            cert.sign();
            return cert.getPEMString();
        case "gm":
            gmCUs.getGMCertificate(certFields.gmUserID, cb);
            return null;
        default:
            throw new Error("Not supported certificate creator ", creator);
    }
};

/**
 * 生成符合X.509标准的自签名证书信息
 * 
 * @param {Object} certFields 证书具体信息
 * @param {number} certFields.serialNumber 证书序列号
 * @param {string} certFields.sigAlg 证书签名算法
 * @param {string} certFields.DN 符合X500标准的代表证书所有者身份标识的Distinguished Name
 * @param {number} certFields.notBefore 代表证书有效性起始时间的unix时间戳
 * @param {number} certFields.notAfter 代表证书有效性终止时间的unix时间戳
 * @param {Object} certFields.keypair 证书拥有方的密钥对,含有jsrsasign提供的prvKeyObj和pubKeyObj对象
 * @param {Object} [certFields.extensions] v3证书扩展属性
 * @returns {string} certPEM pem格式的自签名证书信息
 */
const CreateSelfSignedCertificate = (certFields) => {
    const certFieldsCoverted = {
        serialNumber: certFields.serialNumber,
        sigAlg: certFields.sigAlg,
        subjectDN: certFields.DN,
        issuerDN: certFields.DN,
        notBefore: certFields.notBefore,
        notAfter: certFields.notAfter,
        subjectPubKey: certFields.keypair.pubKeyObj,
        issuerPrvKey: certFields.keypair.prvKeyObj,
        extensions: certFields.extensions,
    };
    const certPEM = CreateCertificate(certFieldsCoverted);
    return certPEM;
};

/**
 * 导入已有的公钥证书信息
 * 
 * @param {string} certPEM 符合X.509标准的公钥证书信息
 * @returns {Object} x509 jsrsasign提供的X509对象实例
 */
const ImportCertificate = (certPEM) => {
    const x509 = new X509();
    x509.readCertPEM(certPEM);

    const getUnixTimestamp = (notBeforeOrNotAfterFormatTimestampStr) => {
        const format = notBeforeOrNotAfterFormatTimestampStr.length === 15 ? "YYYYMMDDHHmmssZ" : "YYMMDDHHmmssZ";
        return moment.utc(notBeforeOrNotAfterFormatTimestampStr, format).unix();
    };

    x509.getNotBeforeUnixTimestamp = () => getUnixTimestamp(x509.getNotBefore());

    x509.getNotAfterUnixTimestamp = () => getUnixTimestamp(x509.getNotAfter());

    return x509;
};

/**
 * 验证证书签名信息
 * 
 * @param {string} certPEM 符合X509标准的公钥证书信息
 * @param {Object} pubKey 证书签发者的公钥对象
 * @returns {boolean} 证书签名验证结果
 */
const VerifyCertificateSignature = (certPEM, pubKey) => {
    const x509 = ImportCertificate(certPEM);
    return x509.verifySignature(pubKey);
};

export {
    GetHashVal,
    CreateKeypair,
    ImportKey,
    GetKeyPEM,
    Sign,
    VerifySign,
    CalculateAddr,
    CreateCSR,
    GetCSRInfoFromPEM,
    CreateCertificate,
    CreateSelfSignedCertificate,
    VerifyCertificateSignature,
    ImportCertificate,
};