diff --git a/src/bip32.js b/src/bip32.js index 9888131..9139d78 100644 --- a/src/bip32.js +++ b/src/bip32.js @@ -5,16 +5,12 @@ const crypto = require("./crypto"); const testecc_1 = require("./testecc"); const base_1 = require("@scure/base"); const sha256_1 = require("@noble/hashes/sha256"); +const uint8array_utils_1 = require("./uint8array-utils"); const typeforce = require('typeforce'); -const wif = require('wif'); -const _bs58check = (0, base_1.base58check)(sha256_1.sha256); -const bs58check = { - encode: (data) => _bs58check.encode(Uint8Array.from(data)), - decode: (str) => Buffer.from(_bs58check.decode(str)), -}; +const bs58check = (0, base_1.base58check)(sha256_1.sha256); function BIP32Factory(ecc) { (0, testecc_1.testEcc)(ecc); - const UINT256_TYPE = typeforce.BufferN(32); + const UINT256_TYPE = (0, uint8array_utils_1.Uint8ArrayTypeN)(32); const NETWORK_TYPE = typeforce.compile({ wif: typeforce.UInt8, bip32: { @@ -42,7 +38,7 @@ function BIP32Factory(ecc) { return typeforce.UInt32(value) && value <= UINT31_MAX; } function toXOnly(pubKey) { - return pubKey.length === 32 ? pubKey : pubKey.slice(1, 33); + return pubKey.length === 32 ? pubKey : pubKey.subarray(1, 33); } class Bip32Signer { constructor(__D, __Q) { @@ -52,7 +48,7 @@ function BIP32Factory(ecc) { } get publicKey() { if (this.__Q === undefined) - this.__Q = Buffer.from(ecc.pointFromScalar(this.__D, true)); + this.__Q = ecc.pointFromScalar(this.__D, true); return this.__Q; } get privateKey() { @@ -64,18 +60,19 @@ function BIP32Factory(ecc) { if (lowR === undefined) lowR = this.lowR; if (lowR === false) { - return Buffer.from(ecc.sign(hash, this.privateKey)); + return ecc.sign(hash, this.privateKey); } else { - let sig = Buffer.from(ecc.sign(hash, this.privateKey)); - const extraData = Buffer.alloc(32, 0); + let sig = ecc.sign(hash, this.privateKey); + const extraData = new Uint8Array(32); + const extraDataView = new DataView(extraData.buffer); let counter = 0; // if first try is lowR, skip the loop // for second try and on, add extra entropy counting up while (sig[0] > 0x7f) { counter++; - extraData.writeUIntLE(counter, 0, 6); - sig = Buffer.from(ecc.sign(hash, this.privateKey, extraData)); + extraDataView.setUint32(0, counter, true); + sig = ecc.sign(hash, this.privateKey, extraData); } return sig; } @@ -85,7 +82,7 @@ function BIP32Factory(ecc) { throw new Error('Missing private key'); if (!ecc.signSchnorr) throw new Error('signSchnorr not supported by ecc library'); - return Buffer.from(ecc.signSchnorr(hash, this.privateKey)); + return ecc.signSchnorr(hash, this.privateKey); } verify(hash, signature) { return ecc.verify(hash, this.publicKey, signature); @@ -119,7 +116,7 @@ function BIP32Factory(ecc) { return crypto.hash160(this.publicKey); } get fingerprint() { - return this.identifier.slice(0, 4); + return this.identifier.subarray(0, 4); } get compressed() { return true; @@ -137,56 +134,53 @@ function BIP32Factory(ecc) { const version = !this.isNeutered() ? network.bip32.private : network.bip32.public; - const buffer = Buffer.allocUnsafe(78); + const buffer = new Uint8Array(78); + const bufferView = new DataView(buffer.buffer); // 4 bytes: version bytes - buffer.writeUInt32BE(version, 0); + bufferView.setUint32(0, version, false); // 1 byte: depth: 0x00 for master nodes, 0x01 for level-1 descendants, .... - buffer.writeUInt8(this.depth, 4); + bufferView.setUint8(4, this.depth); // 4 bytes: the fingerprint of the parent's key (0x00000000 if master key) - buffer.writeUInt32BE(this.parentFingerprint, 5); + bufferView.setUint32(5, this.parentFingerprint, false); // 4 bytes: child number. This is the number i in xi = xpar/i, with xi the key being serialized. // This is encoded in big endian. (0x00000000 if master key) - buffer.writeUInt32BE(this.index, 9); + bufferView.setUint32(9, this.index, false); // 32 bytes: the chain code - this.chainCode.copy(buffer, 13); + buffer.set(this.chainCode, 13); // 33 bytes: the public key or private key data if (!this.isNeutered()) { // 0x00 + k for private keys - buffer.writeUInt8(0, 45); - this.privateKey.copy(buffer, 46); + bufferView.setUint8(45, 0); + buffer.set(this.privateKey, 46); // 33 bytes: the public key } else { // X9.62 encoding for public keys - this.publicKey.copy(buffer, 45); + buffer.set(this.publicKey, 45); } return bs58check.encode(buffer); } - toWIF() { - if (!this.privateKey) - throw new TypeError('Missing private key'); - return wif.encode(this.network.wif, this.privateKey, true); - } // https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki#child-key-derivation-ckd-functions derive(index) { typeforce(typeforce.UInt32, index); const isHardened = index >= HIGHEST_BIT; - const data = Buffer.allocUnsafe(37); + const data = new Uint8Array(37); + const dataView = new DataView(data.buffer); // Hardened child if (isHardened) { if (this.isNeutered()) throw new TypeError('Missing private key for hardened child key'); // data = 0x00 || ser256(kpar) || ser32(index) data[0] = 0x00; - this.privateKey.copy(data, 1); - data.writeUInt32BE(index, 33); + data.set(this.privateKey, 1); + dataView.setUint32(33, index, false); // Normal child } else { // data = serP(point(kpar)) || ser32(index) // = serP(Kpar) || ser32(index) - this.publicKey.copy(data, 0); - data.writeUInt32BE(index, 33); + data.set(this.publicKey, 0); + dataView.setUint32(33, index, false); } const I = crypto.hmacSHA512(this.chainCode, data); const IL = I.slice(0, 32); @@ -198,21 +192,21 @@ function BIP32Factory(ecc) { let hd; if (!this.isNeutered()) { // ki = parse256(IL) + kpar (mod n) - const ki = Buffer.from(ecc.privateAdd(this.privateKey, IL)); + const ki = ecc.privateAdd(this.privateKey, IL); // In case ki == 0, proceed with the next value for i if (ki == null) return this.derive(index + 1); - hd = fromPrivateKeyLocal(ki, IR, this.network, this.depth + 1, index, this.fingerprint.readUInt32BE(0)); + hd = fromPrivateKeyLocal(ki, IR, this.network, this.depth + 1, index, new DataView(this.fingerprint.buffer).getUint32(0, false)); // Public parent key -> public child key } else { // Ki = point(parse256(IL)) + Kpar // = G*IL + Kpar - const Ki = Buffer.from(ecc.pointAddScalar(this.publicKey, IL, true)); + const Ki = ecc.pointAddScalar(this.publicKey, IL, true); // In case Ki is the point at infinity, proceed with the next value for i if (Ki === null) return this.derive(index + 1); - hd = fromPublicKeyLocal(Ki, IR, this.network, this.depth + 1, index, this.fingerprint.readUInt32BE(0)); + hd = fromPublicKeyLocal(Ki, IR, this.network, this.depth + 1, index, new DataView(this.fingerprint.buffer).getUint32(0, false)); } return hd; } @@ -253,13 +247,12 @@ function BIP32Factory(ecc) { const tweakedPublicKey = ecc.xOnlyPointAddTweak(xOnlyPubKey, t); if (!tweakedPublicKey || tweakedPublicKey.xOnlyPubkey === null) throw new Error('Cannot tweak public key!'); - const parityByte = Buffer.from([ + const parityByte = Uint8Array.from([ tweakedPublicKey.parity === 0 ? 0x02 : 0x03, ]); - const tweakedPublicKeyCompresed = Buffer.concat([ - parityByte, - tweakedPublicKey.xOnlyPubkey, - ]); + const tweakedPublicKeyCompresed = new Uint8Array(tweakedPublicKey.xOnlyPubkey.length + 1); + tweakedPublicKeyCompresed.set(parityByte); + tweakedPublicKeyCompresed.set(tweakedPublicKey.xOnlyPubkey, 1); return new Bip32Signer(undefined, tweakedPublicKeyCompresed); } tweakFromPrivateKey(t) { @@ -276,44 +269,45 @@ function BIP32Factory(ecc) { const tweakedPrivateKey = ecc.privateAdd(privateKey, t); if (!tweakedPrivateKey) throw new Error('Invalid tweaked private key!'); - return new Bip32Signer(Buffer.from(tweakedPrivateKey), undefined); + return new Bip32Signer(tweakedPrivateKey, undefined); } } function fromBase58(inString, network) { const buffer = bs58check.decode(inString); + const bufferView = new DataView(buffer.buffer); if (buffer.length !== 78) throw new TypeError('Invalid buffer length'); network = network || BITCOIN; // 4 bytes: version bytes - const version = buffer.readUInt32BE(0); + const version = bufferView.getUint32(0, false); if (version !== network.bip32.private && version !== network.bip32.public) throw new TypeError('Invalid network version'); // 1 byte: depth: 0x00 for master nodes, 0x01 for level-1 descendants, ... const depth = buffer[4]; // 4 bytes: the fingerprint of the parent's key (0x00000000 if master key) - const parentFingerprint = buffer.readUInt32BE(5); + const parentFingerprint = bufferView.getUint32(5, false); if (depth === 0) { if (parentFingerprint !== 0x00000000) throw new TypeError('Invalid parent fingerprint'); } // 4 bytes: child number. This is the number i in xi = xpar/i, with xi the key being serialized. // This is encoded in MSB order. (0x00000000 if master key) - const index = buffer.readUInt32BE(9); + const index = bufferView.getUint32(9, false); if (depth === 0 && index !== 0) throw new TypeError('Invalid index'); // 32 bytes: the chain code - const chainCode = buffer.slice(13, 45); + const chainCode = buffer.subarray(13, 45); let hd; // 33 bytes: private key data (0x00 + k) if (version === network.bip32.private) { - if (buffer.readUInt8(45) !== 0x00) + if (bufferView.getUint8(45) !== 0x00) throw new TypeError('Invalid private key'); - const k = buffer.slice(46, 78); + const k = buffer.subarray(46, 78); hd = fromPrivateKeyLocal(k, chainCode, network, depth, index, parentFingerprint); // 33 bytes: public key data (0x02 + X or 0x03 + X) } else { - const X = buffer.slice(45, 78); + const X = buffer.subarray(45, 78); hd = fromPublicKeyLocal(X, chainCode, network, depth, index, parentFingerprint); } return hd; @@ -336,7 +330,7 @@ function BIP32Factory(ecc) { } function fromPublicKeyLocal(publicKey, chainCode, network, depth, index, parentFingerprint) { typeforce({ - publicKey: typeforce.BufferN(33), + publicKey: (0, uint8array_utils_1.Uint8ArrayTypeN)(33), chainCode: UINT256_TYPE, }, { publicKey, chainCode }); network = network || BITCOIN; @@ -346,13 +340,14 @@ function BIP32Factory(ecc) { return new BIP32(undefined, publicKey, chainCode, network, depth, index, parentFingerprint); } function fromSeed(seed, network) { - typeforce(typeforce.Buffer, seed); + typeforce(uint8array_utils_1.Uint8ArrayType, seed); if (seed.length < 16) throw new TypeError('Seed should be at least 128 bits'); if (seed.length > 64) throw new TypeError('Seed should be at most 512 bits'); network = network || BITCOIN; - const I = crypto.hmacSHA512(Buffer.from('Bitcoin seed', 'utf8'), seed); + const encoder = new TextEncoder(); + const I = crypto.hmacSHA512(encoder.encode('Bitcoin seed'), seed); const IL = I.slice(0, 32); const IR = I.slice(32); return fromPrivateKey(IL, IR, network); diff --git a/src/crypto.js b/src/crypto.js index a8a1b61..3180fe5 100644 --- a/src/crypto.js +++ b/src/crypto.js @@ -6,11 +6,10 @@ const ripemd160_1 = require("@noble/hashes/ripemd160"); const sha256_1 = require("@noble/hashes/sha256"); const sha512_1 = require("@noble/hashes/sha512"); function hash160(buffer) { - const sha256Hash = (0, sha256_1.sha256)(Uint8Array.from(buffer)); - return Buffer.from((0, ripemd160_1.ripemd160)(sha256Hash)); + return (0, ripemd160_1.ripemd160)((0, sha256_1.sha256)(buffer)); } exports.hash160 = hash160; function hmacSHA512(key, data) { - return Buffer.from((0, hmac_1.hmac)(sha512_1.sha512, key, data)); + return (0, hmac_1.hmac)(sha512_1.sha512, key, data); } exports.hmacSHA512 = hmacSHA512; diff --git a/src/testecc.js b/src/testecc.js index a8bbfb3..68157eb 100644 --- a/src/testecc.js +++ b/src/testecc.js @@ -1,7 +1,9 @@ "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.testEcc = void 0; -const h = (hex) => Buffer.from(hex, 'hex'); +const utils_1 = require("@noble/hashes/utils"); +const uint8array_utils_1 = require("./uint8array-utils"); +const h = (hex) => (0, utils_1.hexToBytes)(hex); function testEcc(ecc) { assert(ecc.isPoint(h('0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798'))); assert(!ecc.isPoint(h('030000000000000000000000000000000000000000000000000000000000000005'))); @@ -14,24 +16,24 @@ function testEcc(ecc) { assert(!ecc.isPrivate(h('fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141'))); // order + 1 assert(!ecc.isPrivate(h('fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364142'))); - assert(Buffer.from(ecc.pointFromScalar(h('b1121e4088a66a28f5b6b0f5844943ecd9f610196d7bb83b25214b60452c09af'))).equals(h('02b07ba9dca9523b7ef4bd97703d43d20399eb698e194704791a25ce77a400df99'))); + assert((0, uint8array_utils_1.areUint8ArraysEqual)(ecc.pointFromScalar(h('b1121e4088a66a28f5b6b0f5844943ecd9f610196d7bb83b25214b60452c09af')), h('02b07ba9dca9523b7ef4bd97703d43d20399eb698e194704791a25ce77a400df99'))); if (ecc.xOnlyPointAddTweak) { assert(ecc.xOnlyPointAddTweak(h('79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798'), h('fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364140')) === null); let xOnlyRes = ecc.xOnlyPointAddTweak(h('1617d38ed8d8657da4d4761e8057bc396ea9e4b9d29776d4be096016dbd2509b'), h('a8397a935f0dfceba6ba9618f6451ef4d80637abf4e6af2669fbc9de6a8fd2ac')); - assert(Buffer.from(xOnlyRes.xOnlyPubkey).equals(h('e478f99dab91052ab39a33ea35fd5e6e4933f4d28023cd597c9a1f6760346adf')) && xOnlyRes.parity === 1); + assert((0, uint8array_utils_1.areUint8ArraysEqual)(xOnlyRes.xOnlyPubkey, h('e478f99dab91052ab39a33ea35fd5e6e4933f4d28023cd597c9a1f6760346adf')) && xOnlyRes.parity === 1); xOnlyRes = ecc.xOnlyPointAddTweak(h('2c0b7cf95324a07d05398b240174dc0c2be444d96b159aa6c7f7b1e668680991'), h('823c3cd2142744b075a87eade7e1b8678ba308d566226a0056ca2b7a76f86b47')); } - assert(Buffer.from(ecc.pointAddScalar(h('0379be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798'), h('0000000000000000000000000000000000000000000000000000000000000003'))).equals(h('02c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5'))); - assert(Buffer.from(ecc.privateAdd(h('fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd036413e'), h('0000000000000000000000000000000000000000000000000000000000000002'))).equals(h('fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364140'))); + assert((0, uint8array_utils_1.areUint8ArraysEqual)(ecc.pointAddScalar(h('0379be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798'), h('0000000000000000000000000000000000000000000000000000000000000003')), h('02c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5'))); + assert((0, uint8array_utils_1.areUint8ArraysEqual)(ecc.privateAdd(h('fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd036413e'), h('0000000000000000000000000000000000000000000000000000000000000002')), h('fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364140'))); if (ecc.privateNegate) { - assert(Buffer.from(ecc.privateNegate(h('0000000000000000000000000000000000000000000000000000000000000001'))).equals(h('fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364140'))); - assert(Buffer.from(ecc.privateNegate(h('fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd036413e'))).equals(h('0000000000000000000000000000000000000000000000000000000000000003'))); - assert(Buffer.from(ecc.privateNegate(h('b1121e4088a66a28f5b6b0f5844943ecd9f610196d7bb83b25214b60452c09af'))).equals(h('4eede1bf775995d70a494f0a7bb6bc11e0b8cccd41cce8009ab1132c8b0a3792'))); + assert((0, uint8array_utils_1.areUint8ArraysEqual)(ecc.privateNegate(h('0000000000000000000000000000000000000000000000000000000000000001')), h('fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364140'))); + assert((0, uint8array_utils_1.areUint8ArraysEqual)(ecc.privateNegate(h('fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd036413e')), h('0000000000000000000000000000000000000000000000000000000000000003'))); + assert((0, uint8array_utils_1.areUint8ArraysEqual)(ecc.privateNegate(h('b1121e4088a66a28f5b6b0f5844943ecd9f610196d7bb83b25214b60452c09af')), h('4eede1bf775995d70a494f0a7bb6bc11e0b8cccd41cce8009ab1132c8b0a3792'))); } - assert(Buffer.from(ecc.sign(h('5e9f0a0d593efdcf78ac923bc3313e4e7d408d574354ee2b3288c0da9fbba6ed'), h('fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364140'))).equals(h('54c4a33c6423d689378f160a7ff8b61330444abb58fb470f96ea16d99d4a2fed07082304410efa6b2943111b6a4e0aaa7b7db55a07e9861d1fb3cb1f421044a5'))); + assert((0, uint8array_utils_1.areUint8ArraysEqual)(ecc.sign(h('5e9f0a0d593efdcf78ac923bc3313e4e7d408d574354ee2b3288c0da9fbba6ed'), h('fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364140')), h('54c4a33c6423d689378f160a7ff8b61330444abb58fb470f96ea16d99d4a2fed07082304410efa6b2943111b6a4e0aaa7b7db55a07e9861d1fb3cb1f421044a5'))); assert(ecc.verify(h('5e9f0a0d593efdcf78ac923bc3313e4e7d408d574354ee2b3288c0da9fbba6ed'), h('0379be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798'), h('54c4a33c6423d689378f160a7ff8b61330444abb58fb470f96ea16d99d4a2fed07082304410efa6b2943111b6a4e0aaa7b7db55a07e9861d1fb3cb1f421044a5'))); if (ecc.signSchnorr) { - assert(Buffer.from(ecc.signSchnorr(h('7e2d58d8b3bcdf1abadec7829054f90dda9805aab56c77333024b9d0a508b75c'), h('c90fdaa22168c234c4c6628b80dc1cd129024e088a67cc74020bbea63b14e5c9'), h('c87aa53824b4d7ae2eb035a2b5bbbccc080e76cdc6d1692c4b0b62d798e6d906'))).equals(h('5831aaeed7b44bb74e5eab94ba9d4294c49bcf2a60728d8b4c200f50dd313c1bab745879a5ad954a72c45a91c3a51d3c7adea98d82f8481e0e1e03674a6f3fb7'))); + assert((0, uint8array_utils_1.areUint8ArraysEqual)(ecc.signSchnorr(h('7e2d58d8b3bcdf1abadec7829054f90dda9805aab56c77333024b9d0a508b75c'), h('c90fdaa22168c234c4c6628b80dc1cd129024e088a67cc74020bbea63b14e5c9'), h('c87aa53824b4d7ae2eb035a2b5bbbccc080e76cdc6d1692c4b0b62d798e6d906')), h('5831aaeed7b44bb74e5eab94ba9d4294c49bcf2a60728d8b4c200f50dd313c1bab745879a5ad954a72c45a91c3a51d3c7adea98d82f8481e0e1e03674a6f3fb7'))); } if (ecc.verifySchnorr) { assert(ecc.verifySchnorr(h('7e2d58d8b3bcdf1abadec7829054f90dda9805aab56c77333024b9d0a508b75c'), h('dd308afec5777e13121fa72b9cc1b7cc0139715309b086c960e18fd969774eb8'), h('5831aaeed7b44bb74e5eab94ba9d4294c49bcf2a60728d8b4c200f50dd313c1bab745879a5ad954a72c45a91c3a51d3c7adea98d82f8481e0e1e03674a6f3fb7'))); diff --git a/src/uint8array-utils.js b/src/uint8array-utils.js new file mode 100644 index 0000000..a6e67d9 --- /dev/null +++ b/src/uint8array-utils.js @@ -0,0 +1,50 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.areUint8ArraysEqual = exports.Uint8ArrayTypeN = exports.Uint8ArrayType = void 0; +const typeforce = require('typeforce'); +/** + * Typeforce extensions + */ +function tfCustomError(expected, actual) { + return new typeforce.TfTypeError(expected, {}, actual); +} +function _LengthN(type, length) { + const name = type.toJSON(); + function Length(value) { + if (!type(value)) + return false; + if (value.length === length) + return true; + throw tfCustomError(name + '(Length: ' + length + ')', name + '(Length: ' + value.length + ')'); + } + Length.toJSON = () => { + return name; + }; + return Length; +} +function Uint8ArrayType(value) { + return value instanceof Uint8Array; +} +exports.Uint8ArrayType = Uint8ArrayType; +Uint8ArrayType.toJSON = ((t) => { + return t; +}).bind(null, 'Uint8Array'); +exports.Uint8ArrayTypeN = _LengthN.bind(null, Uint8ArrayType); +/** + * Uint8Array comparison + */ +function areUint8ArraysEqual(a, b) { + if (a === b) { + return true; + } + if (a.length !== b.length) { + return false; + } + for (let index = 0; index < a.length; index++) { + if (a[index] !== b[index]) { + return false; + } + } + return true; +} +exports.areUint8ArraysEqual = areUint8ArraysEqual; diff --git a/test/index.js b/test/index.js index fa9a60d..174ba8d 100644 --- a/test/index.js +++ b/test/index.js @@ -1,4 +1,5 @@ let BIP32Creator = require('..').default +let { bytesToHex, hexToBytes } = require('@noble/hashes/utils') let tape = require('tape') let fixtures = require('./fixtures/index.json') let ecc @@ -29,17 +30,15 @@ fixtures.valid.forEach((f) => { }) function verify (t, hd, prv, f, network) { - t.equal(hd.chainCode.toString('hex'), f.chainCode) + t.equal(bytesToHex(hd.chainCode), f.chainCode) t.equal(hd.depth, f.depth >>> 0) t.equal(hd.index, f.index >>> 0) t.equal(hd.compressed, true) - t.equal(hd.fingerprint.toString('hex'), f.fingerprint) - t.equal(hd.identifier.toString('hex'), f.identifier) - t.equal(hd.publicKey.toString('hex'), f.pubKey) + t.equal(bytesToHex(hd.fingerprint), f.fingerprint) + t.equal(bytesToHex(hd.identifier), f.identifier) + t.equal(bytesToHex(hd.publicKey), f.pubKey) if (prv) t.equal(hd.toBase58(), f.base58Priv) - if (prv) t.equal(hd.privateKey.toString('hex'), f.privKey) - if (prv) t.equal(hd.toWIF(), f.wif) - if (!prv) t.throws(() => hd.toWIF(), /Missing private key/) + if (prv) t.equal(bytesToHex(hd.privateKey), f.privKey) if (!prv) t.equal(hd.privateKey, undefined) t.equal(hd.neutered().toBase58(), f.base58) t.equal(hd.isNeutered(), !prv) @@ -90,7 +89,7 @@ validAll.forEach((ff) => { verify(t, hd, false, ff, network) if (ff.seed) { - let seed = Buffer.from(ff.seed, 'hex') + let seed = hexToBytes(ff.seed) hd = BIP32.fromSeed(seed, network) verify(t, hd, true, ff, network) } @@ -153,7 +152,7 @@ tape('works for Public -> public', (t) => { t.plan(2) t.equal(c.base58, child.toBase58()) - const hdNeutered = BIP32.fromPublicKey(Buffer.from(f.master.pubKey, 'hex'), Buffer.from(f.master.chainCode, 'hex')) + const hdNeutered = BIP32.fromPublicKey(hexToBytes(f.master.pubKey), hexToBytes(f.master.chainCode)) t.equal(child.toBase58(), hdNeutered.derive(c.m).toBase58()) }) @@ -191,12 +190,12 @@ tape('throws on wrong types', (t) => { }, /Expected BIP32Path, got/) }) - let ZERO = Buffer.alloc(32, 0) - let ONES = Buffer.alloc(32, 1) + let ZERO = new Uint8Array(32).fill(0); + let ONES = new Uint8Array(32).fill(1); t.throws(() => { - BIP32.fromPrivateKey(Buffer.alloc(2), ONES) - }, /Expected property "privateKey" of type Buffer\(Length: 32\), got Buffer\(Length: 2\)/) + BIP32.fromPrivateKey(new Uint8Array(2), ONES) + }, /Expected property "privateKey" of type Uint8Array\(Length: 32\), got Uint8Array\(Length: 2\)/) t.throws(() => { BIP32.fromPrivateKey(ZERO, ONES) @@ -210,9 +209,9 @@ tape('works when private key has leading zeros', (t) => { let hdkey = BIP32.fromBase58(key) t.plan(2) - t.equal(hdkey.privateKey.toString('hex'), '00000055378cf5fafb56c711c674143f9b0ee82ab0ba2924f19b64f5ae7cdbfd') + t.equal(bytesToHex(hdkey.privateKey), '00000055378cf5fafb56c711c674143f9b0ee82ab0ba2924f19b64f5ae7cdbfd') let child = hdkey.derivePath('m/44\'/0\'/0\'/0/0\'') - t.equal(child.privateKey.toString('hex'), '3348069561d2a0fb925e74bf198762acc47dce7db27372257d2d959a9e6f8aeb') + t.equal(bytesToHex(child.privateKey), '3348069561d2a0fb925e74bf198762acc47dce7db27372257d2d959a9e6f8aeb') }) tape('fromSeed', (t) => { @@ -220,7 +219,7 @@ tape('fromSeed', (t) => { // 'throws if IL is not within interval [1, n - 1] | IL === n || IL === 0' fixtures.invalid.fromSeed.forEach((f) => { t.throws(() => { - BIP32.fromSeed(Buffer.from(f.seed, 'hex')) + BIP32.fromSeed(hexToBytes(f.seed)) }, new RegExp(f.exception)) }) @@ -228,17 +227,17 @@ tape('fromSeed', (t) => { }) tape('ecdsa', (t) => { - let seed = Buffer.alloc(32, 1) - let hash = Buffer.alloc(32, 2) - let signature = Buffer.from('9636ee2fac31b795a308856b821ebe297dda7b28220fb46ea1fbbd7285977cc04c82b734956246a0f15a9698f03f546d8d96fe006c8e7bd2256ca7c8229e6f5c', 'hex') - let schnorrsig = Buffer.from('2fae8b517cb0e7302ca48a4109d1819e3d75af96bd58d297023e3058c4e98ff812fe6ae32a2b2bc4abab10f88f7fe56efbafc8a4e4fa437af78926f528b0585e', 'hex') - let signatureLowR = Buffer.from('0587a40b391b76596c257bf59565b24eaff2cc42b45caa2640902e73fb97a6e702c3402ab89348a7dae1bf171c3e172fa60353d7b01621a94cb7caca59b995db', 'hex') + let seed = new Uint8Array(32).fill(1) + let hash = new Uint8Array(32).fill(2) + let signature = hexToBytes('9636ee2fac31b795a308856b821ebe297dda7b28220fb46ea1fbbd7285977cc04c82b734956246a0f15a9698f03f546d8d96fe006c8e7bd2256ca7c8229e6f5c') + let schnorrsig = hexToBytes('2fae8b517cb0e7302ca48a4109d1819e3d75af96bd58d297023e3058c4e98ff812fe6ae32a2b2bc4abab10f88f7fe56efbafc8a4e4fa437af78926f528b0585e') + let signatureLowR = hexToBytes('0587a40b391b76596c257bf59565b24eaff2cc42b45caa2640902e73fb97a6e702c3402ab89348a7dae1bf171c3e172fa60353d7b01621a94cb7caca59b995db') let node = BIP32.fromSeed(seed) t.plan(11) - t.equal(node.sign(hash).toString('hex'), signature.toString('hex')) - t.equal(node.sign(hash, true).toString('hex'), signatureLowR.toString('hex')) - t.equal(node.signSchnorr(hash).toString('hex'), schnorrsig.toString('hex')) + t.equal(bytesToHex(node.sign(hash)), bytesToHex(signature)) + t.equal(bytesToHex(node.sign(hash, true)), bytesToHex(signatureLowR)) + t.equal(bytesToHex(node.signSchnorr(hash)), bytesToHex(schnorrsig)) t.equal(node.verify(hash, signature), true) t.equal(node.verify(seed, signature), false) t.equal(node.verify(hash, signatureLowR), true) @@ -252,9 +251,9 @@ tape('ecdsa', (t) => { }) tape('ecdsa - no schnorr', (t) => { - let seed = Buffer.alloc(32, 1) - let hash = Buffer.alloc(32, 2) - const tweak = Buffer.alloc(32, 3) + let seed = new Uint8Array(32).fill(1) + let hash = new Uint8Array(32).fill(2) + const tweak = new Uint8Array(32).fill(3) const bip32NoSchnorr = BIP32Creator({ ...ecc, signSchnorr: null, verifySchnorr: null }) const node = bip32NoSchnorr.fromSeed(seed) @@ -272,8 +271,8 @@ tape('ecdsa - no schnorr', (t) => { }) tape('ecc without tweak support', (t) => { - let seed = Buffer.alloc(32, 1) - const tweak = Buffer.alloc(32, 3) + let seed = new Uint8Array(32).fill(1) + const tweak = new Uint8Array(32).fill(3) const bip32NoTweak = BIP32Creator({ ...ecc, xOnlyPointAddTweak: null, privateNegate: null }) const node = bip32NoTweak.fromSeed(seed) @@ -285,18 +284,18 @@ tape('ecc without tweak support', (t) => { }) tape('tweak', (t) => { - const seed = Buffer.alloc(32, 1) - const hash = Buffer.alloc(32, 2) - const tweak = Buffer.alloc(32, 3) - const signature = Buffer.from('5a38c6652feb5166c9c91cfa5fa4a4c7cec27445d4619499df8afdd05ebc823246d644b0c7d3b960625393df537f900528ec4b14e6ddab8fd0c7e87c98cfe9d0', 'hex') - const schnorrsig = Buffer.from('20506478d341d0ab1afd32671eb1550b1c5329ad5179a19712212b857f06b3210d949964cd513ff25719e2e9b0087d5a9745afd5d38641ce0dfa86f67c86de63', 'hex') - const signatureLowR = Buffer.from('5a38c6652feb5166c9c91cfa5fa4a4c7cec27445d4619499df8afdd05ebc823246d644b0c7d3b960625393df537f900528ec4b14e6ddab8fd0c7e87c98cfe9d0', 'hex') + const seed = new Uint8Array(32).fill(1) + const hash = new Uint8Array(32).fill(2) + const tweak = new Uint8Array(32).fill(3) + const signature = hexToBytes('5a38c6652feb5166c9c91cfa5fa4a4c7cec27445d4619499df8afdd05ebc823246d644b0c7d3b960625393df537f900528ec4b14e6ddab8fd0c7e87c98cfe9d0') + const schnorrsig = hexToBytes('20506478d341d0ab1afd32671eb1550b1c5329ad5179a19712212b857f06b3210d949964cd513ff25719e2e9b0087d5a9745afd5d38641ce0dfa86f67c86de63') + const signatureLowR = hexToBytes('5a38c6652feb5166c9c91cfa5fa4a4c7cec27445d4619499df8afdd05ebc823246d644b0c7d3b960625393df537f900528ec4b14e6ddab8fd0c7e87c98cfe9d0') const signer = BIP32.fromSeed(seed).tweak(tweak) t.plan(9) - t.equal(signer.sign(hash).toString('hex'), signature.toString('hex')) - t.equal(signer.sign(hash, true).toString('hex'), signatureLowR.toString('hex')) - t.equal(signer.signSchnorr(hash).toString('hex'), schnorrsig.toString('hex')) + t.equal(bytesToHex(signer.sign(hash)), bytesToHex(signature)) + t.equal(bytesToHex(signer.sign(hash, true)), bytesToHex(signatureLowR)) + t.equal(bytesToHex(signer.signSchnorr(hash)), bytesToHex(schnorrsig)) t.equal(signer.verify(hash, signature), true) t.equal(signer.verify(seed, signature), false) t.equal(signer.verify(hash, signatureLowR), true) @@ -306,18 +305,18 @@ tape('tweak', (t) => { }) tape('tweak - neutered', (t) => { - const seed = Buffer.alloc(32, 1) - const hash = Buffer.alloc(32, 2) - const tweak = Buffer.alloc(32, 3) - const signature = Buffer.from('5a38c6652feb5166c9c91cfa5fa4a4c7cec27445d4619499df8afdd05ebc823246d644b0c7d3b960625393df537f900528ec4b14e6ddab8fd0c7e87c98cfe9d0', 'hex') - const schnorrsig = Buffer.from('20506478d341d0ab1afd32671eb1550b1c5329ad5179a19712212b857f06b3210d949964cd513ff25719e2e9b0087d5a9745afd5d38641ce0dfa86f67c86de63', 'hex') - const signatureLowR = Buffer.from('5a38c6652feb5166c9c91cfa5fa4a4c7cec27445d4619499df8afdd05ebc823246d644b0c7d3b960625393df537f900528ec4b14e6ddab8fd0c7e87c98cfe9d0', 'hex') + const seed = new Uint8Array(32).fill(1) + const hash = new Uint8Array(32).fill(2) + const tweak = new Uint8Array(32).fill(3) + const signature = hexToBytes('5a38c6652feb5166c9c91cfa5fa4a4c7cec27445d4619499df8afdd05ebc823246d644b0c7d3b960625393df537f900528ec4b14e6ddab8fd0c7e87c98cfe9d0') + const schnorrsig = hexToBytes('20506478d341d0ab1afd32671eb1550b1c5329ad5179a19712212b857f06b3210d949964cd513ff25719e2e9b0087d5a9745afd5d38641ce0dfa86f67c86de63') + const signatureLowR = hexToBytes('5a38c6652feb5166c9c91cfa5fa4a4c7cec27445d4619499df8afdd05ebc823246d644b0c7d3b960625393df537f900528ec4b14e6ddab8fd0c7e87c98cfe9d0') const signer = BIP32.fromSeed(seed).neutered().tweak(tweak) t.plan(8) t.throws(() => signer.sign(hash), /Missing private key/) t.throws(() => signer.signSchnorr(hash), /Missing private key/) - + t.equal(signer.verify(hash, signature), true) t.equal(signer.verify(seed, signature), false) t.equal(signer.verify(hash, signatureLowR), true) diff --git a/ts-src/bip32.ts b/ts-src/bip32.ts index 85f88ad..1020307 100644 --- a/ts-src/bip32.ts +++ b/ts-src/bip32.ts @@ -2,13 +2,9 @@ import * as crypto from './crypto'; import { testEcc } from './testecc'; import { base58check } from '@scure/base'; import { sha256 } from '@noble/hashes/sha256'; +import { Uint8ArrayType, Uint8ArrayTypeN } from './uint8array-utils'; const typeforce = require('typeforce'); -const wif = require('wif'); -const _bs58check = base58check(sha256); -const bs58check = { - encode: (data: Buffer): string => _bs58check.encode(Uint8Array.from(data)), - decode: (str: string): Buffer => Buffer.from(_bs58check.decode(str)), -}; +const bs58check = base58check(sha256); interface Network { wif: number; @@ -22,43 +18,42 @@ interface Network { scriptHash?: number; } export interface Signer { - publicKey: Buffer; + publicKey: Uint8Array; lowR: boolean; - sign(hash: Buffer, lowR?: boolean): Buffer; - verify(hash: Buffer, signature: Buffer): boolean; - signSchnorr(hash: Buffer): Buffer; - verifySchnorr(hash: Buffer, signature: Buffer): boolean; + sign(hash: Uint8Array, lowR?: boolean): Uint8Array; + verify(hash: Uint8Array, signature: Uint8Array): boolean; + signSchnorr(hash: Uint8Array): Uint8Array; + verifySchnorr(hash: Uint8Array, signature: Uint8Array): boolean; } export interface BIP32Interface extends Signer { - chainCode: Buffer; + chainCode: Uint8Array; network: Network; depth: number; index: number; parentFingerprint: number; - privateKey?: Buffer; - identifier: Buffer; - fingerprint: Buffer; + privateKey?: Uint8Array; + identifier: Uint8Array; + fingerprint: Uint8Array; isNeutered(): boolean; neutered(): BIP32Interface; toBase58(): string; - toWIF(): string; derive(index: number): BIP32Interface; deriveHardened(index: number): BIP32Interface; derivePath(path: string): BIP32Interface; - tweak(t: Buffer): Signer; + tweak(t: Uint8Array): Signer; } export interface BIP32API { - fromSeed(seed: Buffer, network?: Network): BIP32Interface; + fromSeed(seed: Uint8Array, network?: Network): BIP32Interface; fromBase58(inString: string, network?: Network): BIP32Interface; fromPublicKey( - publicKey: Buffer, - chainCode: Buffer, + publicKey: Uint8Array, + chainCode: Uint8Array, network?: Network, ): BIP32Interface; fromPrivateKey( - privateKey: Buffer, - chainCode: Buffer, + privateKey: Uint8Array, + chainCode: Uint8Array, network?: Network, ): BIP32Interface; } @@ -96,7 +91,7 @@ export interface TinySecp256k1Interface { export function BIP32Factory(ecc: TinySecp256k1Interface): BIP32API { testEcc(ecc); - const UINT256_TYPE = typeforce.BufferN(32); + const UINT256_TYPE = Uint8ArrayTypeN(32); const NETWORK_TYPE = typeforce.compile({ wif: typeforce.UInt8, bip32: { @@ -130,60 +125,61 @@ export function BIP32Factory(ecc: TinySecp256k1Interface): BIP32API { return typeforce.UInt32(value) && value <= UINT31_MAX; } - function toXOnly(pubKey: Buffer) { - return pubKey.length === 32 ? pubKey : pubKey.slice(1, 33); + function toXOnly(pubKey: Uint8Array) { + return pubKey.length === 32 ? pubKey : pubKey.subarray(1, 33); } class Bip32Signer implements Signer { lowR: boolean = false; constructor( - protected __D: Buffer | undefined, - protected __Q: Buffer | undefined, + protected __D: Uint8Array | undefined, + protected __Q: Uint8Array | undefined, ) {} - get publicKey(): Buffer { + get publicKey(): Uint8Array { if (this.__Q === undefined) - this.__Q = Buffer.from(ecc.pointFromScalar(this.__D!, true)!); + this.__Q = ecc.pointFromScalar(this.__D!, true)!; return this.__Q!; } - get privateKey(): Buffer | undefined { + get privateKey(): Uint8Array | undefined { return this.__D; } - sign(hash: Buffer, lowR?: boolean): Buffer { + sign(hash: Uint8Array, lowR?: boolean): Uint8Array { if (!this.privateKey) throw new Error('Missing private key'); if (lowR === undefined) lowR = this.lowR; if (lowR === false) { - return Buffer.from(ecc.sign(hash, this.privateKey)); + return ecc.sign(hash, this.privateKey); } else { - let sig = Buffer.from(ecc.sign(hash, this.privateKey)); - const extraData = Buffer.alloc(32, 0); + let sig = ecc.sign(hash, this.privateKey); + const extraData = new Uint8Array(32); + const extraDataView = new DataView(extraData.buffer); let counter = 0; // if first try is lowR, skip the loop // for second try and on, add extra entropy counting up while (sig[0] > 0x7f) { counter++; - extraData.writeUIntLE(counter, 0, 6); - sig = Buffer.from(ecc.sign(hash, this.privateKey, extraData)); + extraDataView.setUint32(0, counter, true); + sig = ecc.sign(hash, this.privateKey, extraData); } return sig; } } - signSchnorr(hash: Buffer): Buffer { + signSchnorr(hash: Uint8Array): Uint8Array { if (!this.privateKey) throw new Error('Missing private key'); if (!ecc.signSchnorr) throw new Error('signSchnorr not supported by ecc library'); - return Buffer.from(ecc.signSchnorr(hash, this.privateKey)); + return ecc.signSchnorr(hash, this.privateKey); } - verify(hash: Buffer, signature: Buffer): boolean { + verify(hash: Uint8Array, signature: Uint8Array): boolean { return ecc.verify(hash, this.publicKey, signature); } - verifySchnorr(hash: Buffer, signature: Buffer): boolean { + verifySchnorr(hash: Uint8Array, signature: Uint8Array): boolean { if (!ecc.verifySchnorr) throw new Error('verifySchnorr not supported by ecc library'); return ecc.verifySchnorr(hash, this.publicKey.subarray(1, 33), signature); @@ -192,9 +188,9 @@ export function BIP32Factory(ecc: TinySecp256k1Interface): BIP32API { class BIP32 extends Bip32Signer implements BIP32Interface { constructor( - __D: Buffer | undefined, - __Q: Buffer | undefined, - public chainCode: Buffer, + __D: Uint8Array | undefined, + __Q: Uint8Array | undefined, + public chainCode: Uint8Array, public network: Network, private __DEPTH = 0, private __INDEX = 0, @@ -216,12 +212,12 @@ export function BIP32Factory(ecc: TinySecp256k1Interface): BIP32API { return this.__PARENT_FINGERPRINT; } - get identifier(): Buffer { + get identifier(): Uint8Array { return crypto.hash160(this.publicKey); } - get fingerprint(): Buffer { - return this.identifier.slice(0, 4); + get fingerprint(): Uint8Array { + return this.identifier.subarray(0, 4); } get compressed(): boolean { @@ -250,50 +246,47 @@ export function BIP32Factory(ecc: TinySecp256k1Interface): BIP32API { const version = !this.isNeutered() ? network.bip32.private : network.bip32.public; - const buffer = Buffer.allocUnsafe(78); + const buffer = new Uint8Array(78); + const bufferView = new DataView(buffer.buffer); // 4 bytes: version bytes - buffer.writeUInt32BE(version, 0); + bufferView.setUint32(0, version, false); // 1 byte: depth: 0x00 for master nodes, 0x01 for level-1 descendants, .... - buffer.writeUInt8(this.depth, 4); + bufferView.setUint8(4, this.depth); // 4 bytes: the fingerprint of the parent's key (0x00000000 if master key) - buffer.writeUInt32BE(this.parentFingerprint, 5); + bufferView.setUint32(5, this.parentFingerprint, false); // 4 bytes: child number. This is the number i in xi = xpar/i, with xi the key being serialized. // This is encoded in big endian. (0x00000000 if master key) - buffer.writeUInt32BE(this.index, 9); + bufferView.setUint32(9, this.index, false); // 32 bytes: the chain code - this.chainCode.copy(buffer, 13); + buffer.set(this.chainCode, 13); // 33 bytes: the public key or private key data if (!this.isNeutered()) { // 0x00 + k for private keys - buffer.writeUInt8(0, 45); - this.privateKey!.copy(buffer, 46); + bufferView.setUint8(45, 0); + buffer.set(this.privateKey!, 46); // 33 bytes: the public key } else { // X9.62 encoding for public keys - this.publicKey.copy(buffer, 45); + buffer.set(this.publicKey, 45); } return bs58check.encode(buffer); } - toWIF(): string { - if (!this.privateKey) throw new TypeError('Missing private key'); - return wif.encode(this.network.wif, this.privateKey, true); - } - // https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki#child-key-derivation-ckd-functions derive(index: number): BIP32Interface { typeforce(typeforce.UInt32, index); const isHardened = index >= HIGHEST_BIT; - const data = Buffer.allocUnsafe(37); + const data = new Uint8Array(37); + const dataView = new DataView(data.buffer); // Hardened child if (isHardened) { @@ -302,15 +295,15 @@ export function BIP32Factory(ecc: TinySecp256k1Interface): BIP32API { // data = 0x00 || ser256(kpar) || ser32(index) data[0] = 0x00; - this.privateKey!.copy(data, 1); - data.writeUInt32BE(index, 33); + data.set(this.privateKey!, 1); + dataView.setUint32(33, index, false); // Normal child } else { // data = serP(point(kpar)) || ser32(index) // = serP(Kpar) || ser32(index) - this.publicKey.copy(data, 0); - data.writeUInt32BE(index, 33); + data.set(this.publicKey, 0); + dataView.setUint32(33, index, false); } const I = crypto.hmacSHA512(this.chainCode, data); @@ -324,7 +317,7 @@ export function BIP32Factory(ecc: TinySecp256k1Interface): BIP32API { let hd: BIP32Interface; if (!this.isNeutered()) { // ki = parse256(IL) + kpar (mod n) - const ki = Buffer.from(ecc.privateAdd(this.privateKey!, IL)!); + const ki = ecc.privateAdd(this.privateKey!, IL)!; // In case ki == 0, proceed with the next value for i if (ki == null) return this.derive(index + 1); @@ -335,14 +328,14 @@ export function BIP32Factory(ecc: TinySecp256k1Interface): BIP32API { this.network, this.depth + 1, index, - this.fingerprint.readUInt32BE(0), + new DataView(this.fingerprint.buffer).getUint32(0, false), ); // Public parent key -> public child key } else { // Ki = point(parse256(IL)) + Kpar // = G*IL + Kpar - const Ki = Buffer.from(ecc.pointAddScalar(this.publicKey, IL, true)!); + const Ki = ecc.pointAddScalar(this.publicKey, IL, true)!; // In case Ki is the point at infinity, proceed with the next value for i if (Ki === null) return this.derive(index + 1); @@ -353,7 +346,7 @@ export function BIP32Factory(ecc: TinySecp256k1Interface): BIP32API { this.network, this.depth + 1, index, - this.fingerprint.readUInt32BE(0), + new DataView(this.fingerprint.buffer).getUint32(0, false), ); } @@ -393,30 +386,32 @@ export function BIP32Factory(ecc: TinySecp256k1Interface): BIP32API { ); } - tweak(t: Buffer): Signer { + tweak(t: Uint8Array): Signer { if (this.privateKey) return this.tweakFromPrivateKey(t); return this.tweakFromPublicKey(t); } - private tweakFromPublicKey(t: Buffer): Signer { + private tweakFromPublicKey(t: Uint8Array): Signer { const xOnlyPubKey = toXOnly(this.publicKey); if (!ecc.xOnlyPointAddTweak) throw new Error('xOnlyPointAddTweak not supported by ecc library'); const tweakedPublicKey = ecc.xOnlyPointAddTweak(xOnlyPubKey, t); if (!tweakedPublicKey || tweakedPublicKey.xOnlyPubkey === null) throw new Error('Cannot tweak public key!'); - const parityByte = Buffer.from([ + const parityByte = Uint8Array.from([ tweakedPublicKey.parity === 0 ? 0x02 : 0x03, ]); - const tweakedPublicKeyCompresed = Buffer.concat([ - parityByte, - tweakedPublicKey.xOnlyPubkey, - ]); + + const tweakedPublicKeyCompresed = new Uint8Array( + tweakedPublicKey.xOnlyPubkey.length + 1, + ); + tweakedPublicKeyCompresed.set(parityByte); + tweakedPublicKeyCompresed.set(tweakedPublicKey.xOnlyPubkey, 1); return new Bip32Signer(undefined, tweakedPublicKeyCompresed); } - private tweakFromPrivateKey(t: Buffer): Signer { + private tweakFromPrivateKey(t: Uint8Array): Signer { const hasOddY = this.publicKey[0] === 3 || (this.publicKey[0] === 4 && (this.publicKey[64] & 1) === 1); @@ -429,17 +424,18 @@ export function BIP32Factory(ecc: TinySecp256k1Interface): BIP32API { const tweakedPrivateKey = ecc.privateAdd(privateKey!, t); if (!tweakedPrivateKey) throw new Error('Invalid tweaked private key!'); - return new Bip32Signer(Buffer.from(tweakedPrivateKey), undefined); + return new Bip32Signer(tweakedPrivateKey, undefined); } } function fromBase58(inString: string, network?: Network): BIP32Interface { const buffer = bs58check.decode(inString); + const bufferView = new DataView(buffer.buffer); if (buffer.length !== 78) throw new TypeError('Invalid buffer length'); network = network || BITCOIN; // 4 bytes: version bytes - const version = buffer.readUInt32BE(0); + const version = bufferView.getUint32(0, false); if (version !== network.bip32.private && version !== network.bip32.public) throw new TypeError('Invalid network version'); @@ -447,7 +443,7 @@ export function BIP32Factory(ecc: TinySecp256k1Interface): BIP32API { const depth = buffer[4]; // 4 bytes: the fingerprint of the parent's key (0x00000000 if master key) - const parentFingerprint = buffer.readUInt32BE(5); + const parentFingerprint = bufferView.getUint32(5, false); if (depth === 0) { if (parentFingerprint !== 0x00000000) throw new TypeError('Invalid parent fingerprint'); @@ -455,18 +451,18 @@ export function BIP32Factory(ecc: TinySecp256k1Interface): BIP32API { // 4 bytes: child number. This is the number i in xi = xpar/i, with xi the key being serialized. // This is encoded in MSB order. (0x00000000 if master key) - const index = buffer.readUInt32BE(9); + const index = bufferView.getUint32(9, false); if (depth === 0 && index !== 0) throw new TypeError('Invalid index'); // 32 bytes: the chain code - const chainCode = buffer.slice(13, 45); + const chainCode = buffer.subarray(13, 45); let hd: BIP32Interface; // 33 bytes: private key data (0x00 + k) if (version === network.bip32.private) { - if (buffer.readUInt8(45) !== 0x00) + if (bufferView.getUint8(45) !== 0x00) throw new TypeError('Invalid private key'); - const k = buffer.slice(46, 78); + const k = buffer.subarray(46, 78); hd = fromPrivateKeyLocal( k, @@ -479,7 +475,7 @@ export function BIP32Factory(ecc: TinySecp256k1Interface): BIP32API { // 33 bytes: public key data (0x02 + X or 0x03 + X) } else { - const X = buffer.slice(45, 78); + const X = buffer.subarray(45, 78); hd = fromPublicKeyLocal( X, @@ -495,16 +491,16 @@ export function BIP32Factory(ecc: TinySecp256k1Interface): BIP32API { } function fromPrivateKey( - privateKey: Buffer, - chainCode: Buffer, + privateKey: Uint8Array, + chainCode: Uint8Array, network?: Network, ): BIP32Interface { return fromPrivateKeyLocal(privateKey, chainCode, network); } function fromPrivateKeyLocal( - privateKey: Buffer, - chainCode: Buffer, + privateKey: Uint8Array, + chainCode: Uint8Array, network?: Network, depth?: number, index?: number, @@ -533,16 +529,16 @@ export function BIP32Factory(ecc: TinySecp256k1Interface): BIP32API { } function fromPublicKey( - publicKey: Buffer, - chainCode: Buffer, + publicKey: Uint8Array, + chainCode: Uint8Array, network?: Network, ): BIP32Interface { return fromPublicKeyLocal(publicKey, chainCode, network); } function fromPublicKeyLocal( - publicKey: Buffer, - chainCode: Buffer, + publicKey: Uint8Array, + chainCode: Uint8Array, network?: Network, depth?: number, index?: number, @@ -550,7 +546,7 @@ export function BIP32Factory(ecc: TinySecp256k1Interface): BIP32API { ): BIP32Interface { typeforce( { - publicKey: typeforce.BufferN(33), + publicKey: Uint8ArrayTypeN(33), chainCode: UINT256_TYPE, }, { publicKey, chainCode }, @@ -571,15 +567,17 @@ export function BIP32Factory(ecc: TinySecp256k1Interface): BIP32API { ); } - function fromSeed(seed: Buffer, network?: Network): BIP32Interface { - typeforce(typeforce.Buffer, seed); + function fromSeed(seed: Uint8Array, network?: Network): BIP32Interface { + typeforce(Uint8ArrayType, seed); if (seed.length < 16) throw new TypeError('Seed should be at least 128 bits'); if (seed.length > 64) throw new TypeError('Seed should be at most 512 bits'); network = network || BITCOIN; - const I = crypto.hmacSHA512(Buffer.from('Bitcoin seed', 'utf8'), seed); + const encoder = new TextEncoder(); + + const I = crypto.hmacSHA512(encoder.encode('Bitcoin seed'), seed); const IL = I.slice(0, 32); const IR = I.slice(32); diff --git a/ts-src/crypto.ts b/ts-src/crypto.ts index 00542e7..093c534 100644 --- a/ts-src/crypto.ts +++ b/ts-src/crypto.ts @@ -3,11 +3,10 @@ import { ripemd160 } from '@noble/hashes/ripemd160'; import { sha256 } from '@noble/hashes/sha256'; import { sha512 } from '@noble/hashes/sha512'; -export function hash160(buffer: Buffer): Buffer { - const sha256Hash = sha256(Uint8Array.from(buffer)); - return Buffer.from(ripemd160(sha256Hash)); +export function hash160(buffer: Uint8Array): Uint8Array { + return ripemd160(sha256(buffer)); } -export function hmacSHA512(key: Buffer, data: Buffer): Buffer { - return Buffer.from(hmac(sha512, key, data)); +export function hmacSHA512(key: Uint8Array, data: Uint8Array): Uint8Array { + return hmac(sha512, key, data); } diff --git a/ts-src/testecc.ts b/ts-src/testecc.ts index 666b868..94fcc2b 100644 --- a/ts-src/testecc.ts +++ b/ts-src/testecc.ts @@ -1,6 +1,8 @@ +import { hexToBytes } from '@noble/hashes/utils'; import { TinySecp256k1Interface } from './bip32'; +import { areUint8ArraysEqual } from './uint8array-utils'; -const h = (hex: string): Buffer => Buffer.from(hex, 'hex'); +const h = (hex: string): Uint8Array => hexToBytes(hex); export function testEcc(ecc: TinySecp256k1Interface): void { assert( @@ -43,11 +45,10 @@ export function testEcc(ecc: TinySecp256k1Interface): void { ), ); assert( - Buffer.from( + areUint8ArraysEqual( ecc.pointFromScalar( h('b1121e4088a66a28f5b6b0f5844943ecd9f610196d7bb83b25214b60452c09af'), )!, - ).equals( h('02b07ba9dca9523b7ef4bd97703d43d20399eb698e194704791a25ce77a400df99'), ), ); @@ -64,7 +65,8 @@ export function testEcc(ecc: TinySecp256k1Interface): void { h('a8397a935f0dfceba6ba9618f6451ef4d80637abf4e6af2669fbc9de6a8fd2ac'), ); assert( - Buffer.from(xOnlyRes!.xOnlyPubkey).equals( + areUint8ArraysEqual( + xOnlyRes!.xOnlyPubkey, h('e478f99dab91052ab39a33ea35fd5e6e4933f4d28023cd597c9a1f6760346adf'), ) && xOnlyRes!.parity === 1, ); @@ -75,61 +77,55 @@ export function testEcc(ecc: TinySecp256k1Interface): void { ); } assert( - Buffer.from( + areUint8ArraysEqual( ecc.pointAddScalar( h('0379be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798'), h('0000000000000000000000000000000000000000000000000000000000000003'), )!, - ).equals( h('02c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5'), ), ); assert( - Buffer.from( + areUint8ArraysEqual( ecc.privateAdd( h('fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd036413e'), h('0000000000000000000000000000000000000000000000000000000000000002'), )!, - ).equals( h('fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364140'), ), ); if (ecc.privateNegate) { assert( - Buffer.from( + areUint8ArraysEqual( ecc.privateNegate( h('0000000000000000000000000000000000000000000000000000000000000001'), ), - ).equals( h('fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364140'), ), ); assert( - Buffer.from( + areUint8ArraysEqual( ecc.privateNegate( h('fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd036413e'), ), - ).equals( h('0000000000000000000000000000000000000000000000000000000000000003'), ), ); assert( - Buffer.from( + areUint8ArraysEqual( ecc.privateNegate( h('b1121e4088a66a28f5b6b0f5844943ecd9f610196d7bb83b25214b60452c09af'), ), - ).equals( h('4eede1bf775995d70a494f0a7bb6bc11e0b8cccd41cce8009ab1132c8b0a3792'), ), ); } assert( - Buffer.from( + areUint8ArraysEqual( ecc.sign( h('5e9f0a0d593efdcf78ac923bc3313e4e7d408d574354ee2b3288c0da9fbba6ed'), h('fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364140'), )!, - ).equals( h( '54c4a33c6423d689378f160a7ff8b61330444abb58fb470f96ea16d99d4a2fed07082304410efa6b2943111b6a4e0aaa7b7db55a07e9861d1fb3cb1f421044a5', ), @@ -146,13 +142,12 @@ export function testEcc(ecc: TinySecp256k1Interface): void { ); if (ecc.signSchnorr) { assert( - Buffer.from( + areUint8ArraysEqual( ecc.signSchnorr( h('7e2d58d8b3bcdf1abadec7829054f90dda9805aab56c77333024b9d0a508b75c'), h('c90fdaa22168c234c4c6628b80dc1cd129024e088a67cc74020bbea63b14e5c9'), h('c87aa53824b4d7ae2eb035a2b5bbbccc080e76cdc6d1692c4b0b62d798e6d906'), )!, - ).equals( h( '5831aaeed7b44bb74e5eab94ba9d4294c49bcf2a60728d8b4c200f50dd313c1bab745879a5ad954a72c45a91c3a51d3c7adea98d82f8481e0e1e03674a6f3fb7', ), diff --git a/ts-src/uint8array-utils.ts b/ts-src/uint8array-utils.ts new file mode 100644 index 0000000..f5ee282 --- /dev/null +++ b/ts-src/uint8array-utils.ts @@ -0,0 +1,58 @@ +const typeforce = require('typeforce'); + +/** + * Typeforce extensions + */ +function tfCustomError(expected: any, actual: any) { + return new typeforce.TfTypeError(expected, {}, actual); +} + +function _LengthN(type: any, length: number) { + const name = type.toJSON(); + + function Length(value: any) { + if (!type(value)) return false; + if (value.length === length) return true; + + throw tfCustomError( + name + '(Length: ' + length + ')', + name + '(Length: ' + value.length + ')', + ); + } + Length.toJSON = () => { + return name; + }; + + return Length; +} + +export function Uint8ArrayType(value: unknown): value is Uint8Array { + return value instanceof Uint8Array; +} + +Uint8ArrayType.toJSON = ((t: any) => { + return t; +}).bind(null, 'Uint8Array'); + +export const Uint8ArrayTypeN = _LengthN.bind(null, Uint8ArrayType); + +/** + * Uint8Array comparison + */ +export function areUint8ArraysEqual(a: Uint8Array, b: Uint8Array) { + if (a === b) { + return true; + } + + if (a.length !== b.length) { + return false; + } + + for (let index = 0; index < a.length; index++) { + if (a[index] !== b[index]) { + return false; + } + } + + return true; +} diff --git a/types/bip32.d.ts b/types/bip32.d.ts index de1ccee..044571b 100644 --- a/types/bip32.d.ts +++ b/types/bip32.d.ts @@ -1,4 +1,3 @@ -/// interface Network { wif: number; bip32: { @@ -11,36 +10,35 @@ interface Network { scriptHash?: number; } export interface Signer { - publicKey: Buffer; + publicKey: Uint8Array; lowR: boolean; - sign(hash: Buffer, lowR?: boolean): Buffer; - verify(hash: Buffer, signature: Buffer): boolean; - signSchnorr(hash: Buffer): Buffer; - verifySchnorr(hash: Buffer, signature: Buffer): boolean; + sign(hash: Uint8Array, lowR?: boolean): Uint8Array; + verify(hash: Uint8Array, signature: Uint8Array): boolean; + signSchnorr(hash: Uint8Array): Uint8Array; + verifySchnorr(hash: Uint8Array, signature: Uint8Array): boolean; } export interface BIP32Interface extends Signer { - chainCode: Buffer; + chainCode: Uint8Array; network: Network; depth: number; index: number; parentFingerprint: number; - privateKey?: Buffer; - identifier: Buffer; - fingerprint: Buffer; + privateKey?: Uint8Array; + identifier: Uint8Array; + fingerprint: Uint8Array; isNeutered(): boolean; neutered(): BIP32Interface; toBase58(): string; - toWIF(): string; derive(index: number): BIP32Interface; deriveHardened(index: number): BIP32Interface; derivePath(path: string): BIP32Interface; - tweak(t: Buffer): Signer; + tweak(t: Uint8Array): Signer; } export interface BIP32API { - fromSeed(seed: Buffer, network?: Network): BIP32Interface; + fromSeed(seed: Uint8Array, network?: Network): BIP32Interface; fromBase58(inString: string, network?: Network): BIP32Interface; - fromPublicKey(publicKey: Buffer, chainCode: Buffer, network?: Network): BIP32Interface; - fromPrivateKey(privateKey: Buffer, chainCode: Buffer, network?: Network): BIP32Interface; + fromPublicKey(publicKey: Uint8Array, chainCode: Uint8Array, network?: Network): BIP32Interface; + fromPrivateKey(privateKey: Uint8Array, chainCode: Uint8Array, network?: Network): BIP32Interface; } interface XOnlyPointAddTweakResult { parity: 1 | 0; diff --git a/types/crypto.d.ts b/types/crypto.d.ts index 4b033af..f0bd050 100644 --- a/types/crypto.d.ts +++ b/types/crypto.d.ts @@ -1,3 +1,2 @@ -/// -export declare function hash160(buffer: Buffer): Buffer; -export declare function hmacSHA512(key: Buffer, data: Buffer): Buffer; +export declare function hash160(buffer: Uint8Array): Uint8Array; +export declare function hmacSHA512(key: Uint8Array, data: Uint8Array): Uint8Array; diff --git a/types/uint8array-utils.d.ts b/types/uint8array-utils.d.ts new file mode 100644 index 0000000..39c4abf --- /dev/null +++ b/types/uint8array-utils.d.ts @@ -0,0 +1,12 @@ +export declare function Uint8ArrayType(value: unknown): value is Uint8Array; +export declare namespace Uint8ArrayType { + var toJSON: () => any; +} +export declare const Uint8ArrayTypeN: (length: number) => { + (value: any): boolean; + toJSON(): any; +}; +/** + * Uint8Array comparison + */ +export declare function areUint8ArraysEqual(a: Uint8Array, b: Uint8Array): boolean;