用eccrypto加解密,用crypto签名。互相转换压缩和非压缩公钥

This commit is contained in:
Luk Lu
2020-02-20 13:36:44 +08:00
parent c494dd51de
commit 09d831af00
6 changed files with 726 additions and 79 deletions

300
index.js
View File

@@ -1,17 +1,22 @@
const BigNumber=require('bignumber.js') // https://github.com/MikeMcl/bignumber.js 几个库的比较: node-bignum: 使用到openssl在windows上需要下载二进制包有时下载失败。bigi: 不错。 bignumber.js不错。
// 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 Secword = require('bitcore-mnemonic') // https://bitcore.io/api/mnemonic/ https://github.com/bitpay/bitcore-mnemonic
// 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。 可用 Crypto.getHashes/Ciphers/Curves() 查看支持的种类。
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()
@@ -61,78 +66,117 @@ module.exports = {
return false
}
,
encrypt(data, pwd, option){
if (this.isHashable(data) && typeof(pwd)==='string') {
option=option||{}
let inputEncoding=my.INPUT_LIST.indexOf(option.input)>=0?option.input:my.INPUT // 'utf8' by default, 'ascii', 'latin1' for string or ignored for Buffer/TypedArray/DataView
let outputEncoding=(option.output==='buf')?undefined:(my.OUTPUT_LIST.indexOf(option.output)>=0?option.output:my.OUTPUT) // 'latin1', 'base64', 'hex' by default or 'buf' to Buffer explicitly
let cipher=crypto.createCipher(
my.CIPHER_LIST.indexOf(option.cipher)>=0?option.cipher:my.CIPHER,
this.hash(pwd))
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
}
return null
}
,
decrypt(data, pwd, option){ // data 应当是 encrypt 输出的数据类型
if (data && (typeof(data)==='string' || data instanceof Buffer) && typeof(pwd)==='string') {
option=option||{}
let inputEncoding=my.OUTPUT_LIST.indexOf(option.input)>=0?option.input:my.OUTPUT // input (=output of encrypt) could be 'latin1', 'base64', 'hex' by default for string or ignored for Buffer
let outputEncoding=(option.output==='buf')?undefined:(my.INPUT_LIST.indexOf(option.output)>=0?option.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(option.cipher)>=0?option.cipher:my.CIPHER,
this.hash(pwd))
let decrypted = decipher.update(data, inputEncoding, outputEncoding)
decrypted += decipher.final(outputEncoding) // 但是 Buffer + Buffer 还是会变成string
if (option.format==='json') { // 如果用户输入错误密码deciper也能返回结果。为了判断是否正确结果对应当是 json 格式的原文做解析来验证。
try{
JSON.parse(decrypted)
}catch(exception){
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
}
return decrypted
}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
}
,
sign(data, seckey, option) { // data can be string or buffer or object, results are the same
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
}
,
/* 以下两个方法基于 eccrypto注意其在浏览器上会有问题。
*/
async encryptPubkey(plaindata, pubkey, option){ // plaindata 应当是 utf8 的字符串
let cipherobject = await eccrypto.encrypt(this.hex2buf(pubkey), plaindata)
return cipherobject
}
,
async decryptSeckey(cipherobject, seckey, option){ // cipherobject 需要是 eccrypto 自身encrypt方法返回的对象
let plaindata
if (Buffer) { // nodejs
plaindata = await eccrypto.decrypt(Buffer.from(seckey, 'hex'), cipherobject) // eccrypto 需要调用 Buffer.compare 方法
}else { // browser
plaindata = await eccrypto.decrypt(this.hex2buf(seckey), cipherobject)
}
return plaindata.toString('utf8')
}
,
async sign(data, seckey, option) { // data can be string or buffer or object, results are the same
if (this.isHashable(data) && this.isSeckey(seckey)) {
option=option||{}
// 使用nacl的签名算法。注意nacl.sign需要的seckey是64字节=512位而比特币/以太坊的seckey是32字节。因此本方法只能用于 TIC 币的 keypair。
option.output='buf' // 哈希必须输出为 buffer
var hashBuf = this.hash(data, option)
var signature = nacl.sign.detached(hashBuf, Buffer.from(seckey, 'hex'))
return Buffer.from(signature).toString('hex') // 返回128个hex字符64字节
// 方案1: 使用nacl的签名算法。注意nacl.sign需要的seckey是64字节=512位而比特币/以太坊的seckey是32字节。因此本方法只能用于 TIC 币的 keypair。
// option.output='buf' // 哈希必须输出为 buffer
// var hashBuf = this.hash(data, option)
// var signature = nacl.sign.detached(hashBuf, Buffer.from(seckey, 'hex'))
// return Buffer.from(signature).toString('hex') // 返回128个hex字符64字节
// 方案2:尚未彻底实现。
// let hasher=my.HASHER_LIST.indexOf(option.hasher)>=0?option.hasher:my.HASHER
// 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)
// let signer=crypto.createSign(hasher)
// return signer.update(data, inputEncoding).sign(seckey, outputEncoding) // todo: crypto的sign要求的seckey必须是PEM格式因此这样写是不能用的。
// 方案2: 纯 crypto
let seckeyPEM = await new keyman.Key('oct', this.hex2buf(seckey), {namedCurve:'P-256K'}).export('pem')
let hasher=my.HASHER_LIST.indexOf(option.hasher)>=0?option.hasher:my.HASHER
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)
let signer=crypto.createSign(hasher)
signer.update(data, inputEncoding).end()
let signature = signer.sign(seckeyPEM, outputEncoding)
return signature // 发现同样的输入,每次调用会生成不同的 signature, 但都可以通过 verify。有一次我竟然徒手修改出一个新签名也通过验证。
}
return null
}
,
isSignature(signature){
return /^[a-fA-F0-9]{128}$/.test(signature)
return /^[a-fA-F0-9]{128,144}$/.test(signature)
}
,
verify (data, signature, pubkey, option) { // data could be anything, but converts to string or remains be Buffer/TypedArray/DataView
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)){
option=option||{}
option.output='buf' // 哈希必须输出为 buffer
var bufHash=this.hash(data, option)
var bufSignature = Buffer.from(signature, 'hex')
var bufPubkey = Buffer.from(pubkey, 'hex')
var res = nacl.sign.detached.verify(bufHash, bufSignature, bufPubkey)
return res
// 方案1: nacl
// option=option||{}
// option.output='buf' // 哈希必须输出为 buffer
// var bufHash=this.hash(data, option)
// var bufSignature = Buffer.from(signature, 'hex')
// var bufPubkey = Buffer.from(pubkey, 'hex')
// var res = nacl.sign.detached.verify(bufHash, bufSignature, bufPubkey)
// return res
// 方案2: 纯 crypto
let pubkeyPEM = await new keyman.Key('oct', this.hex2buf(pubkey), {namedCurve:'P-256K'}).export('pem')
let hasher=my.HASHER_LIST.indexOf(option.hasher)>=0?option.hasher:my.HASHER
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)
let verifier = crypto.createVerify(hasher)
verifier.update(data, inputEncoding).end() // end() 在 nodejs 12 里返回verifier自身但在浏览器里返回 undefined因此不能串联运行。
let verified = verifier.verify(pubkeyPEM, signature, 'hex')
return verified
}
return null
}
@@ -230,16 +274,16 @@ module.exports = {
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
let compress = ['compressed', 'uncompressed'].indexOf(option.compress)>=0?option.compress:'compressed' // 默认为压缩格式的公钥
return new crypto.ECDH(curve).setPrivateKey(seckey,'hex').getPublicKey('hex',compress).toString('hex') // ecdh.getPublicKey(不加参数) 默认为 'uncompressed'
// if (option.compressed==='false'){
// return ecc.getPublic(this.hex2arrbuf(seckey)).toString('hex')
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.hex2arrbuf(seckey)).toString('hex')
// return ecc.getPublicCompressed(this.hex2buf(seckey)).toString('hex')
// }
// 从 nodejs 10.0 开始,还有 crypto.ECDH.convertKey 方法,更直接。
// 或者 require('secp256k1').publicKeyCreate(Buffer.from(seckey, 'hex'),compress).toString('hex')
// 或者 require('bitcore-lib').PublicKey.fromPrivateKey(new Btc.PrivateKey(seckey)).toString('hex')
// 注意Buffer.from(nacl.box.keyPair.fromSecretKey(Buffer.from(seckey,'hex')).publicKey).toString('hex') 得到的公钥与上面的不同
}else if (this.isSeckey(seckey) && seckey.length===128 && option.coin==='TIC'){ // 用于64字节=128 hex的 TIC 私钥
let keypair=nacl.sign.keyPair.fromSecretKey(Buffer.from(seckey,'hex'))
@@ -254,10 +298,10 @@ module.exports = {
if (this.isSeckey(seckey)){
let pubkey
if (option.coin==='ETH'){
pubkey = this.seckey2pubkey(seckey, {compress:'uncompressed'})
pubkey = this.seckey2pubkey(seckey, {compress:false})
return this.pubkey2address(pubkey, option)
}else {
pubkey = this.seckey2pubkey(seckey, {compress:'compressed'})
pubkey = this.seckey2pubkey(seckey, {compress:true})
return this.pubkey2address(pubkey, option)
}
}
@@ -285,11 +329,70 @@ module.exports = {
return /^[m|t|d|T][123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{33}$/.test(address) // && address.length>25 && bs58check.decode(address.slice(1)) && ['A'].indexOf(address[0]>=0)) {
}
,
pubkey2address (pubkey, option) { // pubkey 应当是string类型
option=option||{}
pubkey2position (pubkey, {coin}={}){
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, netType}={}){
if (!position) return null
coin = my.COIN_LIST.indexOf(coin)>=0?coin:my.COIN
let address
if (coin==='ETH'){ // 对以太坊,按照 EIP55把纯位置转换为大小写敏感能自我验证的hex地址
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地址
let prefix
switch (netType) {
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 {
let prefix
switch (netType){
case 'mainnet': prefix='42'; break; // '42'=>T, '6E'=>m
case 'testnet': prefix='7F'; break; // '7F'=>t
case 'devnet': prefix='5A'; break; // '5A'=>d
default: prefix='42' // 默认暂且为 42为了兼容已经运行的链。
}
address=bs58check.encode(Buffer.from(prefix+position, 'hex')) // wallet import format
return address
}
return null
}
,
pubkey2address (pubkey, option={}) { // pubkey 应当是string类型
option.coin=my.COIN_LIST.indexOf(option.coin)>=0?option.coin:my.COIN
if (this.isPubkey(pubkey)) {
option = option||{}
let h256 = crypto.createHash('sha256').update(Buffer.from(pubkey, 'hex')).digest()
let h160 = crypto.createHash('ripemd160').update(h256).digest('hex')
let prefix
@@ -308,7 +411,7 @@ module.exports = {
default: prefix='00'
}
}else if (option.coin==='ETH'){
// 注意必须要用非压缩的64字节的公钥的buffer。
// 注意必须要用非压缩的64字节的公钥的buffer,并去掉开头的 04
return '0x' + keccak('keccak256').update(Buffer.from(pubkey.slice(2),'hex')).digest('hex').slice(-40)
// 或 const { keccak256 } = require('ethereumjs-util'); keccak256(Buffer.from(pubkey.slice(2),'hex)).toString('hex').slice(40)
// 或 const { Keccak } = require('sha3'); new Keccak('').update(Bufer.from(pubkey.slice(2),'hex')).digest('hex').slice(-40)
@@ -334,13 +437,13 @@ module.exports = {
return new Secword(Secword.Words[language]).phrase
}
,
randomSeckey(option){
randomSeckey(option){ // todo: 使用 crypto.randomBytes(size)
option=option||{}
option.coin=my.COIN_LIST.indexOf(option.coin)>=0?option.coin:my.COIN
if (option.coin==='TIC'){
return Buffer.from(nacl.sign.keyPair().secretKey).toString('hex') // 64字节
return crypto.randomBytes(64).toString('hex') // Buffer.from(nacl.sign.keyPair().secretKey).toString('hex') // 64字节
}else{
return Buffer.from(nacl.box.keyPair().secretKey).toString('hex') // 32字节
return crypto.randomBytes(32).toString('hex') // Buffer.from(nacl.box.keyPair().secretKey).toString('hex') // 32字节
}
}
,
@@ -436,7 +539,7 @@ module.exports = {
distanceSig(hash, sig){ // hash为64hex字符sig为128hex字符。返回用hex表达的距离。
if (this.isSignature(sig) && this.isHash(hash)){
var hashSig=this.hash(sig) // 把签名也转成32字节的哈希同样长度方便比较
return new BigNumber(hash,16).minus(new BigNumber(hashSig,16)).abs().toString(16)
return new BigInt(hash,16).sub(new BigInt(hashSig,16)).abs().toString(16)
}
return null
}
@@ -520,14 +623,55 @@ module.exports = {
return verifier.update(string2Verify).verify(pubkey, sign, 'base64')
}
,
arrbuf2hex(buffer) { // buffer is an ArrayBuffer
buf2hex(buffer) { // buffer is an ArrayBuffer
return Array.prototype.map.call(new Uint8Array(buffer), x => ('00' + x.toString(16)).slice(-2)).join('');
}
,
hex2arrbuf(hex){
hex2buf(hex){
return new Uint8Array(hex.match(/[\da-f]{2}/gi).map(function (h) {
return parseInt(h, 16)
})) // 注意,arraybuffer没有 toString('hex')功能
})) // 注意,arraybuffer没有 toString('hex')功能, Buffer才有。
}
,
hex2base58check(hex){
return bs58check.encode(Buffer.from(hex, 'hex'))
}
,
base58check2hex(box){
try{
return bs58check.decode(box).toString('hex').toUpperCase()
}catch(exception){
return null
}
}
,
hex2eip55(){
}
,
// test: https://iancoleman.io/bitcoin-key-compression/
// compress: https://hacpai.com/article/1550844562914
compressPubkey(uncompressed){
let [all, x, y]=uncompressed.toLowerCase().match(/^04(.{64})(.{64})$/)
if (/[1,3,5,7,9,b,d,f]$/.test(y)){
return '03'+x // y为奇数=>前缀03
}else{
return '02'+x // y为偶数=>前缀02
}
}
,
// uncompress: https://stackoverflow.com/questions/17171542/algorithm-for-elliptic-curve-point-compression/53478265#53478265
// https://en.bitcoin.it/wiki/Secp256k1
decompressPubkey(compressed){
// 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')
}
}