diff --git a/bitcore.js b/bitcore.js index c1f595a..66de69d 100644 --- a/bitcore.js +++ b/bitcore.js @@ -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; diff --git a/browser/build.js b/browser/build.js index 46cbf07..e900bf8 100644 --- a/browser/build.js +++ b/browser/build.js @@ -24,6 +24,7 @@ var pack = function (params) { var modules = [ 'lib/Address', + 'lib/Armory', 'lib/Base58', 'lib/BIP32', 'lib/Block', diff --git a/examples/Armory.js b/examples/Armory.js new file mode 100644 index 0000000..713cd8b --- /dev/null +++ b/examples/Armory.js @@ -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(); +} + diff --git a/examples/browser/armory.html b/examples/browser/armory.html new file mode 100644 index 0000000..4de54d5 --- /dev/null +++ b/examples/browser/armory.html @@ -0,0 +1,58 @@ + + + + + + + + Enter you paper wallet seed:
+ +
+ +
+

+    
+  
+
diff --git a/lib/Armory.js b/lib/Armory.js
new file mode 100644
index 0000000..94be0e5
--- /dev/null
+++ b/lib/Armory.js
@@ -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;
diff --git a/lib/Curve.js b/lib/Curve.js
index f2d140d..dfdef52 100644
--- a/lib/Curve.js
+++ b/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;
 };
 
diff --git a/lib/Point.js b/lib/Point.js
index 64d4dd7..3864b80 100644
--- a/lib/Point.js
+++ b/lib/Point.js
@@ -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();
diff --git a/lib/browser/Point.js b/lib/browser/Point.js
index 1e935e0..2579d12 100644
--- a/lib/browser/Point.js
+++ b/lib/browser/Point.js
@@ -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();
diff --git a/src/eckey.cc b/src/eckey.cc
index 8f279a6..02329f9 100644
--- a/src/eckey.cc
+++ b/src/eckey.cc
@@ -124,6 +124,7 @@ void Key::Init(Handle 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
+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 point0_buf = args[0]->ToObject();
+  unsigned char *point0 = (unsigned char*) Buffer::Data(point0_buf);
+
+  Handle 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
 Key::VerifySignature(const Arguments& args)
 {
diff --git a/src/eckey.h b/src/eckey.h
index 1c2b922..df447ba 100644
--- a/src/eckey.h
+++ b/src/eckey.h
@@ -89,6 +89,9 @@ public:
   static Handle
     AddUncompressed(const Arguments& args);
 
+  static Handle
+    MultiplyUncompressed(const Arguments& args);
+
   static Handle
     VerifySignature(const Arguments& args);
 
diff --git a/test/test.Armory.js b/test/test.Armory.js
new file mode 100644
index 0000000..8be6fe3
--- /dev/null
+++ b/test/test.Armory.js
@@ -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();
+    }
+  });
+});
+
+
+
+
+