tic-crypto/index.js
2020-05-03 08:22:35 +08:00

824 lines
39 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 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, {strict=false}={}){
if (strict) {
return data && typeof(data)!=='boolean' && data!==Infinity // 允许大多数数据,除了空值、布尔值、无限数
}
return typeof(data)!=='undefined' // 允许一切数据,除非 undefined
}
,
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
}
,
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
}
,
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, {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(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=(output==='buf')?undefined:output // (my.OUTPUT_LIST.indexOf(output)>=0?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 ciph=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 = ciph.update(data, inputEncoding, outputEncoding)
encrypted += ciph.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方法返回的对象
try {
let plaindata = await eccrypto.decrypt(Buffer.from(key, 'hex'), data) // eccrypto 需要调用 Buffer.compare 方法,不能在这里直接用 hex2buf
return plaindata.toString('utf8')
}catch (exception){ // eccrypto 对无法解密的,会抛出异常
return null
}
}
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==='nacl') {
// 这样不行无法和verify共享一套公私钥。
// let naclSeckey = this.buf2hex(nacl.sign.keyPair.fromSeed(seckey).seckey)
// return await this.sign(data, naclSeckey, option)
}else if (option.tool==='eccrypto') { // 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 { // 纯 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却是固定的。
}
}
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 ('nacl' === option.tool) {
// 这样不行无法和sign共享一套公私钥
// let naclPubkey = nacl.sign.keyPair.fromSeed()
}else if ('eccrypto' === option.tool) { // 默认使用 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 { // 纯 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位hexcrypto 的 verify结果也是true! 估计因为一位hex不被转成字节。
return verified
}
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/...
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': default: key=hdmaster.derive("m/44'/60000'/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
}
,
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 path=`${part0}'/${part1}/${part2}/${part3}/${part4}/${part5}`
switch (coin){
case 'BTC': return `m/44'/0'/${path}`
case 'ETH': return `m/44'/60'/${path}`
case 'TIC': default: return `m/44'/60000'/${path}`
}
}
,
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) {
if (option.coin === 'ETH') {
let uncompressedPubkey = this.decompressPubkey(kp.pubkey)
kp.address = this.pubkey2address(uncompressedPubkey,{coin:'ETH'})
}else {
kp.address=this.pubkey2address(kp.pubkey, 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) {
let address
if (option.coin === 'ETH') {
let uncompressedPubkey = this.decompressPubkey(kp.pubkey)
address = this.pubkey2address(uncompressedPubkey,{coin:'ETH'})
}else {
address = this.pubkey2address(kp.pubkey, option)
}
return address
}
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(不加参数) 默认为 '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.buf2hex(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}={}){
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。得到2634个字符大多数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})$/) // 内容合法
if (this.hash(this.hash(prefix+position)).slice(0,6) === checksum) // [todo] 校验码里要不要包含 prefix?
return 'TIC'
}
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字节的种子。其实
return bip39.mnemonicToSeedSync(secword, pass).toString('hex') // 结果一致于 new BitcoreMnemonic(secword).toSeed(pass).toString('hex') 或 ethers.HDNode.mnemonic2Seed(secword)。其实bip39.mnemonicToSeedSync 也接受不合法的 secword只要是个string就行。
}
,
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 = 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
}
,
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
}
,
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
}
,
aiid2regcode(aiid) {
const alphabet = 'e5fcdg3hqa4b1n0pij2rstuv67mwx89klyz'
const base = 16367
let num = (aiid+base)*(base - alphabet.length)
let code = ''
let mod
while ( num > 0) {
mod = num % alphabet.length;
num = (num - mod) / alphabet.length
code = code+alphabet[mod] // 倒序存放
}
return code
}
,
regcode2aiid(code) {
const alphabet = 'e5fcdg3hqa4b1n0pij2rstuv67mwx89klyz'
const base = 16367
let len = code.length
let num = 0
for (let i=0; i < len; i++) {
num += alphabet.indexOf(code[i]) * Math.pow(alphabet.length, i)
}
return num/(base - alphabet.length)-base
}
,
// 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' + this.padStart(x.toString(16), 64, '0') + this.padStart(y.toString(16), 64, '0')
}
}