From ec359d0d983b211bad8c975553e819ad57154e82 Mon Sep 17 00:00:00 2001 From: "luk.lu" Date: Fri, 11 Nov 2022 16:17:18 +0800 Subject: [PATCH] =?UTF-8?q?add:=20=E9=80=9A=E8=BF=87=20ethereum-rsa=20?= =?UTF-8?q?=E5=AE=9E=E7=8E=B0=E4=BA=86=E6=A4=AD=E5=9C=86=E6=9B=B2=E7=BA=BF?= =?UTF-8?q?=E5=85=AC=E7=A7=81=E9=92=A5=E5=8A=A0=E8=A7=A3=E5=AF=86=EF=BC=81?= =?UTF-8?q?fix:=20is=5Fsecword?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 1 + ticc.js | 160 ++++++++++++++++++++++++++++++++------------------- 2 files changed, 101 insertions(+), 60 deletions(-) diff --git a/package.json b/package.json index 8d621da..3190782 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "bip39": "^3.0.4", "bs58check": "^2.1.2", "eccrypto-js": "^5.4.0", + "ethereum-rsa": "^1.0.5", "hdkey": "^2.0.1", "js-crypto-key-utils": "^1.0.4", "keccak": "^3.0.2", diff --git a/ticc.js b/ticc.js index 6e4fb1b..0c8b037 100644 --- a/ticc.js +++ b/ticc.js @@ -6,8 +6,9 @@ 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 ecc = require('eccrypto-js') // 用于加解密。eccrypto 在 windows 上和 openssl 的版本兼容性有点麻烦,所以换用 eccrypto-js const keyman = require('js-crypto-key-utils') // 转换原始密钥和 PER/DER 格式。 +const ethrsa = require('ethereum-rsa') // 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 里的相同功能 @@ -123,7 +124,10 @@ class TicCrypto { // return false //// for bip39. 注意,bip39对当前defaultWordlist之外其他语言的合法 mnemonic 也返回 false,这一点不如 bitcore-mnemonic. 所以不能直接 bip39.validateMnemonic(secword) - if (typeof secword === 'string' && !/(^\s)|\s\s|(\s$)/.test(secword) && [12, 15, 18, 21, 24].includes(secword.split(/\s+/).length)) { + secword = secword + .replace(/^\s+/, '') // 删除开头的空格 + .replace(/\s+$/, '') // 删除末尾的空格 + if (typeof secword === 'string' && [12, 15, 18, 21, 24].includes(secword.split(/\s+/).length)) { if (mode === 'easy') return true // easy模式不检查校验等等严格的合法性了,反正 secword_to_seed 是接受一切字符串的 if (my.langMap[lang?.toLowerCase?.()]) { // 指定了语言则针对该语言词库检查 @@ -131,7 +135,9 @@ class TicCrypto { } else { // 未指定语言则检查所有可能语言词库 for (let lang of my.langList) { - return bip39.validateMnemonic(secword, bip39.wordlists[lang]) + if (bip39.validateMnemonic(secword, bip39.wordlists[lang])) { + return true + } } } } @@ -208,35 +214,40 @@ class TicCrypto { * * @static * @param {*} data - * @param {*} option [{ tool, keytype, key, input, output, cipher }={}] + * @param {*} option [{ mode, key, input, output, cipher }={}] * @return {String} * @memberof TicCrypto */ - static async encrypt_easy ({ data, tool = 'crypto', keytype = 'pwd', key, input = my.INPUT, output = my.OUTPUT, cipher = my.CIPHER } = {}) { - if (tool === 'eccrypto') { + static async encrypt_easy ({ data, mode = 'semkey', key, input = my.INPUT, output = my.OUTPUT, cipher = my.CIPHER } = {}) { + if (typeof data !== 'string' && !(data instanceof Buffer) && !(data instanceof DataView)) data = JSON.stringify(data) + if (mode === 'ethrsa') { + if (key?.prikey && key?.pubkey) { + return ethrsa.encryptMessage(data, key?.prikey, key?.pubkey) + } else { + return ethrsa.encryptMessage(data, key?.senderPrikey, key?.receiverPubkey) + } + } else if (mode === 'ecc') { // 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) + let cipherObject = await ecc.encrypt(Buffer.from(this.hex_to_buf(key)), data) return cipherObject // 返回一个复杂的结构 {iv:Buffer, ciphertext:Buffer, ...}。对同样的key和data,每次返回的结果不一样 - } else if (keytype === 'pwd') { + } else if (mode === 'semkey') { // 对称加密 - if (typeof key === 'string') { - let inputEncoding = input // 'utf8' by default, 'ascii', 'latin1' for string or ignored for Buffer/TypedArray/DataView - let outputEncoding = output === 'buf' ? undefined : output // 'latin1', 'base64', 'hex' by default or 'buf' to Buffer explicitly - const iv = crypto.randomBytes(16) - let encryptor = crypto.createCipheriv(cipher, this.hex_to_buf(this.hash_easy(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 === 'prikey') { - // 尚未走通,不能使用 ticc 生成的 Elliptic curve 椭圆曲线算法公私钥,只能用 crypto.generateKeyPairSync('rsa') 生成的 rsa 公私钥 + let inputEncoding = input // 'utf8' by default, 'ascii', 'latin1' for string or ignored for Buffer/TypedArray/DataView + let outputEncoding = output === 'buf' ? undefined : output // 'latin1', 'base64', 'hex' by default or 'buf' to Buffer explicitly + const iv = crypto.randomBytes(16) + let encryptor = crypto.createCipheriv(cipher, this.hex_to_buf(this.hash_easy(key)), iv) // cipher 和 key 的长度必须相同,例如 cipher 是 ***-192,那么 key 就必须是 192/8=24 字节 = 48 hex 的。 + let ciphertext = encryptor.update(data, inputEncoding, outputEncoding) + ciphertext += encryptor.final(outputEncoding) // 但是 Buffer + Buffer 还是会变成string + return { iv: iv.toString('hex'), ciphertext } // 有 iv,显然每次结果不一样 + } else if (mode === 'prikey') { + // 只能用于 crypto.generateKeyPairSync('rsa') 生成的 rsa 公私钥,不能用于 Elliptic Curve 的公私钥 let prikeyPEM = await new keyman.Key('oct', this.hex_to_buf(key), { namedCurve: 'P-256K' }).export('pem') // 私钥导出的der格式为144字节。 return crypto.privateEncrypt(prikeyPEM, Buffer.from(data)) // 返回 Buffer。每次结果都一样。 - } else if (keytype === 'pubkey') { + } else if (mode === 'pubkey') { + // 只能用于 crypto.generateKeyPairSync('rsa') 生成的 rsa 公私钥,不能用于 Elliptic Curve 的公私钥 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。每次结果不一样。 } @@ -248,39 +259,46 @@ class TicCrypto { * * @static * @param {*} data - * @param {Object} option [{ keytype, key, input, output, cipher, format }={}] + * @param {Object} option [{ mode, key, input, output, cipher, format }={}] * @return {String} * @memberof TicCrypto */ - static async decrypt_easy ({ data = {}, tool = 'crypto', keytype = 'pwd', key, input = my.OUTPUT, output = my.OUTPUT, cipher = my.CIPHER } = {}) { + static async decrypt_easy ({ data = {}, mode = 'semkey', key, input = my.OUTPUT, output = 'utf8', cipher = my.CIPHER } = {}) { // data 应当是 encrypt 输出的数据类型 - if (tool === 'eccrypto') { + if (mode === 'ethrsa') { + if (key?.prikey && key?.pubkey) { + return ethrsa.decryptMessage(data, key?.prikey, key?.pubkey) + } else { + return ethrsa.decryptMessage(data, key?.receiverPrikey, key?.senderPubkey) + } + } else if (mode === 'ecc') { 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 + let plainbuffer = await ecc.decrypt(Buffer.from(key, 'hex'), data) // 返回的是 Buffer return plainbuffer.toString('utf8') } catch (exception) { // eccrypto 对无法解密的,会抛出异常 return null } - } else if (keytype === 'pwd') { + } else if (mode === 'semkey') { // 对称解密 - if ((typeof data.ciphertext === 'string' || data.ciphertext instanceof Buffer) && typeof key === 'string') { + if (typeof data.ciphertext === 'string' || data.ciphertext instanceof Buffer) { let inputEncoding = input // input (=output of encrypt) could be 'latin1', 'base64', 'hex' by default for string or ignored for Buffer let outputEncoding = output === 'buf' ? undefined : output // output (=input of encrypt) could be 'latin1', 'ascii', 'utf8' by default or 'buf' to Buffer explicitly let decryptor = crypto.createDecipheriv(cipher, this.hex_to_buf(this.hash_easy(key)), Buffer.from(data.iv, 'hex')) let decrypted = decryptor.update(data.ciphertext, inputEncoding, outputEncoding) decrypted += decryptor.final(outputEncoding) // 但是 Buffer + Buffer 还是会变成string - // 如果用户输入错误密码,deciper也能解密,无法自动判断是否正确结果。可在返回后人工判断。 + // 如果用户输入错误密钥,deciper也能解密,无法自动判断是否正确结果。可在返回后人工判断。 return decrypted } - } else if (keytype === 'prikey') { - // 尚未走通,不能使用 ticc 生成的 Elliptic curve 椭圆曲线算法公私钥 + } else if (mode === 'prikey') { + // 只能用于 crypto.generateKeyPairSync('rsa') 生成的 rsa 公私钥,不能用于 Elliptic Curve 的公私钥 let prikeyPEM = await new keyman.Key('oct', this.hex_to_buf(key), { namedCurve: 'P-256K' }).export('pem') // 私钥导出的der格式为144字节。 return crypto.privateDecrypt(prikeyPEM, Buffer.from(data)) // 返回 Buffer。每次结果都一样。 - } else if (keytype === 'pubkey') { + } else if (mode === 'pubkey') { + // 只能用于 crypto.generateKeyPairSync('rsa') 生成的 rsa 公私钥,不能用于 Elliptic Curve 的公私钥 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。每次结果不一样。 } @@ -305,9 +323,9 @@ class TicCrypto { let hashBuf = this.hash_easy(data, { output: 'buf' }) // 哈希必须输出为 buffer let signature = nacl.sign.detached(hashBuf, Buffer.from(prikey, 'hex')) return Buffer.from(signature).toString('hex') // 签名是64节,128个hex字符 - } else if (tool === 'eccrypto' && prikey.length === 64) { + } else if (tool === 'ecc' && prikey.length === 64) { // eccrypto 对同一组data, prikey 生成的签名是固定的,观察到hex长度为140或142,是der格式。 - let signature = await eccrypto.sign(Buffer.from(prikey, 'hex'), this.hash_easy(data, { output: 'buf' })) + let signature = await ecc.sign(Buffer.from(prikey, 'hex'), this.hash_easy(data, { output: 'buf' })) return signature.toString('hex') } else if (prikey.length === 64) { // 纯 crypto @@ -343,10 +361,10 @@ class TicCrypto { let bufPubkey = Buffer.from(pubkey, 'hex') let verified = nacl.sign.detached.verify(bufHash, bufSignature, bufPubkey) return verified - } else if ('eccrypto' === tool && signature.length >= 140) { + } else if ('ecc' === tool && signature.length >= 140) { // 默认使用 eccrypto // 发现大小写不影响 eccrypto 验签!都能通过 try { - let result = await eccrypto.verify(Buffer.from(pubkey, 'hex'), this.hash_easy(data, { output: 'buf' }), Buffer.from(signature, 'hex')) // 如果给signature添加1位hex,eccrypto 的 verify结果也是true! 估计因为一位hex不被转成字节。 + let result = await ecc.verify(Buffer.from(pubkey, 'hex'), this.hash_easy(data, { output: 'buf' }), Buffer.from(signature, 'hex')) // 如果给signature添加1位hex,eccrypto 的 verify结果也是true! 估计因为一位hex不被转成字节。 return true } catch (exception) { // 对能够验证的,eccrypto返回 null;对无法验证的,抛出异常 @@ -558,19 +576,20 @@ class TicCrypto { static secword_to_account ({ secword, coin, coinFamily, world, pass, pathSeed, pathIndex, path, tool, hasher } = {}) { // account 比 keypair 多了 address 字段。 coin = coin?.toUpperCase?.() || my.COIN + coinFamily = coinFamily?.toUpperCase?.() || my.COIN_FAMILY let kp = this.secword_to_keypair({ secword, coin, pass, pathSeed, pathIndex, path, tool, hasher }) if (kp) { - if (coin === 'ETH') { + if (coin === 'ETH' || coinFamily === 'ETH') { world = world || 'mainnet' kp.address = this.pubkey_to_address({ pubkey: this.decompress_pubkey(kp.pubkey), coin, coinFamily, world }) - } else if (coin === 'BTC') { + } else if (coin === 'BTC' || coinFamily === 'BTC') { world = world || 'mainnet' kp.address = this.pubkey_to_address({ pubkey: kp.pubkey, coin, coinFamily, world }) } else { world = world || my.WORLD kp.address = this.pubkey_to_address({ pubkey: kp.pubkey, coin, coinFamily, world }) } - return { ...kp, coin, world, secword } + return { ...kp, coin, coinFamily, world, secword } } return null } @@ -1344,27 +1363,27 @@ class TicCrypto { } // https://en.wikipedia.org/wiki/Base32 - static hex_to_b32 (hex, { encoding = 'RFC4648' } = {}) { + static hex_to_b32 (hex) { if (/^[0-9a-fA-F]+$/.test(hex)) { - return base32encode(Buffer.from(hex, 'hex'), encoding) + return base32encode(Buffer.from(hex, 'hex'), 'RFC4648') } return null } - static b32_to_hex (b32, { encoding = 'RFC4648' } = {}) { + static b32_to_hex (b32) { if (/^[A-Za-z2-7=]+$/.test(b32)) { - return Buffer.from(base32decode(b32.toUpperCase(), encoding)).toString('hex') + return Buffer.from(base32decode(b32.toUpperCase(), 'RFC4648')).toString('hex') } return null } - static hex_to_b32h (hex, { encoding = 'RFC4648-HEX' } = {}) { + static hex_to_b32h (hex) { if (/^[0-9a-fA-F]+$/.test(hex)) { - return base32encode(Buffer.from(hex, 'hex'), encoding) + return base32encode(Buffer.from(hex, 'hex'), 'RFC4648-HEX') } 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') + static b32h_to_hex (b32h) { + if (/^[0-9A-Va-v=]+$/.test(b32h)) { + return Buffer.from(base32decode(b32.toUpperCase(), 'RFC4648-HEX')).toString('hex') } return null } @@ -1453,14 +1472,17 @@ class TicCrypto { static cid_to_cosh ({ cid }) { if (/^[Q|1]/.test(cid)) { return this.b58_to_hex(cid).slice(4) - } else if (/^b/.test(cid)) { + } else if (/^[b|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) + } else if (/^[m|M|u|U]/.test(cid)) { + return Buffer.from(cid.substr(1), 'base64').toString('hex') } } static cosh_to_cid ({ cosh, cidBase = 'b32', cidVersion = 1, cidCodec = 'raw', cidAlgo = 'sha256' }) { + // https://github.com/multiformats/multibase const multibase = { identity: 0x00, b2: '0', @@ -1472,38 +1494,56 @@ class TicCrypto { B32: 'B', b32h: 'v', B32h: 'V', + b32hp: 't', + B32hp: 'T', + b32p: 'c', + B32p: 'C', + b32z: 'h', // base32z, z-base-32 b36: 'k', + B36: 'K', b64: 'm', b64p: 'M', b64u: 'u', b64up: 'U', b58: 'z', } + // https://github.com/multiformats/multicodec const multicodec = { + raw: '55', dagpb: '70', p2pkey: '72', - raw: '55', } const multialgo = { identify: '00', sha256: '12', + sha512: '13', + keccak256: '1b', + ripemd160: '1053', + md5: 'd5', } 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}` - console.log(fullHex) + } else if (cidVersion === 1) { + const fullHex = `01${multicodec[cidCodec]}${multialgo[cidAlgo]}${Number(cosh.length / 2).toString(16)}${cosh}` + let converted = '' if (cidBase === 'b32') { - return ( - multibase[cidBase] + - this.hex_to_b32(fullHex) - .toLowerCase() - .replace(/=/g, '') - ) + converted = this.hex_to_b32(fullHex) + .toLowerCase() + .replace(/=/g, '') + } else if (cidBase === 'B32') { + converted = this.hex_to_b32(fullHex) + .toUpperCase() + .replace(/=/g, '') } else if (cidBase === 'b58') { - return multibase[cidBase] + this.hex_to_b58(fullHex) + converted = this.hex_to_b58(fullHex) + } else if (cidBase === 'b64p') { + converted = Buffer.from(fullHex, 'hex').toString('base64') + } else if (cidBase === 'b64') { + converted = Buffer.from(fullHex, 'hex') + .toString('base64') + .replace(/=/g, '') } + return multibase[cidBase] + converted } } }