tic-crypto/ticc.js
2024-11-03 14:04:26 +08:00

1644 lines
63 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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位hexeccrypto 的 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位hexcrypto 的 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|24which 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。得到2634个字符大多数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 位的随机墒
* 2sha256 取前 墒长度/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