otr-transport-enet
Version:
client/server OTR over enet
380 lines (308 loc) • 8.45 kB
JavaScript
var enet = require("enet");
var otr = require("otr4-em");
var EventEmitter = require("events").EventEmitter;
var Stream = require("stream");
var uuid = require("node-uuid");
var DecryptStream = require('./cryptostream.js').DecryptStream;
var EncryptStream = require('./cryptostream.js').EncryptStream;
module.exports.createServer = function (options) {
var server = {}; //otr transport server
var host, listening = false;
options.client = false;
//todo check options.account has key and instag
server.listen = function (callback) {
if (host || listening) {
throw (new Error("already listening"));
}
listening = true;
enet.createServer({
address: new enet.Address(options.address || "0.0.0.0", options.port || 6666),
peers: options.maxpeers || 32,
channels: 2,
down: 0,
up: 0
}, function (err, _host) {
if (err) {
callback(err);
return;
}
host = _host;
host.on("connect", function (peer, data) {
console.error("incoming connection.");
setupStreams(peer, options, undefined, function (err, fingerprint, otr_stream,
aes_stream) {
if (err) {
console.error("connection failed:", err);
return;
}
var connection = {
"otr": otr_stream,
"aes": aes_stream,
"address": peer.address().address,
"port": peer.address().port,
"fingerprint": fingerprint
};
options.onConnect(connection);
peer.on("disconnect", function () {
if (typeof connection.onDisconnect === 'function')
connection.onDisconnect.call();
});
connection.disconnect = function () {
if (peer) peer.disconnectLater();
};
connection.connected = function () {
return (peer ? peer.state() === enet.PEER_STATE.CONNECTED :
false);
};
});
});
host.start();
callback();
});
};
server.close = function () {
setTimeout(function () {
host.stop();
}, 100);
};
return server;
};
module.exports.createClient = function (options) {
var error;
var host; //enet host
var client = {}; //otr transport client
//todo check options.account has key and instag
options.client = true;
//grab the host immediately
host = enet.createClient({
peers: 32,
channels: 2,
down: 0,
up: 0
}, function (err) {
error = err;
return;
});
client.connect = function (address, callback, fingerprint) {
if (error || !host) {
//host is destroyed or was not created - throw it when client tries to connect
callback(error || "client-destroyed");
return;
}
var peer;
var connecting = false;
var connection = {};
peer = host.connect(address);
connecting = true;
peer.on("connect", function () {
connecting = false;
setupStreams(peer, options, fingerprint, function (err, fingerprint, otr_stream, aes_stream) {
if (err) {
peer = undefined;
callback(err);
return;
}
connection.otr = otr_stream;
connection.aes = aes_stream;
connection.address = peer.address().address;
connection.port = peer.address().port;
connection.fingerprint = fingerprint;
callback(undefined, connection);
});
});
peer.on("disconnect", function () {
if (connecting) {
setTimeout(function () {
callback("connect-timeout");
});
}
peer = undefined;
connecting = false;
if (typeof connection.onDisconnect === 'function') connection.onDisconnect.call();
});
connection.disconnect = function () {
if (peer) peer.disconnectLater();
peer = undefined;
};
connection.connected = function () {
return (peer ? peer.state() === enet.PEER_STATE.CONNECTED : false);
};
};
client.close = function () {
setTimeout(function () {
host.stop();
host = undefined;
}, 100);
};
return client;
};
function setupStreams(peer, options, fingerprint, callback) {
var chan0 = peer.createDuplexStream(0);
var remote = options.account.contact(uuid.v4());
var session = remote.openSession({
policy: otr.POLICY.ALWAYS
});
//internally handle OTR fragments from network and inject fragments to network
var fragments = otrFragmentsStream(session);
chan0.pipe(fragments).pipe(chan0);
peer.on("disconnect", function () {
chan0.unpipe(fragments);
fragments.unpipe(chan0);
fragments.end();
});
chan0.on("error", function (e) {
//console.error("error on channel0:", e);
});
function terminate(reason) {
session.end();
peer.disconnectLater();
callback(reason);
}
session.on("gone_secure", function () {
clearTimeout(goSecureTimeout);
var sendingKey, receivingKey;
//todo client access-control on fingerprint
//client verifies fingerprint of server..
if (options.client && fingerprint) {
if (fingerprint !== session.theirFingerprint()) {
terminate("invalid-server-fingerprint");
return;
}
}
//setup extra symmetric keys for AES stream - each side will tell the other they wish to use the extra symmetric key
//which will result in two, keys being computed, one for sending and one for receiving.
if (options.client) {
session.extraSymKey(1, 'aes', function (key) {
sendingKey = key;
});
}
var extraKeyTimeout = setTimeout(function () {
terminate("otr-extra-key-timeout");
}, 10000);
function keysExchanged(use, usedata) {
callback(undefined, session.theirFingerprint(), otrMessagesStream(session, peer),
aesStream(peer, sendingKey, receivingKey, use, usedata));
}
session.on("received_symkey", function (use, usedata, key) {
clearTimeout(extraKeyTimeout);
receivingKey = key;
if (options.client) {
keysExchanged(use, usedata);
} else {
session.extraSymKey(use, 'aes', function (key) {
sendingKey = key;
keysExchanged(use, usedata);
});
}
});
});
if (options.client) {
session.start(); //client starts otr protocol
}
var goSecureTimeout = setTimeout(function () {
terminate("otr-timeout");
}, 30000);
}
function aesStream(peer, sendingKey, receivingKey, useNum, useData) {
var chan = peer.createDuplexStream(useNum);
var stream = new Stream.Duplex();
//review http://tools.ietf.org/html/rfc3686 to do proper AES stream encryption
var encrypt = new EncryptStream(sendingKey.toString());
var decrypt = new DecryptStream(receivingKey.toString());
chan.on("error", function (e) {
//console.error("error on channel1:", e);
});
peer.on("disconnect", function () {
stream.push(null);
stream.end();
});
encrypt.pipe(chan).pipe(decrypt);
stream._write = function (buf, enc, next) {
if (peer.state() === enet.PEER_STATE.CONNECTED) {
encrypt.write(buf);
next();
return;
}
next("peer-not-connected");
};
stream._read = function () {};
decrypt.on("data", function (buf) {
stream.push(buf);
});
return stream;
}
function otrMessagesStream(session, peer) {
var s = new Stream.Duplex();
s._write = function (buf, enc, next) {
if (peer.state() !== enet.PEER_STATE.CONNECTED) {
next("peer-not-connected");
return;
}
if (!session.isEncrypted()) {
next("otr-session-ended");
return;
}
session.send(buf);
next();
};
s._read = function () {};
session.on("disconnect", function () {
s.push(null);
s.end();
});
peer.on("disconnect", function () {
session.end();
s.push(null);
s.end();
});
session.on("message", function (msg, encrypted) {
if (encrypted) {
s.push(msg);
}
});
s.on("finish", function () {
s.push(null);
if (peer.state() === enet.PEER_STATE.CONNECTED) session.end();
});
return s;
}
function otrFragmentsStream(session) {
var s = new Stream.Duplex();
s._write = function (buf, enc, next) {
session.recv(buf);
next();
};
s._read = function () {};
session.on("inject_message", function (msg) {
s.push(msg);
});
return s;
}
/** Return an otr.Account
* loads key from file system or generates a new key if not found
*/
module.exports.account = function (options, callback) {
var name = options.name || "default";
var keys = './keys-' + name;
keys = options.keys || keys;
var user = new otr.User({
keys: keys
});
var account = user.account(name, "otr.transport");
if (account.fingerprint()) {
account.generateInstag();
callback(account);
return;
}
console.log("generating new key...");
account.generateKey(function (err) {
if (err) {
account = undefined;
console.log("error generating key:", err);
} else {
user.saveKeysToFS(keys);
account.generateInstag();
}
callback(account);
});
};