397 lines
8.8 KiB
JavaScript
397 lines
8.8 KiB
JavaScript
/*!
|
|
* 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).
|
|
* https://github.com/bcoin-org/bcoin
|
|
*/
|
|
|
|
'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 chachapoly = require('./chachapoly');
|
|
|
|
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');
|
|
|
|
function BIP151(cipher, key) {
|
|
if (!(this instanceof BIP151))
|
|
return new BIP151(cipher, key);
|
|
|
|
EventEmitter.call(this);
|
|
|
|
this.publicKey = null;
|
|
this.privateKey = key || bcoin.ec.generatePrivateKey();
|
|
this.cipher = cipher || 0;
|
|
this.secret = null;
|
|
this.k1 = null;
|
|
this.k2 = null;
|
|
this.sid = null;
|
|
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;
|
|
this.hasHeader = false;
|
|
this.pending = [];
|
|
this.pendingTotal = 0;
|
|
this.waiting = 0;
|
|
}
|
|
|
|
utils.inherits(BIP151, EventEmitter);
|
|
|
|
BIP151.prototype.init = function init(publicKey) {
|
|
var p = bcoin.writer();
|
|
|
|
this.publicKey = publicKey;
|
|
this.secret = bcoin.ec.ecdh(this.publicKey, this.privateKey);
|
|
|
|
p.writeBytes(this.secret);
|
|
p.writeU8(this.cipher);
|
|
|
|
this.prk = utils.hkdfExtract(p.render(), HKDF_SALT, 'sha256');
|
|
this.k1 = utils.hkdfExpand(this.prk, INFO_KEY1, 32, 'sha256');
|
|
this.k2 = utils.hkdfExpand(this.prk, INFO_KEY2, 32, 'sha256');
|
|
this.sid = utils.hkdfExpand(this.prk, INFO_SID, 32, 'sha256');
|
|
|
|
this.seq = 0;
|
|
|
|
this.chacha.init(this.k1, this.iv());
|
|
this.aead.init(this.k2, this.iv());
|
|
this.aead.aad(this.sid);
|
|
|
|
this.lastRekey = utils.ms();
|
|
};
|
|
|
|
BIP151.prototype.isReady = function isReady() {
|
|
return this.initSent
|
|
&& this.ackReceived
|
|
&& this.initReceived
|
|
&& this.ackSent;
|
|
};
|
|
|
|
BIP151.prototype.rekey = function rekey() {
|
|
assert(this.prk, 'Cannot rekey before initialization.');
|
|
|
|
this.k1 = utils.hash256(this.k1);
|
|
this.k2 = utils.hash256(this.k2);
|
|
|
|
this.seq = 0;
|
|
|
|
this.chacha.init(this.k1, this.iv());
|
|
this.aead.init(this.k2, this.iv());
|
|
this.aead.aad(this.sid);
|
|
|
|
this.lastRekey = utils.ms();
|
|
};
|
|
|
|
BIP151.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() {
|
|
var p = bcoin.writer();
|
|
p.writeU64(this.seq);
|
|
p.writeU32(0);
|
|
return p.render();
|
|
};
|
|
|
|
BIP151.prototype.getPublicKey = function getPublicKey() {
|
|
return bcoin.ec.publicKeyCreate(this.privateKey, true);
|
|
};
|
|
|
|
BIP151.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) {
|
|
data = data.slice(0, 4);
|
|
this.chacha.encrypt(data);
|
|
return data.readUInt32LE(0, true);
|
|
};
|
|
|
|
BIP151.prototype.encrypt = function encrypt(data) {
|
|
return this.aead.encrypt(data);
|
|
};
|
|
|
|
BIP151.prototype.decrypt = function decrypt(data) {
|
|
return this.aead.decrypt(data);
|
|
};
|
|
|
|
BIP151.prototype.finish = function finish(data) {
|
|
this.tag = this.aead.finish(data);
|
|
return this.tag;
|
|
};
|
|
|
|
BIP151.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) {
|
|
var chunk, payload, tag, p, cmd, body;
|
|
|
|
this.maybeRekey(data);
|
|
|
|
while (data) {
|
|
if (!this.hasHeader) {
|
|
this.pendingHeaderTotal += data.length;
|
|
this.pendingHeader.push(data);
|
|
data = null;
|
|
|
|
if (this.pendingHeaderTotal < 4)
|
|
break;
|
|
|
|
chunk = Buffer.concat(this.pendingHeader);
|
|
|
|
this.pendingHeaderTotal = 0;
|
|
this.pendingHeader.length = 0;
|
|
|
|
this.waiting = this.decryptSize(chunk) + 16;
|
|
|
|
if (this.waiting - 32 > constants.MAX_MESSAGE) {
|
|
this.waiting = 0;
|
|
this.emit('error', new Error('Packet too large.'));
|
|
continue;
|
|
}
|
|
|
|
this.hasHeader = true;
|
|
|
|
data = chunk.slice(4);
|
|
|
|
if (data.length === 0)
|
|
break;
|
|
}
|
|
|
|
this.pendingTotal += data.length;
|
|
this.pending.push(data);
|
|
data = null;
|
|
|
|
if (this.pendingTotal < this.waiting)
|
|
break;
|
|
|
|
chunk = Buffer.concat(this.pending);
|
|
payload = chunk.slice(0, this.waiting - 16);
|
|
tag = chunk.slice(this.waiting - 16, this.waiting);
|
|
data = chunk.slice(this.waiting);
|
|
|
|
if (data.length === 0)
|
|
data = null;
|
|
|
|
this.decrypt(payload);
|
|
this.finish();
|
|
this.sequence();
|
|
|
|
this.pendingTotal = 0;
|
|
this.pending.length = 0;
|
|
this.hasHeader = false;
|
|
this.waiting = 0;
|
|
|
|
if (!this.verify(tag)) {
|
|
this.emit('error', new Error('Bad tag.'));
|
|
continue;
|
|
}
|
|
|
|
p = bcoin.reader(payload, true);
|
|
|
|
while (p.left()) {
|
|
try {
|
|
cmd = p.readVarString('ascii');
|
|
body = p.readBytes(p.readU32());
|
|
} catch (e) {
|
|
this.emit('error', e);
|
|
break;
|
|
}
|
|
|
|
this.emit('packet', cmd, body);
|
|
}
|
|
}
|
|
};
|
|
|
|
// TODO: We could batch packets here!
|
|
BIP151.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.encryptSize(payload.length).copy(packet, 0);
|
|
this.encrypt(payload).copy(packet, 4);
|
|
this.finish().copy(packet, 4 + payload.length);
|
|
this.sequence();
|
|
|
|
this.maybeRekey(packet);
|
|
|
|
return packet;
|
|
};
|
|
|
|
module.exports = BIP151;
|