1516 lines
55 KiB
JavaScript
1516 lines
55 KiB
JavaScript
// 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', '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.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 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.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 [{ 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.includes(input) ? input : my.INPUT // 'utf8' by default, 'ascii', 'latin1' for string or ignored for Buffer/TypedArray/DataView
|
||
let outputEncoding = output === 'buf' ? undefined : my.OUTPUT_LIST.includes(output) ? 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.includes(cipher) ? 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') {
|
||
// 尚未走通,不能使用 ticrypto 生成的 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.includes(input) ? 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.includes(output) ? 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.includes(cipher) ? 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') {
|
||
// 尚未走通,不能使用 ticrypto 生成的 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.includes(hasher) ? 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.includes(hasher) ? 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位hex,eccrypto 的 verify结果也是true! 估计因为一位hex不被转成字节。
|
||
return true
|
||
} catch (exception) {
|
||
// 对能够验证的,eccrypto返回 null;对无法验证的,抛出异常
|
||
return false
|
||
}
|
||
} else if (signature.length >= 140) {
|
||
// 纯 crypto // 发现大小写不影响 crypto 验签!都能通过
|
||
let pubkeyPEM = await new keyman.Key('oct', this.hex_to_buf(pubkey), { namedCurve: 'P-256K' }).export('pem') // 公钥导出的der格式为88字节。经测试,同一对压缩和非压缩公钥得出的结果一模一样。
|
||
let verifier = crypto.createVerify(my.HASHER_LIST.includes(hasher) ? hasher : my.HASHER)
|
||
verifier.update(this.hash(data)).end() // end() 在 nodejs 12 里返回verifier自身,但在浏览器里返回 undefined,因此不能串联运行。
|
||
let verified = verifier.verify(pubkeyPEM, signature, 'hex') // 如果给signature添加1位hex,crypto 的 verify结果也是true! 估计因为一位hex不被转成字节。但减少1位会导致false
|
||
// since nodejs 12, 有了 crypto.verify 方法,但在浏览器中无效:
|
||
// let verified = crypto.verify(my.HASHER_LIST.includes(hasher) ? 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.includes(hasher) ? 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 = coin?.toUpperCase() || my.COIN
|
||
|
||
if (tool === 'nacl') {
|
||
// 采用自己的算法:bip39算法从secword到种子,hash后用 nacl.sign.keyPair.fromSeed()方法。
|
||
hasher = my.HASHER_LIST.includes(hasher) ? 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字节密钥。
|
||
tool,
|
||
}
|
||
} 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 {
|
||
// 指定了路径 path 例如 "m/0/2147483647'/1" 则用 path;没有指定路径 则调用 seed2path() 来获取默认的根路径 例如 "m/44'/0'/0'/0/0" 或 "m/44'/60'/0'/0/0"
|
||
path = path || this.seed2path({ coin })
|
||
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'),
|
||
path,
|
||
}
|
||
}
|
||
return null
|
||
}
|
||
|
||
/**
|
||
* 从种子到路径
|
||
*
|
||
* @static
|
||
* @param {*} seed
|
||
* @param {string} option [{ coin = my.COIN }={ coin: my.COIN }]
|
||
* @return {String} path
|
||
* @memberof Ticrypto
|
||
*/
|
||
static seed2path ({ seed, coin = my.COIN } = { coin: my.COIN }) {
|
||
// 路径规范 BIP44: m/Purpose'/Coin'/Account'/Change/Index,
|
||
// 但实际上 Purpose, Coin 都可任意定;' 可有可无;
|
||
// Account/Change/Index 最大到 parseInt(0x7FFFFFFF, 16)
|
||
// 后面还可继续延伸 /xxx/xxx/xxx/......
|
||
let path
|
||
if (seed) {
|
||
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)
|
||
path = `${part0}'/${part1}/${part2}/${part3}/${part4}/${part5}`
|
||
} else {
|
||
path = "0'/0/0"
|
||
}
|
||
coin = coin.toUpperCase() || my.COIN
|
||
if (coin === 'BTC') {
|
||
return `m/44'/0'/${path}`
|
||
} else if (coin === 'ETH') {
|
||
return `m/44'/60'/${path}`
|
||
} else if (coin === 'TIC' || !coin) {
|
||
return `m/44'/60000'/${path}`
|
||
} else if (/[A-Z]{3}/.test(coin)) {
|
||
return `m/44'/60${this.alpha_to_digit(coin)}'/${path}`
|
||
} else {
|
||
return ''
|
||
}
|
||
}
|
||
|
||
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 Ticrypto
|
||
*/
|
||
static secword2account (secword, { coin, pass, path, tool, hasher } = {}) {
|
||
// account 比 keypair 多了 address 字段。
|
||
coin = 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 = 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.includes(curve) ? 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 = 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 = 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字符的。
|
||
coin = coin?.toUpperCase() || 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。得到26~34个字符,大多数34个。
|
||
// Base58: https://en.bitcoin.it/wiki/Base58Check_encoding
|
||
// https://en.bitcoin.it/wiki/List_of_address_prefixes
|
||
let prefix
|
||
switch (world) {
|
||
case 'mainnet':
|
||
prefix = '00'
|
||
break // pubkey hash => 1
|
||
case 'mainnetSh':
|
||
prefix = '05'
|
||
break // script hash => 3
|
||
case 'testnet':
|
||
prefix = '6f'
|
||
break // testnet pubkey hash => m or n
|
||
case 'testnetSh':
|
||
prefix = 'c4'
|
||
break // testnet script hash => 2
|
||
case 'namecoin':
|
||
prefix = '34'
|
||
break // Namecoin pubkey hash => M or N
|
||
case 'compact':
|
||
prefix = '15'
|
||
break // compact pubkey (proposed) => 4
|
||
default:
|
||
prefix = '00'
|
||
}
|
||
address = bs58check.encode(Buffer.from(prefix + position, 'hex')) // wallet import format
|
||
return address
|
||
} else {
|
||
// 默认为 TIC。把纯位置转换为大小写敏感能自我验证的 b64t 地址。
|
||
let prefix
|
||
switch (world) {
|
||
// Base64: https://baike.baidu.com/item/base64
|
||
case 'earth':
|
||
prefix = '4c'
|
||
break // Base58: 0x42=66 => T, Base64: T=0x13=0b00010011 => 0b010011xx = 0x4c~4f
|
||
case 'moon':
|
||
prefix = 'b4'
|
||
break // Base58: 0x7f=127,0x80=128 => t, Base64: t=0x2d=0b00101101 => 0b101101xx = 0xB4~B7
|
||
case 'comet':
|
||
prefix = '74'
|
||
break // Base58: 0x90 => d, Base 64: d=0x1d=0b00011101 => 0b 011101xx = 0x74~77
|
||
default:
|
||
prefix = '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 = 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 直接产生随机密钥
|
||
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.includes(hasher) ? 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
|