763 lines
37 KiB
JavaScript
763 lines
37 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 uuid = require('uuid')
|
||
const keccak = require('keccak')
|
||
const eccrypto = require('eccrypto') // 用于加解密。不知道怎么用 crypto 本身的加解密。
|
||
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')
|
||
|
||
// 全部以hex为默认输入输出格式,方便人的阅读,以及方便函数之间统一接口
|
||
|
||
const my={}
|
||
my.HASHER='sha256' // 默认的哈希算法。could be md5, sha1, sha256, sha512, ripemd160 and much more。 可用 Crypto.getHashes/Ciphers/Curves() 查看支持的种类。
|
||
my.HASHER_LIST=crypto.getHashes()
|
||
my.CIPHER='aes-256-cfb' // 默认的加解密算法
|
||
my.CIPHER_LIST=crypto.getCiphers()
|
||
my.CURVE='secp256k1' // 默认的ECDH曲线,用于把私钥转成公钥。
|
||
my.CURVE_LIST=['secp256k1'] // 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.CHAINNET='mainnet' // 默认的链网
|
||
|
||
module.exports = {
|
||
isHashable(data, option){
|
||
option=option||{}
|
||
if (option.strict) {
|
||
return data && typeof(data)!=='boolean' && data!==Infinity // 允许大多数数据,除了空值、布尔值、无限数
|
||
}
|
||
return typeof(data)!=='undefined' // 允许一切数据,除非 undefined
|
||
}
|
||
,
|
||
isHash(hash, option){
|
||
option=option||{}
|
||
option.hasher=my.HASHER_LIST.indexOf(option.hasher)>=0?option.hasher:my.HASHER
|
||
switch(option.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
|
||
}
|
||
,
|
||
isSecword(secword){ // 注意 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) {
|
||
for (let lang of Object.keys(bip39.wordlists)) {
|
||
bip39.setDefaultWordlist(lang)
|
||
if (bip39.validateMnemonic(secword))
|
||
return true
|
||
}
|
||
}
|
||
return false
|
||
}
|
||
,
|
||
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)
|
||
}
|
||
,
|
||
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"
|
||
}
|
||
,
|
||
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.
|
||
}
|
||
,
|
||
hash(data, option){ // data can be anything, but converts to string or remains be Buffer/TypedArray/DataView
|
||
if (this.isHashable(data)) {
|
||
option=option||{}
|
||
if (typeof(data)!=='string' && !(data instanceof Buffer) && !(data instanceof DataView))
|
||
data=JSON.stringify(data)
|
||
if (option.salt && typeof(option.salt)==='string')
|
||
data=data+this.hash(option.salt)
|
||
let hasher= my.HASHER_LIST.indexOf(option.hasher)>=0?option.hasher:my.HASHER // 默认为 sha256.
|
||
let inputEncoding=my.INPUT_LIST.indexOf(option.input)>=0?option.input:my.INPUT // 'utf8', 'ascii' or 'latin1' for string data, default to utf8 if not specified; ignored for Buffer, TypedArray, or DataView.
|
||
let outputEncoding=(option.output==='buf')?undefined:(my.OUTPUT_LIST.indexOf(option.output)>=0?option.output:my.OUTPUT) // option.output: 留空=》默认输出hex格式;或者手动指定 'buf', hex', 'latin1' or 'base64'
|
||
return crypto.createHash(hasher).update(data, inputEncoding).digest(outputEncoding)
|
||
}
|
||
return null
|
||
}
|
||
,
|
||
async encrypt(data, {keytype, key, input, output, cipher}={}){
|
||
if (keytype==='pwd') {
|
||
if (this.isHashable(data) && 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
|
||
let cipher=crypto.createCipher(
|
||
my.CIPHER_LIST.indexOf(cipher)>=0?cipher:my.CIPHER,
|
||
this.hash(key))
|
||
if (typeof(data)!=='string' && !(data instanceof Buffer) && !(data instanceof DataView))
|
||
data=JSON.stringify(data)
|
||
let encrypted = cipher.update(data, inputEncoding, outputEncoding)
|
||
encrypted += cipher.final(outputEncoding) // 但是 Buffer + Buffer 还是会变成string
|
||
return encrypted
|
||
}
|
||
}else if (keytype==='pubkey') { // data 应当是 utf8 的字符串。// 但在浏览器里不能使用 Failed to execute 'encrypt' on 'SubtleCrypto': The provided value is not of type '(ArrayBuffer or ArrayBufferView)'
|
||
let cipherobject = await eccrypto.encrypt(this.hex2buf(key), data)
|
||
return cipherobject
|
||
}
|
||
return null
|
||
}
|
||
,
|
||
async decrypt(data, {keytype, key, input, output, cipher, format}={}){ // data 应当是 encrypt 输出的数据类型
|
||
if (keytype==='pwd') {
|
||
if (data && (typeof(data)==='string' || data 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 decipher=crypto.createDecipher(
|
||
my.CIPHER_LIST.indexOf(cipher)>=0?cipher:my.CIPHER,
|
||
this.hash(key))
|
||
let decrypted = decipher.update(data, inputEncoding, outputEncoding)
|
||
decrypted += decipher.final(outputEncoding) // 但是 Buffer + Buffer 还是会变成string
|
||
if (format==='json') { // 如果用户输入错误密码,deciper也能返回结果。为了判断是否正确结果,对应当是 json 格式的原文做解析来验证。
|
||
try{
|
||
JSON.parse(decrypted)
|
||
}catch(exception){
|
||
return null
|
||
}
|
||
}
|
||
return decrypted
|
||
}else if (keytype==='seckey'){ // cipherobject 需要是 eccrypto 自身encrypt方法返回的对象
|
||
let plaindata = await eccrypto.decrypt(Buffer.from(key, 'hex'), data) // eccrypto 需要调用 Buffer.compare 方法,不能在这里直接用 hex2buf
|
||
return plaindata.toString('utf8')
|
||
}
|
||
}
|
||
return null
|
||
}
|
||
,
|
||
async sign(data, seckey, option={}) { // data can be string or buffer or object, results are the same
|
||
if (this.isHashable(data) && this.isSeckey(seckey) && seckey.length===64) {
|
||
if (option.tool==='crypto') { // 纯 crypto
|
||
let seckeyPEM = await new keyman.Key('oct', this.hex2buf(seckey), {namedCurve:'P-256K'}).export('pem') // 私钥导出的der格式为144字节。
|
||
let hasher=my.HASHER_LIST.indexOf(option.hasher)>=0?option.hasher:my.HASHER
|
||
let signer=crypto.createSign(hasher)
|
||
signer.update(this.hash(data, option)).end()
|
||
let signature = signer.sign(seckeyPEM, 'hex')
|
||
return signature // 发现同样的输入,nodejs里每次调用会生成不同的 signature, 且长度不定(140,142,144 hex) 但都可以通过 verify。但在浏览器里调用,signature却是固定的。
|
||
}else if (option.tool==='nacl') {
|
||
// 这样不行,无法和verify共享一套公私钥。
|
||
// let naclSeckey = this.buf2hex(nacl.sign.keyPair.fromSeed(seckey).seckey)
|
||
// return await this.sign(data, naclSeckey, option)
|
||
}else { // default to 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')
|
||
}
|
||
}
|
||
if (this.isHashable(data) && this.isSeckey(seckey) && seckey.length===128) { // 使用nacl的签名算法。注意,nacl.sign需要的seckey是64字节=128字符。
|
||
option.output='buf' // 哈希必须输出为 buffer
|
||
let hashBuf = this.hash(data, option)
|
||
let signature = nacl.sign.detached(hashBuf, Buffer.from(seckey, 'hex'))
|
||
return Buffer.from(signature).toString('hex') // 签名是64节,128个hex字符
|
||
}
|
||
return null
|
||
}
|
||
,
|
||
async verify (data, signature, pubkey, option={}) { // data could be anything, but converts to string or remains be Buffer/TypedArray/DataView
|
||
if (this.isHashable(data) && this.isSignature(signature) && this.isPubkey(pubkey) && signature.length>=140){
|
||
if (option.tool==='crypto') { // 纯 crypto
|
||
let pubkeyPEM = await new keyman.Key('oct', this.hex2buf(pubkey), {namedCurve:'P-256K'}).export('pem') // 公钥导出的der格式为88字节。经测试,同一对压缩和非压缩公钥得出的结果一模一样。
|
||
let hasher=my.HASHER_LIST.indexOf(option.hasher)>=0?option.hasher:my.HASHER
|
||
let verifier = crypto.createVerify(hasher)
|
||
verifier.update(this.hash(data, option)).end() // end() 在 nodejs 12 里返回verifier自身,但在浏览器里返回 undefined,因此不能串联运行。
|
||
let verified = verifier.verify(pubkeyPEM, signature, 'hex') // 如果给signature添加1位hex,crypto 的 verify结果也是true! 估计因为一位hex不被转成字节。
|
||
return verified
|
||
}else if ('nacl'===option.tool) {
|
||
// 这样不行,无法和sign共享一套公私钥
|
||
// let naclPubkey = nacl.sign.keyPair.fromSeed()
|
||
}else { // 默认使用 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){
|
||
return false
|
||
}
|
||
}
|
||
}
|
||
if (this.isHashable(data) && this.isSignature(signature) && this.isPubkey(pubkey) && signature.length===128){ // nacl
|
||
option.output='buf' // 哈希必须输出为 buffer
|
||
let bufHash=this.hash(data, option)
|
||
let bufSignature = Buffer.from(signature, 'hex')
|
||
let bufPubkey = Buffer.from(pubkey, 'hex')
|
||
let verified = nacl.sign.detached.verify(bufHash, bufSignature, bufPubkey)
|
||
return verified
|
||
}
|
||
return false
|
||
}
|
||
,
|
||
pass2keypair(pass, option){ // 如果使用其他机制,例如密码、随机数,不使用secword,也可生成keypair
|
||
if (this.isHashable(pass)){
|
||
option=option||{}
|
||
option.hasher=my.HASHER_LIST.indexOf(option.hasher)>=0?option.hasher:my.HASHER
|
||
var hashBuf = crypto.createHash(option.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
|
||
}
|
||
,
|
||
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.
|
||
}
|
||
,
|
||
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.
|
||
}
|
||
,
|
||
secword2keypair(secword, option){
|
||
// option.coin 币种;
|
||
// option.passphase 密码,默认为空;
|
||
// option.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/...
|
||
if (this.isSecword(secword)){
|
||
option=option||{}
|
||
option.coin=my.COIN_LIST.indexOf(option.coin)>=0?option.coin:my.COIN
|
||
|
||
if(option.tool==='nacl') {
|
||
// 采用自己的算法:bip39算法从secword到种子,hash后用 nacl.sign.keyPair.fromSeed()方法。
|
||
option.hasher=my.HASHER_LIST.indexOf(option.hasher)>=0?option.hasher:my.HASHER
|
||
let hashBuf=crypto.createHash(option.hasher).update(this.secword2seed(secword, option.pass)).digest()
|
||
let keypair = nacl.sign.keyPair.fromSeed(hashBuf) // nacl.sign.keyPair.fromSeed 要求32字节的种子,而 this.secword2seed生成的是64字节种子,所以要先做一次sha256
|
||
return {
|
||
coin: option.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, option.pass), 'hex')) // 和 new BitcoreMnemonic(secword).toHDPrivateKey 求出的公私钥一样!
|
||
// let hdmaster=new BitcoreMnemonic(secword).toHDPrivateKey(option.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 (option.path==='master'){
|
||
key=hdmaster
|
||
}else if (!option.path) {
|
||
switch(option.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 'TIC': key=hdmaster.derive("m/44'/66'/0'/0/0"); break
|
||
default: key=hdmaster.derive("m/44'/99'/0'/0/0"); break
|
||
}
|
||
}else { // 指定了路径 option.path,例如 "m/44'/0'/0'/0/6" 或 "m/0/2147483647'/1"
|
||
key=hdmaster.derive(option.path)
|
||
}
|
||
return {
|
||
coin: option.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
|
||
}
|
||
,
|
||
secword2account(secword, option){ // account 比 keypair 多了 address 字段。
|
||
option=option||{}
|
||
option.coin=my.COIN_LIST.indexOf(option.coin)>=0?option.coin:my.COIN
|
||
let kp=this.secword2keypair(secword, option)
|
||
if (kp) {
|
||
kp.address=this.seckey2address(kp.seckey, option)
|
||
return kp
|
||
}
|
||
return null
|
||
}
|
||
,
|
||
secword2address(secword, option){
|
||
option=option||{}
|
||
option.coin=my.COIN_LIST.indexOf(option.coin)>=0?option.coin:my.COIN
|
||
let kp=this.secword2keypair(secword, option)
|
||
if (kp) {
|
||
return this.seckey2address(kp.seckey,option)
|
||
}
|
||
return null
|
||
}
|
||
,
|
||
seckey2pubkey(seckey, option={}){
|
||
option.coin=my.COIN_LIST.indexOf(option.coin)>=0?option.coin:my.COIN
|
||
if (this.isSeckey(seckey) && seckey.length===64){ // 只能用于32字节的私钥(BTC, ETH)。也就是不能用于 TIC 的私钥。
|
||
let curve = my.CURVE_LIST.indexOf(option.curve)>=0?option.curve:my.CURVE // 默认为 secp256k1
|
||
return new crypto.createECDH(curve).setPrivateKey(seckey,'hex').getPublicKey('hex', option.compress===false?'uncompressed':'compressed') // ecdh.getPublicKey(不加参数) 默认为 'uncompressed'
|
||
// 从 nodejs 10.0 开始,还有 crypto.ECDH.convertKey 方法,更直接。但可惜,浏览器里不存在 crypto.ECDH。
|
||
// 或者 return this.buf2hex(require('secp256k1').publicKeyCreate(Buffer.from(seckey, 'hex'), option.compress!==false)) // 可用于浏览器。secp256k1缺省或true时输出压缩公钥,false时输出非压缩公钥。
|
||
// 或者 bitcorelib.PublicKey.fromPrivateKey(new bitcorelib.PrivateKey(seckey)).toString('hex') // 可用于浏览器
|
||
// 或者 const ecc = require('eccrypto')
|
||
// if (option.compress===false){
|
||
// return ecc.getPublic(this.hex2buf(seckey)).toString('hex')
|
||
// }else{
|
||
// return ecc.getPublicCompressed(this.hex2buf(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
|
||
}
|
||
,
|
||
seckey2address(seckey, option){
|
||
option=option||{}
|
||
option.coin=my.COIN_LIST.indexOf(option.coin)>=0?option.coin:my.COIN
|
||
if (this.isSeckey(seckey)){
|
||
let pubkey
|
||
if (option.coin==='ETH'){
|
||
pubkey = this.seckey2pubkey(seckey, {compress:false})
|
||
return this.pubkey2address(pubkey, option)
|
||
}else {
|
||
pubkey = this.seckey2pubkey(seckey, {compress:true})
|
||
return this.pubkey2address(pubkey, option)
|
||
}
|
||
}
|
||
return null
|
||
}
|
||
,
|
||
pubkey2position (pubkey, {coin}={}){ // tic, btc, eth 的 position 都是 20节=40字符的。
|
||
coin = my.COIN_LIST.indexOf(coin)>=0?coin: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
|
||
}
|
||
,
|
||
position2address(position, {coin, net, format}={}){
|
||
if (!/^[\da-fA-F]{40}$/.test(position)) return null // 不论 tic, btc, eth,其 position 都是 40字符的。
|
||
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。得到26~34个字符,大多数34个。
|
||
let prefix
|
||
switch (net) {
|
||
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。把纯位置转换为大小写敏感能自我验证的 b64u(base64 for url) 地址。
|
||
let prefix
|
||
switch (net){
|
||
// Base58: https://en.bitcoin.it/wiki/List_of_address_prefixes
|
||
// Base64: https://baike.baidu.com/item/base64
|
||
case 'mainnet': prefix='4c'; break; // Base58: 0x42=66 => T, Base64: base64 T=0x13=0b00010011 => 0b010011xx = 0x4c~4f
|
||
case 'testnet': prefix='b4'; break; // Base58: 0x7f=127,0x80=128 => t, Base64: t=0x2d=0b00101101 => 0b101101xx = 0xB4~B7
|
||
case 'devnet': 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.hex2eip55(prefix + position + checksum) // 前缀1节,位置20节,校验3节,共24节=48字符(能够完全转化为8个色彩),再转eip55。
|
||
address = this.hex2b64u(prefix + position + checksum) // 实际采用 b64u (named by luk.lu as base 64 for url), 共 32字符。
|
||
return address
|
||
}
|
||
return null
|
||
}
|
||
,
|
||
address2position(){
|
||
if (/^0x[\da-fA-F]{40}$/.test(address)){
|
||
return address.toLowerCase()
|
||
}else if (/^[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{26,34}$/.test(address)){
|
||
let hex = this.b58c2hex(address)
|
||
if (hex) {
|
||
return hex.slice(2) // 去除网络前缀
|
||
}
|
||
}else if (/^[Tt][0-9a-zA-Z\-_]{31}$/.test(address)){ // 格式合法
|
||
let hex = this.b64u2hex(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
|
||
}
|
||
,
|
||
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.b58c2hex(address)
|
||
if (prefixedPosition && prefixedPosition.length===42) // 内容合法
|
||
return 'BTC'
|
||
}else if (/^[Ttd][0-9a-zA-Z\-_]{31}$/.test(address)){ // 格式合法
|
||
let b64 = address.replace('-', '+').replace('_', '/')
|
||
let hex = Buffer.from(b64, 'base64').toString('hex')
|
||
let [all, prefix, position, checksum] = hex.match(/^([\da-fA-F]{2})([\da-fA-F]{40})([\da-fA-F]{6})$/) // 内容合法
|
||
return this.hash(this.hash(prefix+position)).slice(0,6) === checksum // [todo] 校验码里要不要包含 prefix?
|
||
}
|
||
return null
|
||
}
|
||
,
|
||
pubkey2address (pubkey, option={}) { // pubkey 应当是string类型
|
||
option.coin=my.COIN_LIST.indexOf(option.coin)>=0?option.coin:my.COIN
|
||
return this.position2address(this.pubkey2position(pubkey, option), option)
|
||
}
|
||
,
|
||
secword2seed(secword, pass) { // 遵循bip39的算法。和 ether.HDNode.mnemonic2Seed 结果一样,是64字节的种子。
|
||
if (this.isSecword(secword)) { // bip39.validateMnemonic(secword)) {
|
||
return bip39.mnemonicToSeedSync(secword, pass).toString('hex') // 结果一致于 new BitcoreMnemonic(secword).toSeed(pass).toString('hex') 或 ethers.HDNode.mnemonic2Seed(secword)
|
||
// return new BitcoreMnemonic(secword).toSeed(pass).toString('hex')
|
||
}
|
||
return null
|
||
}
|
||
,
|
||
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()
|
||
}
|
||
,
|
||
randomSeckey(option){ // 跳过 secword 直接产生随机密钥
|
||
option=option||{}
|
||
option.coin=my.COIN_LIST.indexOf(option.coin)>=0?option.coin:my.COIN
|
||
if (option.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字节
|
||
}
|
||
}
|
||
,
|
||
randomKeypair(option={}){
|
||
option.coin=my.COIN_LIST.indexOf(option.coin)>=0?option.coin:my.COIN
|
||
let kp
|
||
if (option.tool==='nacl'){
|
||
if (option.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
|
||
}
|
||
}
|
||
}
|
||
,
|
||
randomAccount(option={}){
|
||
let secword=this.randomSecword(option.lang)
|
||
return this.secword2account(secword, option)
|
||
}
|
||
,
|
||
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
|
||
}
|
||
,
|
||
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.toString().padStart(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
|
||
}
|
||
,
|
||
randomUuid:uuid.v4
|
||
,
|
||
getMerkleHash(hashList, option){
|
||
// merkle算法略有难度,暂时用最简单的hash代替
|
||
if(Array.isArray(hashList)){
|
||
option=option||{}
|
||
let output=(option.output==='buf')?undefined:(option.output||my.OUTPUT)
|
||
let hasher=crypto.createHash(my.HASHER_LIST.indexOf(option.hasher)>=0?option.hasher:my.HASHER)
|
||
for (var hash of hashList){
|
||
hasher.update(hash)
|
||
}
|
||
return hasher.digest(output)
|
||
}
|
||
return null
|
||
}
|
||
,
|
||
getMerkleRoot(todoHashList, option){
|
||
//深拷贝传入数组,防止引用对象被改变
|
||
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
|
||
}
|
||
,
|
||
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).sub(new BigInt(hashSig,16)).abs().toString(16)
|
||
}
|
||
return null
|
||
}
|
||
,
|
||
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
|
||
}
|
||
,
|
||
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 拼接完成以后的字符串
|
||
*/
|
||
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 ''
|
||
}
|
||
,
|
||
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'))
|
||
}
|
||
,
|
||
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')
|
||
}
|
||
,
|
||
buf2hex(buffer) { // buffer is an ArrayBuffer
|
||
return Array.prototype.map.call(new Uint8Array(buffer), x => ('00' + x.toString(16)).slice(-2)).join('');
|
||
}
|
||
,
|
||
hex2buf(hex){
|
||
return new Uint8Array(hex.match(/[\da-f]{2}/gi).map(function (h) {
|
||
return parseInt(h, 16)
|
||
})) // 注意,arraybuffer没有 toString('hex')功能, Buffer才有。
|
||
}
|
||
,
|
||
hex2b58c(hex){
|
||
return bs58check.encode(Buffer.from(hex, 'hex'))
|
||
}
|
||
,
|
||
b58c2hex(box){
|
||
try{
|
||
return bs58check.decode(box).toString('hex')
|
||
}catch(exception){
|
||
return null
|
||
}
|
||
}
|
||
,
|
||
hex2b64u(hex){
|
||
if (/^[0-9a-fA-F]+$/.test(hex)) {
|
||
return Buffer.from(hex,'hex').toString('base64').replace(/\+/g,'-').replace(/\//g, '_')
|
||
}
|
||
return null
|
||
}
|
||
,
|
||
b64u2hex(b64u){
|
||
if (/^[0-9a-zA-Z\-_]+$/.test(b64u)){
|
||
let b64 = b64u.replace(/\-/g, '+').replace(/_/g, '/')
|
||
return Buffer.from(b64, 'base64').toString('hex')
|
||
}
|
||
return null
|
||
}
|
||
,
|
||
hex2eip55(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
|
||
}
|
||
,
|
||
// test: https://iancoleman.io/bitcoin-key-compression/
|
||
// compress: https://hacpai.com/article/1550844562914
|
||
compressPubkey(uncompressed){ // 把 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 // 非压缩公钥有错误。
|
||
}
|
||
,
|
||
// uncompress: https://stackoverflow.com/questions/17171542/algorithm-for-elliptic-curve-point-compression/53478265#53478265
|
||
// https://en.bitcoin.it/wiki/Secp256k1
|
||
decompressPubkey(compressed){ // 把 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.substring(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' + x.toString(16).padStart(64, '0') + y.toString(16).padStart(64, '0')
|
||
}
|
||
} |