tic-crypto/index.js
2022-05-28 13:32:19 +08:00

1507 lines
55 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 BigNumber=require('bignumber.js') // 处理整数 https://github.com/MikeMcl/bignumber.js
const BigInt = require('big-integer') // 处理整数 https://github.com/peterolson/BigInteger.js
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 eccrypto = require('eccrypto-js') // 用于加解密。eccrypto 在 windows 上和 openssl 的版本兼容性有点麻烦,所以换用 eccrypto-js
const keyman = require('js-crypto-key-utils') // 转换原始密钥和 PER/DER 格式。
// 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_LIST = ['TIC', 'EXT', 'BTC', 'ETH']
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\._]+$/,
}
/**
*
* @class TICrypto
*/
class TICrypto {
/**
* 测试输入数据是否可哈希混淆
*
* @static
* @param {*} data 需要被哈希混淆的数据
* @param {*} option 可选参数
* @return {Boolean}
* @memberof TICrypto
*/
static isHashable (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 TICrypto
*/
static isHash (hash, { hasher = my.HASHER } = {}) {
if (my.HASHER_LIST.indexOf(hasher) >= 0) {
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 TICrypto
*/
static isSecword (secword, { mode = 'strict' } = {}) {
// 注意 not all 12 words combinations are valid for both bitcore and bip39, because there are checksum in mnemonic. 另外实际上bitcore和bip39对12, 15, 18, ... 长度的合法助记词都返回 true。
//// for bitcore-mnemonic. 注意bitcore-mnemonic 对少于12词的会抛出异常很蠢。
// if (typeof secword==='string' && 12===secword.split(/ +/).length)
// return BitcoreMnemonic.isValid(secword)
// else
// return false
//// for bip39. 注意bip39对当前defaultWordlist之外其他语言的合法 mnemonic 也返回 false这一点不如 bitcore-mnemonic. 所以不能直接 bip39.validateMnemonic(secword)
if (typeof secword === 'string' && !/(^\s)|\s\s|(\s$)/.test(secword) && 12 === secword.split(/\s+/).length) {
if (mode === 'easy') return true // easy模式不检查校验等等严格的合法性了反正 secword2seed是接受一切字符串的
for (let lang of Object.keys(bip39.wordlists)) {
bip39.setDefaultWordlist(lang)
if (bip39.validateMnemonic(secword)) return true
}
}
return false
}
/**
* 测试是否合法的私钥
*
* @static
* @param {String} seckey
* @return {Boolean}
* @memberof TICrypto
*/
static isSeckey (seckey) {
// 比特币、以太坊的私钥64 hex
// nacl.sign 的私钥 128 hex, nacl.box 的私钥 64 hex
return /^([a-fA-F0-9]{128}|[a-fA-F0-9]{64})$/.test(seckey)
}
/**
* 测试是否合法的公钥
*
* @static
* @param {String} pubkey
* @return {Boolean}
* @memberof TICrypto
*/
static isPubkey (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 TICrypto
*/
static isSignature (signature) {
return /^[a-fA-F0-9]{128,144}$/.test(signature) && signature.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 TICrypto
*/
static hash (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.isHashable(data)) {
if (typeof data !== 'string' && !(data instanceof Buffer) && !(data instanceof DataView)) data = JSON.stringify(data)
if (salt && typeof salt === 'string') data = data + this.hash(salt)
let inputEncoding = input // my.INPUT_LIST.indexOf(input)>=0?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.indexOf(output)>=0?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 [{ tool, keytype, key, input, output, cipher }={}]
* @return {String}
* @memberof TICrypto
*/
static async encrypt ({ data, tool = 'crypto', keytype = 'pwd', key, input, output, cipher } = {}) {
if (tool === 'eccrypto') {
// 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 eccrypto.encrypt(Buffer.from(this.hex_to_buf(key)), data)
return cipherObject // 返回一个复杂的结构 {iv:Buffer, ciphertext:Buffer, ...}。对同样的key和data每次返回的结果不一样
} else if (keytype === 'pwd') {
// 对称加密
if (typeof key === 'string') {
let inputEncoding = my.INPUT_LIST.indexOf(input) >= 0 ? input : my.INPUT // 'utf8' by default, 'ascii', 'latin1' for string or ignored for Buffer/TypedArray/DataView
let outputEncoding = output === 'buf' ? undefined : my.OUTPUT_LIST.indexOf(output) >= 0 ? output : my.OUTPUT // 'latin1', 'base64', 'hex' by default or 'buf' to Buffer explicitly
const iv = crypto.randomBytes(16)
let encryptor = crypto.createCipheriv(my.CIPHER_LIST.indexOf(cipher) >= 0 ? cipher : my.CIPHER, this.hex_to_buf(this.hash(key)), iv) // cipher 和 key 的长度必须相同,例如 cipher 是 ***-192那么 key 就必须是 192/8=24 字节 = 48 hex 的。
if (typeof data !== 'string' && !(data instanceof Buffer) && !(data instanceof DataView)) data = JSON.stringify(data)
let ciphertext = encryptor.update(data, inputEncoding, outputEncoding)
ciphertext += encryptor.final(outputEncoding) // 但是 Buffer + Buffer 还是会变成string
return { iv: iv.toString('hex'), ciphertext } // 有 iv显然每次结果不一样
}
} else if (keytype === 'seckey') {
// 尚未走通,不能使用 ticCrypto 生成的 Elliptic curve 椭圆曲线算法公私钥,只能用 crypto.generateKeypairs() 生成的 rsa 公私钥
let seckeyPEM = await new keyman.Key('oct', this.hex_to_buf(key), { namedCurve: 'P-256K' }).export('pem') // 私钥导出的der格式为144字节。
return crypto.privateEncrypt(seckeyPEM, Buffer.from(data)) // 返回 Buffer。每次结果都一样。
} else if (keytype === 'pubkey') {
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 [{ keytype, key, input, output, cipher, format }={}]
* @return {String}
* @memberof TICrypto
*/
static async decrypt ({ data = {}, tool = 'crypto', keytype = 'pwd', key, input, output, cipher } = {}) {
// data 应当是 encrypt 输出的数据类型
if (tool === 'eccrypto') {
try {
// eccrypto 只能接受 Buffer, 不接受 Uint8Array, 因为 eccrypto 需要调用 Buffer.compare 方法,不能在这里直接用 hex_to_buf
// eccrypto 也只能接受 Buffer, 不接受 Uint8Array
// data 需要是 eccrypto 自身encrypt方法返回的 cipherObject. key 是 private key。
let plainbuffer = await eccrypto.decrypt(Buffer.from(key, 'hex'), data) // 返回的是 Buffer
return plainbuffer.toString('utf8')
} catch (exception) {
// eccrypto 对无法解密的,会抛出异常
return null
}
} else if (keytype === 'pwd') {
// 对称解密
if ((typeof data.ciphertext === 'string' || data.ciphertext instanceof Buffer) && typeof key === 'string') {
let inputEncoding = my.OUTPUT_LIST.indexOf(input) >= 0 ? input : my.OUTPUT // input (=output of encrypt) could be 'latin1', 'base64', 'hex' by default for string or ignored for Buffer
let outputEncoding = output === 'buf' ? undefined : my.INPUT_LIST.indexOf(output) >= 0 ? output : my.INPUT // output (=input of encrypt) could be 'latin1', 'ascii', 'utf8' by default or 'buf' to Buffer explicitly
let decryptor = crypto.createDecipheriv(
my.CIPHER_LIST.indexOf(cipher) >= 0 ? cipher : my.CIPHER,
this.hex_to_buf(this.hash(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 (keytype === 'seckey') {
// 尚未走通,不能使用 ticCrypto 生成的 Elliptic curve 椭圆曲线算法公私钥
let seckeyPEM = await new keyman.Key('oct', this.hex_to_buf(key), { namedCurve: 'P-256K' }).export('pem') // 私钥导出的der格式为144字节。
return crypto.privateDecrypt(seckeyPEM, Buffer.from(data)) // 返回 Buffer。每次结果都一样。
} else if (keytype === 'pubkey') {
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} seckey
* @param {Object} option [option={}]
* @return {String}
* @memberof TICrypto
*/
static async sign ({ data, seckey, tool = 'crypto', hasher }) {
// data can be string or buffer or object, results are the same
if (this.isHashable(data) && this.isSeckey(seckey)) {
if (tool === 'nacl' && seckey.length === 128) {
// 使用nacl的签名算法。注意nacl.sign需要的seckey是64字节=128字符。
let hashBuf = this.hash(data, { output: 'buf' }) // 哈希必须输出为 buffer
let signature = nacl.sign.detached(hashBuf, Buffer.from(seckey, 'hex'))
return Buffer.from(signature).toString('hex') // 签名是64节128个hex字符
} else if (tool === 'eccrypto' && seckey.length === 64) {
// eccrypto 对同一组data,seckey生成的签名是固定的观察到hex长度为140或142是der格式。
let signature = await eccrypto.sign(Buffer.from(seckey, 'hex'), this.hash(data, { output: 'buf' }))
return signature.toString('hex')
} else if (seckey.length === 64) {
// 纯 crypto
let seckeyPEM = await new keyman.Key('oct', this.hex_to_buf(seckey), { namedCurve: 'P-256K' }).export('pem') // 私钥导出的der格式为144字节。
let signer = crypto.createSign(my.HASHER_LIST.indexOf(hasher) >= 0 ? hasher : my.HASHER) // 注意不知为何hasher必须含有'sha'才能完成签名,例如 sha1, sha256, sha512, sha3, RSA-SHA1, id-rsassa-pkcs1-v1_5-with-sha3-224, 其他都会报错。
signer.update(this.hash(data)).end()
let signature = signer.sign(seckeyPEM, 'hex')
// since nodejs 12, 有了 crypto.sign 方法,但在浏览器中无效:
// let signature = crypto.sign(my.HASHER_LIST.indexOf(hasher) >= 0 ? hasher : my.HASHER, Buffer.from(this.hash(data)), seckeyPEM).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 TICrypto
*/
static async verify ({ data, signature, pubkey, tool = 'crypto', hasher }) {
// data could be anything, but converts to string or remains be Buffer/TypedArray/DataView
if (this.isHashable(data) && this.isSignature(signature) && this.isPubkey(pubkey)) {
if ('nacl' === tool && signature.length === 128) {
let bufHash = this.hash(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 ('eccrypto' === tool && signature.length >= 140) {
// 默认使用 eccrypto // 发现大小写不影响 eccrypto 验签!都能通过
try {
let result = await eccrypto.verify(Buffer.from(pubkey, 'hex'), this.hash(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(my.HASHER_LIST.indexOf(hasher) >= 0 ? hasher : my.HASHER)
verifier.update(this.hash(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(my.HASHER_LIST.indexOf(hasher) >= 0 ? hasher : my.HASHER, Buffer.from(this.hash(data)), pubkeyPEM, Buffer.from(signature, 'hex'))
return verified
}
}
return false
}
/**
* 从密码到公私钥
*
* @static
* @param {String} pass
* @param {Object} option
* @return {Object} {pubkey, seckey, address,}
* @memberof TICrypto
*/
static pass2keypair (pass, { hasher } = {}) {
// 如果使用其他机制例如密码、随机数不使用secword也可生成keypair
if (this.isHashable(pass)) {
hasher = my.HASHER_LIST.indexOf(hasher) >= 0 ? hasher : my.HASHER
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类型
seckey: Buffer.from(keypair.secretKey).toString('hex'),
}
}
return null
}
/**
* 从墒到助记词
*
* @static
* @param {*} entropy
* @return {String}
* @memberof TICrypto
*/
static entropy2secword (entropy) {
// entropy could be hex string or buffer. Byte length could be of 16, 20, 24, 28, ... which outputs mnemonic of length 12, 15, 18, 21, ...
return bip39.entropyToMnemonic(entropy) // results are the same for the same entropy.
}
/**
* 从助记词到墒
*
* @static
* @param {String} secword
* @return {*}
* @memberof TICrypto
*/
static secword2entropy (secword) {
// secword could be of length 12, 15, 18, ... which outputs hex of length 32, 40, ...
return bip39.mnemonicToEntropy(secword) // results are the same for the same secword.
}
/**
* 从助记词到公私钥
*
* @static
* @param {String} secword
* @param {Object} option
* @return {Object} {pubkey, seckey,}
* @memberof TICrypto
*/
static secword2keypair (secword, { coin, pass, path, tool, hasher } = {}) {
// coin 币种;
// passphase 密码,默认为空;
// path==='master' 生成 HD master key不定义则默认为相应币种的第一对公私钥。
// 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 = my.COIN_LIST.indexOf(coin?.toUpperCase()) >= 0 ? coin.toUpperCase() : my.COIN
if (tool === 'nacl') {
// 采用自己的算法bip39算法从secword到种子hash后用 nacl.sign.keyPair.fromSeed()方法。
hasher = my.HASHER_LIST.indexOf(hasher) >= 0 ? hasher : my.HASHER
let hashBuf = crypto
.createHash(hasher)
.update(this.secword2seed(secword, pass))
.digest()
let keypair = nacl.sign.keyPair.fromSeed(hashBuf) // nacl.sign.keyPair.fromSeed 要求32字节的种子而 this.secword2seed生成的是64字节种子所以要先做一次sha256
return {
coin: coin,
secword: secword,
pubkey: Buffer.from(keypair.publicKey).toString('hex'), // 测试过 不能直接keypair.publicKey.toString('hex')不是buffer类型
seckey: Buffer.from(keypair.secretKey).toString('hex'), // nacl.sign.keyPair.fromSeed 得到的 seckey 是64字节的不同于比特币/以太坊的32字节密钥。
}
} else {
// 用 bip39 算法从 secword 到种子,再用 bip32 算法从种子到根私钥。这是比特币、以太坊的标准方式,结果一致。
let hdmaster = hdkey.fromMasterSeed(Buffer.from(this.secword2seed(secword, pass), 'hex')) // 和 new BitcoreMnemonic(secword).toHDPrivateKey 求出的公私钥一样!
// let hdmaster=new BitcoreMnemonic(secword).toHDPrivateKey(pass) // 和 ethers.HDNode.fromMnemonic(secword)的公私钥一样。而 ethers.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') {
key = hdmaster
} else if (!path) {
switch (coin) {
case 'BTC':
key = hdmaster.derive("m/44'/0'/0'/0/0")
break
case 'ETH':
key = hdmaster.derive("m/44'/60'/0'/0/0")
break
case 'EXT':
key = hdmaster.derive("m/44'/60398'/0'/0/0")
break
case 'TIC':
default:
key = hdmaster.derive("m/44'/60000'/0'/0/0")
break
}
} else {
// 指定了路径 path例如 "m/44'/0'/0'/0/6" 或 "m/0/2147483647'/1"
key = hdmaster.derive(path)
}
return {
coin: coin,
secword: secword,
seckey: key.privateKey.toString('hex'), // 或者 key.toJSON().privateKey。或者 key.privateKey.slice(2) 删除开头的'0x'如果是ethers.HDNode.fromMnemonic(secword)的结果
pubkey: key.publicKey.toString('hex'),
}
}
return null
}
/**
* 从种子到路径
*
* @static
* @param {*} seed
* @param {string} option [{ coin = 'TIC' }={ coin: 'TIC' }]
* @return {String} path
* @memberof TICrypto
*/
static seed2path (seed, { coin = 'TIC' } = { coin: 'TIC' }) {
// 路径规范 BIP44: m/Purpose'/Coin'/Account'/Change/Index,
// 但实际上 Purpose, Coin 都可任意定;' 可有可无;
// Account/Change/Index 最大到 parseInt(0x7FFFFFFF, 16)
// 后面还可继续延伸 /xxx/xxx/xxx/......
let hash = this.hash(seed, { hasher: 'md5' })
let part0 = parseInt(hash.slice(0, 6), 16)
let part1 = parseInt(hash.slice(6, 12), 16)
let part2 = parseInt(hash.slice(12, 18), 16)
let part3 = parseInt(hash.slice(18, 24), 16)
let part4 = parseInt(hash.slice(24, 30), 16)
let part5 = parseInt(hash.slice(30, 32), 16)
let path = `${part0}'/${part1}/${part2}/${part3}/${part4}/${part5}`
switch (coin.toUpperCase()) {
case 'BTC':
return `m/44'/0'/${path}`
case 'ETH':
return `m/44'/60'/${path}`
case 'EXT':
return `m/44'/60398'/${path}`
case 'TIC':
default:
return `m/44'/60000'/${path}`
}
}
/**
* 从助记词到账户
*
* @static
* @param {String} secword
* @param {Object} option
* @return {Object}
* @memberof TICrypto
*/
static secword2account (secword, { coin, pass, path, tool, hasher } = {}) {
// account 比 keypair 多了 address 字段。
coin = my.COIN_LIST.indexOf(coin?.toUpperCase()) >= 0 ? coin.toUpperCase() : my.COIN
let kp = this.secword2keypair(secword, { coin, pass, path, tool, hasher })
if (kp) {
if (coin === 'ETH') {
let uncompressedPubkey = this.decompressPubkey(kp.pubkey)
kp.address = this.pubkey2address(uncompressedPubkey, { coin: 'ETH' })
} else {
kp.address = this.pubkey2address(kp.pubkey, { coin })
}
return kp
}
return null
}
/**
* 从助记词到地址
*
* @static
* @param {String} secword
* @param {Object} option
* @return {String} address
* @memberof TICrypto
*/
static secword2address (secword, { coin, world, pass, path, tool, hasher } = {}) {
coin = my.COIN_LIST.indexOf(coin?.toUpperCase()) >= 0 ? coin.toUpperCase() : my.COIN
let kp = this.secword2keypair(secword, { coin, pass, path, tool, hasher })
if (kp) {
let address
if (coin === 'ETH') {
address = this.pubkey2address(this.decompressPubkey(kp.pubkey), { coin: 'ETH', world })
} else {
address = this.pubkey2address(kp.pubkey, { coin, world })
}
return address
}
return null
}
/**
* 从私钥到公钥
*
* @static
* @param {*} seckey
* @param {*} [option={}]
* @return {*}
* @memberof TICrypto
*/
static seckey2pubkey (seckey, { curve, compress } = {}) {
if (this.isSeckey(seckey) && seckey.length === 64) {
// 只能用于32字节的私钥BTC, ETH)。也就是不能用于 TIC 的私钥。
curve = my.CURVE_LIST.indexOf(curve) >= 0 ? curve : my.CURVE // 默认为 secp256k1
// return new crypto.createECDH(curve).setPrivateKey(seckey,'hex').getPublicKey('hex', compress===false?'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(seckey, 'hex'), compress !== false)) // 可用于浏览器。缺省输出压缩公钥compress=false时输出非压缩公钥。
// 或者 bitcorelib.PublicKey.fromPrivateKey(new bitcorelib.PrivateKey(seckey)).toString('hex') // 可用于浏览器
// 或者 const ecc = require('eccrypto')
// if (compress===false){
// return ecc.getPublic(this.hex_to_buf(seckey)).toString('hex')
// }else{
// return ecc.getPublicCompressed(this.hex_to_buf(seckey)).toString('hex')
// }
// 注意Buffer.from(nacl.box.keyPair.fromSecretKey(Buffer.from(seckey,'hex')).publicKey).toString('hex') 得到的公钥与上面的不同
} else if (this.isSeckey(seckey) && seckey.length === 128) {
// 用于64字节=128 hex的 TIC 私钥
let keypair = nacl.sign.keyPair.fromSecretKey(Buffer.from(seckey, 'hex'))
return Buffer.from(keypair.publicKey).toString('hex') // 测试过 不能直接keypair.publicKey.toString('hex')不是buffer类型
}
return null
}
/**
* 从私钥到地址
*
* @static
* @param {*} seckey
* @param {*} option
* @return {*}
* @memberof TICrypto
*/
static seckey2address (seckey, { coin, world } = {}) {
coin = my.COIN_LIST.indexOf(coin?.toUpperCase()) >= 0 ? coin.toUpperCase() : my.COIN
if (this.isSeckey(seckey)) {
/** @type {*} */
let pubkey
if (coin === 'ETH') {
pubkey = this.seckey2pubkey(seckey, { compress: false })
return this.pubkey2address(pubkey, { coin, world })
} else {
pubkey = this.seckey2pubkey(seckey, { compress: true })
return this.pubkey2address(pubkey, { coin, world })
}
}
return null
}
/**
* 从公钥到位置
*
* @static
* @param {*} pubkey
* @param {*} [{ coin }={}]
* @return {*}
* @memberof TICrypto
* position 就是通常所说的 PubKeyHash出现在比特币交易的锁定脚本里
*/
static pubkey2position (pubkey, { coin } = {}) {
// tic, btc, eth 的 position 都是 20节=40字符的。
coin = my.COIN_LIST.indexOf(coin?.toUpperCase()) >= 0 ? coin.toUpperCase() : my.COIN
if (this.isPubkey(pubkey)) {
if (coin === 'ETH') {
// 注意必须要用非压缩的64字节的公钥的buffer并去掉开头的 04。
if (pubkey.length === 66) {
pubkey = this.decompressPubkey(pubkey)
}
return keccak('keccak256')
.update(Buffer.from(pubkey.slice(2), 'hex'))
.digest('hex')
.slice(-40)
} else {
let h256 = crypto
.createHash('sha256')
.update(Buffer.from(pubkey, 'hex'))
.digest()
let h160 = crypto
.createHash('ripemd160')
.update(h256)
.digest('hex')
return h160
}
}
return null
}
/**
* 从位置到地址
*
* @static
* @param {*} position
* @param {*} [{ coin, world }={}]
* @return {*}
* @memberof TICrypto
*/
static position2address (position, { coin, world } = {}) {
if (!/^[\da-fA-F]{40}$/.test(position)) return null // 不论 tic, btc, eth其 position 都是 40字符的。
if (coin) coin = coin.toUpperCase()
coin = my.COIN_LIST.indexOf(coin) >= 0 ? coin : my.COIN
let address
if (coin === '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') {
// 对比特币把纯位置转换为大小写敏感能自我验证的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 = '4c'
}
let checksum = this.hash(this.hash(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 TICrypto
* 地址和PubKeyHash(即position)之间能互相转化
*/
static address2position () {
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(this.hash(position)).slice(0, 6) === checksum) {
return position
}
}
return null
}
/**
* 测试是否合法的地址
*
* @static
* @param {String} address
* @return {Boolean}
* @memberof TICrypto
*/
static isAddress (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(this.hash(prefix + position)).slice(0, 6) === checksum)
// [todo] 校验码里要不要包含 prefix?
return 'TIC'
}
return null
}
/**
* 从公钥到地址
*
* @static
* @param {*} pubkey
* @param {*} [option={}]
* @return {*}
* @memberof TICrypto
*/
static pubkey2address (pubkey, { coin, world } = {}) {
// pubkey 应当是string类型
coin = my.COIN_LIST.indexOf(coin?.toUpperCase()) >= 0 ? coin.toUpperCase() : my.COIN
return this.position2address(this.pubkey2position(pubkey, { coin }), { coin, world })
}
/**
* 从助记词到种子
*
* @static
* @param {*} secword
* @param {*} pass
* @return {*}
* @memberof TICrypto
*/
static secword2seed (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)。
}
/**
* 生成随机的助记词
*
* @static
* @param {string} [lang='english']
* @return {*}
* @memberof TICrypto
*/
static randomSecword (lang = 'english') {
// accepts case-insensitive lang, such as 'chinese, cn, tw, en'
//// 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 langMap = {
zhcn: 'chinese_simplified',
zhtw: 'chinese_traditional',
enus: 'english',
frfr: 'french',
itit: 'italian',
jajp: 'japanese',
kokr: 'korean',
eses: 'spanish',
}
langMap.chinese = langMap.cn = langMap.zh = langMap.china = langMap.zhcn
langMap.taiwanese = langMap.tw = langMap.zhtw
langMap.en = langMap.us = langMap.uk = langMap.enus
langMap.fr = langMap.france = langMap.frfr
langMap.it = langMap.italy = langMap.itit
langMap.ko = langMap.kr = langMap.korean = langMap.kokr
langMap.ja = langMap.jp = langMap.japan = langMap.jajp
let language = 'english'
if (typeof lang === 'string') {
lang = lang.toLowerCase()
language = langMap[lang] || (bip39.wordlists[lang] ? lang : 'english')
}
bip39.setDefaultWordlist(language)
return bip39.generateMnemonic()
}
/**
* 生成随机的私钥
*
* @static
* @param {*} [option={}]
* @return {*}
* @memberof TICrypto
*/
static randomSeckey ({ coin, tool } = {}) {
// 跳过 secword 直接产生随机密钥
coin = my.COIN_LIST.indexOf(coin?.toUpperCase()) >= 0 ? coin : my.COIN
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 TICrypto
*/
static randomKeypair ({ tool, purpose } = {}) {
let kp
if (tool === 'nacl') {
if (purpose === 'encrypt') {
kp = nacl.box.keyPair()
} else {
kp = nacl.sign.keyPair()
}
return {
seckey: Buffer.from(kp.secretKey).toString('hex'),
pubkey: Buffer.from(kp.publicKey).toString('hex'),
}
} else {
let seckey = this.randomSeckey()
let pubkey = this.seckey2pubkey(seckey)
return {
seckey,
pubkey,
}
}
}
/**
* 生成随机的账户
*
* @static
* @param {*} [option={}]
* @return {*}
* @memberof TICrypto
*/
static randomAccount ({ lang, coin, pass, path, tool, hasher } = {}) {
let secword = this.randomSecword(lang)
return this.secword2account(secword, { coin, pass, path, tool, hasher })
}
/**
* 生成随机的字符串
*
* @static
* @param {number} [length=6]
* @param {*} alphabet
* @return {*}
* @memberof TICrypto
*/
static randomString (length = 6, alphabet) {
// 长度为 length字母表为 alphabet 的随机字符串
alphabet = alphabet || 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789#$%^&*@'
var text = ''
for (var i = 0; i < length; i++) {
text += alphabet.charAt(Math.floor(Math.random() * alphabet.length))
}
return text
}
/**
* 生成随机的数字
*
* @static
* @param {*} [{ length, min, max }={}]
* @return {*}
* @memberof TICrypto
*/
static randomNumber ({ length, min, max } = {}) {
// 长度为 length 的随机数字,或者 (min||0) <= num < max
var 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 TICrypto
*/
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 TICrypto
*/
static randomUuid () {
return uuid.v4()
}
/**
* 获取梅克哈希
*
* @static
* @param {*} hashList
* @param {*} [option={}]
* @return {*}
* @memberof TICrypto
*/
static getMerkleHash (hashList, { output, hasher } = {}) {
// merkle算法略有难度暂时用最简单的hash代替
if (Array.isArray(hashList)) {
myhasher = crypto.createHash(my.HASHER_LIST.indexOf(hasher) >= 0 ? hasher : my.HASHER)
for (var hash of hashList) {
myhasher.update(hash)
}
return myhasher.digest(output === 'buf' ? undefined : output || my.OUTPUT)
}
return null
}
/**
* 获取梅克根
*
* @static
* @param {*} todoHashList
* @param {*} option
* @return {*}
* @memberof TICrypto
*/
static getMerkleRoot (todoHashList) {
//深拷贝传入数组,防止引用对象被改变
let hashList = [...todoHashList]
if (!Array.isArray(hashList)) return null
var border = hashList.length
if (border == 0) return this.hash('')
if (border == 1) return this.hash(hashList[0])
while (1) {
let i = 1,
j = 0
for (; i < border; i = i + 2) {
hashList[j] = this.hash(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(hashList[i])
break
}
}
border = j + 1
}
return hashList
}
/**
* 计算哈希距离
*
* @static
* @param {*} hash
* @param {*} sig
* @return {*}
* @memberof TICrypto
*/
static distanceSig (hash, sig) {
// hash为64hex字符sig为128hex字符。返回用hex表达的距离。
if (this.isSignature(sig) && this.isHash(hash)) {
var hashSig = this.hash(sig) // 把签名也转成32字节的哈希同样长度方便比较
return new BigInt(hash, 16)
.subtract(new BigInt(hashSig, 16))
.abs()
.toString(16)
}
return null
}
/**
* 比较签名
*
* @static
* @param {*} hash
* @param {*} sig1
* @param {*} sig2
* @return {*}
* @memberof TICrypto
*/
static compareSig (hash, sig1, sig2) {
// 返回距离hash更近的sig
if (this.isHash(hash)) {
if (this.isSignature(sig2) && this.isSignature(sig1)) {
var dis1 = this.distanceSig(hash, sig1)
var dis2 = this.distanceSig(hash, 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.isSignature(sig2)) {
// 允许其中一个signature是非法的例如undefined
return sig2
} else if (this.isSignature(sig1)) {
return sig1
}
}
return null
}
/**
* 排序签名集
*
* @static
* @param {*} hash
* @param {*} sigList
* @return {*}
* @memberof TICrypto
*/
static sortSigList (hash, sigList) {
if (Array.isArray(sigList) && this.isHash(hash)) {
sigList.sort(function (sig1, sig2) {
if (this.isSignature(sig1) && this.isSignature(sig2)) {
var winner = this.compareSig(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 TICrypto
*/
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 TICrypto
*/
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 TICrypto
*/
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 TICrypto
*/
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 TICrypto
* 如果出现非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 null
}
}
static hex_to_b58 (hex) {
try {
return bs58.encode(Buffer.from(hex, 'hex'))
} catch (exception) {
return null
}
}
/**
* b58c 转十六进制
*
* @static
* @param {*} box
* @return {*}
* @memberof TICrypto
*/
static b58c_to_hex (box) {
try {
return bs58check.decode(box).toString('hex')
} catch (exception) {
return null
}
}
static b58_to_hex (box) {
try {
return bs58.decode(box).toString('hex')
} catch (exception) {
return null
}
}
/**
* 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 TICrypto
*/
static hex_to_b64t (hex) {
if (/^[0-9a-fA-F]+$/.test(hex)) {
return this.b64_to_b64t(Buffer.from(hex, 'hex').toString('base64'))
}
return null
}
/**
* b64t转16进制
*
* @static
* @param {*} b64t
* @return {*}
* @memberof TICrypto
*/
static b64t_to_hex (b64t) {
if (/^[0-9a-zA-Z\._]+$/.test(b64t)) {
return Buffer.from(this.b64t_to_b64(b64t), 'base64').toString('hex')
}
return null
}
// https://en.wikipedia.org/wiki/Base32
static hex_to_b32 (hex, { encoding = 'RFC4648' } = {}) {
if (/^[0-9a-fA-F]+$/.test(hex)) {
return base32encode(Buffer.from(hex, 'hex'), encoding)
}
return null
}
static b32_to_hex (b32, { encoding = 'RFC4648' } = {}) {
if (/^[A-Za-z2-7=]+$/.test(b32)) {
return Buffer.from(base32decode(b32.toUpperCase(), encoding)).toString('hex')
}
return null
}
static hex_to_b32h (hex, { encoding = 'RFC4648-HEX' } = {}) {
if (/^[0-9a-fA-F]+$/.test(hex)) {
return base32encode(Buffer.from(hex, 'hex'), encoding)
}
return null
}
static b32h_to_hex (b32, { encoding = 'RFC4648-HEX' } = {}) {
if (/^[0-9A-Va-v=]+$/.test(b32)) {
return Buffer.from(base32decode(b32.toUpperCase(), encoding)).toString('hex')
}
return null
}
/**
* 十六进制转eip55
*
* @static
* @param {*} hex
* @return {*}
* @memberof TICrypto
*/
static hex_to_eip55 (hex) {
if (/^(0x)?[\da-fA-F]+$/.test(hex)) {
hex = hex.toLowerCase().replace('0x', '')
let hash = keccak('keccak256')
.update(hex)
.digest('hex')
let result = ''
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 null
}
/**
* 压缩公钥
*
* @static
* @param {*} uncompressed
* @return {*}
* @memberof TICrypto
*/
static compressPubkey (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 (/[1,3,5,7,9,b,d,f]$/.test(y)) {
compressed = '03' + x // y为奇数=>前缀03
} else {
compressed = '02' + x // y为偶数=>前缀02
}
if (this.decompressPubkey(compressed) === uncompressed) {
return compressed
}
return null // 非压缩公钥有错误。
}
/**
* 解压缩公钥
*
* @static
* @param {*} compressed
* @return {*}
* @memberof TICrypto
*/
static decompressPubkey (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 = new BigInt('fffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f', 16) // 2^256 - 2^32 - 2^9 - 2^8 - 2^7 - 2^6 - 2^4 - 1
const pIdent = new BigInt('3fffffffffffffffffffffffffffffffffffffffffffffffffffffffbfffff0c', 16) // prime.add(1).divide(4);
var signY = new Number(compressed[1]) - 2
var x = new 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')
}
static cid_to_cosh ({ cid }) {
if (/^[Q|1]/.test(cid)) {
return this.b58_to_hex(cid).slice(4)
} else if (/^b/.test(cid)) {
return this.b32_to_hex(cid.substr(1)).slice(8)
} else if (/^z/.test(cid)) {
return this.b58_to_hex(cid.substr(1)).slice(8)
}
}
static cosh_to_cid ({ cosh, cidBase = 'b32', cidVersion = 1, cidCodec = 'raw', cidAlgo = 'sha256' }) {
const multibase = {
identity: 0x00,
b2: '0',
b8: '7',
b10: '9',
b16: 'f',
B16: 'F',
b32: 'b',
B32: 'B',
b32h: 'v',
B32h: 'V',
b36: 'k',
b64: 'm',
b64p: 'M',
b64u: 'u',
b64up: 'U',
b58: 'z',
}
const multicodec = {
dagpb: '70',
p2pkey: '72',
raw: '55'
}
const multialgo = {
identify: '00',
sha256: '12'
}
if (cidVersion === 0) {
return this.hex_to_b58(`${multialgo[cidAlgo]}${Number(cosh.length/2).toString(16)}${cosh}`)
}
if (cidVersion === 1) {
let fullHex = `01${multicodec[cidCodec]}${multialgo[cidAlgo]}${Number(cosh.length/2).toString(16)}${cosh}`
if (cidBase==='b32') {
return multibase[cidBase] + this.hex_to_b32(fullHex).toLowerCase().replace(/=/g,'')
}else if (cidBase==='b58') {
return multibase[cidBase] + this.hex_to_b58(fullHex)
}
}
}
static string_to_raw_cid (str) {
return this.cosh_to_cid({ cosh: this.hash(str), cidVersion:1, cidCodec:'raw' })
}
}
// 必须单独写 module.exports不要和类定义写在一起否则会导致 jsdoc 解析不到类内文档。
module.exports = TICrypto