diff --git a/js/models/core/Message.js b/js/models/core/Message.js
new file mode 100644
index 000000000..b281eecd8
--- /dev/null
+++ b/js/models/core/Message.js
@@ -0,0 +1,82 @@
+'use strict';
+
+var imports = require('soop').imports();
+var bitcore = require('bitcore');
+
+/* Encrypted, authenticated messages to be shared between copayers */
+var Message = function() {
+};
+
+Message.encode = function(topubkey, fromkey, payload) {
+ var version = new Buffer([0]);
+ var toencrypt = Buffer.concat([version, payload]);
+ var encrypted = Message._encrypt(topubkey, toencrypt);
+ var sig = Message._sign(fromkey, encrypted);
+ var encoded = {
+ pubkey: fromkey.public.toString('hex'),
+ sig: sig.toString('hex'),
+ encrypted: encrypted.toString('hex')
+ };
+ return encoded;
+};
+
+Message.decode = function(key, encoded) {
+ try {
+ var frompubkey = new Buffer(encoded.pubkey, 'hex');
+ } catch (e) {
+ throw new Error('Error decoding public key: ' + e);
+ }
+
+ try {
+ var sig = new Buffer(encoded.sig, 'hex');
+ var encrypted = new Buffer(encoded.encrypted, 'hex');
+ } catch (e) {
+ throw new Error('Error decoding data: ' + e);
+ }
+
+ try {
+ var v = Message._verify(frompubkey, sig, encrypted);
+ } catch (e) {
+ throw new Error('Error verifying signature: ' + e);
+ }
+
+ if (!v)
+ throw new Error('Invalid signature');
+
+ try {
+ var decrypted = Message._decrypt(key.private, encrypted);
+ } catch (e) {
+ throw new Error('Cannot decrypt data: ' + e);
+ }
+
+ if (decrypted[0] !== 0)
+ throw new Error('Invalid version number');
+
+ if (decrypted.length === 0)
+ throw new Error('No data present');
+
+ var payload = decrypted.slice(1);
+ return payload;
+};
+
+Message._encrypt = function(topubkey, payload, r, iv) {
+ var encrypted = bitcore.ECIES.encrypt(topubkey, payload, r, iv);
+ return encrypted;
+};
+
+Message._decrypt = function(privkey, encrypted) {
+ var decrypted = bitcore.ECIES.decrypt(privkey, encrypted);
+ return decrypted;
+};
+
+Message._sign = function(key, payload) {
+ var sig = bitcore.Message.sign(payload, key);
+ return sig;
+};
+
+Message._verify = function(pubkey, signature, payload) {
+ var v = bitcore.Message.verifyWithPubKey(pubkey, payload, signature);
+ return v;
+};
+
+module.exports = require('soop')(Message);
diff --git a/js/models/core/PrivateKey.js b/js/models/core/PrivateKey.js
index ff8b70e79..b1149f28b 100644
--- a/js/models/core/PrivateKey.js
+++ b/js/models/core/PrivateKey.js
@@ -32,9 +32,17 @@ PrivateKey.prototype.getIdPriv = function() {
return this.idpriv;
};
+PrivateKey.prototype.getIdKey = function() {
+ if (!this.idkey) {
+ this.cacheId();
+ }
+ return this.idkey;
+};
+
PrivateKey.prototype.cacheId = function() {
var path = Structure.IdFullBranch;
var idhk = this.bip.derive(path);
+ this.idkey = idhk.eckey;
this.id = idhk.eckey.public.toString('hex');
this.idpriv = idhk.eckey.private.toString('hex');
};
diff --git a/js/models/core/Wallet.js b/js/models/core/Wallet.js
index 3ad5f5cd4..d34943848 100644
--- a/js/models/core/Wallet.js
+++ b/js/models/core/Wallet.js
@@ -280,6 +280,7 @@ Wallet.prototype.netStart = function(callback) {
var myId = self.getMyCopayerId();
var myIdPriv = self.getMyCopayerIdPriv();
+
var startOpts = {
copayerId: myId,
privkey: myIdPriv,
diff --git a/js/models/core/WalletFactory.js b/js/models/core/WalletFactory.js
index c1ffbfeb4..b593d3e17 100644
--- a/js/models/core/WalletFactory.js
+++ b/js/models/core/WalletFactory.js
@@ -224,7 +224,8 @@ WalletFactory.prototype.joinCreateSession = function(secret, nickname, passphras
this.log('\t### PrivateKey Initialized');
var opts = {
copayerId: privateKey.getId(),
- privkey: privateKey.getIdPriv()
+ privkey: privateKey.getIdPriv(),
+ key: privateKey.getIdKey()
};
self.network.cleanUp();
self.network.start(opts, function() {
diff --git a/js/models/network/WebRTC.js b/js/models/network/WebRTC.js
index 98e660a7a..bfd2ad10a 100644
--- a/js/models/network/WebRTC.js
+++ b/js/models/network/WebRTC.js
@@ -5,6 +5,7 @@ var EventEmitter = imports.EventEmitter || require('events').EventEmitter;
var bitcore = require('bitcore');
var util = bitcore.util;
var extend = require('util')._extend;
+var Message = require('../core/Message');
/*
* Emits
* 'connect'
@@ -45,6 +46,7 @@ Network.prototype.cleanUp = function() {
this.connectedPeers = [];
this.peerId = null;
this.privkey = null; //TODO: hide privkey in a closure
+ this.key = null;
this.copayerId = null;
this.signingKey = null;
this.allowedCopayerIds = null;
@@ -151,16 +153,29 @@ Network.prototype._addConnectedCopayer = function(copayerId, isInbound) {
this.emit('connect', copayerId);
};
-Network.prototype._onData = function(enchex, isInbound, peerId) {
- var sig, payload;
- var encUint8Array = new Uint8Array(enchex);
+Network.prototype.getKey = function() {
+ if (!this.key) {
+ var key = new bitcore.Key();
+ key.private = new Buffer(this.privkey, 'hex');
+ key.regenerateSync();
+ this.key = key;
+ }
+ return this.key;
+};
+
+Network.prototype._onData = function(enc, isInbound, peerId) {
+ var encUint8Array = new Uint8Array(enc);
var encbuf = new Buffer(encUint8Array);
+ var encstr = encbuf.toString();
var privkey = this.privkey;
+ var key = this.getKey();
try {
- var data = this._decrypt(privkey, encbuf);
- payload = JSON.parse(data);
+ var encoded = JSON.parse(encstr);
+ var databuf = this._decode(key, encoded);
+ var datastr = databuf.toString();
+ var payload = JSON.parse(datastr);
} catch (e) {
this._deletePeer(peerId);
return;
@@ -169,6 +184,13 @@ Network.prototype._onData = function(enchex, isInbound, peerId) {
if (isInbound && payload.type === 'hello') {
var payloadStr = JSON.stringify(payload);
+ //ensure claimed public key is actually the public key of the peer
+ //e.g., their public key should hash to be their peerId
+ if (peerId.toString() !== this.peerFromCopayer(payload.copayerId) || peerId.toString() !== this.peerFromCopayer(encoded.pubkey)) {
+ this._deletePeer(peerId, 'incorrect pubkey for peerId');
+ return;
+ }
+
if (this.allowedCopayerIds && !this.allowedCopayerIds[payload.copayerId]) {
this._deletePeer(peerId);
return;
@@ -349,18 +371,20 @@ Network.prototype.getPeer = function() {
return this.peer;
};
-Network.prototype._encrypt = function(pubkey, payload) {
- var encrypted = bitcore.ECIES.encrypt(pubkey, payload);
- return encrypted;
+Network.prototype._encode = function(topubkey, fromkey, payload) {
+ var encoded = Message.encode(topubkey, fromkey, payload);
+ return encoded;
};
-Network.prototype._decrypt = function(privkey, encrypted) {
- var decrypted = bitcore.ECIES.decrypt(privkey, encrypted);
- return decrypted;
+Network.prototype._decode = function(key, encoded) {
+ var payload = Message.decode(key, encoded);
+ return payload;
};
-Network.prototype._sendToOne = function(copayerId, payload, sig, cb) {
+Network.prototype._sendToOne = function(copayerId, payload, cb) {
+ if (!Buffer.isBuffer(payload))
+ throw new Error('payload must be a buffer');
var peerId = this.peerFromCopayer(copayerId);
if (peerId !== this.peerId) {
var dataConn = this.connections[peerId];
@@ -383,7 +407,6 @@ Network.prototype.send = function(copayerIds, payload, cb) {
if (typeof copayerIds === 'string')
copayerIds = [copayerIds];
- var sig;
var payloadStr = JSON.stringify(payload);
var payloadBuf = new Buffer(payloadStr);
@@ -391,8 +414,9 @@ Network.prototype.send = function(copayerIds, payload, cb) {
var i = 0;
copayerIds.forEach(function(copayerId) {
var copayerIdBuf = new Buffer(copayerId, 'hex');
- var encPayload = self._encrypt(copayerIdBuf, payloadBuf);
- self._sendToOne(copayerId, encPayload, sig, function() {
+ var encPayload = self._encode(copayerIdBuf, self.getKey(), payloadBuf);
+ var enc = new Buffer(JSON.stringify(encPayload));
+ self._sendToOne(copayerId, enc, function() {
if (++i === l && typeof cb === 'function') cb();
});
});
diff --git a/test/index.html b/test/index.html
index 127df85f3..120a98b2b 100644
--- a/test/index.html
+++ b/test/index.html
@@ -15,6 +15,7 @@
+
diff --git a/test/test.Message.js b/test/test.Message.js
new file mode 100644
index 000000000..6abe1e3e3
--- /dev/null
+++ b/test/test.Message.js
@@ -0,0 +1,103 @@
+'use strict';
+
+var chai = chai || require('chai');
+var should = chai.should();
+var sinon = require('sinon');
+var Message = require('../js/models/core/Message');
+var bitcore = bitcore || require('bitcore');
+var Buffer = bitcore.Buffer;
+
+describe('Message model', function() {
+ var key = new bitcore.Key();
+ key.private = bitcore.util.sha256(new Buffer('test'));
+ key.regenerateSync();
+
+ var key2 = new bitcore.Key();
+ key2.private = bitcore.util.sha256(new Buffer('test 2'));
+ key2.regenerateSync();
+
+ describe('#encode', function() {
+
+ it('should encode a message', function() {
+ var message = new Buffer('message');
+ var encoded = Message.encode(key2.public, key, message);
+ should.exist(encoded.pubkey);
+ should.exist(encoded.sig);
+ should.exist(encoded.encrypted);
+ });
+
+ });
+
+ describe('#decode', function() {
+
+ it('should decode an encoded message', function() {
+ var message = new Buffer('message');
+ var messagehex = message.toString('hex');
+ var encoded = Message.encode(key2.public, key, message);
+
+ var decoded = Message.decode(key2, encoded);
+ decoded.toString('hex').should.equal(messagehex);
+ });
+
+ it('should fail if the version number is incorrect', function() {
+ var payload = new Buffer('message');
+ var fromkey = key;
+ var topubkey = key2.public;
+ var version = new Buffer([1]);
+ var toencrypt = Buffer.concat([version, payload]);
+ var encrypted = Message._encrypt(topubkey, toencrypt);
+ var sig = Message._sign(fromkey, encrypted);
+ var encoded = {
+ pubkey: fromkey.public.toString('hex'),
+ sig: sig.toString('hex'),
+ encrypted: encrypted.toString('hex')
+ };
+
+ (function() {Message.decode(key2, encoded);}).should.throw('Invalid version number');
+ });
+
+ });
+
+ describe('#_encrypt', function() {
+
+ it('should encrypt data', function() {
+ var payload = new Buffer('payload');
+ var encrypted = Message._encrypt(key.public, payload);
+ encrypted.length.should.equal(129);
+ });
+
+ });
+
+ describe('#_decrypt', function() {
+ var payload = new Buffer('payload');
+ var payloadhex = payload.toString('hex');
+
+ it('should decrypt encrypted data', function() {
+ var encrypted = Message._encrypt(key.public, payload);
+ var decrypted = Message._decrypt(key.private, encrypted);
+ decrypted.toString('hex').should.equal(payloadhex);
+ });
+
+ });
+
+ describe('#_sign', function() {
+
+ it('should sign data', function() {
+ var payload = new Buffer('payload');
+ var sig = Message._sign(key, payload);
+ sig.length.should.be.greaterThan(60);
+ });
+
+ });
+
+ describe('#_verify', function() {
+ var payload = new Buffer('payload');
+ var sig = Message._sign(key, payload);
+
+ it('should verify signed data', function() {
+ Message._verify(key.public, sig, payload).should.equal(true);
+ });
+
+ });
+
+});
diff --git a/test/test.network.WebRTC.js b/test/test.network.WebRTC.js
index d880d006d..451773e37 100644
--- a/test/test.network.WebRTC.js
+++ b/test/test.network.WebRTC.js
@@ -46,26 +46,28 @@ describe('Network / WebRTC', function() {
});
- describe('#_encrypt', function() {
+ describe('#_encode', function() {
- it('should encrypt data successfully', function() {
+ it('should encode data successfully', function() {
var n = new WebRTC();
- var data = new bitcore.Buffer('my data to encrypt');
+ var data = new bitcore.Buffer('my data to encode');
var privkeystr = new bitcore.Buffer('test privkey');
var privkey = bitcore.util.sha256(privkeystr);
var key = new bitcore.Key();
key.private = privkey;
key.regenerateSync();
- var encrypted = n._encrypt(key.public, data);
- encrypted.length.should.not.equal(0);
- encrypted.length.should.equal(145);
+ var encoded = n._encode(key.public, key, data);
+ should.exist(encoded);
+ encoded.sig.length.should.not.equal(0);
+ encoded.pubkey.length.should.not.equal(0);
+ encoded.encrypted.length.should.not.equal(0);
});
});
- describe('#_decrypt', function() {
+ describe('#_decode', function() {
- it('should decrypt that which was encrypted', function() {
+ it('should decode that which was encoded', function() {
var n = new WebRTC();
var data = new bitcore.Buffer('my data to encrypt');
var privkeystr = new bitcore.Buffer('test privkey');
@@ -73,10 +75,10 @@ describe('Network / WebRTC', function() {
var key = new bitcore.Key();
key.private = privkey;
key.regenerateSync();
- var encrypted = n._encrypt(key.public, data);
- var decrypted = n._decrypt(key.private, encrypted);
- encrypted.length.should.not.equal(0);
- decrypted.toString().should.equal('my data to encrypt');
+ var encoded = n._encode(key.public, key, data);
+ var decoded = n._decode(key, encoded);
+ encoded.sig.should.not.equal(0);
+ decoded.toString().should.equal('my data to encrypt');
});
});
@@ -85,6 +87,7 @@ describe('Network / WebRTC', function() {
it('should call _sendToOne for a copayer', function(done) {
var n = new WebRTC();
+ n.privkey = bitcore.util.sha256('test');
var data = new bitcore.Buffer('my data to send');
@@ -95,7 +98,7 @@ describe('Network / WebRTC', function() {
key.regenerateSync();
var copayerId = key.public.toString('hex');
- n._sendToOne = function(a1, a2, a3, cb) {
+ n._sendToOne = function(a1, a2, cb) {
cb();
};
var sig = undefined;
@@ -107,6 +110,7 @@ describe('Network / WebRTC', function() {
it('should call _sendToOne with encrypted data for a copayer', function(done) {
var n = new WebRTC();
+ n.privkey = bitcore.util.sha256('test');
var data = new bitcore.Buffer('my data to send');
@@ -117,8 +121,9 @@ describe('Network / WebRTC', function() {
key.regenerateSync();
var copayerId = key.public.toString('hex');
- n._sendToOne = function(a1, encPayload, a3, cb) {
- encPayload.length.should.be.greaterThan(0);
+ n._sendToOne = function(a1, enc, cb) {
+ var encPayload = JSON.parse(enc.toString());
+ encPayload.sig.length.should.be.greaterThan(0);
cb();
};
var sig = undefined;
@@ -130,6 +135,7 @@ describe('Network / WebRTC', function() {
it('should call _sendToOne for a list of copayers', function(done) {
var n = new WebRTC();
+ n.privkey = bitcore.util.sha256('test');
var data = new bitcore.Buffer('my data to send');
@@ -140,7 +146,7 @@ describe('Network / WebRTC', function() {
key.regenerateSync();
var copayerIds = [key.public.toString('hex')];
- n._sendToOne = function(a1, a2, a3, cb) {
+ n._sendToOne = function(a1, a2, cb) {
cb();
};
var sig = undefined;
@@ -151,4 +157,73 @@ describe('Network / WebRTC', function() {
});
});
+ describe('#_onData', function() {
+ var privkey1 = bitcore.util.sha256('test privkey 1');
+ var privkey2 = bitcore.util.sha256('test privkey 2');
+ var privkey3 = bitcore.util.sha256('test privkey 2');
+
+ var key1 = new bitcore.Key();
+ key1.private = privkey1;
+ key1.regenerateSync();
+
+ var key2 = new bitcore.Key();
+ key2.private = privkey2;
+ key2.regenerateSync();
+
+ var key3 = new bitcore.Key();
+ key3.private = privkey3;
+ key3.regenerateSync();
+
+ it('should not reject data sent from a peer with hijacked pubkey', function() {
+ var n = new WebRTC();
+ n.privkey = key2.private.toString('hex');
+
+ var message = {
+ type: 'hello',
+ copayerId: key1.public.toString('hex')
+ };
+ var messagestr = JSON.stringify(message);
+ var messagebuf = new Buffer(messagestr);
+
+ var encoded = n._encode(key2.public, key1, messagebuf);
+ var encodedstr = JSON.stringify(encoded);
+ var encodeduint = new Buffer(encodedstr);
+
+ var isInbound = true;
+ var peerId = new bitcore.SIN(key1.public);
+
+ n._deletePeer = sinon.spy();
+
+ n._onData(encodeduint, isInbound, peerId);
+ n._deletePeer.calledOnce.should.equal(false);
+ });
+
+ it('should reject data sent from a peer with hijacked pubkey', function() {
+ var n = new WebRTC();
+ n.privkey = key2.private.toString('hex');
+
+ var message = {
+ type: 'hello',
+ copayerId: key3.public.toString('hex') //MITM pubkey 3
+ };
+ var messagestr = JSON.stringify(message);
+ var messagebuf = new Buffer(messagestr);
+
+ var encoded = n._encode(key2.public, key1, messagebuf);
+ var encodedstr = JSON.stringify(encoded);
+ var encodeduint = new Buffer(encodedstr);
+
+ var isInbound = true;
+ var peerId = new bitcore.SIN(key1.public);
+
+ n._deletePeer = sinon.spy();
+
+ n._onData(encodeduint, isInbound, peerId);
+ n._deletePeer.calledOnce.should.equal(true);
+ n._deletePeer.getCall(0).args[0].should.equal(peerId);
+ n._deletePeer.getCall(0).args[1].should.equal('incorrect pubkey for peerId');
+ });
+
+ });
+
});
diff --git a/util/build.js b/util/build.js
index 2af0ca892..e7cf06fa5 100755
--- a/util/build.js
+++ b/util/build.js
@@ -84,6 +84,9 @@ var createBundle = function(opts) {
b.require('./js/models/core/Passphrase', {
expose: '../js/models/core/Passphrase'
});
+ b.require('./js/models/core/Message', {
+ expose: '../js/models/core/Message'
+ });
if (opts.dontminify) {
//include dev dependencies