// const bignum=require('bignumber.js') // 处理整数 https://github.com/MikeMcl/bignumber.js // size: 360K // const bigint = require('big-integer') // 处理整数 https://github.com/peterolson/BigInteger.js // size: 188K. ethers.js 24M. // 20241005 发现,在 node 控制台里,导入本库命名为 BigInt 后运行 BigInt(xxx) 会导致失败!因为现在已经有 JS 的新 primitive 也叫 BigInt, 不知道作为 server 运行时,怎么没有报错。改名为 bigint const crypto = require('crypto') const nacl = require('tweetnacl') const bs58check = require('bs58check') const bs58 = require('bs58') // bs58check depends on bs58 //const uuid = require('uuid') const keccak = require('keccak') const ecc = require('eccrypto-js') // 用于加解密。eccrypto 在 windows 上和 openssl 的版本兼容性有点麻烦,所以换用 eccrypto-js const keyman = require('js-crypto-key-utils') // 转换原始密钥和 PER/DER 格式。 const ecrsa = require('ethereum-rsa') // const BitcoreMnemonic = require('bitcore-mnemonic') // https://bitcore.io/api/mnemonic/ https://github.com/bitpay/bitcore-mnemonic // 打包成 app 里常有问题,试图访问 window 变量,无法生成 secword const bip39 = require('bip39') // https://github.com/bitcoinjs/bip39 // 有更多语言,但不方便选择语言,也不能使用 pass const hdkey = require('hdkey') // https://github.com/cryptocoinjs/hdkey // 或者用 bitcore-mnemonic 或者 ethers 里的相同功能 // const bitcorelib = require('bitcore-lib') const secp256k1 = require('secp256k1') const base32encode = require('base32-encode') const base32decode = require('base32-decode') // 全部以hex为默认输入输出格式,方便人的阅读,以及方便函数之间统一接口 const my = {} my.HASHER = 'sha256' // 默认的哈希算法。could be md5, sha1, sha256, sha512, ripemd160 and much more。 可用 Crypto.getHashes/Ciphers/Curves() 查看支持的种类。 my.HASHER_LIST = typeof crypto.getHashes === 'function' ? crypto.getHashes() : [my.HASHER] my.CIPHER = 'aes-256-cfb' // 默认的加解密算法 my.CIPHER_LIST = typeof crypto.getCiphers === 'function' ? crypto.getCiphers() : [my.CIPHER] my.CURVE = 'secp256k1' // 默认的ECDH曲线,用于把私钥转成公钥。 my.CURVE_LIST = typeof crypto.getCurves === 'function' ? crypto.getCurves() : [my.CURVE] // crypto.getCurves() 引入到浏览器里后出错,不支持 getCurves. my.OUTPUT = 'hex' // 默认的哈希或加密的输入格式 my.OUTPUT_LIST = ['hex', 'latin1', 'base64'] // or 'buf' to Buffer explicitly my.INPUT = 'utf8' // 默认的加密方法的明文格式。utf8 能够兼容 latin1, ascii 的情形 my.INPUT_LIST = ['utf8', 'ascii', 'latin1'] // ignored for Buffer/TypedArray/DataView my.COIN = 'TIC' // 默认的币种 my.COIN_FAMILY = 'TIC' my.COIN_FAMILY_LIST = ['TIC', 'BTC', 'ETH'] my.WORLD = 'COMET' my.REGEXP_ALPHABET = { hex: /^[0-9a-fA-F]+$/, b32: /^[A-Za-z2-7=]+$/, b32h: /^[0-9A-Va-v=]+$/, b36: /^[0-9A-Z-a-z]+$/, b58: /^[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]+$/, b62: /^[A-Za-z0-9]+$/, b64: /^[A-Za-z0-9\+\/=]+$/, b64u: /^[A-Za-z0-9\-_]+$/, b64t: /^[A-Za-z0-9\._]+$/, } let lm = {} lm.cn = lm.zh = lm.zhcn = lm.china = lm.chinese = lm.chinese_simplified = 'chinese_simplified' lm.tw = lm.zhtw = lm.taiwanese = lm.chinese_traditional = 'chinese_traditional' lm.en = lm.us = lm.uk = lm.enus = lm.enlish = 'english' lm.fr = lm.frfr = lm.france = lm.french = 'french' lm.it = lm.itit = lm.italy = lm.italian = 'italian' lm.ko = lm.kr = lm.kokr = lm.korean = lm.koren = 'korean' lm.ja = lm.jp = lm.jajp = lm.japan = lm.japanese = 'japanese' lm.es = lm.eses = lm.spanish = 'spanish' my.langMap = lm my.langList = [lm.cn, lm.tw, lm.en, lm.fr, lm.it, lm.ko, lm.ja, lm.es] my.LANG = 'english' /** * * @class TicCrypto */ class TicCrypto { /** * 测试输入数据是否可哈希混淆 * * @static * @param {*} data 需要被哈希混淆的数据 * @param {*} option 可选参数 * @return {Boolean} * @memberof TicCrypto */ static is_hashable ({ data, strict = false } = {}) { if (strict) { return typeof data !== 'boolean' && data !== Infinity && data ? true : false // 允许大多数数据,除了null、''、0、布尔值、无限数。注意 data 要放在最后,否则会被 return 直接返回,而不是返回 Boolean } return typeof data !== 'undefined' // 允许一切数据,除非 undefined } /** * 测试是否有效的哈希值 * * @static * @param {String} hash * @param {Object} option [{ hasher = my.HASHER }={}] * @return {Boolean} * @memberof TicCrypto */ static is_hash ({ hash, hasher = my.HASHER } = {}) { if (my.HASHER_LIST.includes(hasher)) { switch (hasher) { case 'sha256': return /^[a-fA-F0-9]{64}$/.test(hash) case 'md5': return /^[a-fA-F0-9]{32}$/.test(hash) case 'ripemd160': case 'sha1': return /^[a-fA-F0-9]{40}$/.test(hash) case 'sha512': return /^[a-fA-F0-9]{128}$/.test(hash) } } return false } /** * 测试是否合法的助记词 * * @static * @param {String} secword * @param {Object} option [{ mode = 'strict' }={}] * @return {Boolean} * @memberof TicCrypto */ static is_secword ({ secword = '', mode = 'strict', lang } = {}) { // 注意 not all 12 words combinations are valid for both bitcore and bip39, because there are checksum in mnemonic. 另外,实际上bitcore和bip39对 12, 15, 18, 21, 24 长度的合法助记词都返回 true。 //// for bitcore-mnemonic. 注意,bitcore-mnemonic 对少于12词的会抛出异常,很蠢。 // if (typeof secword==='string' && [12,15,18,21,24].includes(secword.split(/ +/).length)) // return BitcoreMnemonic.isValid(secword) // else // return false //// for bip39. 注意,bip39对当前defaultWordlist之外其他语言的合法 mnemonic 也返回 false,这一点不如 bitcore-mnemonic. 所以不能直接 bip39.validateMnemonic(secword) secword = secword.trim() if (typeof secword === 'string' && [12, 15, 18, 21, 24].includes(secword.split(/\s+/).length)) { if (mode === 'easy') return true // easy模式不检查校验等等严格的合法性了,反正 secword_to_seed 是接受一切字符串的 if (my.langMap[lang?.toLowerCase?.()]) { // 指定了语言则针对该语言词库检查 return bip39.validateMnemonic(secword, bip39.wordlists[my.langMap[lang.toLowerCase()]]) } else { // 未指定语言则检查所有可能语言词库 for (let lang of my.langList) { if (bip39.validateMnemonic(secword, bip39.wordlists[lang])) { return true } } } } return false } /** * 测试是否合法的私钥 * * @static * @param {String} prikey * @return {Boolean} * @memberof TicCrypto */ static is_prikey ({ prikey } = {}) { // 比特币、以太坊的私钥:64 hex // nacl.sign 的私钥 128 hex, nacl.box 的私钥 64 hex return /^([a-fA-F0-9]{128}|[a-fA-F0-9]{64})$/.test(prikey) } /** * 测试是否合法的公钥 * * @static * @param {String} pubkey * @return {Boolean} * @memberof TicCrypto */ static is_pubkey ({ pubkey } = {}) { // 比特币的公钥:压缩型 '02|03' + 64 hex 或 无压缩型 '04' + 128 hex // 以太坊的公钥:'02|03' + 64 hex // nacl.sign 的公钥:64 hex return /^((02|03)?[a-fA-F0-9]{64}|04[a-fA-F0-9]{128})$/.test(pubkey) // "d2f186a630f5558ba3ede10a4dd0549da5854eab3ed28ee8534350c2535d38b0" } /** * 测试是否合法的签名 * * @static * @param {String} signature * @return {Boolean} * @memberof TicCrypto */ static is_signature ({ sig } = {}) { return /^[a-fA-F0-9]{128,144}$/.test(sig) && sig.length % 2 === 0 // 128 for nacl, 140/142/144 for crypto and eccrypto in der format. } /** * 哈希混淆 * * @static * @param {*} data * @param {option} [{ hasher = my.HASHER, salt, input = my.INPUT, output = my.OUTPUT }={}] * @return {String} * @memberof TicCrypto * 返回结果不包含 0x */ static hash_easy (data, { hasher = my.HASHER, salt, input = my.INPUT, output = my.OUTPUT } = {}) { // data can be anything, but converts to string or remains be Buffer/TypedArray/DataView if (this.is_hashable({ data })) { if (typeof data !== 'string' && !(data instanceof Buffer) && !(data instanceof DataView)) data = JSON.stringify(data) if (salt && typeof salt === 'string') data = data + this.hash_easy(salt) let inputEncoding = input // my.INPUT_LIST.includes(input)?input:my.INPUT // 'utf8', 'ascii' or 'latin1' for string data, default to utf8 if not specified; ignored for Buffer, TypedArray, or DataView. let outputEncoding = output === 'buf' ? undefined : output // (my.OUTPUT_LIST.includes(output)?output:my.OUTPUT) // output: 留空=》默认输出hex格式;或者手动指定 'buf', hex', 'latin1' or 'base64' return crypto.createHash(hasher).update(data, inputEncoding).digest(outputEncoding) } return null } /** * 加密 * * @static * @param {*} data * @param {*} option [{ mode, key, input, output, cipher }={}] * @return {String} * @memberof TicCrypto */ static async encrypt_easy ({ data, mode = 'semkey', key, input = my.INPUT, output = my.OUTPUT, cipher = my.CIPHER } = {}) { if (typeof data !== 'string' && !(data instanceof Buffer) && !(data instanceof DataView)) data = JSON.stringify(data) if (mode === 'ecrsa') { if (key?.prikey && key?.pubkey) { return ecrsa.encryptMessage(data, key?.prikey, key?.pubkey) } else { return ecrsa.encryptMessage(data, key?.senderPrikey, key?.receiverPubkey) } } else if (mode === 'ecc') { // data 应当是 utf8 的字符串。key 必须是 pubkey // eccrypto 能用 Uint8Array 和 Buffer // eccrypto-js 只能用 Buffer // 在浏览器里 https://github.com/bitchan/eccrypto 库报错,即使用了 Uint8Array: Failed to execute 'encrypt' on 'SubtleCrypto': The provided value is not of type '(ArrayBuffer or ArrayBufferView)' let cipherObject = await ecc.encrypt(Buffer.from(this.hex_to_buf(key)), data) return cipherObject // 返回一个复杂的结构 {iv:Buffer, ciphertext:Buffer, ...}。对同样的key和data,每次返回的结果不一样 } else if (mode === 'semkey') { // 对称加密 let inputEncoding = input // 'utf8' by default, 'ascii', 'latin1' for string or ignored for Buffer/TypedArray/DataView let outputEncoding = output === 'buf' ? undefined : output // 'latin1', 'base64', 'hex' by default or 'buf' to Buffer explicitly const iv = crypto.randomBytes(16) let encryptor = crypto.createCipheriv(cipher, this.hex_to_buf(this.hash_easy(key)), iv) // cipher 和 key 的长度必须相同,例如 cipher 是 ***-192,那么 key 就必须是 192/8=24 字节 = 48 hex 的。 let ciphertext = encryptor.update(data, inputEncoding, outputEncoding) ciphertext += encryptor.final(outputEncoding) // 但是 Buffer + Buffer 还是会变成string return { iv: iv.toString('hex'), ciphertext } // 有 iv,显然每次结果不一样 } else if (mode === 'prikey') { // 只能用于 crypto.generateKeyPairSync('rsa') 生成的 rsa 公私钥,不能用于 Elliptic Curve 的公私钥 let prikeyPEM = await new keyman.Key('oct', this.hex_to_buf(key), { namedCurve: 'P-256K' }).export('pem') // 私钥导出的der格式为144字节。 return crypto.privateEncrypt(prikeyPEM, Buffer.from(data)) // 返回 Buffer。每次结果都一样。 } else if (mode === 'pubkey') { // 只能用于 crypto.generateKeyPairSync('rsa') 生成的 rsa 公私钥,不能用于 Elliptic Curve 的公私钥 let pubkeyPEM = await new keyman.Key('oct', this.hex_to_buf(key), { namedCurve: 'P-256K' }).export('pem') return crypto.publicEncrypt(pubkeyPEM, Buffer.from(data)) // 返回 Buffer。每次结果不一样。 } return null } /** * 解密 * * @static * @param {*} data * @param {Object} option [{ mode, key, input, output, cipher, format }={}] * @return {String} * @memberof TicCrypto */ static async decrypt_easy ({ data = {}, mode = 'semkey', key, input = my.OUTPUT, output = 'utf8', cipher = my.CIPHER } = {}) { // data 应当是 encrypt 输出的数据类型 if (mode === 'ecrsa') { if (key?.prikey && key?.pubkey) { return ecrsa.decryptMessage(data, key.prikey, key.pubkey) } else if (key?.receiverPrikey && key?.senderPubkey) { return ecrsa.decryptMessage(data, key.receiverPrikey, key.senderPubkey) } else { return null } } else if (mode === 'ecc') { try { // eccrypto 只能接受 Buffer, 不接受 Uint8Array, 因为 eccrypto 需要调用 Buffer.compare 方法,不能在这里直接用 hex_to_buf // eccrypto 也只能接受 Buffer, 不接受 Uint8Array // data 需要是 eccrypto 自身encrypt方法返回的 cipherObject. key 是 private key。 let plainbuffer = await ecc.decrypt(Buffer.from(key, 'hex'), data) // 返回的是 Buffer return plainbuffer.toString('utf8') } catch (exception) { // eccrypto 对无法解密的,会抛出异常 return null } } else if (mode === 'semkey' && data?.cipher && data?.iv) { // 对称解密 if (typeof data.ciphertext === 'string' || data.ciphertext instanceof Buffer) { let inputEncoding = input // input (=output of encrypt) could be 'latin1', 'base64', 'hex' by default for string or ignored for Buffer let outputEncoding = output === 'buf' ? undefined : output // output (=input of encrypt) could be 'latin1', 'ascii', 'utf8' by default or 'buf' to Buffer explicitly let decryptor = crypto.createDecipheriv(cipher, this.hex_to_buf(this.hash_easy(key)), Buffer.from(data.iv, 'hex')) let decrypted = decryptor.update(data.ciphertext, inputEncoding, outputEncoding) decrypted += decryptor.final(outputEncoding) // 但是 Buffer + Buffer 还是会变成string // 如果用户输入错误密钥,deciper也能解密,无法自动判断是否正确结果。可在返回后人工判断。 return decrypted } } else if (mode === 'prikey') { // 只能用于 crypto.generateKeyPairSync('rsa') 生成的 rsa 公私钥,不能用于 Elliptic Curve 的公私钥 let prikeyPEM = await new keyman.Key('oct', this.hex_to_buf(key), { namedCurve: 'P-256K' }).export('pem') // 私钥导出的der格式为144字节。 return crypto.privateDecrypt(prikeyPEM, Buffer.from(data)) // 返回 Buffer。 } else if (mode === 'pubkey') { // 只能用于 crypto.generateKeyPairSync('rsa') 生成的 rsa 公私钥,不能用于 Elliptic Curve 的公私钥 let pubkeyPEM = await new keyman.Key('oct', this.hex_to_buf(key), { namedCurve: 'P-256K' }).export('pem') return crypto.publicDecrypt(pubkeyPEM, Buffer.from(data)) // 返回 Buffer。 } return null } /** * 签名 * * @static * @param {*} data * @param {String} prikey * @param {Object} option [option={}] * @return {String} * @memberof TicCrypto */ static async sign_easy ({ data, prikey, tool = 'crypto', hasher = my.HASHER }) { // data can be string or buffer or object, results are the same if (this.is_hashable({ data }) && this.is_prikey({ prikey })) { if (tool === 'nacl' && prikey.length === 128) { // 使用 nacl 的签名算法。注意,nacl.sign 需要的 prikey 是64字节=128字符。 let hashBuf = this.hash_easy(data, { output: 'buf' }) // 哈希必须输出为 buffer let signature = nacl.sign.detached(hashBuf, Buffer.from(prikey, 'hex')) return Buffer.from(signature).toString('hex') // 签名是64节,128个hex字符 } else if (tool === 'ecc' && prikey.length === 64) { // eccrypto 对同一组data, prikey 生成的签名是固定的,观察到hex长度为140或142,是der格式。 let signature = await ecc.sign(Buffer.from(prikey, 'hex'), this.hash_easy(data, { output: 'buf' })) return signature.toString('hex') } else if (prikey.length === 64) { // 纯 crypto let prikeyPEM = await new keyman.Key('oct', this.hex_to_buf(prikey), { namedCurve: 'P-256K' }).export('pem') // 私钥导出的der格式为144字节。 let signer = crypto.createSign(hasher) // 注意,不知为何,hasher必须含有'sha'才能完成签名,例如 sha1, sha256, sha512, sha3, RSA-SHA1, id-rsassa-pkcs1-v1_5-with-sha3-224, 其他都会报错。 signer.update(this.hash_easy(data)).end() let signature = signer.sign(prikeyPEM, 'hex') // since nodejs 12, 有了 crypto.sign 方法,但在浏览器中无效: // let signature = crypto.sign(hasher, Buffer.from(this.hash_easy(data)), prikeyPEM).toString('hex') return signature // 发现同样的输入,nodejs里每次调用会生成不同的 signature, 且长度不定(140,142,144 hex) 但都可以通过 verify。但在浏览器里调用,signature却是固定的。 } } return null } /** * 验证签名 * * @static * @param {*} data * @param {String} signature * @param {String} pubkey * @param {Object} option [option={}] * @return {Boolean} * @memberof TicCrypto */ static async verify_easy ({ data, signature, pubkey, tool = 'crypto', hasher = my.HASHER }) { // data could be anything, but converts to string or remains be Buffer/TypedArray/DataView if (this.is_hashable({ data }) && this.is_signature({ sig: signature }) && this.is_pubkey({ pubkey })) { if ('nacl' === tool && signature.length === 128) { let bufHash = this.hash_easy(data, { output: 'buf' }) let bufSignature = Buffer.from(signature, 'hex') let bufPubkey = Buffer.from(pubkey, 'hex') let verified = nacl.sign.detached.verify(bufHash, bufSignature, bufPubkey) return verified } else if ('ecc' === tool && signature.length >= 140) { // 默认使用 eccrypto // 发现大小写不影响 eccrypto 验签!都能通过 try { let result = await ecc.verify(Buffer.from(pubkey, 'hex'), this.hash_easy(data, { output: 'buf' }), Buffer.from(signature, 'hex')) // 如果给signature添加1位hex,eccrypto 的 verify结果也是true! 估计因为一位hex不被转成字节。 return true } catch (exception) { // 对能够验证的,eccrypto返回 null;对无法验证的,抛出异常 return false } } else if (signature.length >= 140) { // 纯 crypto // 发现大小写不影响 crypto 验签!都能通过 let pubkeyPEM = await new keyman.Key('oct', this.hex_to_buf(pubkey), { namedCurve: 'P-256K' }).export('pem') // 公钥导出的der格式为88字节。经测试,同一对压缩和非压缩公钥得出的结果一模一样。 let verifier = crypto.createVerify(hasher) verifier.update(this.hash_easy(data)).end() // end() 在 nodejs 12 里返回verifier自身,但在浏览器里返回 undefined,因此不能串联运行。 let verified = verifier.verify(pubkeyPEM, signature, 'hex') // 如果给signature添加1位hex,crypto 的 verify结果也是true! 估计因为一位hex不被转成字节。但减少1位会导致false // since nodejs 12, 有了 crypto.verify 方法,但在浏览器中无效: // let verified = crypto.verify(hasher, Buffer.from(this.hash_easy(data)), pubkeyPEM, Buffer.from(signature, 'hex')) return verified } } return false } /** * 从密码到公私钥 * * @static * @param {String} pass * @param {Object} option * @return {Object} {pubkey, prikey, address,} * @memberof TicCrypto */ static pass_to_keypair ({ pass, hasher = my.HASHER } = {}) { // 如果使用其他机制,例如密码、随机数,不使用secword,也可生成keypair if (this.is_hashable({ data: pass })) { var hashBuf = crypto.createHash(hasher).update(pass).digest() var keypair = nacl.sign.keyPair.fromSeed(hashBuf) // nacl的seed要求是32字节 return { hash: hashBuf.toString('hex'), pubkey: Buffer.from(keypair.publicKey).toString('hex'), // 测试过 不能直接keypair.publicKey.toString('hex'),不是buffer类型 prikey: Buffer.from(keypair.secretKey).toString('hex'), } } return null } /** * 从墒到助记词 * * @static * @param {*} entropy * @return {String} * @memberof TicCrypto */ static entropy_to_secword ({ entropy } = {}) { // entropy could be hex string or buffer. 位数可为 128|160|192|224|256 位,即 16|20|24|28|32 字节,最后可生成 12|15|18|21|24 个单词的助记词。 return bip39.entropyToMnemonic(entropy) // results are the same for the same entropy. } /** * 从助记词到墒 * * @static * @param {String} secword * @return {*} * @memberof TicCrypto */ static secword_to_entropy ({ secword, lang = my.LANG } = {}) { // secword could be of length 12|15|18|21|24,which outputs hex of length 32|40|48|56|64. try { return bip39.mnemonicToEntropy(secword, bip39.wordlists[my.langMap[lang.toLowerCase()]]) // results are the same for the same secword } catch (exception) { // 如果助记词不合法(例如,语言不符合,长度非法,校验码不正确),会抛出异常。 return '' } } /** * 从助记词到公私钥 * * @static * @param {String} secword * @param {Object} option * @return {Object} {pubkey, prikey,} * @memberof TicCrypto */ static secword_to_keypair ({ secword, coin, pass, pathSeed, pathIndex, path, tool, hasher = my.HASHER } = {}) { // coin 币种; // passphase 密码,默认为空; // path 规范为 m/Purpose'/CoinType'/Account'/Change/Index (https://learnblockchain.cn/2018/09/28/hdwallet/), 其中 // Purpose===44 for BIP44, // CoinType===0 for BTC, 60 for ETH. (https://github.com/satoshilabs/slips/blob/master/slip-0044.md) // Change===常量 0 用于外部链,常量 1 用于内部链(也称为更改地址)。外部链用于在钱包外可见的地址(例如,用于接收付款)。内部链用于在钱包外部不可见的地址,用于返回交易变更。 (所以一般使用 0) // Index 地址索引,从 0 开始,代表生成第几个地址,官方建议,每个 account 下的 address_index 不要超过 20。 // 据测试, Purpose和CoinType都可以任意其他值,不必要如规范所示;' 引号可有可无,导致的密钥不一样; // Account 最大为 0x7FFFFFFF, Change/Index 最大均为 0xFFFFFFFF(=4294967295) // 但可以不断延伸下去:/xxx/xxx/xxx/xxx/... coin = coin?.toUpperCase?.() || my.COIN if (!this.is_secword({ secword })) { // 由于 secword_to_seed 可以对一切字符串都正常返回,为防止secword为空,在这里先做检查。 return null } if (tool === 'nacl') { // 采用自己的算法:bip39算法从secword到种子,hash后用 nacl.sign.keyPair.fromSeed()方法。 let hashBuf = crypto.createHash(hasher).update(this.secword_to_seed({ secword, pass })).digest() let keypair = nacl.sign.keyPair.fromSeed(hashBuf) // nacl.sign.keyPair.fromSeed 要求32字节的种子,而 this.secword2seed生成的是64字节种子,所以要先做一次sha256 return { pubkey: Buffer.from(keypair.publicKey).toString('hex'), // 测试过 不能直接keypair.publicKey.toString('hex'),不是buffer类型 prikey: Buffer.from(keypair.secretKey).toString('hex'), // nacl.sign.keyPair.fromSeed 得到的 prikey 是64字节的,不同于比特币/以太坊的32字节密钥。 tool, } } else { // 用 bip39 算法从 secword 到种子,再用 bip32 算法从种子到根私钥。这是比特币、以太坊的标准方式,结果一致。 let hdmaster = hdkey.fromMasterSeed(Buffer.from(this.secword_to_seed({ secword, pass }), 'hex')) // == new BitcoreMnemonic(secword).toHDPrivateKey(pass) 返回公私钥 == ethers.utils.HDNode.fromMnemonic(secword) 返回地址/密语/公私钥。而进一步 hdkey.fromMasterSeed(...).derive("m/44'/60'/0'/0/0") == ethers.utils.HDNode.fromMnemonic(secword).derivePath("m/44'/60'/0'/0/0") == ethers.Wallet.fromMnemonic(secword [,"m/44'/60'/0'/0/0"]) (注意,不是完全等价,数据结构有所不同,但是代表的公私钥或地址的本质是相等的) let key = hdmaster if (path === 'master') { // 不建议使用主钥,因此强制必须指定 master 才返回主钥。 key = hdmaster } else if (path) { // 指定了path 例如 "m/0/2147483647'/1" 则用 path 例如 不存在 pathSeed 时获取的是根路径 "m/44'/0'/0'/0/0" 或 "m/44'/60'/0'/0/0" key = hdmaster.derive(path) } else { // 调用 root_to_path() 来获取路径。如果 path/pathSeed/pathIndex 全都不存在,就返回标准默认路径,和 ethers.Wallet.fromMnemonic(...) 结果保持一致 path = this.root_to_path({ pathSeed, pathIndex, coin }) key = hdmaster.derive(path) } return { path, prikey: key.privateKey.toString('hex'), // 或者 key.toJSON().privateKey 如果来自 BitcoreMnemonic。或者 key.privateKey.slice(2) 删除开头的'0x'如果来自ethers.utils.HDNode.fromMnemonic(secword) pubkey: key.publicKey.toString('hex'), } } } /** * 从种子到路径 * * @static * @param {*} pathSeed * @param {string} option [{ coin = my.COIN }={ }] * @return {String} path * @memberof TicCrypto */ static root_to_path ({ pathSeed, pathIndex, coin = my.COIN } = {}) { // 路径规范 BIP44: m/Purpose'/Coin'/Account'/Change/Index, // 但实际上 Purpose, Coin 都可任意定;' 可有可无; // 后面还可继续延伸 /xxx/xxx/xxx/...... // 每个数字最大到 parseInt("0x7FFFFFFF", 16)=parseInt(0x7FFFFFFF)=2147483647,更大就报错。 let path if (pathSeed) { let pathHash = this.hash_easy(pathSeed, { hasher: 'md5' }) let part0 = parseInt(pathHash.slice(0, 6), 16) let part1 = parseInt(pathHash.slice(6, 12), 16) let part2 = parseInt(pathHash.slice(12, 18), 16) let part3 = parseInt(pathHash.slice(18, 24), 16) let part4 = parseInt(pathHash.slice(24, 30), 16) let part5 = parseInt(pathHash.slice(30, 32), 16) path = `${part0}'/${part1}/${part2}/${part3}/${part4}/${part5}` } else { // 本方法也可用来生成默认路径,例如 "m/44'/0'/0'/0/0" path = "0'/0" // Account'/Change } // 注意,如果 pathIndex 为 undefinded 或者非数字,得出的 path 是不同的! pathIndex = parseInt(pathIndex) || 0 if (0 <= pathIndex && pathIndex <= 0x7fffffff) { path += `/${pathIndex}` } coin = coin?.toUpperCase?.() if (coin === 'BTC') { return `m/44'/0'/${path}` } else if (coin === 'ETH') { return `m/44'/60'/${path}` } else if (coin === 'TIC') { return `m/44'/60000'/${path}` } else if (coin === 'MATIC' || coin === 'POL') { // Polygon 测试网 (Mumbai): 80001 return `m/44'/137'/${path}` } else { return `m/44'/60${this.alpha_to_digit(coin)}'/${path}` } } static alpha_to_digit (name = '') { let digits = name .toLowerCase() .replace(/[abc]/g, 2) .replace(/[def]/g, 3) .replace(/[ghi]/g, 4) .replace(/[jkl]/g, 5) .replace(/[mno]/g, 6) .replace(/[pqrs]/g, 7) .replace(/[tuv]/g, 8) .replace(/[wxyz]/g, 9) return parseInt(digits) } /** * 从助记词到账户 * * @static * @param {String} secword * @param {Object} option * @return {Object} * @memberof TicCrypto * 只要提供了 path 或 pathRoot,就创建 bip39 账户。如果都不存在,那就创建主账户。 */ static secword_to_account ({ secword, coin, coinFamily, world, pass, pathSeed, pathIndex, path, tool, hasher } = {}) { // account 比 keypair 多了 address 字段。 coin = coin?.toUpperCase?.() || my.COIN coinFamily = coinFamily?.toUpperCase?.() || my.COIN_FAMILY let kp = this.secword_to_keypair({ secword, coin, pass, pathSeed, pathIndex, path, tool, hasher }) if (kp) { if (coin === 'ETH' || coinFamily === 'ETH') { world = world || 'mainnet' kp.address = this.pubkey_to_address({ pubkey: this.decompress_pubkey(kp.pubkey), coin, coinFamily, world }) } else if (coin === 'BTC' || coinFamily === 'BTC') { world = world || 'mainnet' kp.address = this.pubkey_to_address({ pubkey: kp.pubkey, coin, coinFamily, world }) } else { world = world || my.WORLD kp.address = this.pubkey_to_address({ pubkey: kp.pubkey, coin, coinFamily, world }) } return { ...kp, coin, coinFamily, world, secword } } return null } /** * 从助记词到地址 * * @static * @param {String} secword * @param {Object} option * @return {String} address * @memberof TicCrypto */ static secword_to_address (options = {}) { return this.secword_to_account(options)?.address } /** * 从私钥到公钥 * * @static * @param {*} prikey * @param {*} [option={}] * @return {*} * @memberof TicCrypto */ static prikey_to_pubkey ({ prikey, curve, uncompress } = {}) { if (this.is_prikey({ prikey }) && prikey.length === 64) { // 只能用于32字节的私钥(BTC, ETH)。也就是不能用于 TIC 的私钥。 curve = my.CURVE_LIST.includes(curve) ? curve : my.CURVE // 默认为 secp256k1 // return new crypto.createECDH(curve).setPrivateKey(prikey,'hex').getPublicKey('hex', uncompress?'uncompressed':'compressed') // ecdh.getPublicKey(不加参数) 默认为 'compressed'。用 HBuilderX 2.6.4 打包成ios或安卓 app 后 setPrivateKey() 报错:TypeError: null is not an object (evaluating 'this.rand.getBytes') // 从 nodejs 10.0 开始,还有 crypto.ECDH.convertKey 方法,更直接。但可惜,浏览器里不存在 crypto.ECDH。 return this.buf_to_hex(secp256k1.publicKeyCreate(Buffer.from(prikey, 'hex'), !uncompress)) // 可用于浏览器。缺省输出压缩公钥,第二个参数必须正好为false时输出非压缩公钥,为undefined或true时输出压缩公钥,其他时报错。 // 或者 bitcorelib.PublicKey.fromPrivateKey(new bitcorelib.PrivateKey(prikey)).toString('hex') // 可用于浏览器 // 或者 const ecc = require('eccrypto') // if (!uncompress){ // return ecc.getPublicCompressed(this.hex_to_buf(prikey)).toString('hex') // } else{ // return ecc.getPublic(this.hex_to_buf(prikey)).toString('hex') // } // 注意,Buffer.from(nacl.box.keyPair.fromSecretKey(Buffer.from(prikey,'hex')).publicKey).toString('hex') 得到的公钥与上面的不同 } else if (this.is_prikey({ prikey }) && prikey.length === 128) { // 用于64字节=128 hex的 TIC 私钥 let keypair = nacl.sign.keyPair.fromSecretKey(Buffer.from(prikey, 'hex')) return Buffer.from(keypair.publicKey).toString('hex') // 测试过 不能直接keypair.publicKey.toString('hex'),不是buffer类型 } return null } /** * 从私钥到地址 * * @static * @param {*} prikey * @param {*} option * @return {*} * @memberof TicCrypto */ static prikey_to_address ({ prikey, coin, coinFamily, world } = {}) { coin = coin?.toUpperCase?.() || my.COIN if (this.is_prikey({ prikey })) { /** @type {*} */ let pubkey if (coin === 'ETH' || coinFamily === 'ETH') { pubkey = this.prikey_to_pubkey({ prikey, uncompress: true }) return this.pubkey_to_address({ pubkey, coin, coinFamily, world }) // 实际上发现,不论是否 compressed,最后转成的地址都是一样的,因为在 pubkey_to_position 里已经自动处理了。 } else { pubkey = this.prikey_to_pubkey({ prikey, uncompress: false }) return this.pubkey_to_address({ pubkey, coin, coinFamily, world }) } } return null } /** * 从公钥到位置 * * @static * @param {*} pubkey * @param {*} [{ coin }={}] * @return {*} * @memberof TicCrypto * position 就是通常所说的 PubKeyHash,出现在比特币交易的锁定脚本里 */ static pubkey_to_position ({ pubkey, coin, coinFamily = my.COIN_FAMILY } = {}) { // tic, btc, eth 的 position 都是 20节=40字符的。 coin = coin?.toUpperCase?.() || my.COIN if (this.is_pubkey({ pubkey })) { if (coin === 'ETH' || coinFamily === 'ETH') { // 注意,必须要用非压缩的64字节的公钥的buffer,并去掉开头的 04。 if (pubkey.length === 66) { pubkey = this.decompress_pubkey(pubkey) } return keccak('keccak256') .update(Buffer.from(pubkey.slice(2), 'hex')) .digest('hex') .slice(-40) } else { let h256buf = crypto.createHash('sha256').update(Buffer.from(pubkey, 'hex')).digest() let h160 = crypto.createHash('ripemd160').update(h256buf).digest('hex') return h160 } } return null } /** * 从位置到地址 * * @static * @param {*} position * @param {*} [{ coin, world }={}] * @return {*} * @memberof TicCrypto */ static position_to_address ({ position, coin, world, coinFamily } = {}) { if (!/^[\da-fA-F]{40}$/.test(position)) return null // 不论 tic, btc, eth,其 position 都是 40字符的。 coin = coin?.toUpperCase?.() || my.COIN let address if (coin === 'ETH' || coinFamily === 'ETH') { // 对以太坊,按照 EIP55,把纯位置转换为大小写敏感能自我验证的hex地址。仍然为20节=40符。 position = position.toLowerCase().replace('0x', '') let hash = keccak('keccak256').update(position).digest('hex') address = '0x' for (var i = 0; i < position.length; i++) { if (parseInt(hash[i], 16) >= 8) { address += position[i].toUpperCase() } else { address += position[i] } } return address } else if (coin === 'BTC' || coinFamily === 'BTC') { // 对比特币,把纯位置转换为大小写敏感能自我验证的bs58check地址:先加前缀1节,再加校验4节,共25字节,再转base58。得到26~34个字符,大多数34个。 // Base58: https://en.bitcoin.it/wiki/Base58Check_encoding // https://en.bitcoin.it/wiki/List_of_address_prefixes let prefix switch (world) { case 'mainnet': prefix = '00' break // pubkey hash => 1 case 'mainnetSh': prefix = '05' break // script hash => 3 case 'testnet': prefix = '6f' break // testnet pubkey hash => m or n case 'testnetSh': prefix = 'c4' break // testnet script hash => 2 case 'namecoin': prefix = '34' break // Namecoin pubkey hash => M or N case 'compact': prefix = '15' break // compact pubkey (proposed) => 4 default: prefix = '00' } address = bs58check.encode(Buffer.from(prefix + position, 'hex')) // wallet import format return address } else { // 默认为 TIC 系列。把纯位置转换为大小写敏感能自我验证的 b64t 地址。 let prefix switch (world) { // Base64: https://baike.baidu.com/item/base64 case 'EARTH': prefix = '4c' break // Base58: 0x42=66 => T, Base64: T=0x13=0b00010011 => 0b010011xx = 0x4c~4f case 'MOON': prefix = 'b4' break // Base58: 0x7f=127,0x80=128 => t, Base64: t=0x2d=0b00101101 => 0b101101xx = 0xB4~B7 case 'COMET': prefix = '74' break // Base58: 0x90 => d, Base 64: d=0x1d=0b00011101 => 0b 011101xx = 0x74~77 default: prefix = '74' } let checksum = this.hash_easy(this.hash_easy(prefix + position)).slice(0, 6) // 添加 checksum 使得能够检测大小写错误。[todo] 校验码里要不要包含 prefix? // address = this.hex_to_eip55(prefix + position + checksum) // 前缀1节,位置20节,校验3节,共24节=48字符(能够完全转化为8个色彩),再转eip55。 address = this.hex_to_b64t(prefix + position + checksum) // 实际采用 b64t, 共 32字符。 return address } return null } /** * 从地址到位置 * * @static * @return {*} * @memberof TicCrypto * 地址和PubKeyHash(即position)之间能互相转化 */ static address_to_position () { if (/^0x[\da-fA-F]{40}$/.test(address)) { // ETH // todo: 如果是大小写敏感的,进行有效性验证 return address.toLowerCase() } else if (/^[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{26,34}$/.test(address)) { // BTC let hex = this.b58c_to_hex(address) if (hex) { return hex.slice(2) // 去除网络前缀 } } else if (/^[Tt][0-9a-zA-Z\._]{31}$/.test(address)) { // TIC // 格式合法 let hex = this.b64t_to_hex(address) let [all, prefix, position, checksum] = hex.match(/^([\da-fA-F]{2})([\da-fA-F]{40})([\da-fA-F]{6})$/) if (this.hash_easy(this.hash_easy(position)).slice(0, 6) === checksum) { return position } } return null } /** * 测试是否合法的地址 * * @static * @param {String} address * @return {Boolean} * @memberof TicCrypto */ static which_chain_address ({ address }) { if (/^(0x)?[\da-fA-F]{40}$/.test(address)) { return 'ETH' } else if (/^[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{26,34}$/.test(address) && address.length !== 32) { // 格式合法。常见的是 33或34字符长度 let prefixedPosition = this.b58c_to_hex(address) if (prefixedPosition && prefixedPosition.length === 42) // 内容合法 return 'BTC' } else if (/^[Ttd][0-9a-zA-Z\._]{31}$/.test(address)) { // 格式合法 let hex = Buffer.from(this.b64t_to_b64(address), 'base64').toString('hex') let [all, prefix, position, checksum] = hex.match(/^([\da-fA-F]{2})([\da-fA-F]{40})([\da-fA-F]{6})$/) // 内容合法 if (this.hash_easy(this.hash_easy(prefix + position)).slice(0, 6) === checksum) // [todo] 校验码里要不要包含 prefix? return 'TIC' } return null } /** * 从公钥到地址 * * @static * @param {*} pubkey * @param {*} [option={}] * @return {*} * @memberof TicCrypto */ static pubkey_to_address ({ pubkey, coin, world, coinFamily = my.COIN_FAMILY } = {}) { // pubkey 应当是string类型 coin = coin?.toUpperCase?.() || my.COIN return this.position_to_address({ position: this.pubkey_to_position({ pubkey, coin, coinFamily }), coin, world, coinFamily }) } /** * 从助记词到种子 * * @static * @param {*} secword * @param {*} pass * @return {*} * @memberof TicCrypto */ static secword_to_seed ({ secword, pass } = {}) { // 遵循bip39的算法。和 ether.HDNode.mnemonic2Seed 结果一样,是64字节的种子。 // !!! 警告,bip39.mnemonicToSeedSync 也接受不合法的 secword,只要是个string,或者是 undefined/null/0/''/false(这几个的结果都一样) return bip39.mnemonicToSeedSync(secword, pass).toString('hex') // 结果一致与 new BitcoreMnemonic(secword).toSeed(pass).toString('hex') 或 ethers.HDNode.mnemonic2Seed(secword)。 } /** * 生成随机的助记词 * accepts case-insensitive lang, such as 'chinese, cn, tw, en' * 1) 生成 128、160、192、224、256 位的随机墒 * 2)sha256 取前 墒长度/32 位作为校验和,即可为 4、5、6、7、8 位 * 3)在2048(=2^11)个词的表中,每11位指向一个词,共可生成 (128+4)/11=12, (160+5)/11=15, (192+6)/11=18, (224+7)/11=21, (256+8)/11=24 个词 * * @static * @param {string} [lang='english'] * @return {*} * @memberof TicCrypto */ static randomize_secword ({ lang = my.LANG, wordCount = 12 } = {}) { //// for BitcoreMnemonic // lang=lang?.toUpperCase?.() // let language = { ZHCN: 'CHINESE', ENUS: 'ENGLISH', FRFR: 'FRENCH', ITIT: 'ITALIAN', JAJP: 'JAPANESE', KOKR: 'KOREAN', ESES: 'SPANISH' }[lang] // || (BitcoreMnemonic.Words.hasOwnProperty(lang?.toUpperCase?.()) ? lang?.toUpperCase?.() : 'ENGLISH') // return new BitcoreMnemonic(BitcoreMnemonic.Words[language]).phrase // for bip39 const bitLength = { 12: 128, 15: 160, 18: 192, 21: 224, 24: 256 }[wordCount] || 128 // bip39.setDefaultWordlist(langMap[lang.toLowerCase()]) return bip39.generateMnemonic(bitLength, undefined, bip39.wordlists[my.langMap[lang.toLowerCase()]]) // 内部使用 crypto.randomBytes 来获取随机墒 } /** * 生成随机的私钥 * * @static * @param {*} [option={}] * @return {*} * @memberof TicCrypto */ static randomize_seckey ({ coin, tool } = {}) { // 跳过 secword 直接产生随机密钥 if (tool === 'nacl') { return crypto.randomBytes(64).toString('hex') // Buffer.from(nacl.sign.keyPair().secretKey).toString('hex') // 64字节 } else { return crypto.randomBytes(32).toString('hex') // Buffer.from(nacl.box.keyPair().secretKey).toString('hex') // 32字节 } } /** * 生成随机的公私钥 * * @static * @param {*} [option={}] * @return {*} * @memberof TicCrypto */ static randomize_keypair ({ tool, purpose = 'sign' } = {}) { let kp if (tool === 'nacl') { if (purpose === 'secret') { kp = nacl.box.keyPair() } else { kp = nacl.sign.keyPair() } return { prikey: Buffer.from(kp.secretKey).toString('hex'), pubkey: Buffer.from(kp.publicKey).toString('hex'), } } else { let prikey = this.randomize_seckey() let pubkey = this.prikey_to_pubkey({ prikey, uncompress: false }) return { prikey, pubkey, } } } /** * 生成随机的账户 * * @static * @param {*} [option={}] * @return {*} * @memberof TicCrypto */ static randomize_account ({ lang, wordCount, coin, coinFamily, world, pass, pathSeed, pathIndex, path, tool, hasher } = {}) { let secword = this.randomize_secword({ lang, wordCount }) return this.secword_to_account({ secword, coin, coinFamily, world, pass, pathSeed, pathIndex, path, tool, hasher }) } /** * * @param {*} param0 * @returns * Example: ({addressFormat:'0x.*55$', coin:'PEX', coinFamily:'ETH}) */ static customize_account ({ addressFormat = '^.*$', secwordFormat = '^.*$', ...rest } = {}) { let acc = this.randomize_account(rest) while (!new RegExp(addressFormat).test(acc.address) || !new RegExp(secwordFormat).test(acc.secword)) { acc = this.randomize_account(rest) } return acc } /** * 生成随机的字符串 * * @static * @param {number} [length=6] * @param {*} alphabet * @return {*} * @memberof TicCrypto */ static randomize_string ({ length = 6, alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789#$%^&*@' } = {}) { // 长度为 length,字母表为 alphabet 的随机字符串 var text = '' for (var i = 0; i < length; i++) { text += alphabet.charAt(Math.floor(Math.random() * alphabet.length)) } return text } static randomize_hex ({ length = 64 } = {}) { // 长度为 length 的随机 hex 字符串。注意 randomBytes 在一些环境里可能不存在,例如在 HBuilderX 的内置浏览器里。 if (crypto.randomBytes) { return crypto .randomBytes(Math.ceil(length / 2)) .toString('hex') .slice(0, length) } return this.randomize_string({ length, alphabet: '0123456789abcdef' }) } /** * 生成随机的数字 * * @static * @param {*} [{ length, min, max }={}] * @return {*} * @memberof TicCrypto */ static randomize_number ({ length, min, max } = {}) { // 长度为 length 的随机数字,或者 (min||0) <= num < max let num = 0 if (typeof length === 'number' && length > 0) { num = parseInt(Math.random() * Math.pow(10, length)) num = this.padStart(num.toString(), length, '0') } else if (typeof max === 'number' && max > 0) { min = typeof min === 'number' && min >= 0 ? min : 0 num = parseInt(Math.random() * (max - min)) + min } else { // 如果 option 为空 num = Math.random() } return num } /** * 向前补足 * * @static * @param {*} string * @param {*} targetLength * @param {*} symbol * @return {*} * @memberof TicCrypto */ static padStart (string, targetLength, symbol) { // 2020-03: 发现在浏览器里,还不支持 string.padStart(),只好自己写个暂代。 let padLength = targetLength - string.length for (let index = 1; index <= padLength; index++) { string = symbol + string } return string } /** * 生成 uuid * * @static * @memberof TicCrypto */ static randomize_uuid () { return crypto.randomUUID() // uuid.v4() } /** * 获取梅克哈希 * * @static * @param {*} hashList * @param {*} [option={}] * @return {*} * @memberof TicCrypto */ static get_merkle_hash ({ hashList, output = my.OUTPUT, hasher = my.HASHER } = {}) { // merkle算法略有难度,暂时用最简单的hash代替 if (Array.isArray(hashList)) { const myhasher = crypto.createHash(hasher) for (let hash of hashList) { myhasher.update(hash) } return myhasher.digest(output === 'buf' ? undefined : output) } return null } /** * 获取梅克根 * * @static * @param {*} hashList * @param {*} option * @return {*} * @memberof TicCrypto */ static get_merkle_root ({ hashList } = {}) { //深拷贝传入数组,防止引用对象被改变 hashList = [...hashList] if (!Array.isArray(hashList)) return null var border = hashList.length if (border == 0) return this.hash_easy('') if (border == 1) return this.hash_easy(hashList[0]) while (1) { let i = 1, j = 0 for (; i < border; i = i + 2) { hashList[j] = this.hash_easy(hashList[i - 1] + hashList[i]) if (border == 2) { return hashList[0] } if (i + 1 == border) break j = j + 1 if (i + 2 == border) { i = i + 1 hashList[j] = this.hash_easy(hashList[i]) break } } border = j + 1 } return hashList } /** * 计算哈希距离 * * @static * @param {*} hash * @param {*} sig * @return {*} * @memberof TicCrypto */ static hash_to_sig_distance ({ hash, sig } = {}) { // hash为64hex字符,sig为128hex字符。返回用hex表达的距离。 if (this.is_signature({ sig: sig }) && this.is_hash({ hash })) { var hashSig = this.hash_easy(sig) // 把签名也转成32字节的哈希,同样长度方便比较 // 20241005 注意到,原来通过 require('big-integer') 进行直接减法,可能是错误的!换用原生 BigInt 配合直接减法。 return (BigInt('0x' + hash) - BigInt('0x' + hashSig)).toString(16).replace(/^-/, '') // if using bignumber.js: (bignum('0x' + hash) - bignum('0x' + hashSig)).toString(16) } return null } /** * 比较签名 * * @static * @param {*} hash * @param {*} sig1 * @param {*} sig2 * @return {*} * @memberof TicCrypto */ static compare_signatures ({ hash, sig1, sig2 } = {}) { // 返回距离hash更近的sig if (this.is_hash({ hash })) { if (this.is_signature({ sig: sig2 }) && this.is_signature({ sig: sig1 })) { var dis1 = this.hash_to_sig_distance({ hash, sig: sig1 }) var dis2 = this.hash_to_sig_distance({ hash, sig: sig2 }) if (dis1 < dis2) { return sig1 } else if (dis1 > dis2) { return sig2 } else if (dis1 === dis2) { // 如果极其巧合的距离相等,也可能是一个在左、一个在右,那就按 signature 本身的字符串排序来比较。 return sig1 < sig2 ? sig1 : sig1 === sig2 ? sig1 : sig2 } } else if (this.is_signature({ sig: sig2 })) { // 允许其中一个signature是非法的,例如undefined return sig2 } else if (this.is_signature({ sig: sig1 })) { return sig1 } } return null } /** * 排序签名集 * * @static * @param {*} hash * @param {*} sigList * @return {*} * @memberof TicCrypto */ static sort_sig_list ({ hash, sigList } = {}) { if (Array.isArray(sigList) && this.is_hash({ hash })) { sigList.sort(function (sig1, sig2) { if (this.is_signature({ sig: sig1 }) && this.is_signature({ sig: sig2 })) { var winner = this.compare_signatures({ hash, sig1, sig2 }) if (sig1 === sig2) return 0 else if (winner === sig1) return -1 else if (winner === sig2) return 1 } else { // 如果 sig1 或 sig2 不是 signature 格式 throw 'Not a signature!' } }) return sigList } return null } /** * 用于支付宝的支付交易接口 * * @param $para 需要拼接的数组,把数组所有元素,按照“参数=参数值”的模式用“&”字符拼接成字符串 * @return 拼接完成以后的字符串 */ static getString2Sign (paramSet, converter, delimiter) { if (paramSet && typeof paramSet === 'object') { var string2Sign = '' var converter = converter || '' var delimiter = delimiter || '' for (var key of Object.keys(paramSet).sort()) { var value = paramSet[key] if (value && typeof value === 'object') { // 万一 bis_content 等对象直接送了进来。 value = JSON.stringify(value) } if ((typeof value === 'string' && value !== '') || typeof value === 'number') { if (converter === 'urlencode') value = encodeURIComponent(value) string2Sign += key + '=' + delimiter + value + delimiter + '&' // 根据产品、版本、请求或响应的不同,有的需要key="vlaue",有的只要key=value。 } } string2Sign = string2Sign.replace(/&$/, '') // 删除末尾的 & // if (get_magic_quotes_gpc()) { $string2Sign = stripslashes($string2Sign); } // string2Sign=string2Sign.replace(/\\/g, ''); // 去除转义符 \ (似乎其实不去除,也完全不会影响,因为编程语言内部就会处理掉\) // string2Sign=string2Sign.replace(/\//g, '\\/'); // 为了verify:把正斜杠进行转义 / 参见 https://openclub.alipay.com/read.php?tid=559&fid=2 return string2Sign } return '' } /** * rsa签名 * * @static * @param {*} string2Sign * @param {*} prikey * @param {*} signType * @return {*} * @memberof TicCrypto */ static rsaSign (string2Sign, prikey, signType) { signType = signType || 'RSA-SHA1' // could be RSA-SHA256, RSA-SHA1 or more let signer = crypto.createSign(signType) return encodeURIComponent(signer.update(string2Sign).sign(prikey, 'base64')) } /** * rsa验证签名 * * @static * @param {*} string2Verify * @param {*} signature * @param {*} pubkey * @param {*} signType * @return {*} * @memberof TicCrypto */ static rsaVerify (string2Verify, signature, pubkey, signType) { signType = signType || 'RSA-SHA1' // could be RSA-SHA256, RSA-SHA1 or more let verifier = crypto.createVerify(signType) return verifier.update(string2Verify).verify(pubkey, signature, 'base64') } /** * 缓存转十六进制 * * @static * @param {*} buffer * @return {*} * @memberof TicCrypto */ static buf_to_hex (buffer) { // buffer is an ArrayBuffer return Array.prototype.map.call(new Uint8Array(buffer), (x) => ('00' + x.toString(16)).slice(-2)).join('') } /** * 十六进制转缓存 * * @static * @param {*} hex * @return {*} * @memberof TicCrypto */ static hex_to_buf (hex) { return new Uint8Array( hex.match(/[\da-f]{2}/gi).map(function (h) { return parseInt(h, 16) }) ) // 注意,arraybuffer没有 toString('hex')功能, Buffer才有。 } /** * 十六进制转b58c * * @static * @param {*} hex * @return {*} * @memberof TicCrypto * 如果出现非HEX的字符,从这个字符(及其同Byte的另一个字符)起直到末尾,都会被忽略掉,但仍然成功返回一个串。 * bs58check 和 bs58 可接受string, Buffer, ArrayBuffer, Array (包括空字符串'', 各种内容的数组例如包含 undefined,{...},等等); * 不可接受 undefined, null, {...}, 等等,会返回 exception */ static hex_to_b58c (hex) { try { return bs58check.encode(Buffer.from(hex, 'hex')) } catch (exception) { return '' } } static hex_to_b58 (hex) { try { return bs58.encode(Buffer.from(hex, 'hex')) } catch (exception) { return '' } } /** * b58c 转十六进制 * * @static * @param {*} box * @return {*} * @memberof TicCrypto */ static b58c_to_hex (box) { try { return bs58check.decode(box).toString('hex') } catch (exception) { return '' } } static b58_to_hex (box) { try { return bs58.decode(box).toString('hex') } catch (exception) { return '' } } /** * b64 字符串为 a-zA-Z0-9+/ * 其中,+ 和 / 会在 url query string 里被转成 %2B 和 %2F * 因此定义 b64t (base64 for tic),用 . 和 _ 替换。 * (为何不用 base64url, 因为 base64url 把 + 变成 - 和空格一样导致 css white-space 自动换行。) * @param {*} b64 * @returns */ static b64_to_b64t (b64 = '') { return b64.replace(/\+/g, '.').replace(/\//g, '_').replace(/=/g, '') } static b64t_to_b64 (b64t = '') { return b64t.replace(/\./g, '+').replace(/_/g, '/') } /** * 十六进制转b64t * * @static * @param {*} hex * @return {*} * @memberof TicCrypto */ static hex_to_b64t (hex) { if (my.REGEXP_ALPHABET.hex.test(hex)) { return this.b64_to_b64t(Buffer.from(hex, 'hex').toString('base64')) } return '' } /** * b64t转16进制 * * @static * @param {*} b64t * @return {*} * @memberof TicCrypto */ static b64t_to_hex (b64t) { if (my.REGEXP_ALPHABET.b64t.test(b64t)) { return Buffer.from(this.b64t_to_b64(b64t), 'base64').toString('hex') } return '' } // https://en.wikipedia.org/wiki/Base32 static hex_to_b32 (hex) { if (my.REGEXP_ALPHABET.hex.test(hex)) { return base32encode(Buffer.from(hex, 'hex'), 'RFC4648') } return '' } static b32_to_hex (b32) { if (my.REGEXP_ALPHABET.b32.test(b32)) { return Buffer.from(base32decode(b32.toUpperCase(), 'RFC4648')).toString('hex') } return '' } static hex_to_b32h (hex) { if (my.REGEXP_ALPHABET.hex.test(hex)) { return base32encode(Buffer.from(hex, 'hex'), 'RFC4648-HEX') } return '' } static b32h_to_hex (b32h) { if (my.REGEXP_ALPHABET.b32h.test(b32h)) { return Buffer.from(base32decode(b32.toUpperCase(), 'RFC4648-HEX')).toString('hex') } return '' } /** * 十六进制转eip55 * * @static * @param {*} hex * @return {*} * @memberof TicCrypto */ static hex_to_eip55 (hex) { if (/^(0x)?[\da-fA-F]+$/.test(hex)) { let result = /^0x/.test(hex) ? '0x' : '' hex = hex.toLowerCase().replace('0x', '') let hash = keccak('keccak256').update(hex).digest('hex') for (var i = 0; i < hex.length; i++) { if (parseInt(hash[i], 16) >= 8) { result += hex[i].toUpperCase() } else { result += hex[i] } } return result } return '' } /** * 压缩公钥 * * @static * @param {*} uncompressed: strings like '0x1234567890abcedf...' * @return {*} * @memberof TicCrypto */ static compress_pubkey (uncompressed) { // test: https://iancoleman.io/bitcoin-key-compression/ // compress: https://hacpai.com/article/1550844562914 // 把 04xy 的非压缩公钥 转成 02x 或 03x 的压缩公钥 let [all, x, y] = uncompressed.toLowerCase().match(/^04(.{64})(.{64})$/) let compressed if (/[13579bdf]$/.test(y)) { compressed = '03' + x // y为奇数=>前缀03 } else { compressed = '02' + x // y为偶数=>前缀02 } if (this.decompress_pubkey(compressed) === uncompressed) { return compressed } return '' // 非压缩公钥有错误。 } /** * * decompress_pubkey 需要用到 big-integer 的 modPow 方法。如果想用原生的 BigInt,就需要自己实现 modPow * @param {*} base * @param {*} exponent * @param {*} modulus * @returns */ static modPow (base, exponent, modulus) { let result = BigInt(1) base = BigInt(base) exponent = BigInt(exponent) modulus = BigInt(modulus) while (exponent > 0n) { if (exponent & BigInt(1)) { result = (result * base) % modulus } base = (base * base) % modulus exponent = exponent >> BigInt(1) } return result } /** * 解压缩公钥 * * @static * @param {*} compressed: strings like '020123456789abcdef...' * @return {*} * @memberof TicCrypto */ static decompress_pubkey (compressed = '') { // uncompress: https://stackoverflow.com/questions/17171542/algorithm-for-elliptic-curve-point-compression/53478265#53478265 // https://en.bitcoin.it/wiki/Secp256k1 // 把 02x 或 03x 的压缩公钥 转成 04xy 的非压缩公钥 // Consts for secp256k1 curve. Adjust accordingly const prime = BigInt('0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f', 16) // 2^256 - 2^32 - 2^9 - 2^8 - 2^7 - 2^6 - 2^4 - 1 const pIdent = BigInt('0x3fffffffffffffffffffffffffffffffffffffffffffffffffffffffbfffff0c', 16) // prime.add(1).divide(4); var signY = BigInt(Number(compressed[1]) - 2) var x = BigInt('0x' + compressed.substr(2)) var y = this.modPow((this.modPow(x, 3, prime) + BigInt(7)) % prime, pIdent, prime) // y mod p = +-(x^3 + 7)^((p+1)/4) mod p if (y % BigInt(2) !== signY) { // If the parity doesn't match it's the *other* root y = prime - y } return '04' + this.padStart(x.toString(16), 64, '0') + this.padStart(y.toString(16), 64, '0') } /** * 解压缩公钥 * * @static * @param {*} compressed * @return {*} * @memberof TicCrypto */ static decompress_pubkey_bigint (compressed) { const bigint = globalThis.bigint || require('big-integer') const prime = bigint('fffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f', 16) // 2^256 - 2^32 - 2^9 - 2^8 - 2^7 - 2^6 - 2^4 - 1 const pIdent = bigint('3fffffffffffffffffffffffffffffffffffffffffffffffffffffffbfffff0c', 16) // prime.add(1).divide(4); var signY = new Number(compressed[1]) - 2 var x = bigint(compressed.substr(2), 16) var y = x.modPow(3, prime).add(7).mod(prime).modPow(pIdent, prime) // y mod p = +-(x^3 + 7)^((p+1)/4) mod p if (y.mod(2).toJSNumber() !== signY) { // If the parity doesn't match it's the *other* root y = prime.subtract(y) // y = prime - y } return '04' + this.padStart(x.toString(16), 64, '0') + this.padStart(y.toString(16), 64, '0') } // cosh: content hash. 最核心的纯hex的内容地址,没有任何额外标记。同一个内容的cosh是唯一的,而cid是在cosh基础上有各种不同的编码。cid建议叫做 coid. static cid_to_cosh ({ cid }) { try { if (/^[Q1]/.test(cid)) { return this.b58_to_hex(cid).slice(4) // 前2字节是 cid0 的字节序数标记 } else if (/^[bB]/.test(cid)) { return this.b32_to_hex(cid.substr(1)).slice(8) // 前4字节是 cid1 的标记 } else if (/^z/.test(cid)) { return this.b58_to_hex(cid.substr(1)).slice(8) } else if (/^[mMuU]/.test(cid)) { return Buffer.from(cid.substr(1), 'base64').toString('hex') } else if (/^[fF]/) { return cid.substr(9).toLowerCase() } else if (/^9/.test(cid)) { return BigInt(cid.slice(1)).toString(16).slice(7) // toString(16) 后,去掉了 01551220... 的打头的 0,所以只有7位需要跳过了 } } catch { return '' } } static cosh_to_cid ({ cosh, cidBase = 'b32', cidVersion = 1, cidCodec = 'raw', cidAlgo = 'sha256' }) { // https://github.com/multiformats/multibase const multibase = { identity: 0x00, b2: '0', b8: '7', b10: '9', b16: 'f', B16: 'F', b32: 'b', B32: 'B', b32h: 'v', B32h: 'V', b32hp: 't', B32hp: 'T', b32p: 'c', B32p: 'C', b32z: 'h', // base32z, z-base-32 b36: 'k', B36: 'K', b64: 'm', b64p: 'M', b64u: 'u', b64up: 'U', b58: 'z', } // https://github.com/multiformats/multicodec const multicodec = { raw: '55', dagpb: '70', p2pkey: '72', } const multialgo = { identify: '00', sha256: '12', sha512: '13', keccak256: '1b', ripemd160: '1053', md5: 'd5', } try { if (cidVersion === 0) { return this.hex_to_b58(`${multialgo[cidAlgo]}${Number(cosh.length / 2).toString(16)}${cosh}`) } else if (cidVersion === 1) { const fullHex = `01${multicodec[cidCodec]}${multialgo[cidAlgo]}${Number(cosh.length / 2).toString(16)}${cosh}` let converted = '' if (cidBase === 'b16') { converted = fullHex.toLowerCase() } else if (cidBase === 'B16') { converted = fullHex.toUpperCase() } else if (cidBase === 'b32') { converted = this.hex_to_b32(fullHex)?.toLowerCase?.()?.replace?.(/=/g, '') } else if (cidBase === 'B32') { converted = this.hex_to_b32(fullHex)?.toUpperCase?.()?.replace?.(/=/g, '') } else if (cidBase === 'b58') { converted = this.hex_to_b58(fullHex) } else if (cidBase === 'b64p') { converted = Buffer.from(fullHex, 'hex').toString('base64') } else if (cidBase === 'b64') { converted = Buffer.from(fullHex, 'hex').toString('base64').replace(/=/g, '') } else if (cidBase === 'b10') { converted = BigInt('0x' + fullHex).toString() } if (converted) { return multibase[cidBase] + converted } else { return '' } } } catch { return '' } } static convert_pexid (key) { key = key.toLowerCase() let pextokenCid, pextokenCosh, nftToid, tokenURI try { if (key.length < 64 && /^bafkrei/.test(key)) { pextokenCid = key pextokenCosh = this.cid_to_cosh({ cid: pextokenCid }) nftToid = BigInt('0x' + pextokenCosh).toString() } else if (key.length > 64 && /^\d+$/.test(key)) { nftToid = key pextokenCosh = BigInt(nftToid).toString(16) pextokenCid = this.cosh_to_cid({ cosh: pextokenCosh }) } else if (/^[0-9a-f]{64}$/.test(key)) { pextokenCosh = key pextokenCid = this.cosh_to_cid({ cosh: pextokenCosh }) nftToid = BigInt('0x' + pextokenCosh).toString() } tokenURI = pextokenCosh ? `https://ipfs.tic.cc/ipfs/f01551220${pextokenCosh}` : undefined } catch {} return { pextokenCid, pextokenCosh, nftToid, tokenURI, } } } // 必须单独写 module.exports,不要和类定义写在一起,否则会导致 jsdoc 解析不到类内文档。 module.exports = TicCrypto