From 543c42a6a8b42e05c990aed7b06647cc80acead5 Mon Sep 17 00:00:00 2001 From: "Ryan X. Charles" Date: Thu, 26 Jun 2014 08:49:22 -0700 Subject: [PATCH 1/3] add authentication via new Message core class --- js/models/core/Message.js | 82 +++++++++++++++++++++++++++++++ js/models/core/PrivateKey.js | 8 ++++ js/models/core/Wallet.js | 1 + js/models/core/WalletFactory.js | 3 +- js/models/network/WebRTC.js | 35 ++++++++++---- test/index.html | 1 + test/test.Message.js | 85 +++++++++++++++++++++++++++++++++ test/test.network.WebRTC.js | 31 +++++++----- util/build.js | 3 ++ 9 files changed, 226 insertions(+), 23 deletions(-) create mode 100644 js/models/core/Message.js create mode 100644 test/test.Message.js 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 0bfa3dcf4..eb84b1f8c 100644 --- a/js/models/core/Wallet.js +++ b/js/models/core/Wallet.js @@ -279,6 +279,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 03d59e2ea..b7ccc60e0 100644 --- a/js/models/core/WalletFactory.js +++ b/js/models/core/WalletFactory.js @@ -222,7 +222,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..e56134c14 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,30 @@ Network.prototype._addConnectedCopayer = function(copayerId, isInbound) { this.emit('connect', copayerId); }; +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(enchex, isInbound, peerId) { var sig, payload; var encUint8Array = new Uint8Array(enchex); 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(); + payload = JSON.parse(datastr); } catch (e) { this._deletePeer(peerId); return; @@ -349,18 +365,19 @@ 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) { + console.log('payload: ' + payload); var peerId = this.peerFromCopayer(copayerId); if (peerId !== this.peerId) { var dataConn = this.connections[peerId]; @@ -391,7 +408,7 @@ 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); + var encPayload = self._encode(copayerIdBuf, self.getKey(), payloadBuf); self._sendToOne(copayerId, encPayload, sig, 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..cd71458ca --- /dev/null +++ b/test/test.Message.js @@ -0,0 +1,85 @@ +'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() { + var message = new Buffer('message'); + var messagehex = message.toString('hex'); + var encoded = Message.encode(key2.public, key, message); + + it('should decode an encoded message', function() { + var decoded = Message.decode(key2, encoded); + decoded.toString('hex').should.equal(messagehex); + }); + + }); + + 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..8297762bf 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'); @@ -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'); @@ -118,7 +122,7 @@ describe('Network / WebRTC', function() { var copayerId = key.public.toString('hex'); n._sendToOne = function(a1, encPayload, a3, cb) { - encPayload.length.should.be.greaterThan(0); + encPayload.sig.length.should.be.greaterThan(0); cb(); }; var sig = undefined; @@ -130,6 +134,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'); 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 From 08a741d880ed8e825873221b4cf5cace4446708a Mon Sep 17 00:00:00 2001 From: "Ryan X. Charles" Date: Thu, 26 Jun 2014 13:58:45 -0700 Subject: [PATCH 2/3] confirm peerId matches claimed pubkey to prevent MITM attacks --- js/models/network/WebRTC.js | 20 ++++++---- test/test.Message.js | 26 +++++++++++-- test/test.network.WebRTC.js | 75 +++++++++++++++++++++++++++++++++++-- 3 files changed, 106 insertions(+), 15 deletions(-) diff --git a/js/models/network/WebRTC.js b/js/models/network/WebRTC.js index e56134c14..86f86d26e 100644 --- a/js/models/network/WebRTC.js +++ b/js/models/network/WebRTC.js @@ -163,9 +163,8 @@ Network.prototype.getKey = function() { return this.key; }; -Network.prototype._onData = function(enchex, isInbound, peerId) { - var sig, payload; - var encUint8Array = new Uint8Array(enchex); +Network.prototype._onData = function(enc, isInbound, peerId) { + var encUint8Array = new Uint8Array(enc); var encbuf = new Buffer(encUint8Array); var encstr = encbuf.toString(); @@ -176,7 +175,7 @@ Network.prototype._onData = function(enchex, isInbound, peerId) { var encoded = JSON.parse(encstr); var databuf = this._decode(key, encoded); var datastr = databuf.toString(); - payload = JSON.parse(datastr); + var payload = JSON.parse(datastr); } catch (e) { this._deletePeer(peerId); return; @@ -185,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; @@ -376,8 +382,7 @@ Network.prototype._decode = function(key, encoded) { return payload; }; -Network.prototype._sendToOne = function(copayerId, payload, sig, cb) { - console.log('payload: ' + payload); +Network.prototype._sendToOne = function(copayerId, payload, cb) { var peerId = this.peerFromCopayer(copayerId); if (peerId !== this.peerId) { var dataConn = this.connections[peerId]; @@ -400,7 +405,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); @@ -409,7 +413,7 @@ Network.prototype.send = function(copayerIds, payload, cb) { copayerIds.forEach(function(copayerId) { var copayerIdBuf = new Buffer(copayerId, 'hex'); var encPayload = self._encode(copayerIdBuf, self.getKey(), payloadBuf); - self._sendToOne(copayerId, encPayload, sig, function() { + self._sendToOne(copayerId, encPayload, function() { if (++i === l && typeof cb === 'function') cb(); }); }); diff --git a/test/test.Message.js b/test/test.Message.js index cd71458ca..6abe1e3e3 100644 --- a/test/test.Message.js +++ b/test/test.Message.js @@ -29,15 +29,33 @@ describe('Message model', function() { }); describe('#decode', function() { - var message = new Buffer('message'); - var messagehex = message.toString('hex'); - var encoded = Message.encode(key2.public, key, message); - + 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() { diff --git a/test/test.network.WebRTC.js b/test/test.network.WebRTC.js index 8297762bf..a818937e7 100644 --- a/test/test.network.WebRTC.js +++ b/test/test.network.WebRTC.js @@ -98,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; @@ -121,7 +121,7 @@ describe('Network / WebRTC', function() { key.regenerateSync(); var copayerId = key.public.toString('hex'); - n._sendToOne = function(a1, encPayload, a3, cb) { + n._sendToOne = function(a1, encPayload, cb) { encPayload.sig.length.should.be.greaterThan(0); cb(); }; @@ -145,7 +145,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; @@ -156,4 +156,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'); + }); + + }); + }); From c0b95a58793cbb1f980f573f844feb71613d2031 Mon Sep 17 00:00:00 2001 From: "Ryan X. Charles" Date: Thu, 26 Jun 2014 14:26:58 -0700 Subject: [PATCH 3/3] fix encoding mistake - payload should be buffer-like --- js/models/network/WebRTC.js | 5 ++++- test/test.network.WebRTC.js | 3 ++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/js/models/network/WebRTC.js b/js/models/network/WebRTC.js index 86f86d26e..bfd2ad10a 100644 --- a/js/models/network/WebRTC.js +++ b/js/models/network/WebRTC.js @@ -383,6 +383,8 @@ Network.prototype._decode = function(key, encoded) { }; 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]; @@ -413,7 +415,8 @@ Network.prototype.send = function(copayerIds, payload, cb) { copayerIds.forEach(function(copayerId) { var copayerIdBuf = new Buffer(copayerId, 'hex'); var encPayload = self._encode(copayerIdBuf, self.getKey(), payloadBuf); - self._sendToOne(copayerId, encPayload, function() { + var enc = new Buffer(JSON.stringify(encPayload)); + self._sendToOne(copayerId, enc, function() { if (++i === l && typeof cb === 'function') cb(); }); }); diff --git a/test/test.network.WebRTC.js b/test/test.network.WebRTC.js index a818937e7..451773e37 100644 --- a/test/test.network.WebRTC.js +++ b/test/test.network.WebRTC.js @@ -121,7 +121,8 @@ describe('Network / WebRTC', function() { key.regenerateSync(); var copayerId = key.public.toString('hex'); - n._sendToOne = function(a1, encPayload, cb) { + n._sendToOne = function(a1, enc, cb) { + var encPayload = JSON.parse(enc.toString()); encPayload.sig.length.should.be.greaterThan(0); cb(); };