add nonce support to WebRTC and Wallet

Each person keeps track of their own nonce, and the nonces of the other
copayers. The nonce is iterated for each message. If a person ever doesn't
iterate their nonce, that message is discarded by the copayers.

The nonces are saved as networkNonce (your nonce) and networkNonces (the nonces
of your copayers) in the wallet file.

In order to support restoring old wallets, the first four bytes of the 8 byte
nonce are actually the current time in seconds. Thus you can restore an old
wallet, because certainly at least one second has passed since your last
message. Only if you try to restore an old wallet within 1 second from the time
of your last message will you have a problem (or if your system clock is
grossly inaccurate).
This commit is contained in:
Ryan X. Charles 2014-07-08 23:03:30 -07:00
commit 88ab38eb00
6 changed files with 370 additions and 23 deletions

View file

@ -50,6 +50,11 @@ function Wallet(opts) {
this.registeredPeerIds = [];
this.addressBook = opts.addressBook || {};
this.publicKey = this.privateKey.publicHex;
//network nonces are 8 byte buffers, representing a big endian number
//one nonce for oneself, and then one nonce for each copayer
this.network.setHexNonce(opts.networkNonce);
this.network.setHexNonces(opts.networkNonces);
}
Wallet.parent = EventEmitter;
@ -343,8 +348,14 @@ Wallet.prototype.store = function() {
Wallet.prototype.toObj = function() {
var optsObj = this._optsToObj();
var networkNonce = this.network.getHexNonce();
var networkNonces = this.network.getHexNonces();
var walletObj = {
opts: optsObj,
networkNonce: networkNonce, //yours
networkNonces: networkNonces, //copayers
publicKeyRing: this.publicKeyRing.toObj(),
txProposals: this.txProposals.toObj(),
privateKey: this.privateKey ? this.privateKey.toObj() : undefined,
@ -374,6 +385,10 @@ Wallet.prototype.toEncryptedObj = function() {
return this.storage.export(walletObj);
};
Wallet.prototype.send = function(recipients, obj) {
this.network.send(recipients, obj);
};
Wallet.prototype.sendAllTxProposals = function(recipients) {
var ntxids = this.txProposals.getNtxids();
for (var i in ntxids) {
@ -386,7 +401,7 @@ Wallet.prototype.sendTxProposal = function(ntxid, recipients) {
preconditions.checkArgument(ntxid);
preconditions.checkState(this.txProposals.txps[ntxid]);
this.log('### SENDING txProposal ' + ntxid + ' TO:', recipients || 'All', this.txProposals);
this.network.send(recipients, {
this.send(recipients, {
type: 'txProposal',
txProposal: this.txProposals.txps[ntxid].toObj(),
walletId: this.id,
@ -396,7 +411,7 @@ Wallet.prototype.sendTxProposal = function(ntxid, recipients) {
Wallet.prototype.sendWalletReady = function(recipients) {
this.log('### SENDING WalletReady TO:', recipients);
this.network.send(recipients, {
this.send(recipients, {
type: 'walletReady',
walletId: this.id,
});
@ -405,7 +420,7 @@ Wallet.prototype.sendWalletReady = function(recipients) {
Wallet.prototype.sendWalletId = function(recipients) {
this.log('### SENDING walletId TO:', recipients || 'All', this.id);
this.network.send(recipients, {
this.send(recipients, {
type: 'walletId',
walletId: this.id,
opts: this._optsToObj(),
@ -419,7 +434,7 @@ Wallet.prototype.sendPublicKeyRing = function(recipients) {
var publicKeyRing = this.publicKeyRing.toObj();
delete publicKeyRing.publicKeysCache; // exclude publicKeysCache from network obj
this.network.send(recipients, {
this.send(recipients, {
type: 'publicKeyRing',
publicKeyRing: publicKeyRing,
walletId: this.id,
@ -429,7 +444,7 @@ Wallet.prototype.sendIndexes = function(recipients) {
var indexes = AddressIndex.serialize(this.publicKeyRing.indexes);
this.log('### INDEXES TO:', recipients || 'All', indexes);
this.network.send(recipients, {
this.send(recipients, {
type: 'indexes',
indexes: indexes,
walletId: this.id,
@ -438,7 +453,7 @@ Wallet.prototype.sendIndexes = function(recipients) {
Wallet.prototype.sendAddressBook = function(recipients) {
this.log('### SENDING addressBook TO:', recipients || 'All', this.addressBook);
this.network.send(recipients, {
this.send(recipients, {
type: 'addressbook',
addressBook: this.addressBook,
walletId: this.id,

View file

@ -163,6 +163,57 @@ Network.prototype.getKey = function() {
return this.key;
};
//hex version of one's own nonce
Network.prototype.setHexNonce = function(networkNonce) {
if (networkNonce) {
if (networkNonce.length !== 16)
throw new Error('incorrect length of hex nonce');
this.networkNonce = new Buffer(networkNonce, 'hex');
}
else
this.iterateNonce();
};
//hex version of copayers' nonces
Network.prototype.setHexNonces = function(networkNonces) {
for (var i in networkNonces) {
if (!this.networkNonces)
this.networkNonces = {};
if (networkNonces[i].length === 16)
this.networkNonces[i] = new Buffer(networkNonces[i], 'hex');
}
};
//for oneself
Network.prototype.getHexNonce = function() {
return this.networkNonce.toString('hex');
};
//for copayers
Network.prototype.getHexNonces = function() {
var networkNoncesHex = [];
for (var i in this.networkNonces) {
networkNoncesHex[i] = this.networkNonces[i].toString('hex');
}
return networkNoncesHex;
};
Network.prototype.iterateNonce = function() {
if (!this.networkNonce || this.networkNonce.length !== 8) {
this.networkNonce = new Buffer(8);
this.networkNonce.fill(0);
}
//the first 4 bytes of a nonce is a unix timestamp in seconds
//the second 4 bytes is just an iterated "sub" nonce
//the whole thing is interpreted as one big endian number
var noncep1 = this.networkNonce.slice(0, 4);
noncep1.writeUInt32BE(Math.floor(Date.now()/1000), 0);
var noncep2uint = this.networkNonce.slice(4, 8).readUInt32BE(0);
var noncep2 = this.networkNonce.slice(4, 8);
noncep2.writeUInt32BE(noncep2uint + 1, 0);
return this.networkNonce;
};
Network.prototype._onData = function(enc, isInbound, peerId) {
var encUint8Array = new Uint8Array(enc);
var encbuf = new Buffer(encUint8Array);
@ -173,7 +224,16 @@ Network.prototype._onData = function(enc, isInbound, peerId) {
try {
var encoded = JSON.parse(encstr);
var databuf = this._decode(key, encoded);
var prevnonce = this.networkNonces ? this.networkNonces[peerId] : undefined;
var opts = {prevnonce: prevnonce};
var decoded = this._decode(key, encoded, opts);
//if no error thrown in the last step, we can set the copayer's nonce
if (!this.networkNonces)
this.networkNonces = {};
this.networkNonces[peerId] = decoded.nonce;
var databuf = decoded.payload;
var datastr = databuf.toString();
var payload = JSON.parse(datastr);
} catch (e) {
@ -384,13 +444,13 @@ Network.prototype._encode = function(topubkey, fromkey, payload, opts) {
Network.prototype._decode = function(key, encoded, opts) {
var decoded = Message.decode(key, encoded, opts);
var payload = decoded.payload;
return payload;
return decoded;
};
Network.prototype._sendToOne = function(copayerId, payload, opts, 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];
@ -401,7 +461,7 @@ Network.prototype._sendToOne = function(copayerId, payload, opts, cb) {
if (typeof cb === 'function') cb();
};
Network.prototype.send = function(copayerIds, payload, opts, cb) {
Network.prototype.send = function(copayerIds, payload, cb) {
if (!payload) return cb();
var self = this;
@ -419,10 +479,12 @@ Network.prototype.send = function(copayerIds, payload, opts, cb) {
var l = copayerIds.length;
var i = 0;
copayerIds.forEach(function(copayerId) {
self.iterateNonce();
var opts = {nonce: self.networkNonce};
var copayerIdBuf = new Buffer(copayerId, 'hex');
var encPayload = self._encode(copayerIdBuf, self.getKey(), payloadBuf);
var encPayload = self._encode(copayerIdBuf, self.getKey(), payloadBuf, opts);
var enc = new Buffer(JSON.stringify(encPayload));
self._sendToOne(copayerId, enc, opts, function() {
self._sendToOne(copayerId, enc, function() {
if (++i === l && typeof cb === 'function') cb();
});
});