Merge pull request #320 from olalonde/armory

Armory style key derivation from (chain code, public key)
This commit is contained in:
Ryan X. Charles 2014-05-07 20:52:19 -04:00
commit 982548f3e3
11 changed files with 448 additions and 3 deletions

View File

@ -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;

View File

@ -24,6 +24,7 @@ var pack = function (params) {
var modules = [
'lib/Address',
'lib/Armory',
'lib/Base58',
'lib/BIP32',
'lib/Block',

66
examples/Armory.js Normal file
View File

@ -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();
}

View File

@ -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>

115
lib/Armory.js Normal file
View File

@ -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;

View File

@ -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;
};

View File

@ -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();

View File

@ -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();

View File

@ -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)
{

View File

@ -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);

92
test/test.Armory.js Normal file
View File

@ -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();
}
});
});