lightning-noise
Version:
Noise protocol lightning network implimentation.
501 lines (380 loc) • 10.9 kB
JavaScript
/* !
* noise.js - peer-to-peer communication encryption.
* Copyright (c) 2019, YusukeShimizu (MIT License).
* Resources:
* https://github.com/lightningnetwork/lightning-rfc/blob/master/08-transport.md
*
* Parts of this software are based on LND and hsd:
* Copyright (C) 2015-2017 The Lightning Network Developers
* https://github.com/lightningnetwork/lnd/blob/master/brontide/noise.go
* https://github.com/lightningnetwork/lnd/blob/master/brontide/noise_test.go
* brontide.js - peer-to-peer communication encryption.
* Copyright (c) 2018, Christopher Jeffrey (MIT License).
*/
'use strict';
const assert = require('bsert');
const EventEmitter = require('events');
const sha256 = require('bcrypto/lib/sha256');
const aead = require('bcrypto/lib/aead');
const hkdf = require('bcrypto/lib/hkdf');
const secp256k1 = require('bcrypto/lib/secp256k1');
/*
* Constants
*/
const ZERO_KEY = Buffer.alloc(32, 0x00);
const ZERO_PUB = Buffer.alloc(33, 0x00);
const EMPTY = Buffer.alloc(0);
const PROTOCOL_NAME = 'Noise_XK_secp256k1_ChaChaPoly_SHA256';
const PROLOGUE = 'lightning';
const ROTATION_INTERVAL = 1000;
const VERSION = 0;
const ACT_ONE_SIZE = 50;
const ACT_TWO_SIZE = 50;
const ACT_THREE_SIZE = 66;
/**
* CipherState
* @extends {EventEmitter}
*/
class CipherState extends EventEmitter {
constructor() {
super();
this.nonce = 0;
this.iv = Buffer.alloc(12, 0x00);
this.key = ZERO_KEY; // secret key
this.salt = ZERO_KEY;
}
update() {
this.iv.writeUInt32LE(this.nonce, 4, true);
return this.iv;
}
initKey(key) {
assert(Buffer.isBuffer(key));
this.key = key;
this.nonce = 0;
this.update();
return this;
}
initSalt(key, salt) {
assert(Buffer.isBuffer(salt));
this.salt = salt;
this.initKey(key);
return this;
}
rotateKey() {
const info = EMPTY;
const old = this.key;
const [salt, next] = expand(old, this.salt, info);
this.salt = salt;
this.initKey(next);
return this;
}
encrypt(pt, ad) {
const tag = aead.encrypt(this.key, this.iv, pt, ad);
this.nonce += 1;
this.update();
if (this.nonce === ROTATION_INTERVAL) {
this.rotateKey();
}
return tag;
}
decrypt(ct, tag, ad) {
if (!aead.decrypt(this.key, this.iv, ct, tag, ad)) {
return false;
}
this.nonce += 1;
this.update();
if (this.nonce === ROTATION_INTERVAL) {
this.rotateKey();
}
return true;
}
}
/**
* SymmetricState
* @extends {CipherState}
*/
class SymmetricState extends CipherState {
constructor() {
super();
this.chain = ZERO_KEY; // chaining key
this.temp = ZERO_KEY; // temp key
this.digest = ZERO_KEY; // handshake digest
}
initSymmetric(protocolName) {
assert(typeof protocolName === 'string');
const empty = ZERO_KEY;
const proto = Buffer.from(protocolName, 'ascii');
this.digest = sha256.digest(proto);
this.chain = this.digest;
this.initKey(empty);
return this;
}
mixKey(input) {
const info = EMPTY;
const secret = input;
const salt = this.chain;
[this.chain, this.temp] = expand(secret, salt, info);
this.initKey(this.temp);
return this;
}
mixDigest(data, tag) {
return sha256.multi(this.digest, data, tag);
}
mixHash(data, tag) {
this.digest = this.mixDigest(data, tag);
return this;
}
encryptHash(pt) {
const tag = this.encrypt(pt, this.digest);
this.mixHash(pt, tag);
return tag;
}
decryptHash(ct, tag) {
assert(Buffer.isBuffer(tag));
const digest = this.mixDigest(ct, tag);
if (!this.decrypt(ct, tag, this.digest)) {
return false;
}
this.digest = digest;
return true;
}
}
/**
* HandshakeState
* @extends {SymmetricState}
*/
class HandshakeState extends SymmetricState {
constructor() {
super();
this.initiator = false;
this.localStatic = ZERO_KEY;
this.localEphemeral = ZERO_KEY;
this.remoteStatic = ZERO_PUB;
this.remoteEphemeral = ZERO_PUB;
this.generateKey = () => secp256k1.privateKeyGenerate();
}
initState(initiator, prologue, localPub, remotePub) {
assert(typeof initiator === 'boolean');
assert(typeof prologue === 'string');
assert(Buffer.isBuffer(localPub));
assert(!remotePub || Buffer.isBuffer(remotePub));
this.initiator = initiator;
this.localStatic = localPub; // private
this.remoteStatic = remotePub || ZERO_PUB;
this.initSymmetric(PROTOCOL_NAME);
this.mixHash(Buffer.from(prologue, 'ascii'));
if (initiator) {
this.mixHash(remotePub);
} else {
const pub = getPublic(localPub);
this.mixHash(pub);
}
return this;
}
}
/**
* Noise
* @extends {HandshakeState}
*/
class Noise extends HandshakeState {
constructor() {
super();
this.sendCipher = new CipherState();
this.recvCipher = new CipherState();
}
init(initiator, localPub, remotePub) {
return this.initState(initiator, PROLOGUE, localPub, remotePub);
}
genActOne() {
// e
this.localEphemeral = this.generateKey();
const ephemeral = getPublic(this.localEphemeral);
this.mixHash(ephemeral);
// es
const s = ecdh(this.remoteStatic, this.localEphemeral);
this.mixKey(s);
const tag = this.encryptHash(EMPTY);
const actOne = Buffer.allocUnsafe(ACT_ONE_SIZE);
actOne[0] = VERSION;
ephemeral.copy(actOne, 1);
tag.copy(actOne, 34);
return actOne;
}
recvActOne(actOne) {
assert(Buffer.isBuffer(actOne));
if (actOne.length !== ACT_ONE_SIZE) {
throw new Error('Act one: bad size.');
}
if (actOne[0] !== VERSION) {
throw new Error('Act one: bad version.');
}
const e = actOne.slice(1, 34);
const p = actOne.slice(34);
if (!secp256k1.publicKeyVerify(e)) {
throw new Error('Act one: bad key.');
}
// e
this.remoteEphemeral = e;
this.mixHash(this.remoteEphemeral);
// es
const s = ecdh(this.remoteEphemeral, this.localStatic);
this.mixKey(s);
if (!this.decryptHash(EMPTY, p)) {
throw new Error('Act one: bad tag.');
}
return this;
}
genActTwo() {
// e
this.localEphemeral = this.generateKey();
const ephemeral = getPublic(this.localEphemeral);
this.mixHash(ephemeral);
// ee
const s = ecdh(this.remoteEphemeral, this.localEphemeral);
this.mixKey(s);
const tag = this.encryptHash(EMPTY);
const actTwo = Buffer.allocUnsafe(ACT_TWO_SIZE);
actTwo[0] = VERSION;
ephemeral.copy(actTwo, 1);
tag.copy(actTwo, 34);
return actTwo;
}
recvActTwo(actTwo) {
assert(Buffer.isBuffer(actTwo));
if (actTwo.length !== ACT_TWO_SIZE) {
throw new Error('Act two: bad size.');
}
if (actTwo[0] !== VERSION) {
throw new Error('Act two: bad version.');
}
const e = actTwo.slice(1, 34);
const p = actTwo.slice(34);
if (!secp256k1.publicKeyVerify(e)) {
throw new Error('Act two: bad key.');
}
// e
this.remoteEphemeral = e;
this.mixHash(this.remoteEphemeral);
// ee
const s = ecdh(this.remoteEphemeral, this.localEphemeral);
this.mixKey(s);
if (!this.decryptHash(EMPTY, p)) {
throw new Error('Act two: bad tag.');
}
return this;
}
genActThree() {
const ourPubkey = getPublic(this.localStatic);
const tag1 = this.encryptHash(ourPubkey);
const ct = ourPubkey;
const s = ecdh(this.remoteEphemeral, this.localStatic);
this.mixKey(s);
const tag2 = this.encryptHash(EMPTY);
const actThree = Buffer.allocUnsafe(ACT_THREE_SIZE);
actThree[0] = VERSION;
ct.copy(actThree, 1);
tag1.copy(actThree, 34);
tag2.copy(actThree, 50);
this.split();
return actThree;
}
recvActThree(actThree) {
assert(Buffer.isBuffer(actThree));
if (actThree.length !== ACT_THREE_SIZE) {
throw new Error('Act three: bad size.');
}
if (actThree[0] !== VERSION) {
throw new Error('Act three: bad version.');
}
const s1 = actThree.slice(1, 34);
const p1 = actThree.slice(34, 50);
const s2 = actThree.slice(50, 50);
const p2 = actThree.slice(50, 66);
// s
if (!this.decryptHash(s1, p1)) {
throw new Error('Act three: bad tag.');
}
const remotePub = s1;
if (!secp256k1.publicKeyVerify(remotePub)) {
throw new Error('Act three: bad key.');
}
this.remoteStatic = remotePub;
// se
const se = ecdh(this.remoteStatic, this.localEphemeral);
this.mixKey(se);
if (!this.decryptHash(s2, p2)) {
throw new Error('Act three: bad tag.');
}
this.split();
return this;
}
split() {
const [h1, h2] = expand(EMPTY, this.chain, EMPTY);
if (this.initiator) {
const sendKey = h1;
this.sendCipher.initSalt(sendKey, this.chain);
const recvKey = h2;
this.recvCipher.initSalt(recvKey, this.chain);
} else {
const recvKey = h1;
this.recvCipher.initSalt(recvKey, this.chain);
const sendKey = h2;
this.sendCipher.initSalt(sendKey, this.chain);
}
return this;
}
write(data) {
assert(Buffer.isBuffer(data));
assert(data.length <= 0xffff);
const packet = Buffer.allocUnsafe(2 + 16 + data.length + 16);
packet.writeUInt16BE(data.length, 0);
data.copy(packet, 2 + 16);
const len = packet.slice(0, 2);
const ta1 = packet.slice(2, 18);
const msg = packet.slice(18, 18 + data.length);
const ta2 = packet.slice(18 + data.length, 18 + data.length + 16);
const tag1 = this.sendCipher.encrypt(len);
tag1.copy(ta1, 0);
const tag2 = this.sendCipher.encrypt(msg);
tag2.copy(ta2, 0);
return packet;
}
read(packet) {
assert(Buffer.isBuffer(packet));
const len = packet.slice(0, 2);
const ta1 = packet.slice(2, 18);
if (!this.recvCipher.decrypt(len, ta1)) {
throw new Error('Bad tag for header.');
}
const size = len.readUInt16BE(0, true);
assert(packet.length === 18 + size + 16);
const msg = packet.slice(18, 18 + size);
const ta2 = packet.slice(18 + size, 18 + size + 16);
if (!this.recvCipher.decrypt(msg, ta2)) {
throw new Error('Bad tag for message.');
}
return msg;
}
}
/*
* Helpers
*/
function ecdh(publicKey, privateKey) {
const secret = secp256k1.derive(publicKey, privateKey, true);
return sha256.digest(secret);
}
function getPublic(priv) {
return secp256k1.publicKeyCreate(priv, true);
}
function expand(secret, salt, info) {
const prk = hkdf.extract(sha256, secret, salt);
const out = hkdf.expand(sha256, prk, info, 64);
return [out.slice(0, 32), out.slice(32, 64)];
}
/*
* Expose
*/
exports.CipherState = CipherState;
exports.SymmetricState = SymmetricState;
exports.HandshakeState = HandshakeState;
exports.Noise = Noise;