peer/bip151: rewrite bip151. handle bip151 on p2p network.

This commit is contained in:
Christopher Jeffrey 2016-07-21 09:24:09 -07:00
parent 233af72b7f
commit b455352708
No known key found for this signature in database
GPG Key ID: 8962AB9DE6666BBD
5 changed files with 318 additions and 185 deletions

View File

@ -19,9 +19,9 @@ var INFO_KEY1 = new Buffer('BitcoinK1', 'ascii');
var INFO_KEY2 = new Buffer('BitcoinK2', 'ascii');
var INFO_SID = new Buffer('BitcoinSessionID', 'ascii');
function BIP151(cipher, key) {
if (!(this instanceof BIP151))
return new BIP151(cipher, key);
function BIP151Stream(cipher, key) {
if (!(this instanceof BIP151Stream))
return new BIP151Stream(cipher, key);
EventEmitter.call(this);
@ -29,25 +29,21 @@ function BIP151(cipher, key) {
this.privateKey = key || bcoin.ec.generatePrivateKey();
this.cipher = cipher || 0;
this.secret = null;
this.prk = null;
this.k1 = null;
this.k2 = null;
this.sid = null;
assert(this.cipher === 0, 'Unknown cipher type.');
this.chacha = new chachapoly.ChaCha20();
this.aead = new chachapoly.AEAD();
this.prk = null;
this.tag = null;
this.seq = 0;
this.initReceived = false;
this.ackReceived = false;
this.initSent = false;
this.ackSent = false;
this.highWaterMark = 1024 * (1 << 20);
this.processed = 0;
this.lastRekey = 0;
this.timeout = null;
this.callback = null;
this.completed = false;
this.handshake = false;
this.pendingHeader = [];
this.pendingHeaderTotal = 0;
@ -57,9 +53,9 @@ function BIP151(cipher, key) {
this.waiting = 0;
}
utils.inherits(BIP151, EventEmitter);
utils.inherits(BIP151Stream, EventEmitter);
BIP151.prototype.init = function init(publicKey) {
BIP151Stream.prototype.init = function init(publicKey) {
var p = bcoin.writer();
this.publicKey = publicKey;
@ -82,14 +78,17 @@ BIP151.prototype.init = function init(publicKey) {
this.lastRekey = utils.ms();
};
BIP151.prototype.isReady = function isReady() {
return this.initSent
&& this.ackReceived
&& this.initReceived
&& this.ackSent;
BIP151Stream.prototype.maybeRekey = function maybeRekey(data) {
var self = this;
this.processed += data.length;
if (this.processed >= this.highWaterMark) {
this.processed -= this.highWaterMark;
this.rekey();
this.emit('rekey');
}
};
BIP151.prototype.rekey = function rekey() {
BIP151Stream.prototype.rekey = function rekey() {
assert(this.prk, 'Cannot rekey before initialization.');
this.k1 = utils.hash256(this.k1);
@ -104,193 +103,54 @@ BIP151.prototype.rekey = function rekey() {
this.lastRekey = utils.ms();
};
BIP151.prototype.sequence = function sequence() {
BIP151Stream.prototype.sequence = function sequence() {
this.seq++;
this.chacha.init(this.k1, this.iv());
this.aead.init(this.k2, this.iv());
this.aead.aad(this.sid);
};
BIP151.prototype.iv = function iv() {
BIP151Stream.prototype.iv = function iv() {
var p = bcoin.writer();
p.writeU64(this.seq);
p.writeU32(0);
return p.render();
};
BIP151.prototype.getPublicKey = function getPublicKey() {
BIP151Stream.prototype.getPublicKey = function getPublicKey() {
return bcoin.ec.publicKeyCreate(this.privateKey, true);
};
BIP151.prototype.encryptSize = function encryptSize(size) {
BIP151Stream.prototype.encryptSize = function encryptSize(size) {
var data = new Buffer(4);
data.writeUInt32LE(size, 0, true);
return this.chacha.encrypt(data);
};
BIP151.prototype.decryptSize = function decryptSize(data) {
BIP151Stream.prototype.decryptSize = function decryptSize(data) {
data = data.slice(0, 4);
this.chacha.encrypt(data);
return data.readUInt32LE(0, true);
};
BIP151.prototype.encrypt = function encrypt(data) {
BIP151Stream.prototype.encrypt = function encrypt(data) {
return this.aead.encrypt(data);
};
BIP151.prototype.decrypt = function decrypt(data) {
BIP151Stream.prototype.decrypt = function decrypt(data) {
return this.aead.decrypt(data);
};
BIP151.prototype.finish = function finish(data) {
BIP151Stream.prototype.finish = function finish(data) {
this.tag = this.aead.finish(data);
return this.tag;
};
BIP151.prototype.verify = function verify(tag) {
BIP151Stream.prototype.verify = function verify(tag) {
return chachapoly.Poly1305.verify(this.tag, tag);
};
BIP151.prototype.toEncinit = function toEncinit(writer) {
var p = bcoin.writer(writer);
p.writeBytes(this.getPublicKey());
p.writeU8(this.cipher);
if (!writer)
p = p.render();
this.initSent = true;
return p;
};
BIP151.prototype.encinit = function encinit(data) {
var p = bcoin.reader(data);
var publicKey = p.readBytes(33);
// this.cipher = p.readU8();
assert(p.readU8() === this.cipher, 'Wrong cipher type.');
assert(!this.initReceived, 'Already initialized.');
if (!this.ackReceived) {
this.init(publicKey);
} else {
assert(utils.equal(publicKey, this.publicKey),
'Bad pubkey.');
}
this.initReceived = true;
return this;
};
BIP151.fromEncinit = function fromEncinit(data) {
return new BIP151().encinit(data);
};
BIP151.prototype.toEncack = function toEncack(writer) {
var p = bcoin.writer(writer);
p.writeBytes(this.getPublicKey());
if (!writer)
p = p.render();
if (!this.ackSent) {
this.ackSent = true;
if (this.isReady()) {
this.handshake = true;
this.emit('handshake');
}
}
return p;
};
BIP151.prototype.toRekey = function toRekey(writer) {
var p = bcoin.writer(writer);
p.writeBytes(constants.ZERO_KEY);
if (!writer)
p = p.render();
return p;
};
BIP151.prototype.maybeRekey = function maybeRekey(data) {
var self = this;
this.processed += data.length;
if (this.processed >= this.highWaterMark) {
this.processed -= this.highWaterMark;
utils.nextTick(function() {
self.emit('rekey');
self.rekey();
});
}
};
BIP151.prototype.complete = function complete(err) {
assert(!this.completed, 'Already completed.');
assert(this.callback, 'No completion callback.');
this.completed = true;
clearTimeout(this.timeout);
this.timeout = null;
this.callback(err);
this.callback = null;
};
BIP151.prototype.wait = function wait(timeout, callback) {
var self = this;
assert(!this.handshake, 'Cannot wait for init after handshake.');
this.callback = callback;
this.timeout = setTimeout(function() {
self.complete(new Error('Timed out.'));
}, timeout);
this.once('handshake', function() {
self.complete();
});
};
BIP151.prototype.encack = function encack(data) {
var p = bcoin.reader(data);
var publicKey = p.readBytes(33);
assert(this.initSent, 'Unsolicited ACK.');
if (utils.equal(publicKey, constants.ZERO_KEY)) {
assert(this.ackReceived, 'No ACK before rekey.');
assert(this.handshake, 'No initialization before rekey.');
this.rekey();
return;
}
assert(!this.ackReceived, 'Already ACKed.');
this.ackReceived = true;
if (!this.initReceived) {
this.init(publicKey);
} else {
assert(utils.equal(publicKey, this.publicKey),
'Bad pubkey.');
}
if (this.isReady()) {
this.handshake = true;
this.emit('handshake');
}
};
BIP151.prototype.feed = function feed(data) {
BIP151Stream.prototype.feed = function feed(data) {
var chunk, payload, tag, p, cmd, body;
this.maybeRekey(data);
@ -371,7 +231,7 @@ BIP151.prototype.feed = function feed(data) {
};
// TODO: We could batch packets here!
BIP151.prototype.packet = function packet(cmd, body) {
BIP151Stream.prototype.packet = function packet(cmd, body) {
var p = bcoin.writer();
var payload, packet;
@ -393,4 +253,167 @@ BIP151.prototype.packet = function packet(cmd, body) {
return packet;
};
function BIP151(cipher) {
if (!(this instanceof BIP151))
return new BIP151(cipher);
EventEmitter.call(this);
this.input = new BIP151Stream(cipher);
this.output = null;
this.initReceived = false;
this.ackReceived = false;
this.initSent = false;
this.ackSent = false;
this.timeout = null;
this.callback = null;
this.completed = false;
this.handshake = false;
this._init();
}
utils.inherits(BIP151, EventEmitter);
BIP151.prototype._init = function _init() {
var self = this;
this.input.on('rekey', function() {
self.emit('rekey');
});
this.input.on('packet', function(cmd, body) {
self.emit('packet', cmd, body);
});
};
BIP151.prototype.isReady = function isReady() {
return this.initSent
&& this.ackReceived
&& this.initReceived
&& this.ackSent;
};
BIP151.prototype.toEncinit = function toEncinit(writer) {
var p = bcoin.writer(writer);
p.writeBytes(this.input.getPublicKey());
p.writeU8(this.input.cipher);
if (!writer)
p = p.render();
this.initSent = true;
return p;
};
BIP151.prototype.encack = function encack(data) {
var p = bcoin.reader(data);
var publicKey = p.readBytes(33);
assert(this.initSent, 'Unsolicited ACK.');
if (utils.equal(publicKey, constants.ZERO_KEY)) {
assert(this.handshake, 'No initialization before rekey.');
this.output.rekey();
return;
}
assert(!this.ackReceived, 'Already ACKed.');
this.ackReceived = true;
this.input.init(publicKey);
if (this.isReady()) {
this.handshake = true;
this.emit('handshake');
}
};
BIP151.prototype.encinit = function encinit(data) {
var p = bcoin.reader(data);
var publicKey = p.readBytes(33);
assert(!this.initReceived, 'Already initialized.');
this.output = new BIP151Stream(p.readU8());
this.output.init(publicKey);
this.initReceived = true;
return this;
};
BIP151.prototype.toEncack = function toEncack(writer) {
var p = bcoin.writer(writer);
assert(this.output, 'Cannot ack before init.');
p.writeBytes(this.output.getPublicKey());
if (!writer)
p = p.render();
if (!this.ackSent) {
this.ackSent = true;
if (this.isReady()) {
this.handshake = true;
this.emit('handshake');
}
}
return p;
};
BIP151.prototype.toRekey = function toRekey(writer) {
var p = bcoin.writer(writer);
p.writeBytes(constants.ZERO_KEY);
if (!writer)
p = p.render();
return p;
};
BIP151.prototype.complete = function complete(err) {
assert(!this.completed, 'Already completed.');
assert(this.callback, 'No completion callback.');
this.completed = true;
clearTimeout(this.timeout);
this.timeout = null;
this.callback(err);
this.callback = null;
};
BIP151.prototype.wait = function wait(timeout, callback) {
var self = this;
assert(!this.handshake, 'Cannot wait for init after handshake.');
this.callback = callback;
this.timeout = setTimeout(function() {
self.complete(new Error('BIP151 handshake timed out.'));
}, timeout);
this.once('handshake', function() {
self.complete();
});
};
BIP151.prototype.feed = function feed(data) {
return this.input.feed(data);
};
BIP151.prototype.packet = function packet(cmd, body) {
return this.output.packet(cmd, body);
};
module.exports = BIP151;

View File

@ -18,6 +18,7 @@ var VersionPacket = bcoin.packets.VersionPacket;
var GetBlocksPacket = bcoin.packets.GetBlocksPacket;
var RejectPacket = bcoin.packets.RejectPacket;
var NetworkAddress = bcoin.packets.NetworkAddress;
var Packet = bcoin.protocol.parser.Packet;
/**
* Represents a remote peer.
@ -109,6 +110,10 @@ function Peer(pool, options) {
this.compactMode = null;
this.compactBlocks = {};
this.sentAddr = false;
this.bip151 = null;
if (options.bip151)
this.bip151 = new bcoin.bip151();
this.challenge = null;
this.lastPong = -1;
@ -215,7 +220,10 @@ Peer.prototype._init = function init() {
});
this.socket.on('data', function(chunk) {
self.parser.feed(chunk);
if (self.bip151 && self.bip151.handshake)
self.bip151.feed(chunk);
else
self.parser.feed(chunk);
});
this.parser.on('packet', function(packet) {
@ -227,6 +235,16 @@ Peer.prototype._init = function init() {
self.reject(null, 'malformed', 'error parsing message', 10);
});
if (this.bip151) {
this.bip151.on('error', function(err) {
self.reject(null, 'malformed', 'error parsing message', 10);
self._error(err, true);
});
this.bip151.on('rekey', function() {
self.write(self.framer.encack(self.bip151.toRekey()));
});
}
if (this.connected) {
utils.nextTick(function() {
self._onConnect();
@ -253,6 +271,36 @@ Peer.prototype._onConnect = function _onConnect() {
this._connectTimeout = null;
}
// Send encinit. Wait for handshake to complete.
if (this.bip151 && !this.bip151.completed) {
this.logger.info('Attempting BIP151 handshake (%s).', this.hostname);
this.write(this.framer.encinit(this.bip151.toEncinit()));
this.bip151.wait(5000, function(err) {
if (err)
self.logger.error(err);
assert(self.bip151.completed);
self._onConnect();
});
return;
}
if (this.bip151 && this.bip151.handshake) {
this.logger.info('BIP151 handshake complete (%s).', this.hostname);
this.logger.info('Connection is encrypted (%s).', this.hostname);
this.bip151.on('packet', function(cmd, body) {
var packet = new Packet(cmd, body.length);
try {
packet.payload = self.parser.parsePayload(cmd, body);
} catch (e) {
return self.parser._error(e);
}
self.parser.emit('packet', packet);
});
this.framer.packet = function packet(cmd, body) {
return self.bip151.packet(cmd, body);
};
}
this.request('verack', function(err) {
self._onAck(err);
});
@ -766,6 +814,13 @@ Peer.prototype._onPacket = function onPacket(packet) {
var cmd = packet.cmd;
var payload = packet.payload;
if (this.bip151
&& !this.bip151.completed
&& cmd !== 'encinit'
&& cmd !== 'encack') {
this.bip151.complete(new Error('Message before handshake.'));
}
if (this.lastBlock && cmd !== 'tx')
this._flushMerkle();
@ -1819,6 +1874,18 @@ Peer.prototype._handleAlert = function _handleAlert(alert) {
*/
Peer.prototype._handleEncinit = function _handleEncinit(payload) {
if (!this.bip151)
return;
try {
this.bip151.encinit(payload);
} catch (e) {
this._error(e);
return;
}
this.write(this.framer.encack(this.bip151.toEncack()));
this.fire('encinit', payload);
};
@ -1829,6 +1896,16 @@ Peer.prototype._handleEncinit = function _handleEncinit(payload) {
*/
Peer.prototype._handleEncack = function _handleEncack(payload) {
if (!this.bip151)
return;
try {
this.bip151.encack(payload);
} catch (e) {
this._error(e);
return;
}
this.fire('encack', payload);
};

View File

@ -936,7 +936,11 @@ Framer.feeFilter = function feeFilter(data, writer) {
*/
Framer.encinit = function encinit(data, writer) {
return data.toEncinit(writer);
if (writer) {
writer.writeBytes(data);
return writer;
}
return data;
};
/**
@ -947,7 +951,11 @@ Framer.encinit = function encinit(data, writer) {
*/
Framer.encack = function encack(data, writer) {
return data.toEncack(writer);
if (writer) {
writer.writeBytes(data);
return writer;
}
return data;
};
/**

View File

@ -705,6 +705,8 @@ function Packet(cmd, size, checksum) {
this.payload = null;
}
Parser.Packet = Packet;
/*
* Expose
*/

View File

@ -10,7 +10,9 @@ var assert = require('assert');
describe('BIP151', function() {
var client = new bcoin.bip151();
var server = new bcoin.bip151();
var payload = new Buffer('deadbeef', 'hex');
function payload() {
return new Buffer('deadbeef', 'hex');
}
it('should do encinit', function() {
client.encinit(server.toEncinit());
@ -34,7 +36,7 @@ describe('BIP151', function() {
});
it('should encrypt payload from client to server', function() {
var packet = client.packet('fake', payload);
var packet = client.packet('fake', payload());
var emitted = false;
server.once('packet', function(cmd, body) {
emitted = true;
@ -46,7 +48,7 @@ describe('BIP151', function() {
});
it('should encrypt payload from server to client', function() {
var packet = server.packet('fake', payload);
var packet = server.packet('fake', payload());
var emitted = false;
client.once('packet', function(cmd, body) {
emitted = true;
@ -58,7 +60,7 @@ describe('BIP151', function() {
});
it('should encrypt payload from client to server (2)', function() {
var packet = client.packet('fake', payload);
var packet = client.packet('fake', payload());
var emitted = false;
server.once('packet', function(cmd, body) {
emitted = true;
@ -70,7 +72,7 @@ describe('BIP151', function() {
});
it('should encrypt payload from server to client (2)', function() {
var packet = server.packet('fake', payload);
var packet = server.packet('fake', payload());
var emitted = false;
client.once('packet', function(cmd, body) {
emitted = true;
@ -83,7 +85,7 @@ describe('BIP151', function() {
it('client should rekey', function() {
var rekeyed = false;
var bytes = client.processed;
var bytes = client.input.processed;
client.once('rekey', function() {
rekeyed = true;
@ -99,19 +101,19 @@ describe('BIP151', function() {
});
// Force a rekey after 1gb processed.
client.maybeRekey({ length: 1024 * (1 << 20) });
client.input.maybeRekey({ length: 1024 * (1 << 20) });
utils.nextTick(function() {
assert(rekeyed);
// Reset so as not to mess up
// the symmetry of client and server.
client.processed = bytes + 33 + 31;
client.input.processed = bytes + 33 + 31;
});
});
it('should encrypt payload from client to server after rekey', function() {
var packet = client.packet('fake', payload);
var packet = client.packet('fake', payload());
var emitted = false;
server.once('packet', function(cmd, body) {
emitted = true;
@ -123,7 +125,7 @@ describe('BIP151', function() {
});
it('should encrypt payload from server to client after rekey', function() {
var packet = server.packet('fake', payload);
var packet = server.packet('fake', payload());
var emitted = false;
client.once('packet', function(cmd, body) {
emitted = true;
@ -135,7 +137,7 @@ describe('BIP151', function() {
});
it('should encrypt payload from client to server after rekey (2)', function() {
var packet = client.packet('fake', payload);
var packet = client.packet('fake', payload());
var emitted = false;
server.once('packet', function(cmd, body) {
emitted = true;
@ -147,7 +149,7 @@ describe('BIP151', function() {
});
it('should encrypt payload from server to client after rekey (2)', function() {
var packet = server.packet('fake', payload);
var packet = server.packet('fake', payload());
var emitted = false;
client.once('packet', function(cmd, body) {
emitted = true;
@ -157,4 +159,25 @@ describe('BIP151', function() {
client.feed(packet);
assert(emitted);
});
it('should encrypt payloads both ways asynchronously', function() {
var spacket = server.packet('fake', payload());
var cpacket = client.packet('fake', payload());
var cemitted = false;
var semitted = false;
client.once('packet', function(cmd, body) {
cemitted = true;
assert.equal(cmd, 'fake');
assert.equal(body.toString('hex'), 'deadbeef');
});
server.once('packet', function(cmd, body) {
semitted = true;
assert.equal(cmd, 'fake');
assert.equal(body.toString('hex'), 'deadbeef');
});
client.feed(spacket);
server.feed(cpacket);
assert(cemitted);
assert(semitted);
});
});