diff options
Diffstat (limited to 'comm/chat/modules/OTR.sys.mjs')
-rw-r--r-- | comm/chat/modules/OTR.sys.mjs | 1506 |
1 files changed, 1506 insertions, 0 deletions
diff --git a/comm/chat/modules/OTR.sys.mjs b/comm/chat/modules/OTR.sys.mjs new file mode 100644 index 0000000000..33784c6bd0 --- /dev/null +++ b/comm/chat/modules/OTR.sys.mjs @@ -0,0 +1,1506 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { BasePromiseWorker } from "resource://gre/modules/PromiseWorker.sys.mjs"; +import { ctypes } from "resource://gre/modules/ctypes.sys.mjs"; +import { IMServices } from "resource:///modules/IMServices.sys.mjs"; +import { CLib } from "resource:///modules/CLib.sys.mjs"; +import { OTRLibLoader } from "resource:///modules/OTRLib.sys.mjs"; +import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; + +const lazy = {}; + +XPCOMUtils.defineLazyGetter( + lazy, + "l10n", + () => new Localization(["messenger/otr/otr.ftl"], true) +); + +function _str(id) { + return lazy.l10n.formatValueSync(id); +} + +function _strArgs(id, args) { + return lazy.l10n.formatValueSync(id, args); +} + +// some helpers + +function setInterval(fn, delay) { + let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); + timer.init(fn, delay, Ci.nsITimer.TYPE_REPEATING_SLACK); + return timer; +} + +function clearInterval(timer) { + timer.cancel(); +} + +// See: https://developer.mozilla.org/en-US/docs/Mozilla/js-ctypes/Using_js-ctypes/Working_with_data#Determining_if_two_pointers_are_equal +function comparePointers(p, q) { + p = ctypes.cast(p, ctypes.uintptr_t).value.toString(); + q = ctypes.cast(q, ctypes.uintptr_t).value.toString(); + return p === q; +} + +function trustFingerprint(fingerprint) { + return ( + !fingerprint.isNull() && + !fingerprint.contents.trust.isNull() && + fingerprint.contents.trust.readString().length > 0 + ); +} + +// Report whether you think the given user is online. Return 1 if you think +// they are, 0 if you think they aren't, -1 if you're not sure. +function isOnline(conv) { + let ret = -1; + if (conv.buddy) { + ret = conv.buddy.online ? 1 : 0; + } + return ret; +} + +/** + * + * @param {string} filename - File in the profile. + * @returns {string} Full path to given file in the profile directory. + */ +function profilePath(filename) { + return PathUtils.join( + Services.dirsvc.get("ProfD", Ci.nsIFile).path, + filename + ); +} + +// OTRLib context wrapper + +function Context(context) { + this._context = context; +} + +Context.prototype = { + constructor: Context, + get username() { + return this._context.contents.username.readString(); + }, + get account() { + return this._context.contents.accountname.readString(); + }, + get protocol() { + return this._context.contents.protocol.readString(); + }, + get msgstate() { + return this._context.contents.msgstate; + }, + get fingerprint() { + return this._context.contents.active_fingerprint; + }, + get trust() { + return trustFingerprint(this.fingerprint); + }, +}; + +// otr module + +var OTRLib; + +export var OTR = { + hasRan: false, + libLoaded: false, + once() { + this.hasRan = true; + try { + OTRLib = OTRLibLoader.init(); + if (!OTRLib) { + return; + } + if (OTRLib && OTRLib.init()) { + this.initUiOps(); + OTR.libLoaded = true; + } + } catch (e) { + console.log(e); + } + }, + + privateKeyPath: profilePath("otr.private_key"), + fingerprintsPath: profilePath("otr.fingerprints"), + instanceTagsPath: profilePath("otr.instance_tags"), + + init(opts) { + opts = opts || {}; + + if (!this.hasRan) { + this.once(); + } + + if (!OTR.libLoaded) { + return; + } + + this.userstate = OTRLib.otrl_userstate_create(); + + // A map of UIConvs, keyed on the target.id + this._convos = new Map(); + this._observers = []; + this._buffer = []; + this._pendingSystemMessages = []; + this._poll_timer = null; + + // Async sending may fail in the transport protocols, so periodically + // drop old messages from the internal buffer. Should be rare. + const pluck_time = 1 * 60 * 1000; + this._pluck_timer = setInterval(() => { + let buf = this._buffer; + let i = 0; + while (i < buf.length) { + if (Date.now() - buf[i].time > pluck_time) { + this.log("dropping an old message: " + buf[i].display); + buf.splice(i, 1); + } else { + i += 1; + } + } + this._pendingSystemMessages = this._pendingSystemMessages.filter( + info => info.time + pluck_time < Date.now() + ); + }, pluck_time); + }, + + close() { + if (this._poll_timer) { + clearInterval(this._poll_timer); + this._poll_timer = null; + } + if (this._pluck_timer) { + clearInterval(this._pluck_timer); + this._pluck_timer = null; + } + this._buffer = null; + }, + + log(msg) { + this.notifyObservers(msg, "otr:log"); + }, + + // load stored files from my profile + loadFiles() { + return Promise.all([ + IOUtils.exists(this.privateKeyPath).then(exists => { + if ( + exists && + OTRLib.otrl_privkey_read(this.userstate, this.privateKeyPath) + ) { + throw new Error("Failed to read private keys."); + } + }), + IOUtils.exists(this.fingerprintsPath).then(exists => { + if ( + exists && + OTRLib.otrl_privkey_read_fingerprints( + this.userstate, + this.fingerprintsPath, + null, + null + ) + ) { + throw new Error("Failed to read fingerprints."); + } + }), + IOUtils.exists(this.instanceTagsPath).then(exists => { + if ( + exists && + OTRLib.otrl_instag_read(this.userstate, this.instanceTagsPath) + ) { + throw new Error("Failed to read instance tags."); + } + }), + ]); + }, + + // generate a private key in a worker + generatePrivateKey(account, protocol) { + let newkey = new ctypes.void_t.ptr(); + let err = OTRLib.otrl_privkey_generate_start( + OTR.userstate, + account, + protocol, + newkey.address() + ); + if (err || newkey.isNull()) { + return Promise.reject("otrl_privkey_generate_start (" + err + ")"); + } + + let keyPtrSrc = newkey.toSource(); + let re = new RegExp( + '^ctypes\\.voidptr_t\\(ctypes\\.UInt64\\("0x([0-9a-fA-F]+)"\\)\\)$' + ); + let address; + let match = re.exec(keyPtrSrc); + if (match) { + address = match[1]; + } + + if (!address) { + OTRLib.otrl_privkey_generate_cancelled(OTR.userstate, newkey); + throw new Error( + "generatePrivateKey failed to parse ptr.toSource(): " + keyPtrSrc + ); + } + + let worker = new BasePromiseWorker("chrome://chat/content/otrWorker.js"); + return worker + .post("generateKey", [OTRLib.path, OTRLib.otrl_version, address]) + .then(function () { + let err = OTRLib.otrl_privkey_generate_finish( + OTR.userstate, + newkey, + OTR.privateKeyPath + ); + if (err) { + throw new Error("otrl_privkey_generate_calculate (" + err + ")"); + } + }) + .catch(function (err) { + if (!newkey.isNull()) { + OTRLib.otrl_privkey_generate_cancelled(OTR.userstate, newkey); + } + throw err; + }); + }, + + generatePrivateKeySync(account, protocol) { + let newkey = new ctypes.void_t.ptr(); + let err = OTRLib.otrl_privkey_generate_start( + OTR.userstate, + account, + protocol, + newkey.address() + ); + if (err || newkey.isNull()) { + return "otrl_privkey_generate_start (" + err + ")"; + } + + err = OTRLib.otrl_privkey_generate_calculate(newkey); + if (!err) { + err = OTRLib.otrl_privkey_generate_finish( + OTR.userstate, + newkey, + OTR.privateKeyPath + ); + } + if (err && !newkey.isNull()) { + OTRLib.otrl_privkey_generate_cancelled(OTR.userstate, newkey); + } + + if (err) { + return "otrl_privkey_generate_calculate (" + err + ")"; + } + return null; + }, + + // write fingerprints to file synchronously + writeFingerprints() { + if ( + OTRLib.otrl_privkey_write_fingerprints( + this.userstate, + this.fingerprintsPath + ) + ) { + throw new Error("Failed to write fingerprints."); + } + }, + + // generate instance tag synchronously + generateInstanceTag(account, protocol) { + if ( + OTRLib.otrl_instag_generate( + this.userstate, + this.instanceTagsPath, + account, + protocol + ) + ) { + throw new Error("Failed to generate instance tag."); + } + }, + + // get my fingerprint + privateKeyFingerprint(account, protocol) { + let fingerprint = OTRLib.otrl_privkey_fingerprint( + this.userstate, + new OTRLib.fingerprint_t(), + account, + protocol + ); + return fingerprint.isNull() ? null : fingerprint.readString(); + }, + + // return a human readable string for a fingerprint + hashToHuman(fingerprint) { + let hash; + try { + hash = fingerprint.contents.fingerprint; + } catch (e) {} + if (!hash || hash.isNull()) { + throw new Error("No fingerprint found."); + } + let human = new OTRLib.fingerprint_t(); + OTRLib.otrl_privkey_hash_to_human(human, hash); + return human.readString(); + }, + + base64encode(data, dataLen) { + // CData objects are initialized with zeroes. The plus one gives us + // our null byte so that readString below is safe. + let buf = ctypes.char.array(Math.floor((dataLen + 2) / 3) * 4 + 1)(); + OTRLib.otrl_base64_encode(buf, data, dataLen); // ignore returned size + return buf.readString(); // str + }, + + base64decode(str) { + let size = str.length; + // +1 here so that we're safe in calling readString on data in the tests. + let data = ctypes.unsigned_char.array(Math.floor((size + 3) / 4) * 3 + 1)(); + OTRLib.otrl_base64_decode(data, str, size); // ignore returned len + // We aren't returning the dataLen since we know the hash length in our + // one use case so far. + return data; + }, + + // Fetch list of known fingerprints, either for the given account, + // or for all accounts, if parameter is null. + knownFingerprints(forAccount) { + let fps = []; + for ( + let context = this.userstate.contents.context_root; + !context.isNull(); + context = context.contents.next + ) { + // skip child contexts + if (!comparePointers(context.contents.m_context, context)) { + continue; + } + let wContext = new Context(context); + + if (forAccount) { + if ( + forAccount.normalizedName != wContext.account || + forAccount.protocol.normalizedName != wContext.protocol + ) { + continue; + } + } + + for ( + let fingerprint = context.contents.fingerprint_root.next; + !fingerprint.isNull(); + fingerprint = fingerprint.contents.next + ) { + let trust = trustFingerprint(fingerprint); + fps.push({ + fpointer: fingerprint.contents.address(), + fingerprint: OTR.hashToHuman(fingerprint), + screenname: wContext.username, + trust, + purge: false, + }); + } + } + return fps; + }, + + /** + * Returns true, if all requested fps were removed. + * Returns false, if at least one fps couldn't get removed, + * because it's currently actively used. + */ + forgetFingerprints(fps) { + let result = true; + let write = false; + fps.forEach(function (obj, i) { + if (!obj.purge) { + return; + } + obj.purge = false; // reset early + let fingerprint = obj.fpointer; + if (fingerprint.isNull()) { + return; + } + // don't remove if fp is active and we're in an encrypted state + let context = fingerprint.contents.context.contents.m_context; + for ( + let context_itr = context; + !context_itr.isNull() && + comparePointers(context_itr.contents.m_context, context); + context_itr = context_itr.contents.next + ) { + if ( + context_itr.contents.msgstate === + OTRLib.messageState.OTRL_MSGSTATE_ENCRYPTED && + comparePointers(context_itr.contents.active_fingerprint, fingerprint) + ) { + result = false; + return; + } + } + write = true; + OTRLib.otrl_context_forget_fingerprint(fingerprint, 1); + fps[i] = null; // null out removed fps + }); + if (write) { + OTR.writeFingerprints(); + } + return result; + }, + + addFingerprint(context, hex) { + let fingerprint = new OTRLib.hash_t(); + if (hex.length != 40) { + throw new Error("Invalid fingerprint value."); + } + let bytes = hex.match(/.{1,2}/g); + for (let i = 0; i < 20; i++) { + fingerprint[i] = parseInt(bytes[i], 16); + } + return OTRLib.otrl_context_find_fingerprint( + context._context, + fingerprint, + 1, + null + ); + }, + + getFingerprintsForRecipient(account, protocol, recipient) { + let fingers = OTR.knownFingerprints(); + return fingers.filter(function (fg) { + return ( + fg.account == account && + fg.protocol == protocol && + fg.screenname == recipient + ); + }); + }, + + isFingerprintTrusted(fingerprint) { + return !!OTRLib.otrl_context_is_fingerprint_trusted(fingerprint); + }, + + // update trust in fingerprint + setTrust(fingerprint, trust, context) { + // ignore if no change in trust + if (context && trust === context.trust) { + return; + } + OTRLib.otrl_context_set_trust(fingerprint, trust ? "verified" : ""); + this.writeFingerprints(); + if (context) { + this.notifyTrust(context); + } + }, + + notifyTrust(context) { + this.notifyObservers(context, "otr:msg-state"); + this.notifyObservers(context, "otr:trust-state"); + }, + + authUpdate(context, progress, success) { + this.notifyObservers( + { + context, + progress, + success, + }, + "otr:auth-update" + ); + }, + + // expose message states + getMessageState() { + return OTRLib.messageState; + }, + + // get context from conv + getContext(conv) { + let context = OTRLib.otrl_context_find( + this.userstate, + conv.normalizedName, + conv.account.normalizedName, + // TODO: check why sometimes normalizedName is undefined, and if + // that's ok. Fallback wasn't necessary in the original code. + conv.account.protocol.normalizedName || "", + OTRLib.instag.OTRL_INSTAG_BEST, + 1, + null, + null, + null + ); + return new Context(context); + }, + + getContextFromRecipient(account, protocol, recipient) { + let context = OTRLib.otrl_context_find( + this.userstate, + recipient, + account, + protocol, + OTRLib.instag.OTRL_INSTAG_BEST, + 1, + null, + null, + null + ); + return new Context(context); + }, + + getUIConvFromContext(context) { + return this.getUIConvForRecipient( + context.account, + context.protocol, + context.username + ); + }, + + getUIConvForRecipient(account, protocol, recipient) { + let uiConvs = this._convos.values(); + let uiConv = uiConvs.next(); + while (!uiConv.done) { + let conv = uiConv.value.target; + if ( + conv.account.normalizedName === account && + conv.account.protocol.normalizedName === protocol && + conv.normalizedName === recipient + ) { + // console.log("=== getUIConvForRecipient found, account: " + account + " protocol: " + protocol + " recip: " + recipient); + return uiConv.value; + } + uiConv = uiConvs.next(); + } + throw new Error("Couldn't find conversation."); + }, + + getUIConvFromConv(conv) { + // return this._convos.get(conv.id); + return IMServices.conversations.getUIConversation(conv); + }, + + disconnect(conv, remove) { + OTRLib.otrl_message_disconnect( + this.userstate, + this.uiOps.address(), + null, + conv.account.normalizedName, + conv.account.protocol.normalizedName, + conv.normalizedName, + OTRLib.instag.OTRL_INSTAG_BEST + ); + if (remove) { + let uiConv = this.getUIConvFromConv(conv); + if (uiConv) { + this.removeConversation(uiConv); + } + } else { + this.notifyObservers(this.getContext(conv), "otr:disconnected"); + } + }, + + getAccountPref(prefName, accountId, defaultVal) { + return Services.prefs.getBoolPref( + "messenger.account." + accountId + ".options." + prefName, + defaultVal + ); + }, + + sendQueryMsg(conv) { + let req = this.getAccountPref( + "otrRequireEncryption", + conv.account.id, + Services.prefs.getBoolPref("chat.otr.default.requireEncryption") + ); + let query = OTRLib.otrl_proto_default_query_msg( + conv.account.normalizedName, + req ? OTRLib.OTRL_POLICY_ALWAYS : OTRLib.OTRL_POLICY_OPPORTUNISTIC + ); + if (query.isNull()) { + console.error(new Error("Sending query message failed.")); + return; + } + // Use the default msg to format the version. + // We don't support v1 of the protocol so this should be fine. + let queryMsg = /^\?OTR.*?\?/.exec(query.readString())[0] + "\n"; + // Avoid sending any numbers in the query message, because receiving + // software could misinterpret it as a protocol version. + // See https://bugzilla.mozilla.org/show_bug.cgi?id=1536108 + let noNumbersName = conv.account.normalizedName.replace(/[0-9]/g, "#"); + queryMsg += _strArgs("query-msg", { name: noNumbersName }); + this.sendOTRSystemMessage(conv, queryMsg); + OTRLib.otrl_message_free(query); + }, + + _pendingSystemMessages: null, + /** + * Wrapper for system messages sent by OTR to ensure they are correctly + * handled through the OutgoingMessage event handlers. + * + * @param {prplIConversation} conv + * @param {string} message + */ + sendOTRSystemMessage(conv, message) { + this._pendingSystemMessages.push({ + message, + convId: conv.id, + time: Date.now(), + }); + conv.sendMsg(message, false, false); + }, + + trustState: { + TRUST_NOT_PRIVATE: 0, + TRUST_UNVERIFIED: 1, + TRUST_PRIVATE: 2, + TRUST_FINISHED: 3, + }, + + // Check the attributes of the OTR context, and derive how that maps + // to one of the above trust states, which we'll show to the user. + // If we have an encrypted channel, it depends on the presence of a + // context.trust object, if we treat is as private or unverified. + trust(context) { + let level = this.trustState.TRUST_NOT_PRIVATE; + switch (context.msgstate) { + case OTRLib.messageState.OTRL_MSGSTATE_ENCRYPTED: + level = context.trust + ? this.trustState.TRUST_PRIVATE + : this.trustState.TRUST_UNVERIFIED; + break; + case OTRLib.messageState.OTRL_MSGSTATE_FINISHED: + level = this.trustState.TRUST_FINISHED; + break; + } + return level; + }, + + /** @param {Context} wContext - wrapped context. */ + getAccountPrefBranch(wContext) { + let account = IMServices.accounts + .getAccounts() + .find( + acc => + wContext.account == acc.normalizedName && + wContext.protocol == acc.protocol.normalizedName + ); + if (!account) { + return null; + } + return Services.prefs.getBranch(`messenger.account.${account.id}.`); + }, + + // uiOps callbacks + + /** + * Return the OTR policy for the given context. + */ + policy_cb(opdata, context) { + let wContext = new Context(context); + let pb = OTR.getAccountPrefBranch(wContext); + if (!pb) { + return new ctypes.unsigned_int(0); + } + try { + let conv = OTR.getUIConvFromContext(wContext); + // Ensure we never try to layer OTR on top of protocol native encryption. + if ( + conv.encryptionState !== Ci.prplIConversation.ENCRYPTION_NOT_SUPPORTED + ) { + return new ctypes.unsigned_int(0); + } + } catch (error) { + // No conversation found for the context, fall through to default logic. + } + let prefRequire = pb.getBoolPref( + "options.otrRequireEncryption", + Services.prefs.getBoolPref("chat.otr.default.requireEncryption") + ); + return prefRequire + ? OTRLib.OTRL_POLICY_ALWAYS + : OTRLib.OTRL_POLICY_OPPORTUNISTIC; + }, + + /** + * Create a private key for the given accountname/protocol if desired. + */ + create_privkey_cb(opdata, accountname, protocol) { + let args = { + account: accountname.readString(), + protocol: protocol.readString(), + }; + this.notifyObservers(args, "otr:generate"); + }, + + /** + * Report whether you think the given user is online. Return 1 if you + * think they are, 0 if you think they aren't, -1 if you're not sure. + */ + is_logged_in_cb(opdata, accountname, protocol, recipient) { + let conv = this.getUIConvForRecipient( + accountname.readString(), + protocol.readString(), + recipient.readString() + ).target; + return isOnline(conv); + }, + + /** + * Send the given IM to the given recipient from the given + * accountname/protocol. + */ + inject_message_cb(opdata, accountname, protocol, recipient, message) { + let aMsg = message.readString(); + this.log("inject_message_cb (msglen:" + aMsg.length + "): " + aMsg); + this.sendOTRSystemMessage( + this.getUIConvForRecipient( + accountname.readString(), + protocol.readString(), + recipient.readString() + ).target, + aMsg + ); + }, + + /** + * new fingerprint for the given user has been received. + */ + new_fingerprint_cb(opdata, us, accountname, protocol, username, fingerprint) { + let context = OTRLib.otrl_context_find( + us, + username, + accountname, + protocol, + OTRLib.instag.OTRL_INSTAG_MASTER, + 1, + null, + null, + null + ); + + let seen = false; + let fp = context.contents.fingerprint_root.next; + while (!fp.isNull()) { + if ( + CLib.memcmp(fingerprint, fp.contents.fingerprint, new ctypes.size_t(20)) + ) { + seen = true; + break; + } + fp = fp.contents.next; + } + + let wContext = new Context(context); + let defaultNudge = Services.prefs.getBoolPref( + "chat.otr.default.verifyNudge" + ); + let prefNudge = defaultNudge; + let pb = OTR.getAccountPrefBranch(wContext); + if (pb) { + prefNudge = pb.getBoolPref("options.otrVerifyNudge", defaultNudge); + } + + // Only nudge on new fingerprint, as opposed to always. + if (!prefNudge) { + this.notifyObservers( + wContext, + "otr:unverified", + seen ? "seen" : "unseen" + ); + } + }, + + /** + * The list of known fingerprints has changed. Write them to disk. + */ + write_fingerprint_cb(opdata) { + this.writeFingerprints(); + }, + + /** + * A ConnContext has entered a secure state. + */ + gone_secure_cb(opdata, context) { + let wContext = new Context(context); + let defaultNudge = Services.prefs.getBoolPref( + "chat.otr.default.verifyNudge" + ); + let prefNudge = defaultNudge; + let pb = OTR.getAccountPrefBranch(wContext); + if (pb) { + prefNudge = pb.getBoolPref("options.otrVerifyNudge", defaultNudge); + } + let strid = wContext.trust + ? "context-gone-secure-private" + : "context-gone-secure-unverified"; + this.notifyObservers(wContext, "otr:msg-state"); + this.sendAlert(wContext, _strArgs(strid, { name: wContext.username })); + if (prefNudge && !wContext.trust) { + this.notifyObservers(wContext, "otr:unverified", "unseen"); + } + }, + + /** + * A ConnContext has left a secure state. + */ + gone_insecure_cb(opdata, context) { + // This isn't used. See: https://bugs.otr.im/lib/libotr/issues/48 + }, + + /** + * We have completed an authentication, using the D-H keys we already + * knew. + * + * @param is_reply indicates whether we initiated the AKE. + */ + still_secure_cb(opdata, context, is_reply) { + // Indicate the private conversation was refreshed. + if (!is_reply) { + context = new Context(context); + this.notifyObservers(context, "otr:msg-state"); + this.sendAlert( + context, + _strArgs("context-still-secure", { name: context.username }) + ); + } + }, + + /** + * Find the maximum message size supported by this protocol. + */ + max_message_size_cb(opdata, context) { + context = new Context(context); + // These values are, for the most part, from pidgin-otr's mms_table. + switch (context.protocol) { + case "irc": + case "prpl-irc": + return 417; + case "facebook": + case "gtalk": + case "odnoklassniki": + case "jabber": + case "xmpp": + return 65536; + case "prpl-yahoo": + return 799; + case "prpl-msn": + return 1409; + case "prpl-icq": + return 2346; + case "prpl-gg": + return 1999; + case "prpl-aim": + case "prpl-oscar": + return 2343; + case "prpl-novell": + return 1792; + default: + return 0; + } + }, + + /** + * We received a request from the buddy to use the current "extra" + * symmetric key. + */ + received_symkey_cb(opdata, context, use, usedata, usedatalen, symkey) { + // Ignore until we have a use. + }, + + /** + * Return a string according to the error event. + */ + otr_error_message_cb(opdata, context, err_code) { + context = new Context(context); + let msg; + switch (err_code) { + case OTRLib.errorCode.OTRL_ERRCODE_ENCRYPTION_ERROR: + msg = _str("error-enc"); + break; + case OTRLib.errorCode.OTRL_ERRCODE_MSG_NOT_IN_PRIVATE: + msg = _strArgs("error-not-priv", context.username); + break; + case OTRLib.errorCode.OTRL_ERRCODE_MSG_UNREADABLE: + msg = _str("error-unreadable"); + break; + case OTRLib.errorCode.OTRL_ERRCODE_MSG_MALFORMED: + msg = _str("error-malformed"); + break; + default: + return null; + } + return CLib.strdup(msg); + }, + + /** + * Deallocate a string returned by otr_error_message_cb. + */ + otr_error_message_free_cb(opdata, err_msg) { + if (!err_msg.isNull()) { + CLib.free(err_msg); + } + }, + + /** + * Return a string that will be prefixed to any resent message. + */ + resent_msg_prefix_cb(opdata, context) { + return CLib.strdup(_str("resent")); + }, + + /** + * Deallocate a string returned by resent_msg_prefix. + */ + resent_msg_prefix_free_cb(opdata, prefix) { + if (!prefix.isNull()) { + CLib.free(prefix); + } + }, + + /** + * Update the authentication UI with respect to SMP events. + */ + handle_smp_event_cb(opdata, smp_event, context, progress_percent, question) { + context = new Context(context); + switch (smp_event) { + case OTRLib.smpEvent.OTRL_SMPEVENT_NONE: + break; + case OTRLib.smpEvent.OTRL_SMPEVENT_ASK_FOR_ANSWER: + case OTRLib.smpEvent.OTRL_SMPEVENT_ASK_FOR_SECRET: + this.notifyObservers( + { + context, + progress: progress_percent, + question: question.isNull() ? null : question.readString(), + }, + "otr:auth-ask" + ); + break; + case OTRLib.smpEvent.OTRL_SMPEVENT_CHEATED: + OTR.abortSMP(context); + /* falls through */ + case OTRLib.smpEvent.OTRL_SMPEVENT_IN_PROGRESS: + case OTRLib.smpEvent.OTRL_SMPEVENT_SUCCESS: + case OTRLib.smpEvent.OTRL_SMPEVENT_FAILURE: + case OTRLib.smpEvent.OTRL_SMPEVENT_ABORT: + this.authUpdate( + context, + progress_percent, + smp_event === OTRLib.smpEvent.OTRL_SMPEVENT_SUCCESS + ); + break; + case OTRLib.smpEvent.OTRL_SMPEVENT_ERROR: + OTR.abortSMP(context); + break; + default: + this.log("smp event: " + smp_event); + } + }, + + /** + * Handle and send the appropriate message(s) to the sender/recipient + * depending on the message events. + */ + handle_msg_event_cb(opdata, msg_event, context, message, err) { + context = new Context(context); + switch (msg_event) { + case OTRLib.messageEvent.OTRL_MSGEVENT_NONE: + break; + case OTRLib.messageEvent.OTRL_MSGEVENT_ENCRYPTION_REQUIRED: + this.sendAlert( + context, + _strArgs("msgevent-encryption-required-part1", { + name: context.username, + }) + ); + this.sendAlert(context, _str("msgevent-encryption-required-part2")); + break; + case OTRLib.messageEvent.OTRL_MSGEVENT_ENCRYPTION_ERROR: + this.sendAlert(context, _str("msgevent-encryption-error")); + break; + case OTRLib.messageEvent.OTRL_MSGEVENT_CONNECTION_ENDED: + this.sendAlert( + context, + _strArgs("msgevent-connection-ended", { name: context.username }) + ); + break; + case OTRLib.messageEvent.OTRL_MSGEVENT_SETUP_ERROR: + this.sendAlert( + context, + _strArgs("msgevent-setup-error", { name: context.username }) + ); + break; + case OTRLib.messageEvent.OTRL_MSGEVENT_MSG_REFLECTED: + this.sendAlert(context, _str("msgevent-msg-reflected")); + break; + case OTRLib.messageEvent.OTRL_MSGEVENT_MSG_RESENT: + this.sendAlert( + context, + _strArgs("msgevent-msg-resent", { name: context.username }) + ); + break; + case OTRLib.messageEvent.OTRL_MSGEVENT_RCVDMSG_NOT_IN_PRIVATE: + this.sendAlert( + context, + _strArgs("msgevent-rcvdmsg-not-private", { name: context.username }) + ); + break; + case OTRLib.messageEvent.OTRL_MSGEVENT_RCVDMSG_UNREADABLE: + this.sendAlert( + context, + _strArgs("msgevent-rcvdmsg-unreadable", { name: context.username }) + ); + break; + case OTRLib.messageEvent.OTRL_MSGEVENT_RCVDMSG_MALFORMED: + this.sendAlert( + context, + _strArgs("msgevent-rcvdmsg-malformed", { name: context.username }) + ); + break; + case OTRLib.messageEvent.OTRL_MSGEVENT_LOG_HEARTBEAT_RCVD: + this.log( + _strArgs("msgevent-log-heartbeat-rcvd", { name: context.username }) + ); + break; + case OTRLib.messageEvent.OTRL_MSGEVENT_LOG_HEARTBEAT_SENT: + this.log( + _strArgs("msgevent-log-heartbeat-sent", { name: context.username }) + ); + break; + case OTRLib.messageEvent.OTRL_MSGEVENT_RCVDMSG_GENERAL_ERR: + this.sendAlert(context, _str("msgevent-rcvdmsg-general-err")); + break; + case OTRLib.messageEvent.OTRL_MSGEVENT_RCVDMSG_UNENCRYPTED: + this.sendAlert( + context, + _strArgs("msgevent-rcvdmsg-unencrypted", { + name: context.username, + msg: message.isNull() ? "" : message.readString(), + }) + ); + break; + case OTRLib.messageEvent.OTRL_MSGEVENT_RCVDMSG_UNRECOGNIZED: + this.sendAlert( + context, + _strArgs("msgevent-rcvdmsg-unrecognized", { name: context.username }) + ); + break; + case OTRLib.messageEvent.OTRL_MSGEVENT_RCVDMSG_FOR_OTHER_INSTANCE: + this.log( + _strArgs("msgevent-rcvdmsg-for-other-instance", { + name: context.username, + }) + ); + break; + default: + this.log("msg event: " + msg_event); + } + }, + + /** + * Create an instance tag for the given accountname/protocol if + * desired. + */ + create_instag_cb(opdata, accountname, protocol) { + this.generateInstanceTag(accountname.readString(), protocol.readString()); + }, + + /** + * When timer_control is called, turn off any existing periodic timer. + * Additionally, if interval > 0, set a new periodic timer to go off + * every interval seconds. + */ + timer_control_cb(opdata, interval) { + if (this._poll_timer) { + clearInterval(this._poll_timer); + this._poll_timer = null; + } + if (interval > 0) { + this._poll_timer = setInterval(() => { + OTRLib.otrl_message_poll(this.userstate, this.uiOps.address(), null); + }, interval * 1000); + } + }, + + // end of uiOps + + initUiOps() { + this.uiOps = new OTRLib.OtrlMessageAppOps(); + + let methods = [ + "policy", + "create_privkey", + "is_logged_in", + "inject_message", + "update_context_list", // not implemented + "new_fingerprint", + "write_fingerprint", + "gone_secure", + "gone_insecure", + "still_secure", + "max_message_size", + "account_name", // not implemented + "account_name_free", // not implemented + "received_symkey", + "otr_error_message", + "otr_error_message_free", + "resent_msg_prefix", + "resent_msg_prefix_free", + "handle_smp_event", + "handle_msg_event", + "create_instag", + "convert_msg", // not implemented + "convert_free", // not implemented + "timer_control", + ]; + + for (let i = 0; i < methods.length; i++) { + let m = methods[i]; + if (!this[m + "_cb"]) { + this.uiOps[m] = null; + continue; + } + // keep a pointer to this in memory to avoid crashing + this[m + "_cb"] = OTRLib[m + "_cb_t"](this[m + "_cb"].bind(this)); + this.uiOps[m] = this[m + "_cb"]; + } + }, + + sendAlert(context, msg) { + this.getUIConvFromContext(context).systemMessage(msg, false, true); + }, + + observe(aObject, aTopic, aMsg) { + switch (aTopic) { + case "sending-message": + this.onSend(aObject); + break; + case "received-message": + this.onReceive(aObject); + break; + case "new-ui-conversation": + this.addConversation(aObject); + break; + case "conversation-update-type": + if (this._convos.has(aObject.target.id)) { + this._convos.get(aObject.target.id).removeObserver(this); + } + this.addConversation(aObject); + break; + case "update-conv-encryption": { + // Disable OTR encryption when the chat protocol initiates encryption + // for the conversation. + const context = this.getContext(aObject); + const trust = this.trust(context); + if ( + trust === this.trustState.TRUST_NOT_PRIVATE || + trust === this.trustState.TRUST_PRIVATE + ) { + this.disconnect(aObject, false); + } + break; + } + } + }, + + addConversation(uiConv) { + let conv = uiConv.target; + if (conv.isChat) { + return; + } + this._convos.set(conv.id, uiConv); + uiConv.addObserver(this); + }, + + removeConversation(uiConv) { + uiConv.removeObserver(this); + this._convos.delete(uiConv.target.id); + this.clearMsgs(uiConv.target.id); + }, + + sendSecret(context, secret, question) { + let str = ctypes.char.array()(secret); + let strlen = new ctypes.size_t(str.length - 1); + OTRLib.otrl_message_initiate_smp_q( + this.userstate, + this.uiOps.address(), + null, + context._context, + question ? question : null, + str, + strlen + ); + }, + + sendResponse(context, response) { + let str = ctypes.char.array()(response); + let strlen = new ctypes.size_t(str.length - 1); + OTRLib.otrl_message_respond_smp( + this.userstate, + this.uiOps.address(), + null, + context._context, + str, + strlen + ); + }, + + abortSMP(context) { + OTRLib.otrl_message_abort_smp( + this.userstate, + this.uiOps.address(), + null, + context._context + ); + }, + + onSend(om) { + if (om.cancelled) { + return; + } + + let conv = om.conversation; + if (conv.isChat) { + return; + } + + if (om.action) { + // embed /me into the message text for encrypted actions. + let context = this.getContext(conv); + if (context.msgstate != this.trustState.TRUST_NOT_PRIVATE) { + om.cancelled = true; + conv.sendMsg("/me " + om.message, false, false); + } + return; + } + + // Skip if OTR sent this message. + let pendingIndex = this._pendingSystemMessages.findIndex( + info => info.convId == conv.id && info.message == om.message + ); + if (pendingIndex > -1) { + this._pendingSystemMessages.splice(pendingIndex, 1); + return; + } + + let newMessage = new ctypes.char.ptr(); + + this.log("pre sending: " + om.message); + + let err = OTRLib.otrl_message_sending( + this.userstate, + this.uiOps.address(), + null, + conv.account.normalizedName, + conv.account.protocol.normalizedName, + conv.normalizedName, + OTRLib.instag.OTRL_INSTAG_BEST, + om.message, + null, + newMessage.address(), + OTRLib.fragPolicy.OTRL_FRAGMENT_SEND_ALL_BUT_LAST, + null, + null, + null + ); + + let msg = om.message; + + if (err) { + om.cancelled = true; + console.error(new Error("Failed to send message. Returned code: " + err)); + } else if (!newMessage.isNull()) { + msg = newMessage.readString(); + // https://bugs.otr.im/lib/libotr/issues/52 + if (!msg) { + om.cancelled = true; + } + } + + if (!om.cancelled) { + // OTR handshakes only work while both peers are online. + // Sometimes we want to include a special whitespace suffix, + // which the OTR protocol uses to signal that the sender is willing + // to start an OTR session. Don't do that for offline messages. + // See: https://bugs.otr.im/lib/libotr/issues/102 + if (isOnline(conv) === 0) { + let ind = msg.indexOf(OTRLib.OTRL_MESSAGE_TAG_BASE); + if (ind > -1) { + msg = msg.substring(0, ind); + let context = this.getContext(conv); + context._context.contents.otr_offer = OTRLib.otr_offer.OFFER_NOT; + } + } + + this.bufferMsg(conv.id, om.message, msg); + om.message = msg; + } + + this.log("post sending (" + !om.cancelled + "): " + om.message); + OTRLib.otrl_message_free(newMessage); + }, + + /** + * + * @param {imIMessage} im - Incoming message. + */ + onReceive(im) { + if (im.cancelled || im.system) { + return; + } + + let conv = im.conversation; + if (conv.isChat) { + return; + } + + // After outgoing messages have been handled in onSend, + // they are again passed back to us, here in onReceive. + // This is our chance to prevent both outgoing and incoming OTR + // messages from being logged here. + if (im.originalMessage.startsWith("?OTR")) { + im.otrEncrypted = true; + } + + if (im.outgoing) { + this.log("outgoing message to display: " + im.displayMessage); + this.pluckMsg(im); + return; + } + + let newMessage = new ctypes.char.ptr(); + let tlvs = new OTRLib.OtrlTLV.ptr(); + + let err = OTRLib.otrl_message_receiving( + this.userstate, + this.uiOps.address(), + null, + conv.account.normalizedName, + conv.account.protocol.normalizedName, + conv.normalizedName, + im.displayMessage, + newMessage.address(), + tlvs.address(), + null, + null, + null + ); + + // An OTR message was properly decrypted. + if (!newMessage.isNull()) { + im.displayMessage = newMessage.readString(); + // Check if it was an encrypted action message. + if (im.displayMessage.startsWith("/me ")) { + im.action = true; + im.displayMessage = im.displayMessage.slice(4); + } + } + + // search tlvs for a disconnect msg + // https://bugs.otr.im/lib/libotr/issues/54 + let tlv = OTRLib.otrl_tlv_find(tlvs, OTRLib.tlvs.OTRL_TLV_DISCONNECTED); + if (!tlv.isNull()) { + let context = this.getContext(conv); + this.notifyObservers(context, "otr:disconnected"); + this.sendAlert( + context, + _strArgs("tlv-disconnected", { name: conv.normalizedName }) + ); + } + + if (err) { + this.log("error (" + err + ") ignoring: " + im.displayMessage); + im.cancelled = true; // ignore + } + + OTRLib.otrl_tlv_free(tlvs); + OTRLib.otrl_message_free(newMessage); + }, + + // observer interface + + addObserver(observer) { + if (!this._observers.includes(observer)) { + this._observers.push(observer); + } + }, + + removeObserver(observer) { + this._observers = this._observers.filter(o => o !== observer); + }, + + notifyObservers(aSubject, aTopic, aData) { + for (let observer of this._observers) { + observer.observe(aSubject, aTopic, aData); + } + }, + + // buffer messages + + /** + * Remove messages that were making it through the system related to a + * conversation. + * + * @param {number} convId - ID of the conversation to purge all messages for. + */ + clearMsgs(convId) { + this._buffer = this._buffer.filter(msg => msg.convId !== convId); + this._pendingSystemMessages = this._pendingSystemMessages.filter( + info => info.convId !== convId + ); + }, + + /** + * Save unencrypted outgoing message to a buffer so we can restore it later + * on when displaying it. + * + * @param {number} convId - ID of the conversation. + * @param {string} display - Message to display. + * @param {string} sent - Message that was sent. + */ + bufferMsg(convId, display, sent) { + this._buffer.push({ + convId, + display, + sent, + time: Date.now(), + }); + }, + + /** + * Get the unencrypted version of an outgoing OTR encrypted message that we + * are handling in the incoming message path for displaying. Also discards + * magic OTR bytes and such for displaying. + * + * @param {imIMessage} incomingMessage - Message with an outgoing tag. + * @returns + */ + pluckMsg(incomingMessage) { + for (let i = 0; i < this._buffer.length; i++) { + let bufferedInfo = this._buffer[i]; + if ( + bufferedInfo.convId === incomingMessage.conversation.id && + bufferedInfo.sent === incomingMessage.displayMessage + ) { + incomingMessage.displayMessage = bufferedInfo.display; + this._buffer.splice(i, 1); + this.log("displaying: " + bufferedInfo.display); + return; + } + } + // don't display if message wasn't buffered + if (incomingMessage.otrEncrypted) { + incomingMessage.cancelled = true; + this.log("not displaying: " + incomingMessage.displayMessage); + } + }, +}; + +// exports |