Daniel Cousens
10 years ago
3 changed files with 478 additions and 0 deletions
@ -0,0 +1,142 @@ |
|||||
|
var assert = require('assert') |
||||
|
var base58check = require('bs58check') |
||||
|
var bcrypto = require('./crypto') |
||||
|
var ecdsa = require('./ecdsa') |
||||
|
var ecurve = require('ecurve') |
||||
|
var networks = require('./networks') |
||||
|
var randomBytes = require('randombytes') |
||||
|
var typeForce = require('typeforce') |
||||
|
|
||||
|
var Address = require('./address') |
||||
|
var BigInteger = require('bigi') |
||||
|
|
||||
|
function findNetworkByWIFVersion (version) { |
||||
|
for (var networkName in networks) { |
||||
|
var network = networks[networkName] |
||||
|
|
||||
|
if (network.wif === version) return network |
||||
|
} |
||||
|
|
||||
|
assert(false, 'Unknown network') |
||||
|
} |
||||
|
|
||||
|
function ECPair (d, Q, options) { |
||||
|
options = options || {} |
||||
|
|
||||
|
var compressed = options.compressed === undefined ? true : options.compressed |
||||
|
var network = options.network === undefined ? networks.bitcoin : options.network |
||||
|
|
||||
|
typeForce('Boolean', compressed) |
||||
|
assert('pubKeyHash' in network, 'Unknown pubKeyHash constants for network') |
||||
|
|
||||
|
if (d) { |
||||
|
assert(d.signum() > 0, 'Private key must be greater than 0') |
||||
|
assert(d.compareTo(ECPair.curve.n) < 0, 'Private key must be less than the curve order') |
||||
|
|
||||
|
assert(!Q, 'Unexpected publicKey parameter') |
||||
|
Q = ECPair.curve.G.multiply(d) |
||||
|
|
||||
|
// enforce Q is a public key if no private key given
|
||||
|
} else { |
||||
|
typeForce('Point', Q) |
||||
|
} |
||||
|
|
||||
|
this.compressed = compressed |
||||
|
this.d = d |
||||
|
this.Q = Q |
||||
|
this.network = network |
||||
|
} |
||||
|
|
||||
|
// Public access to secp256k1 curve
|
||||
|
ECPair.curve = ecurve.getCurveByName('secp256k1') |
||||
|
|
||||
|
ECPair.fromPublicKeyBuffer = function (buffer, network) { |
||||
|
var Q = ecurve.Point.decodeFrom(ECPair.curve, buffer) |
||||
|
|
||||
|
return new ECPair(null, Q, { |
||||
|
compressed: Q.compressed, |
||||
|
network: network |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
ECPair.fromWIF = function (string) { |
||||
|
var payload = base58check.decode(string) |
||||
|
var version = payload.readUInt8(0) |
||||
|
var compressed |
||||
|
|
||||
|
if (payload.length === 34) { |
||||
|
assert.strictEqual(payload[33], 0x01, 'Invalid compression flag') |
||||
|
|
||||
|
// truncate the version/compression bytes
|
||||
|
payload = payload.slice(1, -1) |
||||
|
compressed = true |
||||
|
|
||||
|
// no compression flag
|
||||
|
} else { |
||||
|
assert.equal(payload.length, 33, 'Invalid WIF payload length') |
||||
|
|
||||
|
// Truncate the version byte
|
||||
|
payload = payload.slice(1) |
||||
|
compressed = false |
||||
|
} |
||||
|
|
||||
|
var network = findNetworkByWIFVersion(version) |
||||
|
var d = BigInteger.fromBuffer(payload) |
||||
|
|
||||
|
return new ECPair(d, null, { |
||||
|
compressed: compressed, |
||||
|
network: network |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
ECPair.makeRandom = function (options) { |
||||
|
options = options || {} |
||||
|
|
||||
|
var rng = options.rng || randomBytes |
||||
|
var buffer = rng(32) |
||||
|
typeForce('Buffer', buffer) |
||||
|
assert.equal(buffer.length, 32, 'Expected 256-bit Buffer from RNG') |
||||
|
|
||||
|
var d = BigInteger.fromBuffer(buffer) |
||||
|
d = d.mod(ECPair.curve.n) |
||||
|
|
||||
|
return new ECPair(d, null, options) |
||||
|
} |
||||
|
|
||||
|
ECPair.prototype.toWIF = function () { |
||||
|
assert(this.d, 'Missing private key') |
||||
|
|
||||
|
var bufferLen = this.compressed ? 34 : 33 |
||||
|
var buffer = new Buffer(bufferLen) |
||||
|
|
||||
|
buffer.writeUInt8(this.network.wif, 0) |
||||
|
this.d.toBuffer(32).copy(buffer, 1) |
||||
|
|
||||
|
if (this.compressed) { |
||||
|
buffer.writeUInt8(0x01, 33) |
||||
|
} |
||||
|
|
||||
|
return base58check.encode(buffer) |
||||
|
} |
||||
|
|
||||
|
ECPair.prototype.getAddress = function () { |
||||
|
var pubKey = this.getPublicKeyBuffer() |
||||
|
|
||||
|
return new Address(bcrypto.hash160(pubKey), this.network.pubKeyHash) |
||||
|
} |
||||
|
|
||||
|
ECPair.prototype.getPublicKeyBuffer = function () { |
||||
|
return this.Q.getEncoded(this.compressed) |
||||
|
} |
||||
|
|
||||
|
ECPair.prototype.sign = function (hash) { |
||||
|
assert(this.d, 'Missing private key') |
||||
|
|
||||
|
return ecdsa.sign(ECPair.curve, hash, this.d) |
||||
|
} |
||||
|
|
||||
|
ECPair.prototype.verify = function (hash, signature) { |
||||
|
return ecdsa.verify(ECPair.curve, hash, signature, this.Q) |
||||
|
} |
||||
|
|
||||
|
module.exports = ECPair |
@ -0,0 +1,234 @@ |
|||||
|
/* global describe, it, beforeEach */ |
||||
|
/* eslint-disable no-new */ |
||||
|
|
||||
|
var assert = require('assert') |
||||
|
var ecdsa = require('../src/ecdsa') |
||||
|
var ecurve = require('ecurve') |
||||
|
var networks = require('../src/networks') |
||||
|
var proxyquire = require('proxyquire') |
||||
|
var sinon = require('sinon') |
||||
|
|
||||
|
var BigInteger = require('bigi') |
||||
|
var ECPair = require('../src/ecpair') |
||||
|
|
||||
|
var fixtures = require('./fixtures/ecpair.json') |
||||
|
|
||||
|
describe('ECPair', function () { |
||||
|
describe('constructor', function () { |
||||
|
it('defaults to compressed', function () { |
||||
|
var keyPair = new ECPair(BigInteger.ONE) |
||||
|
|
||||
|
assert.equal(keyPair.compressed, true) |
||||
|
}) |
||||
|
|
||||
|
it('supports the uncompressed option', function () { |
||||
|
var keyPair = new ECPair(BigInteger.ONE, null, { |
||||
|
compressed: false |
||||
|
}) |
||||
|
|
||||
|
assert.equal(keyPair.compressed, false) |
||||
|
}) |
||||
|
|
||||
|
it('supports the network option', function () { |
||||
|
var keyPair = new ECPair(BigInteger.ONE, null, { |
||||
|
compressed: false, |
||||
|
network: networks.testnet |
||||
|
}) |
||||
|
|
||||
|
assert.equal(keyPair.network, networks.testnet) |
||||
|
}) |
||||
|
|
||||
|
it('throws if compressed option is not a bool', function () { |
||||
|
assert.throws(function () { |
||||
|
new ECPair(null, null, { |
||||
|
compressed: 2 |
||||
|
}, /Expected Boolean, got 2/) |
||||
|
}) |
||||
|
}) |
||||
|
|
||||
|
it('throws if public and private key given', function () { |
||||
|
var qBuffer = new Buffer('0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798', 'hex') |
||||
|
var Q = ecurve.Point.decodeFrom(ECPair.curve, qBuffer) |
||||
|
|
||||
|
assert.throws(function () { |
||||
|
new ECPair(BigInteger.ONE, Q) |
||||
|
}, /Unexpected publicKey parameter/) |
||||
|
}) |
||||
|
|
||||
|
it('throws if network is missing pubKeyHash constants', function () { |
||||
|
assert.throws(function () { |
||||
|
new ECPair(null, null, { |
||||
|
network: {} |
||||
|
}, /Unknown pubKeyHash constants for network/) |
||||
|
}) |
||||
|
}) |
||||
|
|
||||
|
fixtures.valid.forEach(function (f) { |
||||
|
it('calculates the public point for ' + f.WIF, function () { |
||||
|
var d = new BigInteger(f.d) |
||||
|
var keyPair = new ECPair(d, null, { |
||||
|
compressed: f.compressed |
||||
|
}) |
||||
|
|
||||
|
assert.equal(keyPair.getPublicKeyBuffer().toString('hex'), f.Q) |
||||
|
}) |
||||
|
}) |
||||
|
|
||||
|
fixtures.invalid.constructor.forEach(function (f) { |
||||
|
it('throws on ' + f.d, function () { |
||||
|
var d = new BigInteger(f.d) |
||||
|
|
||||
|
assert.throws(function () { |
||||
|
new ECPair(d) |
||||
|
}, new RegExp(f.exception)) |
||||
|
}) |
||||
|
}) |
||||
|
}) |
||||
|
|
||||
|
describe('getPublicKeyBuffer', function () { |
||||
|
var keyPair |
||||
|
|
||||
|
beforeEach(function () { |
||||
|
keyPair = new ECPair(BigInteger.ONE) |
||||
|
}) |
||||
|
|
||||
|
it('wraps Q.getEncoded', sinon.test(function () { |
||||
|
this.mock(keyPair.Q).expects('getEncoded') |
||||
|
.once().calledWith(keyPair.compressed) |
||||
|
|
||||
|
keyPair.getPublicKeyBuffer() |
||||
|
})) |
||||
|
}) |
||||
|
|
||||
|
describe('fromWIF', function () { |
||||
|
fixtures.valid.forEach(function (f) { |
||||
|
it('imports ' + f.WIF + ' correctly', function () { |
||||
|
var keyPair = ECPair.fromWIF(f.WIF) |
||||
|
|
||||
|
assert.equal(keyPair.d.toString(), f.d) |
||||
|
assert.equal(keyPair.compressed, f.compressed) |
||||
|
assert.equal(keyPair.network, networks[f.network]) |
||||
|
}) |
||||
|
}) |
||||
|
|
||||
|
fixtures.invalid.fromWIF.forEach(function (f) { |
||||
|
it('throws on ' + f.string, function () { |
||||
|
assert.throws(function () { |
||||
|
ECPair.fromWIF(f.string) |
||||
|
}, new RegExp(f.exception)) |
||||
|
}) |
||||
|
}) |
||||
|
}) |
||||
|
|
||||
|
describe('toWIF', function () { |
||||
|
fixtures.valid.forEach(function (f) { |
||||
|
it('exports ' + f.WIF + ' correctly', function () { |
||||
|
var keyPair = ECPair.fromWIF(f.WIF) |
||||
|
var result = keyPair.toWIF() |
||||
|
|
||||
|
assert.equal(result, f.WIF) |
||||
|
}) |
||||
|
}) |
||||
|
}) |
||||
|
|
||||
|
describe('makeRandom', function () { |
||||
|
var d = new Buffer('0404040404040404040404040404040404040404040404040404040404040404', 'hex') |
||||
|
var exWIF = 'KwMWvwRJeFqxYyhZgNwYuYjbQENDAPAudQx5VEmKJrUZcq6aL2pv' |
||||
|
|
||||
|
describe('uses randombytes RNG', function () { |
||||
|
it('generates a ECPair', function () { |
||||
|
var stub = { randombytes: function () { return d } } |
||||
|
var ProxiedECPair = proxyquire('../src/ecpair', stub) |
||||
|
|
||||
|
var keyPair = ProxiedECPair.makeRandom() |
||||
|
assert.equal(keyPair.toWIF(), exWIF) |
||||
|
}) |
||||
|
|
||||
|
it('passes the options param', sinon.test(function () { |
||||
|
var options = { |
||||
|
compressed: true |
||||
|
} |
||||
|
|
||||
|
// FIXME: waiting on https://github.com/cjohansen/Sinon.JS/issues/613
|
||||
|
// this.mock(ECPair).expects('constructor')
|
||||
|
// .once().calledWith(options)
|
||||
|
|
||||
|
ECPair.makeRandom(options) |
||||
|
})) |
||||
|
}) |
||||
|
|
||||
|
it('allows a custom RNG to be used', function () { |
||||
|
var keyPair = ECPair.makeRandom({ |
||||
|
rng: function (size) { return d.slice(0, size) } |
||||
|
}) |
||||
|
|
||||
|
assert.equal(keyPair.toWIF(), exWIF) |
||||
|
}) |
||||
|
}) |
||||
|
|
||||
|
describe('getAddress', function () { |
||||
|
fixtures.valid.forEach(function (f) { |
||||
|
it('returns ' + f.address + ' for ' + f.WIF, function () { |
||||
|
var keyPair = ECPair.fromWIF(f.WIF) |
||||
|
|
||||
|
assert.equal(keyPair.getAddress().toString(), f.address) |
||||
|
}) |
||||
|
}) |
||||
|
}) |
||||
|
|
||||
|
describe('ecdsa wrappers', function () { |
||||
|
var keyPair, hash |
||||
|
|
||||
|
beforeEach(function () { |
||||
|
keyPair = ECPair.makeRandom() |
||||
|
hash = new Buffer(32) |
||||
|
}) |
||||
|
|
||||
|
it('uses the secp256k1 curve by default', function () { |
||||
|
var secp256k1 = ecurve.getCurveByName('secp256k1') |
||||
|
|
||||
|
for (var property in secp256k1) { |
||||
|
// FIXME: circular structures in ecurve
|
||||
|
if (property === 'G') continue |
||||
|
if (property === 'infinity') continue |
||||
|
|
||||
|
var actual = ECPair.curve[property] |
||||
|
var expected = secp256k1[property] |
||||
|
|
||||
|
assert.deepEqual(actual, expected) |
||||
|
} |
||||
|
}) |
||||
|
|
||||
|
describe('signing', function () { |
||||
|
it('wraps ecdsa.sign', sinon.test(function () { |
||||
|
this.mock(ecdsa).expects('sign') |
||||
|
.once().calledWith(ECPair.curve, hash, keyPair.d) |
||||
|
|
||||
|
keyPair.sign(hash) |
||||
|
})) |
||||
|
|
||||
|
it('throws if no private key is found', function () { |
||||
|
keyPair.d = null |
||||
|
|
||||
|
assert.throws(function () { |
||||
|
keyPair.sign(hash) |
||||
|
}, /Missing private key/) |
||||
|
}) |
||||
|
}) |
||||
|
|
||||
|
describe('verify', function () { |
||||
|
var signature |
||||
|
|
||||
|
beforeEach(function () { |
||||
|
signature = keyPair.sign(hash) |
||||
|
}) |
||||
|
|
||||
|
it('wraps ecdsa.verify', sinon.test(function () { |
||||
|
this.mock(ecdsa).expects('verify') |
||||
|
.once().calledWith(ECPair.curve, hash, signature, keyPair.Q) |
||||
|
|
||||
|
keyPair.verify(hash, signature) |
||||
|
})) |
||||
|
}) |
||||
|
}) |
||||
|
}) |
@ -0,0 +1,102 @@ |
|||||
|
{ |
||||
|
"valid": [ |
||||
|
{ |
||||
|
"d": "1", |
||||
|
"Q": "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798", |
||||
|
"compressed": true, |
||||
|
"network": "bitcoin", |
||||
|
"address": "1BgGZ9tcN4rm9KBzDn7KprQz87SZ26SAMH", |
||||
|
"WIF": "KwDiBf89QgGbjEhKnhXJuH7LrciVrZi3qYjgd9M7rFU73sVHnoWn" |
||||
|
}, |
||||
|
{ |
||||
|
"d": "1", |
||||
|
"Q": "0479be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8", |
||||
|
"compressed": false, |
||||
|
"network": "bitcoin", |
||||
|
"address": "1EHNa6Q4Jz2uvNExL497mE43ikXhwF6kZm", |
||||
|
"WIF": "5HpHagT65TZzG1PH3CSu63k8DbpvD8s5ip4nEB3kEsreAnchuDf" |
||||
|
}, |
||||
|
{ |
||||
|
"d": "19898843618908353587043383062236220484949425084007183071220218307100305431102", |
||||
|
"Q": "02b80011a883a0fd621ad46dfc405df1e74bf075cbaf700fd4aebef6e96f848340", |
||||
|
"compressed": true, |
||||
|
"network": "bitcoin", |
||||
|
"address": "1MasfEKgSiaSeri2C6kgznaqBNtyrZPhNq", |
||||
|
"WIF": "KxhEDBQyyEFymvfJD96q8stMbJMbZUb6D1PmXqBWZDU2WvbvVs9o" |
||||
|
}, |
||||
|
{ |
||||
|
"d": "48968302285117906840285529799176770990048954789747953886390402978935544927851", |
||||
|
"Q": "024289801366bcee6172b771cf5a7f13aaecd237a0b9a1ff9d769cabc2e6b70a34", |
||||
|
"compressed": true, |
||||
|
"network": "bitcoin", |
||||
|
"address": "1LwwMWdSEMHJ2dMhSvAHZ3g95tG2UBv9jg", |
||||
|
"WIF": "KzrA86mCVMGWnLGBQu9yzQa32qbxb5dvSK4XhyjjGAWSBKYX4rHx" |
||||
|
}, |
||||
|
{ |
||||
|
"d": "48968302285117906840285529799176770990048954789747953886390402978935544927851", |
||||
|
"Q": "044289801366bcee6172b771cf5a7f13aaecd237a0b9a1ff9d769cabc2e6b70a34cec320a0565fb7caf11b1ca2f445f9b7b012dda5718b3cface369ee3a034ded6", |
||||
|
"compressed": false, |
||||
|
"network": "bitcoin", |
||||
|
"address": "1zXcfvKCLgsFdJDYPuqpu1sF3q92tnnUM", |
||||
|
"WIF": "5JdxzLtFPHNe7CAL8EBC6krdFv9pwPoRo4e3syMZEQT9srmK8hh" |
||||
|
}, |
||||
|
{ |
||||
|
"d": "48968302285117906840285529799176770990048954789747953886390402978935544927851", |
||||
|
"Q": "024289801366bcee6172b771cf5a7f13aaecd237a0b9a1ff9d769cabc2e6b70a34", |
||||
|
"compressed": true, |
||||
|
"network": "testnet", |
||||
|
"address": "n1TteZiR3NiYojqKAV8fNxtTwsrjM7kVdj", |
||||
|
"WIF": "cRD9b1m3vQxmwmjSoJy7Mj56f4uNFXjcWMCzpQCEmHASS4edEwXv" |
||||
|
}, |
||||
|
{ |
||||
|
"d": "48968302285117906840285529799176770990048954789747953886390402978935544927851", |
||||
|
"Q": "044289801366bcee6172b771cf5a7f13aaecd237a0b9a1ff9d769cabc2e6b70a34cec320a0565fb7caf11b1ca2f445f9b7b012dda5718b3cface369ee3a034ded6", |
||||
|
"compressed": false, |
||||
|
"network": "testnet", |
||||
|
"address": "mgWUuj1J1N882jmqFxtDepEC73Rr22E9GU", |
||||
|
"WIF": "92Qba5hnyWSn5Ffcka56yMQauaWY6ZLd91Vzxbi4a9CCetaHtYj" |
||||
|
}, |
||||
|
{ |
||||
|
"d": "115792089237316195423570985008687907852837564279074904382605163141518161494336", |
||||
|
"Q": "0379be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798", |
||||
|
"compressed": true, |
||||
|
"network": "bitcoin", |
||||
|
"address": "1GrLCmVQXoyJXaPJQdqssNqwxvha1eUo2E", |
||||
|
"WIF": "L5oLkpV3aqBjhki6LmvChTCV6odsp4SXM6FfU2Gppt5kFLaHLuZ9" |
||||
|
} |
||||
|
], |
||||
|
"invalid": { |
||||
|
"constructor": [ |
||||
|
{ |
||||
|
"exception": "Private key must be greater than 0", |
||||
|
"d": "-1" |
||||
|
}, |
||||
|
{ |
||||
|
"exception": "Private key must be greater than 0", |
||||
|
"d": "0" |
||||
|
}, |
||||
|
{ |
||||
|
"exception": "Private key must be less than the curve order", |
||||
|
"d": "115792089237316195423570985008687907852837564279074904382605163141518161494337" |
||||
|
}, |
||||
|
{ |
||||
|
"exception": "Private key must be less than the curve order", |
||||
|
"d": "115792089237316195423570985008687907853269984665640564039457584007913129639935" |
||||
|
} |
||||
|
], |
||||
|
"fromWIF": [ |
||||
|
{ |
||||
|
"exception": "Invalid compression flag", |
||||
|
"string": "ju9rooVsmagsb4qmNyTysUSFB1GB6MdpD7eoGjUTPmZRAApJxRz" |
||||
|
}, |
||||
|
{ |
||||
|
"exception": "Invalid WIF payload length", |
||||
|
"string": "7ZEtRQLhCsDQrd6ZKfmcESdXgas8ggZPN24ByEi5ey6VJW" |
||||
|
}, |
||||
|
{ |
||||
|
"exception": "Invalid WIF payload length", |
||||
|
"string": "5qibUKwsnMo1qDiNp3prGaQkD2JfVJa8F8Na87H2CkMHvuVg6uKhw67Rh" |
||||
|
} |
||||
|
] |
||||
|
} |
||||
|
} |
Loading…
Reference in new issue