UNPKG

otr4

Version:

OTR Messaging Protocol [native libotr4.x.x]

464 lines (418 loc) 14.6 kB
var libotr = require("../build/Release/otrnat"); var otr = require("../index.js"); var util = require('util'); var events = require('events'); var str2ab = require("./str2ab.js"); module.exports.Session = Session; util.inherits(Session, events.EventEmitter); var nextSessionID = (function () { var COUNTER = 0; return (function () { COUNTER = COUNTER + 1; return COUNTER; }); })(); var nextTick = process.nextTick; /** Session class is an EventEmitter, that handles the OTR protocol. * You can optionally add an online() method to the instance which should return true if contact is online. * This determines wether to send hearbeats or not. If method is not added contact is assumed to be online. * @constructor * @alias module:otr.Session * @param {User} user - * @param {Account} account - * @param {Contact} contact - * @param {OTRParams} [parameters] - */ function Session(user, account, contact, parameters) { var session = this; events.EventEmitter.call(session); session._id = nextSessionID(); session.user = user; session.context = new libotr.ConnContext(user.state, account.name(), account.protocol(), contact.name()); session.parameters = parameters; session.ops = new libotr.MessageAppOps( function (o) { if (parameters && parameters.debug) { console.error(o); } switch (o.EVENT) { /** * "smp" Session event, combines multiple smp event types * * @event module:otr.Session#smp * @type {function} * @param {string} type - one of "failed", "request","complete", "aborted" * @param {string} [question] - if an smp request came with a question */ case "smp_error": session.smpAbort(); session.emit("smp", "failed"); return; case "smp_request": session.emit("smp", "request", o.question); return; case "smp_complete": session.emit("smp", "complete"); return; case "smp_failed": session.emit("smp", "failed"); return; case "smp_aborted": session.emit("smp", "aborted"); return; case "is_logged_in": if (typeof session.online === 'function') { if (session.online()) return 1; return 0; } return 1; //remote party is assumed to be online case "gone_secure": /** "gone_secure" Session event, is called when connection becomes encrypted * @event module:otr.Session#gone_secure */ session.emit(o.EVENT); return; case "gone_insecure": //never get's called by libotr4.0.0? session.emit(o.EVENT); return; case "policy": if (!session.parameters) { return otr.POLICY.DEFAULT; } if (typeof session.parameters.policy === 'number') { return (session.parameters.policy); //todo: validate policy } return otr.POLICY.DEFAULT; case "max_message_size": if (!session.parameters) return 0; return session.parameters.MTU || 0; case "inject_message": /** "inject_message" Session event, is fired when a message fragment should be sent to contact * @event module:otr.Session#inject_message * @type {function} * @param {string} message - message fragment to be sent. */ if (!session.listeners(o.EVENT).length) { if (parameters && parameters.debug) console.error("no listeners for inject_message event"); } session.emit(o.EVENT, o.message); return; case "new_fingerprint": /** "new_fingerprint" Session event is fired when we see a fingerprint not on file. * This is fired before gone_secure event and will be followed by write_fingerprints event. * @event module:otr.Session#new_fingerprint * @type {function} * @param {string} fingerprint */ session.emit(o.EVENT, o.fingerprint); return; case "write_fingerprints": /** "write_fingerprints" Session event is fired when a new fingerprint is seen and when a fingerprint is * successfully authenticated with SMP * @event module:otr.Session#write_fingerprints */ session.emit(o.EVENT); return; case "still_secure": /** "still_secure" Session event is fired when otr session is re-negotiated. * @event module:otr.Session#still_secure */ session.emit(o.EVENT); return; case "msg_event": /** "msg_event" Session event is fired when some type of exceptional event has occured that your application may want to be aware of. Your application may want to write an event to a log file, display a message to the user, or ignore the event. While it is not required to implement this operation, it is probably a good idea. * @event module:otr.Session#msg_event * @type {function} * @param {Object} msgevent - properties value (otr.MSGEVENT),name,message,error */ session.emit(o.EVENT, otr.MSGEVENT.make(o.event, o.message, o.err)); return; case "received_symkey": /** "received_symkey" Session event is fired when contact has decided to use the extra symmetric key * @event module:otr.Session#received_symkey * @type {function} * @param {number} use * @param {ArrayBuffer} usedata * @param {ArrayBuffer} key */ session.emit(o.EVENT, o.use, (new Uint8Array(o.usedata)).buffer, (new Uint8Array(o.key)).buffer); return; case "remote_disconnected": /** "disconnect" Session event is fired when contact ends the private conversation. The session will be in Finished state. * @event module:otr.Session#disconnect */ session.emit("disconnect"); return; case "update_context_list": /** "update_context_list" Session event is fired when session state changes, * to inform application to update UI/visual state of a session. * @event module:otr.Session#update_context_list */ session.emit(o.EVENT); return; case "create_privkey": /** "create_privkey" Session event will be raised if an OTR conversation was attempted an the account does not have an OTR key. * It is a good practice to make sure a private key is generated before starting a session. * @event module:otr.Session#create_privkey */ if (!session.listeners(o.EVENT).length) { if (parameters && parameters.debug) console.error("no listeners for create_privkey event"); } session.emit(o.EVENT, o.accountname, o.protocol); return; case "create_instag": if (!session.listeners(o.EVENT).length) { if (parameters && parameters.debug) console.error("no listeners for create_instag event"); } /** "create_instag" Session event will be raised if an OTR conversation was attempted an the account does not have an instance tag. * It is a good practice to make sure an instance tag is generated before starting a session. * @event module:otr.Session#create_instag */ session.emit(o.EVENT, o.accountname, o.protocol); return; } } ); session.message_poll_interval = setInterval(function () { user.messagePoll(session.ops, 0); }, user.getMessagePollDefaultInterval() * 1000 || 70 * 1000); } /** Sends the OTR query message to start the OTR protocol. * @method */ Session.prototype.start = function () { return this.send("?OTR?"); }; /** Send a message to contact * @method * @param {string} message - a string or object with a toString() method * @param {number} instag - instance tag of contact if known. */ Session.prototype.send = function (message, instag) { var session = this; var buff = message instanceof Buffer ? new Buffer(message) : message; nextTick(function () { instag = instag || 1; //default instag = BEST //message can be any object that can be serialsed to a string using it's .toString() method. var msgout = session.ops.messageSending(session.user.state, session.context.accountname(), session.context .protocol(), session.context.username(), buff.toString(), instag, session.context); if (msgout) { //frag policy something other than SEND_ALL.. results in a fragment to be sent manually session.emit("inject_message", msgout); } }); }; /** Pass incoming data/messages from contact to this method to be handled by OTR * @method * @param {string} message - a string or object with a toString() method */ Session.prototype.recv = function (message) { var session = this; var buff = message instanceof Buffer ? new Buffer(message) : message; nextTick(function () { //message can be any object that can be serialsed to a string using it's .toString() method. var msg = session.ops.messageReceiving(session.user.state, session.context.accountname(), session.context .protocol(), session.context.username(), buff.toString(), session.context); if (msg) { /** "message" Session event is fired when receiving a plaintext or decrypted message from contact * @event module:otr.Session#message * @type {function} * @param {string} message * @param {boolean} isEncrypted - true if message was received in private */ session.emit("message", msg, session.isEncrypted()); } }); }; /** Ends an OTR conversation and returns to plaintext. * @method */ Session.prototype.end = function () { var session = this; nextTick(function () { if (session.message_poll_interval) { clearInterval(session.message_poll_interval); } session.ops.disconnect(session.user.state, session.context.accountname(), session.context.protocol(), session.context.username(), session.context.their_instance()); /** "plaintext" Session event is fired after ending our side of the private conversation and returning to * plaintext mode. * @event module:otr.Session#plaintext */ session.emit("plaintext"); }); }; /** Starts SMP authentication * @method * @param {string} [secret] - secret to use for authentication. If not provided it will be taken from session parameters. * @throws {Error} */ Session.prototype.smpStart = function (secret) { var session = this; var sec = secret; sec = sec || (this.parameters ? this.parameters.secret : undefined); if (sec) { nextTick(function () { session.ops.initSMP(session.user.state, session.context, sec); }); } else { throw (new Error("No Secret Provided")); } }; /** Starts SMP authentication with a question. * @method * @param {string} question - question to display to contact when they receive the SMP authentication request. * @param {string} secret - secret to use for authentication. * @throws {Error} */ Session.prototype.smpStartQuestion = function (question, secret) { var session = this; if (!question) { throw (new Error("No Question Provided")); } var sec = secret; if (!sec) { sec = this.parameters ? this.parameters.secrets : undefined; if (!sec) { throw (new Error("No Secrets Provided")); } sec = sec[question]; } if (!sec) { throw (new Error("No Secret Matched for Question")); } nextTick(function () { session.ops.initSMP(session.user.state, session.context, sec, question); }); }; /** Respond to an SMP authentication request. * @method * @param {string} [secret] - secret to use for authentication. If not provided it will be taken from session parameters. * @throws {Error} */ Session.prototype.smpRespond = function (secret) { var session = this; var sec = secret || undefined; if (!sec) { sec = this.parameters.secret || undefined; } if (!sec) { throw (new Error("No Secret Provided")); } nextTick(function () { session.ops.respondSMP(session.user.state, session.context, sec); }); }; /** Abort active SMP authentication * @method */ Session.prototype.smpAbort = function () { var session = this; nextTick(function () { session.ops.abortSMP(session.user.state, session.context); }); }; /** Return true if session is encrypted (private) * @method * @returns {boolean} */ Session.prototype.isEncrypted = function () { return (this.context.msgstate() === 1); }; /** Return true if session is in Plaintext mode (not private) * @method * @returns {boolean} */ Session.prototype.isPlaintext = function () { return (this.context.msgstate() === 0); }; /** Return true if remote contact has disconnected * @method * @returns {boolean} */ Session.prototype.isFinished = function () { return (this.context.msgstate() === 2); }; /** Return true if contact's public key fingerpint is trusted. (authenticated with SMP) * @method * @returns {boolean} */ Session.prototype.isAuthenticated = function () { return (this.context.trust()); }; /** Returns the extra symmetric key and informs contact that we want to use it. The key is returned as the first * argument of the callback function. * @method * @param {number} use * @param {ArrayBuffer} usedata * @param {function} callback */ Session.prototype.extraSymKey = function (use, usedata, callback) { var ab = (typeof usedata === 'string') ? str2ab(usedata) : usedata; usedata = new Buffer(new Uint8Array(ab)); var session = this; nextTick(function () { var buf = session.ops.extraSymKey(session.user.state, session.context, use, usedata); //returns a Buffer var key = new Uint8Array(buf).buffer; callback(key); }); }; /** Returns contact's instance tag * @method * @return {number} */ Session.prototype.theirInstance = function () { return this.context.their_instance(); }; /** Returns our instance tag * @method * @return {number} */ Session.prototype.ourInstance = function () { return this.context.our_instance(); }; /** Returns the active OTR protocol version * @method * @return {number} */ Session.prototype.protocolVersion = function () { return this.context.protocol_version(); }; /** Returns the contact's human readable public key fingerprint * @method * @return {string} */ Session.prototype.theirFingerprint = function () { return this.context.fingerprint(); }; /** Call the destroy method to remove all event listeners and release resources used by the session. * @method */ Session.prototype.destroy = function () { var session = this; if (session.message_poll_interval) { clearInterval(session.message_poll_interval); } session.removeAllListeners(); this.user = undefined; this.context = undefined; this.parameters = undefined; this.ops = undefined; //todo - free allocated memory for OtrlMessageAppOps structure. }; /** * Optional OTR Session parameters. * @typedef {Object} OTRParams * @property {number} policy - {@link module:otr.POLICY POLICY} default is otr.POLICY.DEFAULT * @property {string} secret - shared secret to use during SMP authentication if not specified as argument in smp methods. * @property {number} MTU - max message fragment size in bytes, default is 0 which means no fragmentation. */