diff --git a/README.md b/README.md index ce389ea..4cd17a4 100644 --- a/README.md +++ b/README.md @@ -8,10 +8,16 @@ A library for managing SECP256k1 keypairs written in TypeScript with transpiled TypeScript ``` typescript -import { Signer, SignerAsync, ECPairInterface, ECPair } from 'ecpair'; +import { Signer, SignerAsync, ECPairInterface, ECPairFactory, ECPairAPI, TinySecp256k1Interface } from 'ecpair'; import * as crypto from 'crypto'; + +// You need to provide the ECC library. The ECC library must implement +// all the methods of the `TinySecp256k1Interface` interface. +const tinysecp: TinySecp256k1Interface = require('tiny-secp256k1'); +const ECPair: ECPairAPI = ECPairFactory(tinysecp); + // You don't need to explicitly write ECPairInterface, but just to show -// that ECPair implements the interface this example includes it. +// that the keyPair implements the interface this example includes it. // From WIF const keyPair1: ECPairInterface = ECPair.fromWIF('KynD8ZKdViVo5W82oyxvE18BbG6nZPVQ8Td8hYbwU94RmyUALUik'); diff --git a/package-lock.json b/package-lock.json index 735dfcb..c949418 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "ecpair", - "version": "1.0.1", + "version": "2.0.0", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -475,14 +475,6 @@ "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", "dev": true }, - "bindings": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", - "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", - "requires": { - "file-uri-to-path": "1.0.0" - } - }, "bip39": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/bip39/-/bip39-3.0.4.tgz", @@ -524,7 +516,8 @@ "bn.js": { "version": "4.12.0", "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", - "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==" + "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==", + "dev": true }, "brace-expansion": { "version": "1.1.11", @@ -545,11 +538,6 @@ "fill-range": "^7.0.1" } }, - "brorand": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", - "integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=" - }, "browser-stdout": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", @@ -791,6 +779,7 @@ "version": "1.1.7", "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz", "integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==", + "dev": true, "requires": { "cipher-base": "^1.0.3", "create-hash": "^1.1.0", @@ -876,20 +865,6 @@ "integrity": "sha512-TiHlCgl2uP26Z0c67u442c0a2MZCWZNCRnPTQDPhVJ4h9G6z2zU0lApD9H0K9R5yFL5SfdaiVsVD2izOY24xBQ==", "dev": true }, - "elliptic": { - "version": "6.5.4", - "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.4.tgz", - "integrity": "sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ==", - "requires": { - "bn.js": "^4.11.9", - "brorand": "^1.1.0", - "hash.js": "^1.0.0", - "hmac-drbg": "^1.0.1", - "inherits": "^2.0.4", - "minimalistic-assert": "^1.0.1", - "minimalistic-crypto-utils": "^1.0.1" - } - }, "emoji-regex": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", @@ -973,11 +948,6 @@ "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", "dev": true }, - "file-uri-to-path": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", - "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==" - }, "fill-keys": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/fill-keys/-/fill-keys-1.0.2.tgz", @@ -1187,15 +1157,6 @@ "safe-buffer": "^5.2.0" } }, - "hash.js": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", - "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", - "requires": { - "inherits": "^2.0.3", - "minimalistic-assert": "^1.0.1" - } - }, "hasha": { "version": "5.2.2", "resolved": "https://registry.npmjs.org/hasha/-/hasha-5.2.2.tgz", @@ -1212,16 +1173,6 @@ "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", "dev": true }, - "hmac-drbg": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", - "integrity": "sha1-0nRXAQJabHdabFRXk+1QL8DGSaE=", - "requires": { - "hash.js": "^1.0.3", - "minimalistic-assert": "^1.0.0", - "minimalistic-crypto-utils": "^1.0.1" - } - }, "hoodwink": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/hoodwink/-/hoodwink-2.0.0.tgz", @@ -1689,16 +1640,6 @@ "pushdata-bitcoin": "^1.0.1" } }, - "minimalistic-assert": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", - "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==" - }, - "minimalistic-crypto-utils": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", - "integrity": "sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=" - }, "minimatch": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", @@ -1767,11 +1708,6 @@ "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==", "dev": true }, - "nan": { - "version": "2.15.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.15.0.tgz", - "integrity": "sha512-8ZtvEnA2c5aYCZYd1cvgdnU6cqwixRoYg70xPLWUws5ORTa/lnw+u4amixRS/Ac5U5mQVgp9pnlSUnbNWFaWZQ==" - }, "node-environment-flags": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/node-environment-flags/-/node-environment-flags-1.0.6.tgz", @@ -2540,15 +2476,12 @@ } }, "tiny-secp256k1": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/tiny-secp256k1/-/tiny-secp256k1-1.1.6.tgz", - "integrity": "sha512-FmqJZGduTyvsr2cF3375fqGHUovSwDi/QytexX1Se4BPuPZpTE5Ftp5fg+EFSuEf3lhZqgCRjEG3ydUQ/aNiwA==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/tiny-secp256k1/-/tiny-secp256k1-2.1.2.tgz", + "integrity": "sha512-8qPw7zDK6Hco2tVGYGQeOmOPp/hZnREwy2iIkcq0ygAuqc9WHo29vKN94lNymh1QbB3nthtAMF6KTIrdbsIotA==", + "dev": true, "requires": { - "bindings": "^1.3.0", - "bn.js": "^4.11.8", - "create-hmac": "^1.1.7", - "elliptic": "^6.4.0", - "nan": "^2.13.2" + "uint8array-tools": "0.0.6" } }, "to-fast-properties": { @@ -2657,6 +2590,12 @@ "integrity": "sha512-DqGhF5IKoBl8WNf8C1gu8q0xZSInh9j1kJJMqT3a94w1JzVaBU4EXOSMrz9yDqMT0xt3selp83fuFMQ0uzv6qA==", "dev": true }, + "uint8array-tools": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/uint8array-tools/-/uint8array-tools-0.0.6.tgz", + "integrity": "sha512-aIvEHNRX1AlOYAr6qSUjQBn4mCduxx6MtC967aRDTb2UUBPj0Ix2ZFQDcmXUUO/UxRPHcw1f5a5nVbCSKDSOqA==", + "dev": true + }, "unbox-primitive": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.1.tgz", diff --git a/package.json b/package.json index ed402a4..1490514 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ecpair", - "version": "1.0.1", + "version": "2.0.0", "description": "Client-side Bitcoin JavaScript library ECPair", "main": "./src/ecpair.js", "types": "./src/ecpair.d.ts", @@ -47,10 +47,9 @@ "src" ], "dependencies": { - "randombytes": "^2.0.1", - "tiny-secp256k1": "^1.1.6", - "typeforce": "^1.11.3", - "wif": "^2.0.1" + "randombytes": "^2.1.0", + "typeforce": "^1.18.0", + "wif": "^2.0.6" }, "devDependencies": { "@types/mocha": "^5.2.7", @@ -72,6 +71,7 @@ "prettier": "^2.4.1", "proxyquire": "^2.0.1", "rimraf": "^2.6.3", + "tiny-secp256k1": "^2.1.2", "ts-node": "^8.3.0", "tslint": "^6.1.3", "typescript": "^4.4.4" diff --git a/src/ecpair.d.ts b/src/ecpair.d.ts index 124f727..dcfd824 100644 --- a/src/ecpair.d.ts +++ b/src/ecpair.d.ts @@ -26,22 +26,24 @@ export interface ECPairInterface extends Signer { privateKey?: Buffer; toWIF(): string; verify(hash: Buffer, signature: Buffer): boolean; + verifySchnorr(hash: Buffer, signature: Buffer): boolean; + signSchnorr(hash: Buffer): Buffer; } -export declare class ECPair implements ECPairInterface { - private __D?; - private __Q?; - static isPoint(maybePoint: any): boolean; - static fromPrivateKey(buffer: Buffer, options?: ECPairOptions): ECPair; - static fromPublicKey(buffer: Buffer, options?: ECPairOptions): ECPair; - static fromWIF(wifString: string, network?: Network | Network[]): ECPair; - static makeRandom(options?: ECPairOptions): ECPair; - compressed: boolean; - network: Network; - lowR: boolean; - protected constructor(__D?: Buffer | undefined, __Q?: Buffer | undefined, options?: ECPairOptions); - get privateKey(): Buffer | undefined; - get publicKey(): Buffer; - toWIF(): string; - sign(hash: Buffer, lowR?: boolean): Buffer; - verify(hash: Buffer, signature: Buffer): boolean; +export interface ECPairAPI { + isPoint(maybePoint: any): boolean; + fromPrivateKey(buffer: Buffer, options?: ECPairOptions): ECPairInterface; + fromPublicKey(buffer: Buffer, options?: ECPairOptions): ECPairInterface; + fromWIF(wifString: string, network?: Network | Network[]): ECPairInterface; + makeRandom(options?: ECPairOptions): ECPairInterface; +} +export interface TinySecp256k1Interface { + isPoint(p: Uint8Array): boolean; + pointCompress(p: Uint8Array, compressed?: boolean): Uint8Array; + isPrivate(d: Uint8Array): boolean; + pointFromScalar(d?: Uint8Array, compressed?: boolean): Uint8Array; + sign(h: Uint8Array, d: Uint8Array, e?: Uint8Array): Uint8Array; + signSchnorr?(h: Uint8Array, d: Uint8Array, e?: Uint8Array): Uint8Array; + verify(h: Uint8Array, Q: Uint8Array, signature: Uint8Array, strict?: boolean): boolean; + verifySchnorr?(h: Uint8Array, Q: Uint8Array, signature: Uint8Array): boolean; } +export declare function ECPairFactory(ecc: TinySecp256k1Interface): ECPairAPI; diff --git a/src/ecpair.js b/src/ecpair.js index 7fdda3b..88c5e59 100644 --- a/src/ecpair.js +++ b/src/ecpair.js @@ -1,37 +1,36 @@ 'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); -exports.ECPair = exports.networks = void 0; +exports.ECPairFactory = exports.networks = void 0; const networks = require('./networks'); exports.networks = networks; const types = require('./types'); const randomBytes = require('randombytes'); const wif = require('wif'); -const ecc = require('tiny-secp256k1'); +const testecc_1 = require('./testecc'); const isOptions = types.typeforce.maybe( types.typeforce.compile({ compressed: types.maybe(types.Boolean), network: types.maybe(types.Network), }), ); -class ECPair { - __D; - __Q; - static isPoint(maybePoint) { +function ECPairFactory(ecc) { + (0, testecc_1.testEcc)(ecc); + function isPoint(maybePoint) { return ecc.isPoint(maybePoint); } - static fromPrivateKey(buffer, options) { + function fromPrivateKey(buffer, options) { types.typeforce(types.Buffer256bit, buffer); if (!ecc.isPrivate(buffer)) throw new TypeError('Private key not in range [1, n)'); types.typeforce(isOptions, options); return new ECPair(buffer, undefined, options); } - static fromPublicKey(buffer, options) { + function fromPublicKey(buffer, options) { types.typeforce(ecc.isPoint, buffer); types.typeforce(isOptions, options); return new ECPair(undefined, buffer, options); } - static fromWIF(wifString, network) { + function fromWIF(wifString, network) { const decoded = wif.decode(wifString); const version = decoded.version; // list of networks? @@ -47,12 +46,12 @@ class ECPair { network = network || networks.bitcoin; if (version !== network.wif) throw new Error('Invalid network version'); } - return this.fromPrivateKey(decoded.privateKey, { + return fromPrivateKey(decoded.privateKey, { compressed: decoded.compressed, network: network, }); } - static makeRandom(options) { + function makeRandom(options) { types.typeforce(isOptions, options); if (options === undefined) options = {}; const rng = options.rng || randomBytes; @@ -61,53 +60,77 @@ class ECPair { d = rng(32); types.typeforce(types.Buffer256bit, d); } while (!ecc.isPrivate(d)); - return this.fromPrivateKey(d, options); + return fromPrivateKey(d, options); } - compressed; - network; - lowR; - constructor(__D, __Q, options) { - this.__D = __D; - this.__Q = __Q; - this.lowR = false; - if (options === undefined) options = {}; - this.compressed = - options.compressed === undefined ? true : options.compressed; - this.network = options.network || networks.bitcoin; - if (__Q !== undefined) this.__Q = ecc.pointCompress(__Q, this.compressed); - } - get privateKey() { - return this.__D; - } - get publicKey() { - if (!this.__Q) this.__Q = ecc.pointFromScalar(this.__D, this.compressed); - return this.__Q; - } - toWIF() { - if (!this.__D) throw new Error('Missing private key'); - return wif.encode(this.network.wif, this.__D, this.compressed); - } - sign(hash, lowR) { - if (!this.__D) throw new Error('Missing private key'); - if (lowR === undefined) lowR = this.lowR; - if (lowR === false) { - return ecc.sign(hash, this.__D); - } else { - let sig = ecc.sign(hash, this.__D); - const extraData = Buffer.alloc(32, 0); - 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 = ecc.signWithEntropy(hash, this.__D, extraData); + class ECPair { + __D; + __Q; + compressed; + network; + lowR; + constructor(__D, __Q, options) { + this.__D = __D; + this.__Q = __Q; + this.lowR = false; + if (options === undefined) options = {}; + this.compressed = + options.compressed === undefined ? true : options.compressed; + this.network = options.network || networks.bitcoin; + if (__Q !== undefined) + this.__Q = Buffer.from(ecc.pointCompress(__Q, this.compressed)); + } + get privateKey() { + return this.__D; + } + get publicKey() { + if (!this.__Q) + this.__Q = Buffer.from(ecc.pointFromScalar(this.__D, this.compressed)); + return this.__Q; + } + toWIF() { + if (!this.__D) throw new Error('Missing private key'); + return wif.encode(this.network.wif, this.__D, this.compressed); + } + sign(hash, lowR) { + if (!this.__D) throw new Error('Missing private key'); + if (lowR === undefined) lowR = this.lowR; + if (lowR === false) { + return Buffer.from(ecc.sign(hash, this.__D)); + } else { + let sig = ecc.sign(hash, this.__D); + const extraData = Buffer.alloc(32, 0); + 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 = ecc.sign(hash, this.__D, extraData); + } + return Buffer.from(sig); } - return sig; + } + signSchnorr(hash) { + 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)); + } + verify(hash, signature) { + return ecc.verify(hash, this.publicKey, signature); + } + verifySchnorr(hash, signature) { + if (!ecc.verifySchnorr) + throw new Error('verifySchnorr not supported by ecc library'); + return ecc.verifySchnorr(hash, this.publicKey.subarray(1, 33), signature); } } - verify(hash, signature) { - return ecc.verify(hash, this.publicKey, signature); - } + return { + isPoint, + fromPrivateKey, + fromPublicKey, + fromWIF, + makeRandom, + }; } -exports.ECPair = ECPair; +exports.ECPairFactory = ECPairFactory; diff --git a/src/index.d.ts b/src/index.d.ts new file mode 100644 index 0000000..5c001b6 --- /dev/null +++ b/src/index.d.ts @@ -0,0 +1 @@ +export { ECPairFactory as default, ECPairFactory, Signer, SignerAsync, ECPairAPI, ECPairInterface, TinySecp256k1Interface, } from './ecpair'; diff --git a/src/index.js b/src/index.js new file mode 100644 index 0000000..e365a35 --- /dev/null +++ b/src/index.js @@ -0,0 +1,16 @@ +'use strict'; +Object.defineProperty(exports, '__esModule', { value: true }); +exports.ECPairFactory = exports.default = void 0; +var ecpair_1 = require('./ecpair'); +Object.defineProperty(exports, 'default', { + enumerable: true, + get: function () { + return ecpair_1.ECPairFactory; + }, +}); +Object.defineProperty(exports, 'ECPairFactory', { + enumerable: true, + get: function () { + return ecpair_1.ECPairFactory; + }, +}); diff --git a/src/testecc.d.ts b/src/testecc.d.ts new file mode 100644 index 0000000..bff8d27 --- /dev/null +++ b/src/testecc.d.ts @@ -0,0 +1,2 @@ +import { TinySecp256k1Interface } from './ecpair'; +export declare function testEcc(ecc: TinySecp256k1Interface): void; diff --git a/src/testecc.js b/src/testecc.js new file mode 100644 index 0000000..94121bd --- /dev/null +++ b/src/testecc.js @@ -0,0 +1,153 @@ +'use strict'; +Object.defineProperty(exports, '__esModule', { value: true }); +exports.testEcc = void 0; +const h = (hex) => Buffer.from(hex, 'hex'); +function testEcc(ecc) { + assert( + ecc.isPoint( + h('0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798'), + ), + ); + assert( + !ecc.isPoint( + h('030000000000000000000000000000000000000000000000000000000000000005'), + ), + ); + assert( + ecc.isPrivate( + h('79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798'), + ), + ); + // order - 1 + assert( + ecc.isPrivate( + h('fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364140'), + ), + ); + // 0 + assert( + !ecc.isPrivate( + h('0000000000000000000000000000000000000000000000000000000000000000'), + ), + ); + // order + assert( + !ecc.isPrivate( + h('fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141'), + ), + ); + // order + 1 + assert( + !ecc.isPrivate( + h('fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364142'), + ), + ); + assert( + Buffer.from( + ecc.pointCompress( + h( + '0479be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8', + ), + true, + ), + ).equals( + h('0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798'), + ), + ); + assert( + Buffer.from( + ecc.pointCompress( + h( + '0479be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8', + ), + false, + ), + ).equals( + h( + '0479be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8', + ), + ), + ); + assert( + Buffer.from( + ecc.pointCompress( + h('0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798'), + true, + ), + ).equals( + h('0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798'), + ), + ); + assert( + Buffer.from( + ecc.pointCompress( + h('0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798'), + false, + ), + ).equals( + h( + '0479be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8', + ), + ), + ); + assert( + Buffer.from( + ecc.pointFromScalar( + h('b1121e4088a66a28f5b6b0f5844943ecd9f610196d7bb83b25214b60452c09af'), + ), + ).equals( + h('02b07ba9dca9523b7ef4bd97703d43d20399eb698e194704791a25ce77a400df99'), + ), + ); + assert( + Buffer.from( + ecc.sign( + h('5e9f0a0d593efdcf78ac923bc3313e4e7d408d574354ee2b3288c0da9fbba6ed'), + h('fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364140'), + ), + ).equals( + 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', + ), + ), + ); + } + if (ecc.verifySchnorr) { + assert( + ecc.verifySchnorr( + h('7e2d58d8b3bcdf1abadec7829054f90dda9805aab56c77333024b9d0a508b75c'), + h('dd308afec5777e13121fa72b9cc1b7cc0139715309b086c960e18fd969774eb8'), + h( + '5831aaeed7b44bb74e5eab94ba9d4294c49bcf2a60728d8b4c200f50dd313c1bab745879a5ad954a72c45a91c3a51d3c7adea98d82f8481e0e1e03674a6f3fb7', + ), + ), + ); + } +} +exports.testEcc = testEcc; +function assert(bool) { + if (!bool) throw new Error('ecc library invalid'); +} diff --git a/test/ecpair.spec.ts b/test/ecpair.spec.ts index 53ff8a2..0463bb4 100644 --- a/test/ecpair.spec.ts +++ b/test/ecpair.spec.ts @@ -1,11 +1,13 @@ import * as assert from 'assert'; import { beforeEach, describe, it } from 'mocha'; import * as proxyquire from 'proxyquire'; -import { ECPair, ECPairInterface, networks as NETWORKS } from '..'; +import { ECPairFactory, ECPairInterface, networks as NETWORKS } from '..'; import * as fixtures from './fixtures/ecpair.json'; const hoodwink = require('hoodwink'); const tinysecp = require('tiny-secp256k1'); +const ECPair = ECPairFactory(tinysecp); + const NETWORKS_LIST = Object.values(NETWORKS); const ZERO = Buffer.alloc(32, 0); const ONE = Buffer.from( @@ -153,6 +155,16 @@ describe('ECPair', () => { keyPair.toWIF(); }, /Missing private key/); }); + it('throws if from public key only', () => { + assert.throws(() => { + const publicKey = Buffer.from( + '0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798', + 'hex', + ); + const keyPair = ECPair.fromPublicKey(publicKey); + keyPair.toWIF(); + }, /Missing private key/); + }); }); describe('makeRandom', () => { @@ -168,7 +180,7 @@ describe('ECPair', () => { }; const ProxiedECPair = proxyquire('../src/ecpair', stub); - const keyPair = ProxiedECPair.ECPair.makeRandom(); + const keyPair = ProxiedECPair.ECPairFactory(tinysecp).makeRandom(); assert.strictEqual(keyPair.toWIF(), exWIF); }); }); @@ -273,7 +285,7 @@ describe('ECPair', () => { 1, ); - assert.strictEqual(keyPair.sign(hash), signature); + assert.deepStrictEqual(keyPair.sign(hash), signature); }), ); @@ -286,6 +298,59 @@ describe('ECPair', () => { }); }); + describe('schnorr signing', () => { + it('creates signature', () => { + const kP = ECPair.fromPrivateKey(ONE, { + compressed: false, + }); + const h = Buffer.alloc(32, 2); + const schnorrsig = Buffer.from( + '4bc68cbd7c0b769b2dff262e9971756da7ab78402ed6f710c3788ce815e9c06a011bab7a527e33c6a1df0dad5ed05a04b8f3be656d8578502fef07f8215d37db', + 'hex', + ); + + assert.deepStrictEqual( + kP.signSchnorr(h).toString('hex'), + schnorrsig.toString('hex'), + ); + }); + + it( + 'wraps tinysecp.signSchnorr', + hoodwink(function (this: any): void { + this.mock( + tinysecp, + 'signSchnorr', + (h: any) => { + assert.strictEqual(h, hash); + return signature; + }, + 1, + ); + + assert.deepStrictEqual(keyPair.signSchnorr(hash), signature); + }), + ); + + it('throws if no private key is found', () => { + delete (keyPair as any).__D; + + assert.throws(() => { + keyPair.signSchnorr(hash); + }, /Missing private key/); + }); + + it('throws if signSchnorr() not found', () => { + assert.throws(() => { + keyPair = ECPairFactory({ + ...tinysecp, + signSchnorr: null, + }).makeRandom(); + keyPair.signSchnorr(hash); + }, /signSchnorr not supported by ecc library/); + }); + }); + describe('verify', () => { it( 'wraps tinysecp.verify', @@ -306,7 +371,52 @@ describe('ECPair', () => { }), ); }); + + describe('schnorr verify', () => { + it('checks signature', () => { + const kP = ECPair.fromPrivateKey(ONE, { + compressed: false, + }); + const h = Buffer.alloc(32, 2); + const schnorrsig = Buffer.from( + '4bc68cbd7c0b769b2dff262e9971756da7ab78402ed6f710c3788ce815e9c06a011bab7a527e33c6a1df0dad5ed05a04b8f3be656d8578502fef07f8215d37db', + 'hex', + ); + + assert.strictEqual(kP.verifySchnorr(h, schnorrsig), true); + }); + + it( + 'wraps tinysecp.verifySchnorr', + hoodwink(function (this: any): void { + this.mock( + tinysecp, + 'verifySchnorr', + (h: any, q: any, s: any) => { + assert.strictEqual(h, hash); + assert.deepStrictEqual(q, keyPair.publicKey.subarray(1, 33)); + assert.strictEqual(s, signature); + return true; + }, + 1, + ); + + assert.strictEqual(keyPair.verifySchnorr(hash, signature), true); + }), + ); + + it('throws if verifySchnorr() not found', () => { + assert.throws(() => { + keyPair = ECPairFactory({ + ...tinysecp, + verifySchnorr: null, + }).makeRandom(); + keyPair.verifySchnorr(hash, signature); + }, /verifySchnorr not supported by ecc library/); + }); + }); }); + describe('optional low R signing', () => { const sig = Buffer.from( '95a6619140fca3366f1d3b013b0367c4f86e39508a50fdce' + diff --git a/ts_src/ecpair.ts b/ts_src/ecpair.ts index 904037e..514d39c 100644 --- a/ts_src/ecpair.ts +++ b/ts_src/ecpair.ts @@ -3,10 +3,9 @@ import * as networks from './networks'; import * as types from './types'; import * as randomBytes from 'randombytes'; import * as wif from 'wif'; +import { testEcc } from './testecc'; export { networks }; -const ecc = require('tiny-secp256k1'); - const isOptions = types.typeforce.maybe( types.typeforce.compile({ compressed: types.maybe(types.Boolean), @@ -41,14 +40,46 @@ export interface ECPairInterface extends Signer { privateKey?: Buffer; toWIF(): string; verify(hash: Buffer, signature: Buffer): boolean; + verifySchnorr(hash: Buffer, signature: Buffer): boolean; + signSchnorr(hash: Buffer): Buffer; +} + +export interface ECPairAPI { + isPoint(maybePoint: any): boolean; + fromPrivateKey(buffer: Buffer, options?: ECPairOptions): ECPairInterface; + fromPublicKey(buffer: Buffer, options?: ECPairOptions): ECPairInterface; + fromWIF(wifString: string, network?: Network | Network[]): ECPairInterface; + makeRandom(options?: ECPairOptions): ECPairInterface; } -export class ECPair implements ECPairInterface { - static isPoint(maybePoint: any): boolean { +export interface TinySecp256k1Interface { + isPoint(p: Uint8Array): boolean; + pointCompress(p: Uint8Array, compressed?: boolean): Uint8Array; + isPrivate(d: Uint8Array): boolean; + pointFromScalar(d?: Uint8Array, compressed?: boolean): Uint8Array; + + sign(h: Uint8Array, d: Uint8Array, e?: Uint8Array): Uint8Array; + signSchnorr?(h: Uint8Array, d: Uint8Array, e?: Uint8Array): Uint8Array; + + verify( + h: Uint8Array, + Q: Uint8Array, + signature: Uint8Array, + strict?: boolean, + ): boolean; + verifySchnorr?(h: Uint8Array, Q: Uint8Array, signature: Uint8Array): boolean; +} + +export function ECPairFactory(ecc: TinySecp256k1Interface): ECPairAPI { + testEcc(ecc); + function isPoint(maybePoint: any): boolean { return ecc.isPoint(maybePoint); } - static fromPrivateKey(buffer: Buffer, options?: ECPairOptions): ECPair { + function fromPrivateKey( + buffer: Buffer, + options?: ECPairOptions, + ): ECPairInterface { types.typeforce(types.Buffer256bit, buffer); if (!ecc.isPrivate(buffer)) throw new TypeError('Private key not in range [1, n)'); @@ -57,13 +88,19 @@ export class ECPair implements ECPairInterface { return new ECPair(buffer, undefined, options); } - static fromPublicKey(buffer: Buffer, options?: ECPairOptions): ECPair { + function fromPublicKey( + buffer: Buffer, + options?: ECPairOptions, + ): ECPairInterface { types.typeforce(ecc.isPoint, buffer); types.typeforce(isOptions, options); return new ECPair(undefined, buffer, options); } - static fromWIF(wifString: string, network?: Network | Network[]): ECPair { + function fromWIF( + wifString: string, + network?: Network | Network[], + ): ECPairInterface { const decoded = wif.decode(wifString); const version = decoded.version; @@ -85,13 +122,13 @@ export class ECPair implements ECPairInterface { throw new Error('Invalid network version'); } - return this.fromPrivateKey(decoded.privateKey, { + return fromPrivateKey(decoded.privateKey, { compressed: decoded.compressed, network: network as Network, }); } - static makeRandom(options?: ECPairOptions): ECPair { + function makeRandom(options?: ECPairOptions): ECPairInterface { types.typeforce(isOptions, options); if (options === undefined) options = {}; const rng = options.rng || randomBytes; @@ -102,63 +139,87 @@ export class ECPair implements ECPairInterface { types.typeforce(types.Buffer256bit, d); } while (!ecc.isPrivate(d)); - return this.fromPrivateKey(d, options); + return fromPrivateKey(d, options); } - compressed: boolean; - network: Network; - lowR: boolean; + class ECPair implements ECPairInterface { + compressed: boolean; + network: Network; + lowR: boolean; + + constructor( + private __D?: Buffer, + private __Q?: Buffer, + options?: ECPairOptions, + ) { + this.lowR = false; + if (options === undefined) options = {}; + this.compressed = + options.compressed === undefined ? true : options.compressed; + this.network = options.network || networks.bitcoin; + + if (__Q !== undefined) + this.__Q = Buffer.from(ecc.pointCompress(__Q, this.compressed)); + } - protected constructor( - private __D?: Buffer, - private __Q?: Buffer, - options?: ECPairOptions, - ) { - this.lowR = false; - if (options === undefined) options = {}; - this.compressed = - options.compressed === undefined ? true : options.compressed; - this.network = options.network || networks.bitcoin; + get privateKey(): Buffer | undefined { + return this.__D; + } - if (__Q !== undefined) this.__Q = ecc.pointCompress(__Q, this.compressed); - } + get publicKey(): Buffer { + if (!this.__Q) + this.__Q = Buffer.from(ecc.pointFromScalar(this.__D, this.compressed)); + return this.__Q; + } - get privateKey(): Buffer | undefined { - return this.__D; - } + toWIF(): string { + if (!this.__D) throw new Error('Missing private key'); + return wif.encode(this.network.wif, this.__D, this.compressed); + } - get publicKey(): Buffer { - if (!this.__Q) - this.__Q = ecc.pointFromScalar(this.__D, this.compressed) as Buffer; - return this.__Q; - } + sign(hash: Buffer, lowR?: boolean): Buffer { + if (!this.__D) throw new Error('Missing private key'); + if (lowR === undefined) lowR = this.lowR; + if (lowR === false) { + return Buffer.from(ecc.sign(hash, this.__D)); + } else { + let sig = ecc.sign(hash, this.__D); + const extraData = Buffer.alloc(32, 0); + 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 = ecc.sign(hash, this.__D, extraData); + } + return Buffer.from(sig); + } + } - toWIF(): string { - if (!this.__D) throw new Error('Missing private key'); - return wif.encode(this.network.wif, this.__D, this.compressed); - } + signSchnorr(hash: Buffer): Buffer { + 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)); + } - sign(hash: Buffer, lowR?: boolean): Buffer { - if (!this.__D) throw new Error('Missing private key'); - if (lowR === undefined) lowR = this.lowR; - if (lowR === false) { - return ecc.sign(hash, this.__D); - } else { - let sig = ecc.sign(hash, this.__D); - const extraData = Buffer.alloc(32, 0); - 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 = ecc.signWithEntropy(hash, this.__D, extraData); - } - return sig; + verify(hash: Buffer, signature: Buffer): boolean { + return ecc.verify(hash, this.publicKey, signature); } - } - verify(hash: Buffer, signature: Buffer): boolean { - return ecc.verify(hash, this.publicKey, signature); + verifySchnorr(hash: Buffer, signature: Buffer): boolean { + if (!ecc.verifySchnorr) + throw new Error('verifySchnorr not supported by ecc library'); + return ecc.verifySchnorr(hash, this.publicKey.subarray(1, 33), signature); + } } + + return { + isPoint, + fromPrivateKey, + fromPublicKey, + fromWIF, + makeRandom, + }; } diff --git a/ts_src/index.ts b/ts_src/index.ts new file mode 100644 index 0000000..838cb66 --- /dev/null +++ b/ts_src/index.ts @@ -0,0 +1,9 @@ +export { + ECPairFactory as default, + ECPairFactory, + Signer, + SignerAsync, + ECPairAPI, + ECPairInterface, + TinySecp256k1Interface, +} from './ecpair'; diff --git a/ts_src/testecc.ts b/ts_src/testecc.ts new file mode 100644 index 0000000..24ba9ea --- /dev/null +++ b/ts_src/testecc.ts @@ -0,0 +1,153 @@ +import { TinySecp256k1Interface } from './ecpair'; + +const h = (hex: string): Buffer => Buffer.from(hex, 'hex'); + +export function testEcc(ecc: TinySecp256k1Interface): void { + assert( + ecc.isPoint( + h('0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798'), + ), + ); + assert( + !ecc.isPoint( + h('030000000000000000000000000000000000000000000000000000000000000005'), + ), + ); + assert( + ecc.isPrivate( + h('79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798'), + ), + ); + // order - 1 + assert( + ecc.isPrivate( + h('fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364140'), + ), + ); + // 0 + assert( + !ecc.isPrivate( + h('0000000000000000000000000000000000000000000000000000000000000000'), + ), + ); + // order + assert( + !ecc.isPrivate( + h('fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141'), + ), + ); + // order + 1 + assert( + !ecc.isPrivate( + h('fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364142'), + ), + ); + assert( + Buffer.from( + ecc.pointCompress( + h( + '0479be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8', + ), + true, + )!, + ).equals( + h('0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798'), + ), + ); + assert( + Buffer.from( + ecc.pointCompress( + h( + '0479be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8', + ), + false, + )!, + ).equals( + h( + '0479be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8', + ), + ), + ); + assert( + Buffer.from( + ecc.pointCompress( + h('0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798'), + true, + )!, + ).equals( + h('0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798'), + ), + ); + assert( + Buffer.from( + ecc.pointCompress( + h('0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798'), + false, + )!, + ).equals( + h( + '0479be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8', + ), + ), + ); + assert( + Buffer.from( + ecc.pointFromScalar( + h('b1121e4088a66a28f5b6b0f5844943ecd9f610196d7bb83b25214b60452c09af'), + )!, + ).equals( + h('02b07ba9dca9523b7ef4bd97703d43d20399eb698e194704791a25ce77a400df99'), + ), + ); + assert( + Buffer.from( + ecc.sign( + h('5e9f0a0d593efdcf78ac923bc3313e4e7d408d574354ee2b3288c0da9fbba6ed'), + h('fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364140'), + )!, + ).equals( + 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', + ), + ), + ); + } + if (ecc.verifySchnorr) { + assert( + ecc.verifySchnorr( + h('7e2d58d8b3bcdf1abadec7829054f90dda9805aab56c77333024b9d0a508b75c'), + h('dd308afec5777e13121fa72b9cc1b7cc0139715309b086c960e18fd969774eb8'), + h( + '5831aaeed7b44bb74e5eab94ba9d4294c49bcf2a60728d8b4c200f50dd313c1bab745879a5ad954a72c45a91c3a51d3c7adea98d82f8481e0e1e03674a6f3fb7', + ), + ), + ); + } +} + +function assert(bool: boolean): void { + if (!bool) throw new Error('ecc library invalid'); +}