diff --git a/lib/net/bip151.js b/lib/net/bip151.js index 8d10a2be..da6a2c69 100644 --- a/lib/net/bip151.js +++ b/lib/net/bip151.js @@ -20,6 +20,8 @@ var assert = utils.assert; var constants = bcoin.constants; var chachapoly = require('../crypto/chachapoly'); var packets = require('./packets'); +var EncinitPacket = packets.EncinitPacket; +var EncackPacket = packets.EncackPacket; /* * Constants @@ -29,13 +31,13 @@ var HKDF_SALT = new Buffer('bitcoinecdh', 'ascii'); var INFO_KEY1 = new Buffer('BitcoinK1', 'ascii'); var INFO_KEY2 = new Buffer('BitcoinK2', 'ascii'); var INFO_SID = new Buffer('BitcoinSessionID', 'ascii'); +var HIGH_WATER_MARK = 1024 * (1 << 20); /** * Represents a BIP151 input or output stream. * @exports BIP151Stream * @constructor * @param {Number} cipher - * @param {Buffer?} key * @property {Buffer} publicKey * @property {Buffer} privateKey * @property {Number} cipher @@ -47,20 +49,17 @@ var INFO_SID = new Buffer('BitcoinSessionID', 'ascii'); * @property {AEAD} aead * @property {Buffer} tag * @property {Number} seq - * @property {Number} highWaterMark * @property {Number} processed * @property {Number} lastKey */ -function BIP151Stream(cipher, key) { +function BIP151Stream(cipher) { if (!(this instanceof BIP151Stream)) - return new BIP151Stream(cipher, key); + return new BIP151Stream(cipher); - EventEmitter.call(this); - - this.publicKey = null; - this.privateKey = key || bcoin.ec.generatePrivateKey(); this.cipher = cipher || 0; + this.privateKey = bcoin.ec.generatePrivateKey(); + this.publicKey = null; this.secret = null; this.prk = null; this.k1 = null; @@ -76,18 +75,10 @@ function BIP151Stream(cipher, key) { this.iv = new Buffer(8); this.iv.fill(0); - this.highWaterMark = 1024 * (1 << 20); this.processed = 0; this.lastRekey = 0; - - this.pending = []; - this.total = 0; - this.waiting = 4; - this.hasSize = false; } -utils.inherits(BIP151Stream, EventEmitter); - /** * Initialize the stream with peer's public key. * Computes ecdh secret and chacha keys. @@ -122,23 +113,26 @@ BIP151Stream.prototype.init = function init(publicKey) { * Add buffer size to `processed`, * check whether we need to rekey. * @param {Buffer} data + * @returns {Boolean} */ -BIP151Stream.prototype.maybeRekey = function maybeRekey(data) { +BIP151Stream.prototype.shouldRekey = function shouldRekey(data) { var now = utils.now(); this.processed += data.length; if (now >= this.lastRekey + 10 - || this.processed >= this.highWaterMark) { + || this.processed >= HIGH_WATER_MARK) { this.lastRekey = now; this.processed = 0; - this.emit('rekey'); + return true; } + + return false; }; /** - * Generate new chacha keys with `key = HASH256(key)`. + * Generate new chacha keys with `key = HASH256(sid | key)`. * This will reinitialize the state of both ciphers. */ @@ -200,7 +194,7 @@ BIP151Stream.prototype.update = function update() { /** * Get public key tied to private key - * (not the same as BIP151Stream#privateKey). + * (not the same as BIP151Stream#publicKey). * @returns {Buffer} */ @@ -281,170 +275,6 @@ BIP151Stream.prototype.verify = function verify(tag) { return chachapoly.Poly1305.verify(this.tag, tag); }; -/** - * Parse ciphertext data and split into chunks. - * Potentially emits a `packet` event. - * @param {Buffer} data - */ - -BIP151Stream.prototype.feed = function feed(data) { - var chunk; - - this.total += data.length; - this.pending.push(data); - - while (this.total >= this.waiting) { - chunk = this.read(this.waiting); - this.parse(chunk); - } -}; - -/** - * Read and consume a number of bytes - * from the buffered stream. - * @param {Number} size - * @returns {Buffer} - */ - -BIP151Stream.prototype.read = function read(size) { - var pending, chunk, off, len; - - assert(this.total >= size, 'Reading too much.'); - - if (size === 0) - return new Buffer(0); - - pending = this.pending[0]; - - if (pending.length > size) { - chunk = pending.slice(0, size); - this.pending[0] = pending.slice(size); - this.total -= chunk.length; - return chunk; - } - - if (pending.length === size) { - chunk = this.pending.shift(); - this.total -= chunk.length; - return chunk; - } - - chunk = new Buffer(size); - off = 0; - len = 0; - - while (off < chunk.length) { - pending = this.pending[0]; - len = pending.copy(chunk, off); - if (len === pending.length) - this.pending.shift(); - else - this.pending[0] = pending.slice(len); - off += len; - } - - assert.equal(off, chunk.length); - - this.total -= chunk.length; - - return chunk; -}; - -/** - * Parse a ciphertext payload chunk. - * Potentially emits a `packet` event. - * @param {Buffer} data - */ - -BIP151Stream.prototype.parse = function parse(data) { - var size, payload, tag, p, cmd, body; - - if (!this.hasSize) { - size = this.decryptSize(data); - - // Allow 3 batched packets of max message size (12mb). - // Not technically standard, but this protects us - // from buffering tons of data due to either an - // potential dos'er or a cipher state mismatch. - // Note that 6 is the minimum size: - // cmd=varint(1) string(1) length(4) data(0) - if (size < 6 || size > constants.MAX_MESSAGE * 3) { - this.waiting = 4; - this.emit('error', new Error('Bad packet size.')); - return; - } - - this.hasSize = true; - this.waiting = size + 16; - - return; - } - - payload = data.slice(0, this.waiting - 16); - tag = data.slice(this.waiting - 16, this.waiting); - - this.hasSize = false; - this.waiting = 4; - - // Authenticate payload before decrypting. - // This ensures the cipher state isn't altered - // if the payload integrity has been compromised. - this.auth(payload); - this.finish(); - - if (!this.verify(tag)) { - this.sequence(); - this.emit('error', new Error('Bad tag.')); - return; - } - - this.decrypt(payload); - this.sequence(); - - p = bcoin.reader(payload); - - while (p.left()) { - try { - cmd = p.readVarString('ascii'); - body = p.readBytes(p.readU32()); - } catch (e) { - this.emit('error', e); - return; - } - - this.emit('packet', cmd, body); - } -}; - -/** - * Frame and encrypt a plaintext payload. - * @param {String} cmd - * @param {Buffer} body - * @returns {Buffer} Ciphertext payload - */ - -BIP151Stream.prototype.packet = function packet(cmd, body) { - var p = bcoin.writer(); - var payload, packet; - - p.writeVarString(cmd, 'ascii'); - p.writeU32(body.length); - p.writeBytes(body); - - payload = p.render(); - - packet = new Buffer(4 + payload.length + 16); - - this.maybeRekey(packet); - - this.encryptSize(payload.length).copy(packet, 0); - this.encrypt(payload).copy(packet, 4); - this.finish().copy(packet, 4 + payload.length); - this.sequence(); - - return packet; -}; - /** * Represents a BIP151 input and output stream. * Holds state for peer communication. @@ -481,34 +311,16 @@ function BIP151(cipher) { this.completed = false; this.handshake = false; - this.bip150 = null; + this.pending = []; + this.total = 0; + this.waiting = 4; + this.hasSize = false; - this._init(); + this.bip150 = null; } utils.inherits(BIP151, EventEmitter); -/** - * Initialize BIP151. Bind to events. - * @private - */ - -BIP151.prototype._init = function _init() { - var self = this; - - this.output.on('rekey', function() { - self.emit('rekey'); - if (self.bip150 && self.bip150.auth) - self.bip150.rekeyOutput(); - else - self.output.rekey(); - }); - - this.input.on('packet', function(cmd, body) { - self.emit('packet', cmd, body); - }); -}; - /** * Test whether handshake has completed. * @returns {Boolean} @@ -530,7 +342,7 @@ BIP151.prototype.isReady = function isReady() { BIP151.prototype.toEncinit = function toEncinit() { assert(!this.initSent, 'Cannot init twice.'); this.initSent = true; - return new packets.EncinitPacket(this.input.getPublicKey(), this.input.cipher); + return new EncinitPacket(this.input.getPublicKey(), this.input.cipher); }; /** @@ -549,7 +361,7 @@ BIP151.prototype.toEncack = function toEncack() { this.emit('handshake'); } - return new packets.EncackPacket(this.output.getPublicKey()); + return new EncackPacket(this.output.getPublicKey()); }; /** @@ -561,7 +373,7 @@ BIP151.prototype.toEncack = function toEncack() { BIP151.prototype.toRekey = function toRekey() { assert(this.handshake, 'Cannot rekey before handshake.'); - return new packets.EncackPacket(constants.ZERO_KEY); + return new EncackPacket(constants.ZERO_KEY); }; /** @@ -586,10 +398,11 @@ BIP151.prototype.encack = function encack(publicKey) { if (utils.equal(publicKey, constants.ZERO_KEY)) { assert(this.handshake, 'No initialization before rekey.'); - if (this.bip150 && this.bip150.auth) + if (this.bip150 && this.bip150.auth) { this.bip150.rekeyInput(); - else - this.input.rekey(); + return; + } + this.input.rekey(); return; } @@ -657,14 +470,23 @@ BIP151.prototype.destroy = function destroy() { }; /** - * Feed ciphertext payload chunk - * to the input stream. Potentially - * emits a `packet` event. + * Add buffer size to `processed`, + * check whether we need to rekey. * @param {Buffer} data */ -BIP151.prototype.feed = function feed(data) { - return this.input.feed(data); +BIP151.prototype.maybeRekey = function maybeRekey(data) { + if (!this.output.shouldRekey(data)) + return; + + this.emit('rekey'); + + if (this.bip150 && this.bip150.auth) { + this.bip150.rekeyOutput(); + return; + } + + this.output.rekey(); }; /** @@ -675,7 +497,161 @@ BIP151.prototype.feed = function feed(data) { */ BIP151.prototype.packet = function packet(cmd, body) { - return this.output.packet(cmd, body); + var p = bcoin.writer(); + var payload, packet; + + p.writeVarString(cmd, 'ascii'); + p.writeU32(body.length); + p.writeBytes(body); + + payload = p.render(); + + packet = new Buffer(4 + payload.length + 16); + + this.maybeRekey(packet); + + this.output.encryptSize(payload.length).copy(packet, 0); + this.output.encrypt(payload).copy(packet, 4); + this.output.finish().copy(packet, 4 + payload.length); + this.output.sequence(); + + return packet; +}; + +/** + * Feed ciphertext payload chunk + * to the input stream. Potentially + * emits a `packet` event. + * @param {Buffer} data + */ + +BIP151.prototype.feed = function feed(data) { + var chunk; + + this.total += data.length; + this.pending.push(data); + + while (this.total >= this.waiting) { + chunk = this.read(this.waiting); + this.parse(chunk); + } +}; + +/** + * Read and consume a number of bytes + * from the buffered stream. + * @param {Number} size + * @returns {Buffer} + */ + +BIP151.prototype.read = function read(size) { + var pending, chunk, off, len; + + assert(this.total >= size, 'Reading too much.'); + + if (size === 0) + return new Buffer(0); + + pending = this.pending[0]; + + if (pending.length > size) { + chunk = pending.slice(0, size); + this.pending[0] = pending.slice(size); + this.total -= chunk.length; + return chunk; + } + + if (pending.length === size) { + chunk = this.pending.shift(); + this.total -= chunk.length; + return chunk; + } + + chunk = new Buffer(size); + off = 0; + len = 0; + + while (off < chunk.length) { + pending = this.pending[0]; + len = pending.copy(chunk, off); + if (len === pending.length) + this.pending.shift(); + else + this.pending[0] = pending.slice(len); + off += len; + } + + assert.equal(off, chunk.length); + + this.total -= chunk.length; + + return chunk; +}; + +/** + * Parse a ciphertext payload chunk. + * Potentially emits a `packet` event. + * @param {Buffer} data + */ + +BIP151.prototype.parse = function parse(data) { + var size, payload, tag, p, cmd, body; + + if (!this.hasSize) { + size = this.input.decryptSize(data); + + // Allow 3 batched packets of max message size (12mb). + // Not technically standard, but this protects us + // from buffering tons of data due to either an + // potential dos'er or a cipher state mismatch. + // Note that 6 is the minimum size: + // varinti-clen(1) string-cmd(1) uint32-size(4) data(0) + if (size < 6 || size > constants.MAX_MESSAGE * 3) { + this.waiting = 4; + this.emit('error', new Error('Bad packet size.')); + return; + } + + this.hasSize = true; + this.waiting = size + 16; + + return; + } + + payload = data.slice(0, this.waiting - 16); + tag = data.slice(this.waiting - 16, this.waiting); + + this.hasSize = false; + this.waiting = 4; + + // Authenticate payload before decrypting. + // This ensures the cipher state isn't altered + // if the payload integrity has been compromised. + this.input.auth(payload); + this.input.finish(); + + if (!this.input.verify(tag)) { + this.input.sequence(); + this.emit('error', new Error('Bad tag.')); + return; + } + + this.input.decrypt(payload); + this.input.sequence(); + + p = bcoin.reader(payload); + + while (p.left()) { + try { + cmd = p.readVarString('ascii'); + body = p.readBytes(p.readU32()); + } catch (e) { + this.emit('error', e); + return; + } + + this.emit('packet', cmd, body); + } }; /* diff --git a/test/bip150-test.js b/test/bip150-test.js index e428ba13..151806cf 100644 --- a/test/bip150-test.js +++ b/test/bip150-test.js @@ -127,7 +127,7 @@ describe('BIP150', function() { }); // Force a rekey after 1gb processed. - client.output.maybeRekey({ length: 1024 * (1 << 20) }); + client.maybeRekey({ length: 1024 * (1 << 20) }); assert(rekeyed); diff --git a/test/bip151-test.js b/test/bip151-test.js index 5557bb3a..39ac2746 100644 --- a/test/bip151-test.js +++ b/test/bip151-test.js @@ -105,7 +105,7 @@ describe('BIP151', function() { }); // Force a rekey after 1gb processed. - client.output.maybeRekey({ length: 1024 * (1 << 20) }); + client.maybeRekey({ length: 1024 * (1 << 20) }); assert(rekeyed);