From 491462f551cb42cab4b7a915de71e3632d9a961a Mon Sep 17 00:00:00 2001 From: Christopher Jeffrey Date: Sun, 15 Jan 2017 23:44:01 -0800 Subject: [PATCH] bip150: add dns resolution. comments. --- lib/net/bip150.js | 338 ++++++++++++++++++++++++++++++++++++++++---- test/bip150-test.js | 6 +- 2 files changed, 313 insertions(+), 31 deletions(-) diff --git a/lib/net/bip150.js b/lib/net/bip150.js index 5933d63a..705ebba2 100644 --- a/lib/net/bip150.js +++ b/lib/net/bip150.js @@ -18,47 +18,57 @@ var ec = require('../crypto/ec'); var StaticWriter = require('../utils/staticwriter'); var base58 = require('../utils/base58'); var encoding = require('../utils/encoding'); +var IP = require('../utils/ip'); +var dns = require('./dns'); /** - * Represents a BIP150 input and output stream. + * Represents a BIP150 input/output stream. * @exports BIP150 * @constructor * @param {BIP151} bip151 + * @param {String} host + * @param {Boolean} outbound + * @param {AuthDB} db + * @param {Buffer} key - Identity key. + * @property {BIP151} bip151 + * @property {BIP151Stream} input + * @property {BIP151Stream} output + * @property {String} hostname * @property {Boolean} outbound + * @property {AuthDB} db + * @property {Buffer} privateKey + * @property {Buffer} publicKey + * @property {Buffer} peerIdentity * @property {Boolean} challengeReceived * @property {Boolean} replyReceived * @property {Boolean} proposeReceived + * @property {Boolean} challengeSent + * @property {Boolean} auth + * @property {Boolean} completed */ -function BIP150(bip151, hostname, outbound, db, identity) { +function BIP150(bip151, host, outbound, db, key) { if (!(this instanceof BIP150)) - return new BIP150(bip151, hostname, outbound, db, identity); - - assert(bip151, 'BIP150 requires BIP151.'); - assert(typeof hostname === 'string', 'Hostname required.'); - assert(typeof outbound === 'boolean', 'Outbound flag required.'); - assert(db instanceof AuthDB, 'Auth DB required.'); - assert(Buffer.isBuffer(identity), 'Identity key required.'); + return new BIP150(bip151, host, outbound, db, key); EventEmitter.call(this); + assert(bip151, 'BIP150 requires BIP151.'); + assert(typeof host === 'string', 'Hostname required.'); + assert(typeof outbound === 'boolean', 'Outbound flag required.'); + assert(db instanceof AuthDB, 'Auth DB required.'); + assert(Buffer.isBuffer(key), 'Identity key required.'); + this.bip151 = bip151; this.input = bip151.input; this.output = bip151.output; - - this.hostname = hostname; // ip & port - - this.db = db; + this.hostname = host; this.outbound = outbound; + this.db = db; + this.privateKey = key; + this.publicKey = ec.publicKeyCreate(key, true); + this.peerIdentity = null; - - if (this.outbound) - this.peerIdentity = this.db.getKnown(this.hostname); - - // Identity keypair - this.privateKey = identity; - this.publicKey = ec.publicKeyCreate(identity, true); - this.challengeReceived = false; this.replyReceived = false; this.proposeReceived = false; @@ -67,16 +77,44 @@ function BIP150(bip151, hostname, outbound, db, identity) { this.completed = false; this.job = null; this.timeout = null; + this.onAuth = null; + + this._init(); } util.inherits(BIP150, EventEmitter); +/** + * Initialize BIP150. + * @private + */ + +BIP150.prototype._init = function _init() { + if (this.outbound) + this.peerIdentity = this.db.getKnown(this.hostname); +}; + +/** + * Test whether the state should be + * considered authed. This differs + * for inbound vs. outbound. + * @returns {Boolean} + */ + BIP150.prototype.isAuthed = function isAuthed() { if (this.outbound) return this.challengeSent && this.challengeReceived; return this.challengeReceived && this.replyReceived; }; +/** + * Handle a received challenge hash. + * Returns an authreply signature. + * @param {Buffer} hash + * @returns {Buffer} + * @throws on auth failure + */ + BIP150.prototype.challenge = function challenge(hash) { var type = this.outbound ? 'r' : 'i'; var msg, sig; @@ -104,6 +142,14 @@ BIP150.prototype.challenge = function challenge(hash) { return ec.fromDER(sig); }; +/** + * Handle a received reply signature. + * Returns an authpropose hash. + * @param {Buffer} data + * @returns {Buffer} + * @throws on auth failure + */ + BIP150.prototype.reply = function reply(data) { var type = this.outbound ? 'i' : 'r'; var sig, msg, result; @@ -138,6 +184,13 @@ BIP150.prototype.reply = function reply(data) { return this.hash(this.input.sid, 'p', this.publicKey); }; +/** + * Handle a received propose hash. + * Returns an authchallenge hash. + * @param {Buffer} hash + * @returns {Buffer} + */ + BIP150.prototype.propose = function propose(hash) { var match; @@ -162,6 +215,13 @@ BIP150.prototype.propose = function propose(hash) { return this.hash(this.output.sid, 'r', this.peerIdentity); }; +/** + * Create initial authchallenge hash + * for the peer. The peer's identity + * key must be known. + * @returns {AuthChallengePacket} + */ + BIP150.prototype.toChallenge = function toChallenge() { var msg; @@ -177,6 +237,17 @@ BIP150.prototype.toChallenge = function toChallenge() { return new packets.AuthChallengePacket(msg); }; +/** + * Derive new cipher keys based on + * BIP150 data. This differs from + * the regular key derivation of BIP151. + * @param {Buffer} sid - Sesson ID + * @param {Buffer} key - `k1` or `k2` + * @param {Buffer} req - Requesting Identity Key + * @param {Buffer} res - Response Identity Key + * @returns {Buffer} + */ + BIP150.prototype.rekey = function rekey(sid, key, req, res) { var seed = new Buffer(130); sid.copy(seed, 0); @@ -186,6 +257,11 @@ BIP150.prototype.rekey = function rekey(sid, key, req, res) { return crypto.hash256(seed); }; +/** + * Rekey the BIP151 input stream + * using BIP150-style derivation. + */ + BIP150.prototype.rekeyInput = function rekeyInput() { var stream = this.input; var req = this.peerIdentity; @@ -195,6 +271,11 @@ BIP150.prototype.rekeyInput = function rekeyInput() { stream.rekey(k1, k2); }; +/** + * Rekey the BIP151 output stream + * using BIP150-style derivation. + */ + BIP150.prototype.rekeyOutput = function rekeyOutput() { var stream = this.output; var req = this.publicKey; @@ -204,6 +285,14 @@ BIP150.prototype.rekeyOutput = function rekeyOutput() { stream.rekey(k1, k2); }; +/** + * Create a hash using the session ID. + * @param {Buffer} sid + * @param {String} ch + * @param {Buffer} key + * @returns {Buffer} + */ + BIP150.prototype.hash = function hash(sid, ch, key) { var data = new Buffer(66); sid.copy(data, 0); @@ -212,6 +301,16 @@ BIP150.prototype.hash = function hash(sid, ch, key) { return crypto.hash256(data); }; +/** + * Find an authorized peer in the Auth + * DB based on a proposal hash. Note + * that the hash to find is specific + * to the state of BIP151. This results + * in an O(n) search. + * @param {Buffer} hash + * @returns {Buffer|null} + */ + BIP150.prototype.findAuthorized = function findAuthorized(hash) { var i, key, msg; @@ -228,13 +327,29 @@ BIP150.prototype.findAuthorized = function findAuthorized(hash) { } }; +/** + * Destroy the BIP150 stream and + * any current running wait job. + */ + BIP150.prototype.destroy = function destroy() { if (this.timeout != null) { clearTimeout(this.timeout); this.timeout = null; } + + if (this.onAuth) { + this.removeListener('auth', this.onAuth); + this.onAuth = null; + } }; +/** + * Cleanup wait job. + * @private + * @returns {Job} + */ + BIP150.prototype.cleanup = function cleanup(err) { var job = this.job; @@ -249,19 +364,42 @@ BIP150.prototype.cleanup = function cleanup(err) { this.timeout = null; } + if (this.onAuth) { + this.removeListener('auth', this.onAuth); + this.onAuth = null; + } + return job; }; +/** + * Resolve the current wait job. + * @private + * @param {Object} result + */ + BIP150.prototype.resolve = function resolve(result) { var job = this.cleanup(); job.resolve(result); }; +/** + * Reject the current wait job. + * @private + * @param {Error} err + */ + BIP150.prototype.reject = function reject(err) { var job = this.cleanup(); job.reject(err); }; +/** + * Wait for handshake to complete. + * @param {Number} timeout + * @returns {Promise} + */ + BIP150.prototype.wait = function wait(timeout) { var self = this; return new Promise(function(resolve, reject) { @@ -269,6 +407,14 @@ BIP150.prototype.wait = function wait(timeout) { }); }; +/** + * Wait for handshake to complete. + * @private + * @param {Number} timeout + * @param {Function} resolve + * @param {Function} reject + */ + BIP150.prototype._wait = function wait(timeout, resolve, reject) { var self = this; @@ -276,23 +422,36 @@ BIP150.prototype._wait = function wait(timeout, resolve, reject) { this.job = co.job(resolve, reject); - if (this.outbound && !this.peerIdentity) - return this.reject(new Error('No identity for ' + this.hostname + '.')); + if (this.outbound && !this.peerIdentity) { + this.reject(new Error('No identity for ' + this.hostname + '.')); + return; + } this.timeout = setTimeout(function() { self.reject(new Error('BIP150 handshake timed out.')); }, timeout); - this.once('auth', function() { - self.resolve(); - }); + this.onAuth = this.resolve.bind(this); + this.once('auth', this.onAuth); }; +/** + * Serialize the peer's identity + * key as a BIP150 "address". + * @returns {Base58String} + */ + BIP150.prototype.getAddress = function getAddress() { assert(this.peerIdentity, 'Cannot serialize address.'); return BIP150.address(this.peerIdentity); }; +/** + * Serialize an identity key as a + * BIP150 "address". + * @returns {Base58String} + */ + BIP150.address = function address(key) { var bw = new StaticWriter(27); bw.writeU8(0x0f); @@ -312,16 +471,41 @@ function AuthDB(options) { if (!(this instanceof AuthDB)) return new AuthDB(options); + this.logger = null; + this.resolve = dns.resolve; + this.proxyServer = null; + this.known = {}; this.authorized = []; + this.dns = []; this._init(options); } +/** + * Initialize authdb with options. + * @param {Object} options + */ + AuthDB.prototype._init = function _init(options) { if (!options) return; + if (options.logger != null) { + assert(typeof options.logger === 'object'); + this.logger = options.logger; + } + + if (options.resolve != null) { + assert(typeof options.resolve === 'function'); + this.resolve = options.resolve; + } + + if (options.proxyServer != null) { + assert(typeof options.proxyServer === 'string'); + this.proxyServer = options.proxyServer; + } + if (options.knownPeers != null) { assert(typeof options.knownPeers === 'object'); this.setKnown(options.knownPeers); @@ -333,19 +517,46 @@ AuthDB.prototype._init = function _init(options) { } }; +/** + * Add a known peer. + * @param {String} host - Peer Hostname + * @param {Buffer} key - Identity Key + */ + AuthDB.prototype.addKnown = function addKnown(host, key) { + var addr; + assert(typeof host === 'string'); assert(Buffer.isBuffer(key) && key.length === 33, 'Invalid public key for known peer.'); + + addr = IP.parseHost(host); + + // Defer this for resolution. + if (addr.version === -1) { + this.dns.push([addr, key]); + return; + } + this.known[host] = key; }; +/** + * Add an authorized peer. + * @param {Buffer} key - Identity Key + */ + AuthDB.prototype.addAuthorized = function addAuthorized(key) { assert(Buffer.isBuffer(key) && key.length === 33, 'Invalid public key for authorized peer.'); this.authorized.push(key); }; +/** + * Initialize known peers with a host->key map. + * @param {Object} map + */ + AuthDB.prototype.setKnown = function setKnown(map) { var keys = Object.keys(map); var i, host, key; @@ -357,6 +568,11 @@ AuthDB.prototype.setKnown = function setKnown(map) { } }; +/** + * Initialize authorized peers with a list of keys. + * @param {Buffer[]} keys + */ + AuthDB.prototype.setAuthorized = function setAuthorized(keys) { var i, key; @@ -366,10 +582,76 @@ AuthDB.prototype.setAuthorized = function setAuthorized(keys) { } }; -AuthDB.prototype.getKnown = function getKnown(host) { - return this.known[host]; +/** + * Get a known peer key by hostname. + * @param {String} hostname + * @returns {Buffer|null} + */ + +AuthDB.prototype.getKnown = function getKnown(hostname) { + var known = this.known[hostname]; + var addr; + + if (known) + return known; + + addr = IP.parseHost(hostname); + + return this.known[addr.host]; }; +/** + * Lookup any dns-based known peers. + * @private + * @returns {Promise} + */ + +AuthDB.prototype.discover = co(function* discover() { + var jobs = []; + var i, addr; + + for (i = 0; i < this.dns.length; i++) { + addr = this.dns[i]; + jobs.push(this.populate(addr[0], addr[1])); + } + + this.dns.length = 0; + + yield Promise.all(jobs); +}); + +/** + * Populate known peers with a dns-based host. + * @private + * @param {Object} addr + * @param {Buffer} key + * @returns {Promise} + */ + +AuthDB.prototype.populate = co(function* populate(addr, key) { + var i, hosts, host; + + assert(addr.version === -1); + + if (this.logger) + this.logger.info('Resolving authorized hosts from: %s.', addr.host); + + try { + hosts = yield this.resolve(addr.host, this.proxyServer); + } catch (e) { + return; + } + + for (i = 0; i < hosts.length; i++) { + host = hosts[i]; + + if (addr.port !== 0) + host = IP.hostname(host, addr.port); + + this.known[host] = key; + } +}); + /* * Expose */ diff --git a/test/bip150-test.js b/test/bip150-test.js index 528d2ce3..7f48fea8 100644 --- a/test/bip150-test.js +++ b/test/bip150-test.js @@ -11,13 +11,13 @@ describe('BIP150', function() { var sk = ec.generatePrivateKey(); db.addAuthorized(ec.publicKeyCreate(ck, true)); - db.addKnown('server', ec.publicKeyCreate(sk, true)); + db.addKnown('127.0.0.2', ec.publicKeyCreate(sk, true)); var client = new BIP151(); var server = new BIP151(); - client.bip150 = new BIP150(client, 'server', true, db, ck); - server.bip150 = new BIP150(server, 'client', false, db, sk); + client.bip150 = new BIP150(client, '127.0.0.2', true, db, ck); + server.bip150 = new BIP150(server, '127.0.0.1', false, db, sk); function payload() { return new Buffer('deadbeef', 'hex');