Merge pull request #320 from olalonde/armory
Armory style key derivation from (chain code, public key)
This commit is contained in:
commit
982548f3e3
|
@ -60,5 +60,6 @@ requireWhenAccessed('WalletKey', './lib/WalletKey');
|
|||
requireWhenAccessed('PeerManager', './lib/PeerManager');
|
||||
requireWhenAccessed('Message', './lib/Message');
|
||||
requireWhenAccessed('Electrum', './lib/Electrum');
|
||||
requireWhenAccessed('Armory', './lib/Armory');
|
||||
module.exports.Buffer = Buffer;
|
||||
|
||||
|
|
|
@ -24,6 +24,7 @@ var pack = function (params) {
|
|||
|
||||
var modules = [
|
||||
'lib/Address',
|
||||
'lib/Armory',
|
||||
'lib/Base58',
|
||||
'lib/BIP32',
|
||||
'lib/Block',
|
||||
|
|
|
@ -0,0 +1,66 @@
|
|||
var Armory = require('../lib/Armory');
|
||||
var Address = require('../lib/Address');
|
||||
|
||||
// Initial public key can be retrieved from paper backup
|
||||
|
||||
var PublicX = '9df5 23e7 18b9 1f59 a790 2d46 999f 9357 ccf8 7208 24d4 3076 4516 b809 f7ab ce4e';
|
||||
var PublicY = '66ba 5d21 4682 0dae 401d 9506 8437 2516 79f9 0c56 4186 cc50 07df c6d0 6989 1ff4';
|
||||
var pubkey = '04' + PublicX.split(' ').join('') + PublicY.split(' ').join('');
|
||||
|
||||
// Chain code can be generated by entering paper backup
|
||||
// on brainwallet.org/#chains or by using Armory.fromSeed() below
|
||||
|
||||
var chaincode = '84ac14bc4b388b33da099a0b4ee3b507284d99e1476639e36e5ca5e6af86481e';
|
||||
|
||||
var armory = new Armory(chaincode, pubkey);
|
||||
|
||||
console.log('Deriving public keys for');
|
||||
console.log('------------------------');
|
||||
console.log('Chain code: %s', chaincode);
|
||||
console.log('Public key: %s', pubkey);
|
||||
console.log('');
|
||||
|
||||
for (var i = 0; i < 5; i++) {
|
||||
console.log(Address.fromPubKey(armory.pubkey).as('base58'));
|
||||
armory = armory.next();
|
||||
}
|
||||
|
||||
// Derive first public key and chain code from seed
|
||||
var seed = [
|
||||
'aagh hjfj sihk ietj giik wwai awtd uodh hnji',
|
||||
'soss uaku egod utai itos fijj ihgi jhau jtoo'
|
||||
];
|
||||
|
||||
console.log('');
|
||||
console.log('');
|
||||
console.log('Deriving public keys for');
|
||||
console.log('------------------------');
|
||||
console.log('Seed: %s', seed.join(' '));
|
||||
console.log('');
|
||||
|
||||
// skip first public key
|
||||
var a = Armory.fromSeed(seed.join('\n')).next();
|
||||
|
||||
for (var i = 0; i < 5; i++) {
|
||||
console.log(Address.fromPubKey(a.pubkey).as('base58'));
|
||||
a = a.next();
|
||||
}
|
||||
|
||||
|
||||
var mpk = '045a09a3286873a72f164476bde9d1d8e5c2bc044e35aa47eb6e798e325a86417f7c35b61d9905053533e0b4f2a26eca0330aadf21c638969e45aaace50e4c0c8784ac14bc4b388b33da099a0b4ee3b507284d99e1476639e36e5ca5e6af86481e';
|
||||
|
||||
console.log('');
|
||||
console.log('');
|
||||
console.log('Deriving public keys for');
|
||||
console.log('------------------------');
|
||||
console.log('Master Public Key: %s', mpk);
|
||||
console.log('');
|
||||
|
||||
// skip first public key
|
||||
var b = Armory.fromMasterPublicKey(mpk).next();
|
||||
|
||||
for (var i = 0; i < 5; i++) {
|
||||
console.log(Address.fromPubKey(b.pubkey).as('base58'));
|
||||
b = b.next();
|
||||
}
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<style>
|
||||
textarea {
|
||||
width: 400px;
|
||||
height: 100px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<script src="../../browser/bundle.js"></script>
|
||||
Enter you paper wallet seed:<br>
|
||||
<textarea id="seed">aagh hjfj sihk ietj giik wwai awtd uodh hnji
|
||||
soss uaku egod utai itos fijj ihgi jhau jtoo</textarea>
|
||||
<br>
|
||||
<input type="submit" onclick="updateResult()" value="Generate">
|
||||
<div id="result"></div>
|
||||
<pre id="console"></pre>
|
||||
<script>
|
||||
var bitcore = require('bitcore'),
|
||||
Address = bitcore.Address,
|
||||
Armory = bitcore.Armory;
|
||||
|
||||
var logs = document.getElementById('console');
|
||||
function log (msg) {
|
||||
logs.insertAdjacentHTML('beforeend', msg + '\n');
|
||||
}
|
||||
function clear_log () {
|
||||
logs.innerHTML = '';
|
||||
}
|
||||
|
||||
function getSeed() {
|
||||
return document.getElementById('seed').value;
|
||||
}
|
||||
|
||||
function updateResult () {
|
||||
clear_log();
|
||||
var seed = getSeed();
|
||||
|
||||
var a = Armory.fromSeed(seed);
|
||||
log('Armory MPK: ');
|
||||
log('');
|
||||
log('');
|
||||
log('<textarea>' + a.pubkey.toString('hex') + '' + a.chaincode.toString('hex') + '</textarea>');
|
||||
log('');
|
||||
log('');
|
||||
log('Some wallet addresses:');
|
||||
for (var i = 0; i < 5; i++) {
|
||||
log(Address.fromPubKey(a.pubkey).as('base58'));
|
||||
a = a.next();
|
||||
}
|
||||
}
|
||||
|
||||
updateResult();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,115 @@
|
|||
var Point = require('./Point'),
|
||||
Key = require('./Key'),
|
||||
sha256 = require('../util').sha256,
|
||||
twoSha256 = require('../util').twoSha256;
|
||||
|
||||
/**
|
||||
* For now, this class can only supports derivation from public key
|
||||
* It doesn't support private key derivation (TODO).
|
||||
*
|
||||
* @example examples/Armory.js
|
||||
*/
|
||||
function Armory (chaincode, pubkey) {
|
||||
this.chaincode = new Buffer(chaincode, 'hex');
|
||||
this.pubkey = new Buffer(pubkey, 'hex');
|
||||
}
|
||||
|
||||
Armory.prototype.generatePubKey = function () {
|
||||
var pubKey = this.pubkey;
|
||||
var chainCode = this.chaincode;
|
||||
var chainXor = twoSha256(pubKey);
|
||||
|
||||
for (var i = 0; i < 32; i++)
|
||||
chainXor[i] ^= chainCode[i];
|
||||
|
||||
var pt = Point.fromUncompressedPubKey(pubKey);
|
||||
pt = Point.multiply(pt, chainXor);
|
||||
|
||||
var new_pubkey = pt.toUncompressedPubKey();
|
||||
|
||||
return new_pubkey;
|
||||
};
|
||||
|
||||
Armory.prototype.next = function () {
|
||||
var next_pubkey = this.generatePubKey();
|
||||
return new Armory(this.chaincode, next_pubkey);
|
||||
};
|
||||
|
||||
/**
|
||||
* PS: MPK here represents the pubkey concatenated
|
||||
* with the chain code. It is an unofficial standard.
|
||||
*
|
||||
* Armory will soon release an officially supported
|
||||
* format:
|
||||
*
|
||||
* https://github.com/etotheipi/BitcoinArmory/issues/204#issuecomment-42217801
|
||||
*/
|
||||
Armory.fromMasterPublicKey = function (mpk) {
|
||||
var pubkey = mpk.substr(0, 130);
|
||||
var chaincode = mpk.substr(130, mpk.length);
|
||||
return new Armory(chaincode, pubkey);
|
||||
};
|
||||
|
||||
function decode (str) {
|
||||
var from = '0123456789abcdef';
|
||||
var to = 'asdfghjkwertuion';
|
||||
var res = '';
|
||||
for (var i = 0; i < str.length; i++)
|
||||
res += from.charAt(to.indexOf(str.charAt(i)));
|
||||
return res;
|
||||
}
|
||||
|
||||
Armory.decodeSeed = function (seed) {
|
||||
var keys = seed.trim().split('\n');
|
||||
var lines = [];
|
||||
|
||||
for (var i = 0; i < keys.length; i++) {
|
||||
var k = keys[i].replace(' ','');
|
||||
var raw = new Buffer(decode(k), 'hex');
|
||||
var data = raw.slice(0, 16);
|
||||
lines.push(data);
|
||||
}
|
||||
|
||||
var privKey = Buffer.concat([ lines[0], lines[1] ]);
|
||||
var chainCode = (lines.length==4) ?
|
||||
Buffer.concat([ lines[2], lines[3] ]) : Armory.deriveChaincode(privKey);
|
||||
|
||||
return {
|
||||
privKey: privKey,
|
||||
chainCode: chainCode
|
||||
};
|
||||
};
|
||||
|
||||
// Derive chain code from root key
|
||||
Armory.fromSeed = function (seed) {
|
||||
var res = Armory.decodeSeed(seed);
|
||||
// generate first public key
|
||||
var key = new Key();
|
||||
key.private = res.privKey;
|
||||
key.compressed = false;
|
||||
key.regenerateSync();
|
||||
|
||||
return new Armory(res.chainCode, key.public);
|
||||
};
|
||||
|
||||
Armory.deriveChaincode = function (root) {
|
||||
var msg = 'Derive Chaincode from Root Key';
|
||||
var hash = twoSha256(root);
|
||||
|
||||
var okey = [];
|
||||
var ikey = [];
|
||||
for (var i = 0; i < hash.length; i++) {
|
||||
okey.push(0x5c ^ hash[i]);
|
||||
ikey.push(0x36 ^ hash[i]);
|
||||
}
|
||||
|
||||
okey = new Buffer(okey);
|
||||
ikey = new Buffer(ikey);
|
||||
|
||||
var m = new Buffer(msg, 'utf8');
|
||||
var a = sha256(Buffer.concat([ ikey, m ]));
|
||||
var b = sha256(Buffer.concat([ okey, a ]));
|
||||
return b;
|
||||
};
|
||||
|
||||
module.exports = Armory;
|
12
lib/Curve.js
12
lib/Curve.js
|
@ -4,14 +4,20 @@ var bignum = imports.bignum || require('bignum');
|
|||
var Point = imports.Point || require('./Point');
|
||||
|
||||
var n = bignum.fromBuffer(new Buffer("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141", 'hex'), {size: 32});
|
||||
var G = new Point(bignum.fromBuffer(new Buffer("79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798", 'hex'), {size: 32}),
|
||||
bignum.fromBuffer(new Buffer("483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8", 'hex'), {size: 32}));
|
||||
|
||||
/* secp256k1 curve */
|
||||
|
||||
var Curve = function() {
|
||||
};
|
||||
|
||||
/* secp256k1 curve */
|
||||
var G;
|
||||
Curve.getG = function() {
|
||||
// don't use Point in top scope, causes exception in browser
|
||||
// when Point is not loaded yet
|
||||
|
||||
// use cached version if available
|
||||
G = G || new Point(bignum.fromBuffer(new Buffer("79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798", 'hex'), {size: 32}),
|
||||
bignum.fromBuffer(new Buffer("483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8", 'hex'), {size: 32}));
|
||||
return G;
|
||||
};
|
||||
|
||||
|
|
|
@ -19,6 +19,12 @@ Point.add = function(p1, p2) {
|
|||
return Point.fromUncompressedPubKey(pubKey);
|
||||
};
|
||||
|
||||
Point.multiply = function(p1, x) {
|
||||
var u1 = p1.toUncompressedPubKey();
|
||||
var pubKey = CPPKey.multiplyUncompressed(u1, x);
|
||||
return Point.fromUncompressedPubKey(pubKey);
|
||||
};
|
||||
|
||||
//convert the public key of a Key into a Point
|
||||
Point.fromUncompressedPubKey = function(pubkey) {
|
||||
var point = new Point();
|
||||
|
|
|
@ -48,6 +48,32 @@ Point.add = function(p1, p2) {
|
|||
return point;
|
||||
};
|
||||
|
||||
Point.multiply = function(p1, x) {
|
||||
var x = new BigInteger(x.toString('hex'), 16);
|
||||
|
||||
var ecparams = getSECCurveByName('secp256k1');
|
||||
|
||||
var p1xhex = p1.x.toBuffer({size: 32}).toString('hex');
|
||||
var p1x = new BigInteger(p1xhex, 16);
|
||||
var p1yhex = p1.y.toBuffer({size: 32}).toString('hex');
|
||||
var p1y = new BigInteger(p1yhex, 16);
|
||||
var p1px = new ECFieldElementFp(ecparams.getCurve().getQ(), p1x);
|
||||
var p1py = new ECFieldElementFp(ecparams.getCurve().getQ(), p1y);
|
||||
var p1p = new ECPointFp(ecparams.getCurve(), p1px, p1py);
|
||||
|
||||
var p = p1p.multiply(x);
|
||||
|
||||
var point = new Point();
|
||||
var pointxbuf = new Buffer(p.getX().toBigInteger().toByteArrayUnsigned());
|
||||
point.x = bignum.fromBuffer(pointxbuf, {size: pointxbuf.length});
|
||||
assert(pointxbuf.length <= 32);
|
||||
var pointybuf = new Buffer(p.getY().toBigInteger().toByteArrayUnsigned());
|
||||
assert(pointybuf.length <= 32);
|
||||
point.y = bignum.fromBuffer(pointybuf, {size: pointybuf.length});
|
||||
|
||||
return point;
|
||||
};
|
||||
|
||||
//convert the public key of a Key into a Point
|
||||
Point.fromUncompressedPubKey = function(pubkey) {
|
||||
var point = new Point();
|
||||
|
|
71
src/eckey.cc
71
src/eckey.cc
|
@ -124,6 +124,7 @@ void Key::Init(Handle<Object> target)
|
|||
NODE_SET_METHOD(s_ct->GetFunction(), "generateSync", GenerateSync);
|
||||
NODE_SET_METHOD(s_ct->GetFunction(), "fromDER", FromDER);
|
||||
NODE_SET_METHOD(s_ct->GetFunction(), "addUncompressed", AddUncompressed);
|
||||
NODE_SET_METHOD(s_ct->GetFunction(), "multiplyUncompressed", MultiplyUncompressed);
|
||||
|
||||
target->Set(String::NewSymbol("Key"),
|
||||
s_ct->GetFunction());
|
||||
|
@ -486,6 +487,76 @@ Key::AddUncompressed(const Arguments& args)
|
|||
return scope.Close(rbuf->handle_);
|
||||
}
|
||||
|
||||
Handle<Value>
|
||||
Key::MultiplyUncompressed(const Arguments& args)
|
||||
{
|
||||
HandleScope scope;
|
||||
|
||||
if (args.Length() != 2) {
|
||||
return VException("Two arguments expected: point0, x");
|
||||
}
|
||||
if (!Buffer::HasInstance(args[0])) {
|
||||
return VException("Argument 'point0' must be of type Buffer");
|
||||
}
|
||||
if (Buffer::Length(args[0]) != 65) {
|
||||
return VException("Argument 'point0' must have length 65");
|
||||
}
|
||||
if (!Buffer::HasInstance(args[1])) {
|
||||
return VException("Argument 'x' must be of type Buffer");
|
||||
}
|
||||
if (Buffer::Length(args[1]) != 32) {
|
||||
return VException("Argument 'x' must have length 32");
|
||||
}
|
||||
|
||||
Handle<Object> point0_buf = args[0]->ToObject();
|
||||
unsigned char *point0 = (unsigned char*) Buffer::Data(point0_buf);
|
||||
|
||||
Handle<Object> x_buf = args[1]->ToObject();
|
||||
unsigned char *xval = (unsigned char*) Buffer::Data(x_buf);
|
||||
|
||||
EC_KEY *eckey = EC_KEY_new_by_curve_name(NID_secp256k1);
|
||||
const EC_GROUP *group = EC_KEY_get0_group(eckey);
|
||||
|
||||
BN_CTX *ctx;
|
||||
EC_POINT *p0, *r;
|
||||
BIGNUM *p0x, *p0y, *x, *rx, *ry;
|
||||
Buffer *rbuf;
|
||||
|
||||
p0 = EC_POINT_new(group);
|
||||
r = EC_POINT_new(group);
|
||||
|
||||
p0x = BN_bin2bn(&point0[1], 32, BN_new());
|
||||
p0y = BN_bin2bn(&point0[33], 32, BN_new());
|
||||
x = BN_bin2bn(&xval[0], 32, BN_new());
|
||||
|
||||
ctx = BN_CTX_new();
|
||||
|
||||
EC_POINT_set_affine_coordinates_GFp(group, p0, p0x, p0y, ctx);
|
||||
|
||||
EC_POINT_mul(group, r, NULL, p0, x, ctx);
|
||||
|
||||
rx = BN_new();
|
||||
ry = BN_new();
|
||||
EC_POINT_get_affine_coordinates_GFp(group, r, rx, ry, ctx);
|
||||
|
||||
rbuf = Buffer::New(65);
|
||||
EC_POINT_point2oct(group, r, POINT_CONVERSION_UNCOMPRESSED, (unsigned char *)Buffer::Data(rbuf), 65, ctx);
|
||||
|
||||
//free: eckey, p0, p1, r, p0x, p0y, p1x, p1y, ctx, rx, ry, /*rbuf,*/ rcx, rcy
|
||||
BN_clear_free(ry);
|
||||
BN_clear_free(rx);
|
||||
//do not free rbuf - this is returned
|
||||
BN_CTX_free(ctx);
|
||||
BN_clear_free(p0x);
|
||||
BN_clear_free(p0y);
|
||||
BN_clear_free(x);
|
||||
EC_POINT_free(r);
|
||||
EC_POINT_free(p0);
|
||||
EC_KEY_free(eckey);
|
||||
|
||||
return scope.Close(rbuf->handle_);
|
||||
}
|
||||
|
||||
Handle<Value>
|
||||
Key::VerifySignature(const Arguments& args)
|
||||
{
|
||||
|
|
|
@ -89,6 +89,9 @@ public:
|
|||
static Handle<Value>
|
||||
AddUncompressed(const Arguments& args);
|
||||
|
||||
static Handle<Value>
|
||||
MultiplyUncompressed(const Arguments& args);
|
||||
|
||||
static Handle<Value>
|
||||
VerifySignature(const Arguments& args);
|
||||
|
||||
|
|
|
@ -0,0 +1,92 @@
|
|||
'use strict';
|
||||
|
||||
var chai = chai || require('chai');
|
||||
var bitcore = bitcore || require('../bitcore');
|
||||
|
||||
var should = chai.should();
|
||||
|
||||
var Armory = bitcore.Armory;
|
||||
var Address = bitcore.Address;
|
||||
|
||||
/**
|
||||
* This is the Armory root code that was used to generated the hard coded values in
|
||||
* those tests:
|
||||
*/
|
||||
|
||||
var seed = [
|
||||
'aagh hjfj sihk ietj giik wwai awtd uodh hnji',
|
||||
'soss uaku egod utai itos fijj ihgi jhau jtoo'
|
||||
].join('\n');
|
||||
|
||||
/*
|
||||
* It was retrieved by creating a wallet in Armory and creating a paper backup.
|
||||
*
|
||||
* This is the public key as presented on the generated Armory paper wallets:
|
||||
*/
|
||||
|
||||
var PublicX = '9df5 23e7 18b9 1f59 a790 2d46 999f 9357 ccf8 7208 24d4 3076 4516 b809 f7ab ce4e';
|
||||
var PublicY = '66ba 5d21 4682 0dae 401d 9506 8437 2516 79f9 0c56 4186 cc50 07df c6d0 6989 1ff4';
|
||||
var pubkey = '04' + PublicX.split(' ').join('') + PublicY.split(' ').join('');
|
||||
|
||||
/*
|
||||
* This chain code was derived from the seed above:
|
||||
*/
|
||||
var chaincode = '84ac14bc4b388b33da099a0b4ee3b507284d99e1476639e36e5ca5e6af86481e';
|
||||
|
||||
/*
|
||||
* This is some addresses generated from the wallet:
|
||||
*/
|
||||
var address = [
|
||||
'1PUzLkds8eHGjHPaW7v7h23bzmHjrRMVqz',
|
||||
'1CGrip2uQUwhP2f3ARfbcrmtdwvWzELRmj',
|
||||
'1BfBauMP4PX1ZBYrqH4K4R8KWrFfskrs7E',
|
||||
'15emDCBVgBJLDP5cKxuwZ4Q77sfqEcwZvC',
|
||||
'16tDJhMYBv1szZgRZCohWrzEvzX2bG7vEQ'
|
||||
];
|
||||
|
||||
var instance, fromseed, first;
|
||||
|
||||
describe('Armory', function() {
|
||||
it('should initialze the main object', function() {
|
||||
should.exist(Armory);
|
||||
});
|
||||
|
||||
it('should be able to create instance from chaincode, pubkey', function() {
|
||||
instance = new Armory(chaincode, pubkey);
|
||||
should.exist(instance);
|
||||
});
|
||||
|
||||
it('should be able to create instance from seed', function() {
|
||||
fromseed = Armory.fromSeed(seed);
|
||||
should.exist(fromseed);
|
||||
});
|
||||
|
||||
it('fromseed should generate the expected chain code', function() {
|
||||
should.equal(fromseed.chaincode.toString('hex'), chaincode.toString('hex'));
|
||||
should.equal(fromseed.chaincode.toString('hex'), instance.chaincode.toString('hex'));
|
||||
});
|
||||
|
||||
it('fromseed should be able to generate the first public key', function() {
|
||||
first = fromseed.next();
|
||||
should.exist(first);
|
||||
});
|
||||
|
||||
it('instance created from chaincode,pubkey and the first instance generated by fromseed should match', function() {
|
||||
should.equal(first.pubkey.toString('hex'), instance.pubkey.toString('hex'));
|
||||
should.equal(first.chaincode.toString('hex'), instance.chaincode.toString('hex'));
|
||||
});
|
||||
|
||||
it('armory should generate the expected addresses for the given chaincode,pubkey', function() {
|
||||
var addr, a;
|
||||
a = instance;
|
||||
for (var i = 0; i < address.length; i++) {
|
||||
should.equal(Address.fromPubKey(a.pubkey).as('base58'), address[i]);
|
||||
a = a.next();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
|
Loading…
Reference in New Issue