From 1c9d283f261b84ebe1b71a695b2b36d9927b9e2d Mon Sep 17 00:00:00 2001 From: Christopher Jeffrey Date: Tue, 26 Jul 2016 14:07:06 -0700 Subject: [PATCH] bip151: docs. --- lib/bcoin/bip151.js | 206 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 203 insertions(+), 3 deletions(-) diff --git a/lib/bcoin/bip151.js b/lib/bcoin/bip151.js index cf5b4201..b3fa995c 100644 --- a/lib/bcoin/bip151.js +++ b/lib/bcoin/bip151.js @@ -1,8 +1,13 @@ /*! * bip151.js - peer-to-peer communication encryption. - * See: https://github.com/bitcoin/bips/blob/master/bip-0151.mediawiki - * Copyright (c) 2014-2016, Christopher Jeffrey (MIT License). + * Copyright (c) 2016, Christopher Jeffrey (MIT License). * https://github.com/bcoin-org/bcoin + * Resources: + * https://github.com/bitcoin/bips/blob/master/bip-0151.mediawiki + * https://github.com/openssh/openssh-portable/blob/master/PROTOCOL.chacha20poly1305 + * https://github.com/openssh/openssh-portable/blob/master/cipher-chachapoly.c + * https://github.com/openssh/openssh-portable/blob/master/cipher.c + * https://github.com/openssh/openssh-portable/blob/master/packet.c */ 'use strict'; @@ -14,11 +19,37 @@ var assert = utils.assert; var constants = bcoin.protocol.constants; var chachapoly = require('./chachapoly'); +/* + * Constants + */ + var HKDF_SALT = new Buffer('bitcoinechd' /* ecHd (sic?) */, 'ascii'); var INFO_KEY1 = new Buffer('BitcoinK1', 'ascii'); var INFO_KEY2 = new Buffer('BitcoinK2', 'ascii'); var INFO_SID = new Buffer('BitcoinSessionID', 'ascii'); +/** + * 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 + * @property {Buffer} prk + * @property {Buffer} k1 + * @property {Buffer} k2 + * @property {Buffer} sid + * @property {ChaCha20} chacha + * @property {AEAD} aead + * @property {Buffer} tag + * @property {Number} seq + * @property {Number} highWaterMark + * @property {Number} processed + * @property {Number} lastKey + */ + function BIP151Stream(cipher, key) { if (!(this instanceof BIP151Stream)) return new BIP151Stream(cipher, key); @@ -55,6 +86,12 @@ function BIP151Stream(cipher, key) { utils.inherits(BIP151Stream, EventEmitter); +/** + * Initialize the stream with peer's public key. + * Computes ecdh secret and chacha keys. + * @param {Buffer} publicKey + */ + BIP151Stream.prototype.init = function init(publicKey) { var p = bcoin.writer(); @@ -78,6 +115,12 @@ BIP151Stream.prototype.init = function init(publicKey) { this.lastRekey = utils.now(); }; +/** + * Add buffer size to `processed`, + * check whether we need to rekey. + * @param {Buffer} data + */ + BIP151Stream.prototype.maybeRekey = function maybeRekey(data) { var now = utils.now(); @@ -92,6 +135,11 @@ BIP151Stream.prototype.maybeRekey = function maybeRekey(data) { } }; +/** + * Generate new chacha keys with `key = HASH256(key)`. + * This will reinitialize the state of both ciphers. + */ + BIP151Stream.prototype.rekey = function rekey() { assert(this.prk, 'Cannot rekey before initialization.'); @@ -105,6 +153,13 @@ BIP151Stream.prototype.rekey = function rekey() { this.aead.aad(this.sid); }; +/** + * Increment packet sequence number and update IVs + * (note, sequence number overflows after 2^32-1). + * The IV will be updated without reinitializing + * cipher state. + */ + BIP151Stream.prototype.sequence = function sequence() { this.seq++; @@ -119,6 +174,11 @@ BIP151Stream.prototype.sequence = function sequence() { this.aead.aad(this.sid); }; +/** + * Render the IV necessary for cipher streams. + * @returns {Buffer} + */ + BIP151Stream.prototype.iv = function iv() { var p = bcoin.writer(); p.writeU64(this.seq); @@ -126,39 +186,86 @@ BIP151Stream.prototype.iv = function iv() { return p.render(); }; +/** + * Get public key tied to private key + * (not the same as BIP151Stream#privateKey). + * @returns {Buffer} + */ + BIP151Stream.prototype.getPublicKey = function getPublicKey() { return bcoin.ec.publicKeyCreate(this.privateKey, true); }; +/** + * Encrypt a payload size with k1. + * @param {Number} size + * @returns {Buffer} + */ + BIP151Stream.prototype.encryptSize = function encryptSize(size) { var data = new Buffer(4); data.writeUInt32LE(size, 0, true); return this.chacha.encrypt(data); }; +/** + * Decrypt payload size with k1. + * @param {Buffer} data + * @returns {Number} + */ + BIP151Stream.prototype.decryptSize = function decryptSize(data) { data = data.slice(0, 4); this.chacha.encrypt(data); return data.readUInt32LE(0, true); }; +/** + * Encrypt payload with AEAD (update cipher and mac). + * @param {Buffer} data + * @returns {Buffer} data + */ + BIP151Stream.prototype.encrypt = function encrypt(data) { return this.aead.encrypt(data); }; +/** + * Decrypt payload with AEAD (update cipher and mac). + * @param {Buffer} data + * @returns {Buffer} data + */ + BIP151Stream.prototype.decrypt = function decrypt(data) { return this.aead.decrypt(data); }; +/** + * Finalize AEAD and compute MAC. + * @returns {Buffer} + */ + BIP151Stream.prototype.finish = function finish() { this.tag = this.aead.finish(); return this.tag; }; +/** + * Verify tag against mac in constant time. + * @param {Buffer} tag + * @returns {Boolean} + */ + BIP151Stream.prototype.verify = function verify(tag) { return chachapoly.Poly1305.verify(this.tag, tag); }; +/** + * Parse a ciphertext payload chunk. + * Potentially emits a `packet` event. + * @param {Buffer} data + */ + BIP151Stream.prototype.feed = function feed(data) { var chunk, payload, tag, p, cmd, body; @@ -241,7 +348,13 @@ BIP151Stream.prototype.feed = function feed(data) { } }; -// TODO: We could batch packets here! +/** + * 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; @@ -264,6 +377,24 @@ BIP151Stream.prototype.packet = function packet(cmd, body) { return packet; }; +/** + * Represents a BIP151 input and output stream. + * Holds state for peer communication. + * @exports BIP151 + * @constructor + * @param {Number} cipher + * @property {BIP151Stream} input + * @property {BIP151Stream} output + * @property {Boolean} initReceived + * @property {Boolean} ackReceived + * @property {Boolean} initSent + * @property {Boolean} ackSent + * @property {Object} timeout + * @property {Function} callback + * @property {Boolean} completed + * @property {Boolean} handshake + */ + function BIP151(cipher) { if (!(this instanceof BIP151)) return new BIP151(cipher); @@ -287,6 +418,11 @@ function BIP151(cipher) { utils.inherits(BIP151, EventEmitter); +/** + * Initialize BIP151. Bind to events. + * @private + */ + BIP151.prototype._init = function _init() { var self = this; @@ -299,6 +435,11 @@ BIP151.prototype._init = function _init() { }); }; +/** + * Test whether handshake has completed. + * @returns {Boolean} + */ + BIP151.prototype.isReady = function isReady() { return this.initSent && this.ackReceived @@ -306,6 +447,12 @@ BIP151.prototype.isReady = function isReady() { && this.ackSent; }; +/** + * Render an `encinit` packet. Contains the + * input public key and cipher number. + * @returns {Buffer} + */ + BIP151.prototype.toEncinit = function toEncinit(writer) { var p = bcoin.writer(writer); @@ -320,6 +467,11 @@ BIP151.prototype.toEncinit = function toEncinit(writer) { return p; }; +/** + * Handle `encack` from remote peer. + * @param {Buffer} data + */ + BIP151.prototype.encack = function encack(data) { var p = bcoin.reader(data); var publicKey = p.readBytes(33); @@ -343,6 +495,11 @@ BIP151.prototype.encack = function encack(data) { } }; +/** + * Handle `encinit` from remote peer. + * @param {Buffer} + */ + BIP151.prototype.encinit = function encinit(data) { var p = bcoin.reader(data); var publicKey = p.readBytes(33); @@ -356,6 +513,12 @@ BIP151.prototype.encinit = function encinit(data) { this.output.init(publicKey); }; +/** + * Render `encack` packet. Contains the + * output stream public key. + * @returns {Buffer} + */ + BIP151.prototype.toEncack = function toEncack(writer) { var p = bcoin.writer(writer); @@ -377,6 +540,13 @@ BIP151.prototype.toEncack = function toEncack(writer) { return p; }; +/** + * Render `encack` packet with an all + * zero public key, notifying of a rekey + * for the output stream. + * @returns {Buffer} + */ + BIP151.prototype.toRekey = function toRekey(writer) { var p = bcoin.writer(writer); @@ -388,6 +558,12 @@ BIP151.prototype.toRekey = function toRekey(writer) { return p; }; +/** + * Complete the timeout for handshake, + * possibly with an error. + * @param {Error?} err + */ + BIP151.prototype.complete = function complete(err) { assert(!this.completed, 'Already completed.'); assert(this.callback, 'No completion callback.'); @@ -401,6 +577,12 @@ BIP151.prototype.complete = function complete(err) { this.callback = null; }; +/** + * Set a timeout and wait for handshake to complete. + * @param {Number} timeout - Timeout in ms. + * @param {Function} callback + */ + BIP151.prototype.wait = function wait(timeout, callback) { var self = this; @@ -417,12 +599,30 @@ BIP151.prototype.wait = function wait(timeout, callback) { }); }; +/** + * Feed ciphertext payload chunk + * to the input stream. Potentially + * emits a `packet` event. + * @param {Buffer} data + */ + BIP151.prototype.feed = function feed(data) { return this.input.feed(data); }; +/** + * Frame plaintext payload for the output stream. + * @param {String} cmd + * @param {Buffer} body + * @returns {Buffer} Ciphertext payload + */ + BIP151.prototype.packet = function packet(cmd, body) { return this.output.packet(cmd, body); }; +/* + * Expose + */ + module.exports = BIP151;