diff --git a/lib/bcoin/bip150.js b/lib/bcoin/bip150.js new file mode 100644 index 00000000..c1a53067 --- /dev/null +++ b/lib/bcoin/bip150.js @@ -0,0 +1,315 @@ +/*! + * bip150.js - peer auth. + * Copyright (c) 2016, Christopher Jeffrey (MIT License). + * https://github.com/bcoin-org/bcoin + * Resources: + * https://github.com/bitcoin/bips/blob/master/bip-0150.mediawiki + */ + +'use strict'; + +var EventEmitter = require('events').EventEmitter; +var bcoin = require('./env'); +var utils = require('./utils'); +var assert = utils.assert; +var constants = bcoin.protocol.constants; + +var ZERO_SIG = new Buffer(64); +ZERO_SIG.fill(0); + +/** + * Represents a BIP150 input and output stream. + * @exports BIP150 + * @constructor + * @param {BIP151} bip151 + * @property {Boolean} outbound + * @property {Boolean} challengeReceived + * @property {Boolean} replyReceived + * @property {Boolean} proposeReceived + */ + +function BIP150(bip151, hostname, outbound, db, identity) { + if (!(this instanceof BIP150)) + return new BIP150(bip151, hostname, outbound, db, identity); + + assert(bip151, 'BIP150 requires BIP151.'); + + EventEmitter.call(this); + + this.bip151 = bip151; + this.input = bip151.input; + this.output = bip151.output; + + this.hostname = hostname; // ip & port + + this.db = db; + this.outbound = outbound; + this.peerIdentity = null; + + if (this.outbound) + this.peerIdentity = this.db.getKnown(this.hostname); + + // Identity keypair + this.privateKey = identity || bcoin.ec.generatePrivateKey(); + this.publicKey = bcoin.ec.publicKeyCreate(this.privateKey, true); + + this.challengeReceived = false; + this.replyReceived = false; + this.proposeReceived = false; + this.challengeSent = false; + this.auth = false; + this.completed = false; + this.callback = null; + this.timeout = null; +} + +utils.inherits(BIP150, EventEmitter); + +BIP150.prototype.isAuthed = function isAuthed() { + if (this.outbound) + return this.challengeSent && this.challengeReceived; + return this.challengeReceived && this.replyReceived; +}; + +BIP150.prototype.challenge = function challenge(payload) { + var p = bcoin.reader(payload); + var hash = p.readHash(); + var type = this.outbound ? 'r' : 'i'; + var msg, sig; + + assert(!this.challengeReceived, 'Peer challenged twice.'); + this.challengeReceived = true; + + if (utils.equal(hash, constants.ZERO_HASH)) + throw new Error('Auth failure.'); + + msg = this.hash(this.input.sid, type, this.publicKey); + + if (!utils.ccmp(hash, msg)) + return ZERO_SIG; + + if (this.isAuthed()) { + this.auth = true; + this.emit('auth'); + } + + sig = bcoin.ec.sign(msg, this.privateKey); + + // authreply + return bcoin.ec.fromDER(sig); +}; + +BIP150.prototype.reply = function reply(payload) { + var p = bcoin.reader(payload); + var data = p.readBytes(64); + var type = this.outbound ? 'i' : 'r'; + var sig, msg, result; + + assert(this.challengeSent, 'Unsolicited reply.'); + assert(!this.replyReceived, 'Peer replied twice.'); + this.replyReceived = true; + + if (utils.equal(data, ZERO_SIG)) + throw new Error('Auth failure.'); + + if (!this.peerIdentity) + return bcoin.ec.random(32); + + sig = bcoin.ec.toDER(data); + msg = this.hash(this.output.sid, type, this.peerIdentity); + result = bcoin.ec.verify(msg, sig, this.peerIdentity); + + if (!result) + return bcoin.ec.random(32); + + if (this.isAuthed()) { + this.auth = true; + this.emit('auth'); + return; + } + + assert(this.outbound, 'No challenge received before reply on inbound.'); + + // authpropose + return this.hash(this.input.sid, 'p', this.publicKey); +}; + +BIP150.prototype.propose = function propose(payload) { + var p = bcoin.reader(payload); + var hash = p.readHash(); + var match; + + assert(!this.outbound, 'Outbound peer tried to propose.'); + assert(!this.challengeSent, 'Unsolicited propose.'); + assert(!this.proposeReceived, 'Peer proposed twice.'); + this.proposeReceived = true; + + match = this.findAuthorized(hash); + + if (!match) + return constants.ZERO_HASH; + + this.peerIdentity = match; + + // Add them in case we ever connect to them. + this.db.addKnown(this.hostname, this.peerIdentity); + + this.challengeSent = true; + + // authchallenge + return this.hash(this.output.sid, 'r', this.peerIdentity); +}; + +BIP150.prototype.toChallenge = function toChallenge(writer) { + var p = new bcoin.writer(writer); + var msg; + + assert(this.outbound, 'Cannot challenge an inbound connection.'); + assert(this.peerIdentity, 'Cannot challenge without a peer identity.'); + + msg = this.hash(this.output.sid, 'i', this.peerIdentity); + + assert(!this.challengeSent, 'Cannot initiate challenge twice.'); + this.challengeSent = true; + + p.writeBytes(msg); + + if (!writer) + p = p.render(); + + return p; +}; + +BIP150.prototype.rekey = function rekey(sid, key, req, res) { + var seed = new Buffer(130); + sid.copy(seed, 0); + key.copy(seed, 32); + req.copy(seed, 64); + res.copy(seed, 97); + return utils.hash256(seed); +}; + +BIP150.prototype.rekeyInput = function rekeyInput() { + var stream = this.input; + var req = this.peerIdentity; + var res = this.publicKey; + var k1 = this.rekey(stream.sid, stream.k1, req, res); + var k2 = this.rekey(stream.sid, stream.k2, req, res); + stream.rekey(k1, k2); +}; + +BIP150.prototype.rekeyOutput = function rekeyOutput() { + var stream = this.output; + var req = this.publicKey; + var res = this.peerIdentity; + var k1 = this.rekey(stream.sid, stream.k1, req, res); + var k2 = this.rekey(stream.sid, stream.k2, req, res); + stream.rekey(k1, k2); +}; + +BIP150.prototype.hash = function hash(sid, ch, key) { + var data = new Buffer(66); + sid.copy(data, 0); + data[32] = ch.charCodeAt(0); + key.copy(data, 33); + return utils.hash256(data); +}; + +BIP150.prototype.findAuthorized = function findAuthorized(hash) { + var i, key, msg; + + // Scary O(n) stuff. + for (i = 0; i < this.db.auth.length; i++) { + key = this.db.auth[i]; + msg = this.hash(this.output.sid, 'p', key); + + // XXX Do we really need a constant + // time compare here? Do it just to + // be safe I guess. + if (utils.ccmp(msg, hash)) + return key; + } +}; + +BIP150.prototype.complete = function complete(err) { + assert(!this.completed, 'Already completed.'); + assert(this.callback, 'No completion callback.'); + + this.completed = true; + + if (this.timeout != null) { + clearTimeout(this.timeout); + this.timeout = null; + } + + this.callback(err); + this.callback = null; +}; + +BIP150.prototype.wait = function wait(timeout, callback) { + var self = this; + + assert(!this.auth, 'Cannot wait for init after handshake.'); + + this.callback = callback; + + if (this.outbound && !this.peerIdentity) + return this.complete(new Error('No identity for ' + this.hostname + '.')); + + this.timeout = setTimeout(function() { + self.complete(new Error('BIP150 handshake timed out.')); + }, timeout); + + this.once('auth', function() { + self.complete(); + }); +}; + +BIP150.prototype.getAddress = function getAddress() { + var p = new bcoin.writer(); + p.writeU8(0x0f); + p.writeU16BE(0xff01); + p.writeBytes(utils.hash160(this.peerIdentity)); + p.writeChecksum(); + return utils.toBase58(p.render()); +}; + +/** + * AuthDB + * @exports AuthDB + * @constructor + */ + +function AuthDB() { + if (!(this instanceof AuthDB)) + return new AuthDB(); + + this.known = {}; + this.auth = []; +} + +AuthDB.prototype.addKnown = function addKnown(host, key) { + assert(typeof host === 'string'); + assert(Buffer.isBuffer(key) && key.length === 33); + this.known[host] = key; +}; + +AuthDB.prototype.addAuthorized = function addAuthorized(key) { + assert(Buffer.isBuffer(key) && key.length === 33); + this.auth.push(key); +}; + +AuthDB.prototype.getKnown = function getKnown(host) { + return this.known[host]; +}; + +/* + * Expose + */ + +exports = BIP150; + +exports.BIP150 = BIP150; +exports.AuthDB = AuthDB; + +module.exports = exports; diff --git a/lib/bcoin/bip151.js b/lib/bcoin/bip151.js index 3eab713e..468666c7 100644 --- a/lib/bcoin/bip151.js +++ b/lib/bcoin/bip151.js @@ -132,7 +132,6 @@ BIP151Stream.prototype.maybeRekey = function maybeRekey(data) { this.lastRekey = now; this.processed = 0; this.emit('rekey'); - this.rekey(); } }; @@ -141,20 +140,25 @@ BIP151Stream.prototype.maybeRekey = function maybeRekey(data) { * This will reinitialize the state of both ciphers. */ -BIP151Stream.prototype.rekey = function rekey() { +BIP151Stream.prototype.rekey = function rekey(k1, k2) { var seed; assert(this.prk, 'Cannot rekey before initialization.'); - seed = new Buffer(64); + if (!k1) { + seed = new Buffer(64); - this.sid.copy(seed, 0); + this.sid.copy(seed, 0); - this.k1.copy(seed, 32); - this.k1 = utils.hash256(seed); + this.k1.copy(seed, 32); + this.k1 = utils.hash256(seed); - this.k2.copy(seed, 32); - this.k2 = utils.hash256(seed); + this.k2.copy(seed, 32); + this.k2 = utils.hash256(seed); + } else { + this.k1 = k1; + this.k2 = k2; + } // All state is reinitialized // aside from the sequence number. @@ -475,6 +479,8 @@ function BIP151(cipher) { this.completed = false; this.handshake = false; + this.bip150 = null; + this._init(); } @@ -490,6 +496,10 @@ BIP151.prototype._init = function _init() { 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) { @@ -608,7 +618,10 @@ BIP151.prototype.encack = function encack(data) { if (utils.equal(publicKey, constants.ZERO_KEY)) { assert(this.handshake, 'No initialization before rekey.'); - this.input.rekey(); + if (this.bip150 && this.bip150.auth) + this.bip150.rekeyInput(); + else + this.input.rekey(); return; } diff --git a/lib/bcoin/config.js b/lib/bcoin/config.js index 540d82bd..486ffd2d 100644 --- a/lib/bcoin/config.js +++ b/lib/bcoin/config.js @@ -150,6 +150,7 @@ config.parseData = function parseData(data) { options.headers = bool(data.headers); options.compact = bool(data.compact); options.bip151 = bool(data.bip151); + options.bip150 = bool(data.bip150); options.proxyServer = str(data.proxyserver); options.preferredSeed = str(data.preferredseed); options.maxPeers = num(data.maxpeers); diff --git a/lib/bcoin/ec.js b/lib/bcoin/ec.js index 35c310ef..f9310904 100644 --- a/lib/bcoin/ec.js +++ b/lib/bcoin/ec.js @@ -395,6 +395,51 @@ ec.sign = function sign(msg, key) { return sig; }; +/** + * Convert DER signature to R/S. + * @param {Buffer} sig + * @returns {Buffer} R/S-formatted signature. + */ + +ec.fromDER = function fromDER(sig) { + var out; + + assert(Buffer.isBuffer(sig)); + + if (secp256k1) + return secp256k1.signatureImport(sig); + + sig = new ec.signature(sig); + out = new Buffer(64); + + sig.r.toArrayLike(Buffer, 'be', 32).copy(out, 0); + sig.s.toArrayLike(Buffer, 'be', 32).copy(out, 32); + + return out; +}; + +/** + * Convert R/S signature to DER. + * @param {Buffer} sig + * @returns {Buffer} DER-formatted signature. + */ + +ec.toDER = function toDER(sig) { + var out; + + assert(Buffer.isBuffer(sig)); + + if (secp256k1) + return secp256k1.signatureExport(sig); + + out = new ec.signature({ + r: new bn(sig.slice(0, 32), 'be'), + s: new bn(sig.slice(32, 64), 'be') + }); + + return new Buffer(out.toDER()); +}; + /** * Normalize the length of a signature * (only done for historical data). diff --git a/lib/bcoin/env.js b/lib/bcoin/env.js index efb14b1c..dad95e06 100644 --- a/lib/bcoin/env.js +++ b/lib/bcoin/env.js @@ -149,6 +149,7 @@ function Environment() { this.txdb = require('./txdb'); this.abstractblock = require('./abstractblock'); this.bip151 = require('./bip151'); + this.bip150 = require('./bip150'); this.bip152 = require('./bip152'); this.memblock = require('./memblock'); this.block = require('./block'); diff --git a/lib/bcoin/fullnode.js b/lib/bcoin/fullnode.js index 3e268420..0ea4d2b2 100644 --- a/lib/bcoin/fullnode.js +++ b/lib/bcoin/fullnode.js @@ -100,6 +100,9 @@ function Fullnode(options) { headers: this.options.headers, compact: this.options.compact, bip151: this.options.bip151, + bip150: this.options.bip150, + auth: this.options.auth, + identity: this.options.identity, maxPeers: this.options.maxPeers, maxLeeches: this.options.maxLeeches, proxyServer: this.options.proxyServer, diff --git a/lib/bcoin/peer.js b/lib/bcoin/peer.js index 11200615..61525812 100644 --- a/lib/bcoin/peer.js +++ b/lib/bcoin/peer.js @@ -108,15 +108,10 @@ function Peer(pool, options) { this.compactBlocks = {}; this.sentAddr = false; this.bip151 = null; + this.bip150 = null; this.lastSend = 0; this.lastRecv = 0; - if (this.pool.options.bip151) - this.bip151 = new bcoin.bip151(); - - this.parser = new bcoin.protocol.parser(this); - this.framer = new bcoin.protocol.framer(this); - this.challenge = null; this.lastPong = -1; this.lastPing = -1; @@ -145,6 +140,22 @@ function Peer(pool, options) { this.hostname = IP.hostname(this.host, this.port); + if (this.pool.options.bip151) { + this.bip151 = new bcoin.bip151(); + if (this.pool.options.bip150) { + this.bip150 = new bcoin.bip150( + this.bip151, + this.hostname, + this.type !== Peer.types.LEECH, + this.pool.auth, + this.pool.identity); + this.bip151.bip150 = this.bip150; + } + } + + this.parser = new bcoin.protocol.parser(this); + this.framer = new bcoin.protocol.framer(this); + this.requests = { timeout: options.requestTimeout || 10000, skip: {}, @@ -288,11 +299,11 @@ Peer.prototype._onConnect = function _onConnect() { return this.bip151.wait(5000, function(err) { if (err) self._error(err, true); - self._onHandshake(); + self._onBIP151(); }); } - this._onHandshake(); + this._onBIP151(); }; /** @@ -300,15 +311,47 @@ Peer.prototype._onConnect = function _onConnect() { * @private */ -Peer.prototype._onHandshake = function _onHandshake() { +Peer.prototype._onBIP151 = function _onBIP151() { var self = this; if (this.bip151) { assert(this.bip151.completed); + if (this.bip151.handshake) { this.logger.info('BIP151 handshake complete (%s).', this.hostname); this.logger.info('Connection is encrypted (%s).', this.hostname); } + + if (this.bip150) { + assert(!this.bip150.completed); + this.logger.info('Attempting BIP150 handshake (%s).', this.hostname); + if (this.bip150.outbound && this.bip150.peerIdentity) + this.write(this.framer.authChallenge(this.bip150.toChallenge())); + return this.bip150.wait(5000, function(err) { + if (err) + self._error(err, true); + self._onHandshake(); + }); + } + } + + this._onHandshake(); +}; + +/** + * Handle post handshake. + * @private + */ + +Peer.prototype._onHandshake = function _onHandshake() { + var self = this; + + if (this.bip150) { + assert(this.bip150.completed); + if (this.bip150.auth) { + this.logger.info('BIP150 handshake complete (%s).', this.hostname); + this.logger.info('Peer is authed (%s).', this.hostname); + } } this.request('verack', function(err) { @@ -834,6 +877,14 @@ Peer.prototype._onPacket = function onPacket(packet) { this.bip151.complete(new Error('Message before handshake.')); } + if (this.bip150 + && !this.bip150.completed + && cmd !== 'authchallenge' + && cmd !== 'authreply' + && cmd !== 'authpropose') { + this.bip150.complete(new Error('Message before auth.')); + } + if (this.lastBlock && cmd !== 'tx') this._flushMerkle(); @@ -917,6 +968,12 @@ Peer.prototype._onPacket = function onPacket(packet) { return this._handleEncinit(payload); case 'encack': return this._handleEncack(payload); + case 'authchallenge': + return this._handleAuthChallenge(payload); + case 'authreply': + return this._handleAuthReply(payload); + case 'authpropose': + return this._handleAuthPropose(payload); case 'sendcmpct': return this._handleSendCmpct(payload); case 'cmpctblock': @@ -1895,6 +1952,79 @@ Peer.prototype._handleEncack = function _handleEncack(payload) { this.fire('encack', payload); }; +/** + * Handle `authchallenge` packet. + * @private + * @param {Object} + */ + +Peer.prototype._handleAuthChallenge = function _handleAuthChallenge(payload) { + var result; + + if (!this.bip150) + return; + + try { + result = this.bip150.challenge(payload); + } catch (e) { + this._error(e); + return; + } + + this.write(this.framer.authReply(result)); + + this.fire('authchallenge', payload); +}; + +/** + * Handle `authreply` packet. + * @private + * @param {Object} + */ + +Peer.prototype._handleAuthReply = function _handleAuthReply(payload) { + var result; + + if (!this.bip150) + return; + + try { + result = this.bip150.reply(payload); + } catch (e) { + this._error(e); + return; + } + + if (result) + this.write(this.framer.authPropose(result)); + + this.fire('authreply', payload); +}; + +/** + * Handle `authpropose` packet. + * @private + * @param {Object} + */ + +Peer.prototype._handleAuthPropose = function _handleAuthPropose(payload) { + var result; + + if (!this.bip150) + return; + + try { + result = this.bip150.propose(payload); + } catch (e) { + this._error(e); + return; + } + + this.write(this.framer.authChallenge(result)); + + this.fire('authpropose', payload); +}; + /** * Handle `sendcmpct` packet. * @private diff --git a/lib/bcoin/pool.js b/lib/bcoin/pool.js index d9f7a857..dec01c7e 100644 --- a/lib/bcoin/pool.js +++ b/lib/bcoin/pool.js @@ -146,6 +146,14 @@ function Pool(options) { this._createServer = options.createServer; this.locker = new bcoin.locker(this); this.proxyServer = options.proxyServer; + this.auth = null; + this.identity = null; + + if (this.options.bip150) { + this.options.bip151 = true; + this.auth = options.auth || new bcoin.bip150.AuthDB(); + this.identity = options.identity || bcoin.ec.generatePrivateKey(); + } this.syncing = false; this.synced = false; diff --git a/lib/bcoin/protocol/framer.js b/lib/bcoin/protocol/framer.js index ed328fbb..224aeee8 100644 --- a/lib/bcoin/protocol/framer.js +++ b/lib/bcoin/protocol/framer.js @@ -456,6 +456,36 @@ Framer.prototype.blockTxn = function blockTxn(options) { return this.packet('blocktxn', Framer.blockTxn(options)); }; +/** + * Create a authchallenge packet with a header. + * @param {Object} options - See {@link Framer.authChallenge}. + * @returns {Buffer} authChallenge packet. + */ + +Framer.prototype.authChallenge = function authChallenge(options) { + return this.packet('authchallenge', Framer.authChallenge(options)); +}; + +/** + * Create a authreply packet with a header. + * @param {Object} options - See {@link Framer.authReply}. + * @returns {Buffer} authReply packet. + */ + +Framer.prototype.authReply = function authReply(options) { + return this.packet('authreply', Framer.authReply(options)); +}; + +/** + * Create a authpropose packet with a header. + * @param {Object} options - See {@link Framer.authPropose}. + * @returns {Buffer} authPropose packet. + */ + +Framer.prototype.authPropose = function authPropose(options) { + return this.packet('authpropose', Framer.authPropose(options)); +}; + /** * Create a version packet (without a header). * @param {VersionPacket} options @@ -476,7 +506,7 @@ Framer.verack = function verack() { return DUMMY; }; - /** +/** * Create an inv, getdata, or notfound packet. * @private * @param {InvItem[]} items @@ -993,6 +1023,51 @@ Framer.blockTxn = function blockTxn(data, writer) { return data.toRaw(false, writer); }; +/** + * Create an authchallenge packet (without a header). + * @param {Buffer} data + * @param {BufferWriter?} writer - A buffer writer to continue writing from. + * @returns {Buffer} Returns a BufferWriter if `writer` was passed in. + */ + +Framer.authChallenge = function authChallenge(data, writer) { + if (writer) { + writer.writeBytes(data); + return writer; + } + return data; +}; + +/** + * Create an authreply packet (without a header). + * @param {Buffer} data + * @param {BufferWriter?} writer - A buffer writer to continue writing from. + * @returns {Buffer} Returns a BufferWriter if `writer` was passed in. + */ + +Framer.authReply = function authReply(data, writer) { + if (writer) { + writer.writeBytes(data); + return writer; + } + return data; +}; + +/** + * Create an authPropose packet (without a header). + * @param {Buffer} data + * @param {BufferWriter?} writer - A buffer writer to continue writing from. + * @returns {Buffer} Returns a BufferWriter if `writer` was passed in. + */ + +Framer.authPropose = function authPropose(data, writer) { + if (writer) { + writer.writeBytes(data); + return writer; + } + return data; +}; + /* * Expose */ diff --git a/lib/bcoin/protocol/parser.js b/lib/bcoin/protocol/parser.js index fe801167..43dc6742 100644 --- a/lib/bcoin/protocol/parser.js +++ b/lib/bcoin/protocol/parser.js @@ -382,6 +382,12 @@ Parser.prototype.parsePayload = function parsePayload(cmd, p) { return Parser.parseGetBlockTxn(p); case 'blocktxn': return Parser.parseBlockTxn(p); + case 'authchallenge': + return Parser.parseAuthChallenge(p); + case 'authreply': + return Parser.parseAuthReply(p); + case 'authpropose': + return Parser.parseAuthPropose(p); default: return p; } @@ -719,6 +725,21 @@ Parser.parseBlockTxn = function parseBlockTxn(p) { return bcoin.bip152.TXResponse.fromRaw(p); }; +Parser.parseAuthChallenge = function parseAuthChallenge(p) { + // Handled elsewhere. + return p; +}; + +Parser.parseAuthReply = function parseAuthReply(p) { + // Handled elsewhere. + return p; +}; + +Parser.parseAuthPropose = function parseAuthPropose(p) { + // Handled elsewhere. + return p; +}; + /** * Packet * @constructor diff --git a/test/bip150-test.js b/test/bip150-test.js new file mode 100644 index 00000000..f25eebda --- /dev/null +++ b/test/bip150-test.js @@ -0,0 +1,204 @@ +'use strict'; + +var bn = require('bn.js'); +var bcoin = require('../').set('main'); +var utils = bcoin.utils; +var constants = bcoin.protocol.constants; +var network = bcoin.protocol.network; +var assert = require('assert'); + +describe('BIP150', function() { + var db = new bcoin.bip150.AuthDB(); + var ck = bcoin.ec.generatePrivateKey(); + var sk = bcoin.ec.generatePrivateKey(); + + db.addAuthorized(bcoin.ec.publicKeyCreate(ck, true)); + db.addKnown('server', bcoin.ec.publicKeyCreate(sk, true)); + + var client = new bcoin.bip151(); + var server = new bcoin.bip151(); + + client.bip150 = new bcoin.bip150(client, 'server', true, db, ck); + server.bip150 = new bcoin.bip150(server, 'client', false, db, sk); + + function payload() { + return new Buffer('deadbeef', 'hex'); + } + + it('should do encinit', function() { + client.encinit(server.toEncinit()); + server.encinit(client.toEncinit()); + assert(!client.handshake); + assert(!server.handshake); + }); + + it('should do encack', function() { + client.encack(server.toEncack()); + server.encack(client.toEncack()); + assert(client.handshake); + assert(server.handshake); + }); + + it('should have completed ECDH handshake', function() { + assert(client.isReady()); + assert(server.isReady()); + assert(client.handshake); + assert(server.handshake); + }); + + it('should do BIP150 handshake', function() { + var challenge = client.bip150.toChallenge(); + var reply = server.bip150.challenge(challenge); + var propose = client.bip150.reply(reply); + var challenge = server.bip150.propose(propose); + var reply = client.bip150.challenge(challenge); + var result = server.bip150.reply(reply); + assert(!result); + assert(client.bip150.auth); + assert(server.bip150.auth); + }); + + it('should encrypt payload from client to server', function() { + var packet = client.packet('fake', payload()); + var emitted = false; + server.once('packet', function(cmd, body) { + emitted = true; + assert.equal(cmd, 'fake'); + assert.equal(body.toString('hex'), 'deadbeef'); + }); + server.feed(packet); + assert(emitted); + }); + + it('should encrypt payload from server to client', function() { + var packet = server.packet('fake', payload()); + var emitted = false; + client.once('packet', function(cmd, body) { + emitted = true; + assert.equal(cmd, 'fake'); + assert.equal(body.toString('hex'), 'deadbeef'); + }); + client.feed(packet); + assert(emitted); + }); + + it('should encrypt payload from client to server (2)', function() { + var packet = client.packet('fake', payload()); + var emitted = false; + server.once('packet', function(cmd, body) { + emitted = true; + assert.equal(cmd, 'fake'); + assert.equal(body.toString('hex'), 'deadbeef'); + }); + server.feed(packet); + assert(emitted); + }); + + it('should encrypt payload from server to client (2)', function() { + var packet = server.packet('fake', payload()); + var emitted = false; + client.once('packet', function(cmd, body) { + emitted = true; + assert.equal(cmd, 'fake'); + assert.equal(body.toString('hex'), 'deadbeef'); + }); + client.feed(packet); + assert(emitted); + }); + + it('client should rekey', function() { + var rekeyed = false; + var bytes = client.output.processed; + + client.once('rekey', function() { + rekeyed = true; + var packet = client.packet('encack', client.toRekey()); + var emitted = false; + server.once('packet', function(cmd, body) { + emitted = true; + assert.equal(cmd, 'encack'); + server.encack(body); + }); + server.feed(packet); + assert(emitted); + }); + + // Force a rekey after 1gb processed. + client.output.maybeRekey({ length: 1024 * (1 << 20) }); + + assert(rekeyed); + + // Reset so as not to mess up + // the symmetry of client and server. + client.output.processed = bytes + 33 + 31; + }); + + it('should encrypt payload from client to server after rekey', function() { + var packet = client.packet('fake', payload()); + var emitted = false; + server.once('packet', function(cmd, body) { + emitted = true; + assert.equal(cmd, 'fake'); + assert.equal(body.toString('hex'), 'deadbeef'); + }); + server.feed(packet); + assert(emitted); + }); + + it('should encrypt payload from server to client after rekey', function() { + var packet = server.packet('fake', payload()); + var emitted = false; + client.once('packet', function(cmd, body) { + emitted = true; + assert.equal(cmd, 'fake'); + assert.equal(body.toString('hex'), 'deadbeef'); + }); + client.feed(packet); + assert(emitted); + }); + + it('should encrypt payload from client to server after rekey (2)', function() { + var packet = client.packet('fake', payload()); + var emitted = false; + server.once('packet', function(cmd, body) { + emitted = true; + assert.equal(cmd, 'fake'); + assert.equal(body.toString('hex'), 'deadbeef'); + }); + server.feed(packet); + assert(emitted); + }); + + it('should encrypt payload from server to client after rekey (2)', function() { + var packet = server.packet('fake', payload()); + var emitted = false; + client.once('packet', function(cmd, body) { + emitted = true; + assert.equal(cmd, 'fake'); + assert.equal(body.toString('hex'), 'deadbeef'); + }); + 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); + }); +}); diff --git a/test/bip151-test.js b/test/bip151-test.js index 93d4b260..0505c06c 100644 --- a/test/bip151-test.js +++ b/test/bip151-test.js @@ -10,6 +10,7 @@ var assert = require('assert'); describe('BIP151', function() { var client = new bcoin.bip151(); var server = new bcoin.bip151(); + function payload() { return new Buffer('deadbeef', 'hex'); } diff --git a/test/wallet-test.js b/test/wallet-test.js index 8e064e50..09e04e50 100644 --- a/test/wallet-test.js +++ b/test/wallet-test.js @@ -1069,7 +1069,7 @@ describe('Wallet', function() { assert.ifError(err); w1.importKey('default', key, 'test', function(err) { assert.ifError(err); - w1.getKeyring(key.getHash('hex'), function(err, k) { + w1.getKeyRing(key.getHash('hex'), function(err, k) { if (err) return callback(err);