diff --git a/chat/content/jar.mn b/chat/content/jar.mn
index 3a26471559..cf14bbf5ba 100644
--- a/chat/content/jar.mn
+++ b/chat/content/jar.mn
@@ -12,3 +12,19 @@ chat.jar:
* content/chat/imtooltip.xml
content/chat/conversation-browser.js
content/chat/conv.html
+ content/chat/otr-add-fingerprint.js
+ content/chat/otr-add-fingerprint.xul
+ content/chat/otr-auth.js
+ content/chat/otr-auth.xul
+ content/chat/otr-generate-key.js
+ content/chat/otr-generate-key.xul
+ content/chat/otrWorker.js
+ content/chat/otr-auth.dtd
+ content/chat/otr-auth.properties
+ content/chat/otr-add-finger.dtd
+ content/chat/otr-add-finger.properties
+ content/chat/otr.properties
+ content/chat/otr-generate-key.dtd
+ content/chat/otr-generate-key.properties
+ content/chat/otrUI.properties
+ content/chat/otr-chat.dtd
diff --git a/chat/content/otr-add-finger.dtd b/chat/content/otr-add-finger.dtd
new file mode 100644
index 0000000000..7af1713527
--- /dev/null
+++ b/chat/content/otr-add-finger.dtd
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
diff --git a/chat/content/otr-add-finger.properties b/chat/content/otr-add-finger.properties
new file mode 100644
index 0000000000..661e72c0b8
--- /dev/null
+++ b/chat/content/otr-add-finger.properties
@@ -0,0 +1,6 @@
+# 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/.
+
+# LOCALIZATION NOTE (addfinger.title): %S is the name of a chat contact person
+addfinger.title=Enter the fingerprint of the OTR key used by %S
diff --git a/chat/content/otr-add-fingerprint.js b/chat/content/otr-add-fingerprint.js
new file mode 100644
index 0000000000..9232205372
--- /dev/null
+++ b/chat/content/otr-add-fingerprint.js
@@ -0,0 +1,50 @@
+/* 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/. */
+
+const {
+ XPCOMUtils,
+ l10nHelper,
+} = ChromeUtils.import("resource:///modules/imXPCOMUtils.jsm");
+const {OTR} = ChromeUtils.import("resource:///modules/OTR.jsm");
+
+XPCOMUtils.defineLazyGetter(this, "_", () =>
+ l10nHelper("chrome://chat/content/otr-add-finger.properties")
+);
+
+var args = window.arguments[0].wrappedJSObject;
+
+var otrAddFinger = {
+ onload() {
+ document.title = _("addfinger.title", args.screenname);
+
+ document.addEventListener("dialogaccept", () => {
+ return this.add();
+ });
+ },
+
+ oninput(e) {
+ e.value = e.value.replace(/[^0-9a-fA-F]/gi, "");
+ document.documentElement.getButton("accept").disabled = (e.value.length != 40);
+ },
+
+ add(e) {
+ let hex = document.getElementById("finger").value;
+ let context = OTR.getContextFromRecipient(
+ args.account,
+ args.protocol,
+ args.screenname
+ );
+ let finger = OTR.addFingerprint(context, hex);
+ if (finger.isNull())
+ return;
+ try {
+ // Ignore the return, this is just a test.
+ OTR.getUIConvFromContext(context);
+ } catch (error) {
+ // We expect that a conversation may not have been started.
+ context = null;
+ }
+ OTR.setTrust(finger, true, context);
+ },
+};
diff --git a/chat/content/otr-add-fingerprint.xul b/chat/content/otr-add-fingerprint.xul
new file mode 100644
index 0000000000..9c7c72b04b
--- /dev/null
+++ b/chat/content/otr-add-fingerprint.xul
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
diff --git a/chat/content/otr-auth.dtd b/chat/content/otr-auth.dtd
new file mode 100644
index 0000000000..8751028945
--- /dev/null
+++ b/chat/content/otr-auth.dtd
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/chat/content/otr-auth.js b/chat/content/otr-auth.js
new file mode 100644
index 0000000000..7f59f6bf7a
--- /dev/null
+++ b/chat/content/otr-auth.js
@@ -0,0 +1,175 @@
+/* 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/. */
+
+const {Services} = ChromeUtils.import("resource:///modules/imServices.jsm");
+const {
+ XPCOMUtils,
+ l10nHelper,
+} = ChromeUtils.import("resource:///modules/imXPCOMUtils.jsm");
+const {OTR} = ChromeUtils.import("resource:///modules/OTR.jsm");
+
+XPCOMUtils.defineLazyGetter(this, "_", () =>
+ l10nHelper("chrome://chat/content/otr-auth.properties")
+);
+
+var [mode, uiConv, contactInfo] = window.arguments;
+
+// This window implements the interactive authentication of a buddy's
+// key. At open time, we're given several parameters, and the "mode"
+// parameter tells us, from where we've been called.
+// mode == "pref" means that we have been opened from the preferences,
+// and it means we cannot rely on the other user being online, and
+// we there might be no uiConv active currently, so we fall back.
+
+document.title = _("auth.title",
+ (mode === "pref") ? contactInfo.screenname : uiConv.normalizedName);
+
+function showSection(selected, hideMenu) {
+ document.getElementById("how").hidden = !!hideMenu;
+ [ "questionAndAnswer",
+ "sharedSecret",
+ "manualVerification",
+ "ask",
+ ].forEach(function(key) {
+ document.getElementById(key).hidden = (key !== selected);
+ });
+ window.sizeToContent();
+}
+
+function startSMP(context, answer, question) {
+ OTR.sendSecret(context, answer, question);
+ OTR.authUpdate(context, 10);
+}
+
+function manualVerification(fingerprint, context) {
+ let opts = document.getElementById("verifiedOption");
+ let trust = (opts.selectedItem.value === "yes");
+ OTR.setTrust(fingerprint, trust, context);
+}
+
+function populateFingers(context, theirs, trust) {
+ let fingers = document.getElementById("fingerprints");
+ let yours = OTR.privateKeyFingerprint(context.account, context.protocol);
+ if (!yours)
+ throw new Error("Fingerprint should already be generated.");
+ fingers.value =
+ _("auth.yourFingerprint", context.account, yours) + "\n\n" +
+ _("auth.theirFingerprint", context.username, theirs);
+ let opts = document.getElementById("verifiedOption");
+ let verified = trust ? "yes" : "no";
+ for (let item of opts.menupopup.childNodes) {
+ if (verified === item.value) {
+ opts.selectedItem = item;
+ break;
+ }
+ }
+}
+
+var otrAuth = {
+ onload() {
+ document.addEventListener("dialogaccept", () => {
+ return this.accept();
+ });
+
+ document.addEventListener("dialogcancel", () => {
+ return this.cancel();
+ });
+
+ document.addEventListener("dialoghelp", () => {
+ return this.help();
+ });
+
+ let context, theirs;
+ switch (mode) {
+ case "start":
+ context = OTR.getContext(uiConv.target);
+ theirs = OTR.hashToHuman(context.fingerprint);
+ populateFingers(context, theirs, context.trust);
+ showSection("questionAndAnswer");
+ break;
+ case "pref":
+ context = OTR.getContextFromRecipient(
+ contactInfo.account,
+ contactInfo.protocol,
+ contactInfo.screenname
+ );
+ theirs = contactInfo.fingerprint;
+ populateFingers(context, theirs, contactInfo.trust);
+ showSection("manualVerification", true);
+ this.oninput({ value: true });
+ break;
+ case "ask":
+ document.getElementById("askLabel").textContent = contactInfo.question ?
+ _("auth.question", contactInfo.question)
+ : _("auth.secret");
+ showSection("ask", true);
+ break;
+ }
+ },
+
+ accept() {
+ // uiConv may not be present in pref mode
+ let context = uiConv ? OTR.getContext(uiConv.target) : null;
+ if (mode === "pref") {
+ manualVerification(contactInfo.fpointer, context);
+ } else if (mode === "start") {
+ let how = document.getElementById("howOption");
+ switch (how.selectedItem.value) {
+ case "questionAndAnswer":
+ let question = document.getElementById("question").value;
+ let answer = document.getElementById("answer").value;
+ startSMP(context, answer, question);
+ break;
+ case "sharedSecret":
+ let secret = document.getElementById("secret").value;
+ startSMP(context, secret);
+ break;
+ case "manualVerification":
+ manualVerification(context.fingerprint, context);
+ break;
+ default:
+ throw new Error("Unreachable!");
+ }
+ } else if (mode === "ask") {
+ let response = document.getElementById("response").value;
+ OTR.sendResponse(context, response);
+ OTR.authUpdate(context, contactInfo.progress);
+ } else {
+ throw new Error("Unreachable!");
+ }
+ return true;
+ },
+
+ cancel() {
+ if (mode === "ask") {
+ let context = OTR.getContext(uiConv.target);
+ OTR.abortSMP(context);
+ }
+ },
+
+ oninput(e) {
+ document.documentElement.getButton("accept").disabled = !e.value;
+ },
+
+ how() {
+ let how = document.getElementById("howOption").selectedItem.value;
+ switch (how) {
+ case "questionAndAnswer":
+ this.oninput(document.getElementById("answer"));
+ break;
+ case "sharedSecret":
+ this.oninput(document.getElementById("secret"));
+ break;
+ case "manualVerification":
+ this.oninput({ value: true });
+ break;
+ }
+ showSection(how);
+ },
+
+ help() {
+ Services.prompt.alert(window, _("auth.helpTitle"), _("auth.help"));
+ },
+
+};
diff --git a/chat/content/otr-auth.properties b/chat/content/otr-auth.properties
new file mode 100644
index 0000000000..160737c156
--- /dev/null
+++ b/chat/content/otr-auth.properties
@@ -0,0 +1,15 @@
+# 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/.
+
+# LOCALIZATION NOTE (auth.title): %S is the screen name of a chat contact person
+auth.title=Verify the identity of %S
+# LOCALIZATION NOTE (auth.yourFingerprint): 1st %S is the user's own screen name. 2nd %S is the fingerprint (a checksum) of the user's own encryption key.
+auth.yourFingerprint=Fingerprint for you, %S:\n%S
+# LOCALIZATION NOTE (auth.theirFingerprint): 1st %S is the screen name of a chat contact. 2nd %S is the fingerprint (a checksum) of the chat contact's encryption key.
+auth.theirFingerprint=Purported fingerprint for %S:\n%S
+auth.help=Verifying a contact's identity helps ensure that the person you are talking to is who they claim to be.
+auth.helpTitle=Verification help
+# LOCALIZATION NOTE (auth.question): %S is a question (any text is possible) that was received from a chat contact
+auth.question=This is the question asked by your contact:\n\n%S\n\nEnter secret answer here (case sensitive):
+auth.secret=Enter secret here:
diff --git a/chat/content/otr-auth.xul b/chat/content/otr-auth.xul
new file mode 100644
index 0000000000..1c9381c73d
--- /dev/null
+++ b/chat/content/otr-auth.xul
@@ -0,0 +1,71 @@
+
+
+
+
+
+
+
+
diff --git a/chat/content/otr-chat.dtd b/chat/content/otr-chat.dtd
new file mode 100644
index 0000000000..34c6521e92
--- /dev/null
+++ b/chat/content/otr-chat.dtd
@@ -0,0 +1,8 @@
+
+
+
+
+
+
diff --git a/chat/content/otr-generate-key.dtd b/chat/content/otr-generate-key.dtd
new file mode 100644
index 0000000000..6c52847abb
--- /dev/null
+++ b/chat/content/otr-generate-key.dtd
@@ -0,0 +1,6 @@
+
+
+
+
diff --git a/chat/content/otr-generate-key.js b/chat/content/otr-generate-key.js
new file mode 100644
index 0000000000..8e9ad94729
--- /dev/null
+++ b/chat/content/otr-generate-key.js
@@ -0,0 +1,29 @@
+/* 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/. */
+
+const {
+ XPCOMUtils,
+ l10nHelper,
+} = ChromeUtils.import("resource:///modules/imXPCOMUtils.jsm");
+const {OTR} = ChromeUtils.import("resource:///modules/OTR.jsm");
+
+XPCOMUtils.defineLazyGetter(this, "_", () =>
+ l10nHelper("chrome://chat/content/otr-generate-key.properties")
+);
+
+var otrPriv = {
+
+ onload() {
+ let args = window.arguments[0].wrappedJSObject;
+ let priv = document.getElementById("priv");
+ priv.textContent = _("priv.account", args.account, OTR.protocolName(args.protocol));
+ OTR.generatePrivateKey(args.account, args.protocol).then(function() {
+ document.documentElement.getButton("accept").disabled = false;
+ document.documentElement.acceptDialog();
+ }).catch(function(err) {
+ document.documentElement.getButton("accept").disabled = false;
+ priv.textContent = _("priv.failed", String(err));
+ });
+ },
+};
diff --git a/chat/content/otr-generate-key.properties b/chat/content/otr-generate-key.properties
new file mode 100644
index 0000000000..9446fe5ad4
--- /dev/null
+++ b/chat/content/otr-generate-key.properties
@@ -0,0 +1,8 @@
+# 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/.
+
+# LOCALIZATION NOTE (priv.account): 1st %S is the name of the user's own chat account. 2nd %S is the chat communication protocol used by that account.
+priv.account=Generating private key for %S (%S) …
+# LOCALIZATION NOTE (priv.failed): %S contains an error message that describes the cause of the failure
+priv.failed=Generating key failed: %S
diff --git a/chat/content/otr-generate-key.xul b/chat/content/otr-generate-key.xul
new file mode 100644
index 0000000000..50b1a2b343
--- /dev/null
+++ b/chat/content/otr-generate-key.xul
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+ %chatPrivDTD;
+]>
+
+
diff --git a/chat/content/otr.properties b/chat/content/otr.properties
new file mode 100644
index 0000000000..544e1dca18
--- /dev/null
+++ b/chat/content/otr.properties
@@ -0,0 +1,72 @@
+# 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/.
+
+# LOCALIZATION NOTE (msgevent.encryption_required_part1): %S is the name of a chat contact
+msgevent.encryption_required_part1=You attempted to send an unencrypted message to %S. As a policy, unencrypted messages are not allowed.
+msgevent.encryption_required_part2=Attempting to start a private conversation. Your message will be retransmitted when the private conversation starts.
+msgevent.encryption_error=An error occurred when encrypting your message. The message was not sent.
+
+# LOCALIZATION NOTE (msgevent.connection_ended): %S is the name of a chat contact
+msgevent.connection_ended=%S has already closed their private connection to you. Your message was not sent. Either end your private conversation, or restart it.
+
+# LOCALIZATION NOTE (msgevent.setup_error): %S is the name of a chat contact
+msgevent.setup_error=An error occured while setting up a private conversation with %S.
+# LOCALIZATION NOTE (msgevent.msg_reflected): do not translate OTR which is the name of an encryption protocol
+msgevent.msg_reflected=You are receiving your own OTR messages. You are either trying to talk to yourself, or someone is reflecting your messages back at you.
+
+# LOCALIZATION NOTE (msgevent.msg_resent): %S is the name of a chat contact
+msgevent.msg_resent=The last message to %S was resent.
+
+# LOCALIZATION NOTE (msgevent.rcvdmsg_not_private): %S is the name of a chat contact
+msgevent.rcvdmsg_not_private=The encrypted message received from %S is unreadable, as you are not currently communicating privately.
+
+# LOCALIZATION NOTE (msgevent.rcvdmsg_unreadable): %S is the name of a chat contact
+msgevent.rcvdmsg_unreadable=We received an unreadable encrypted message from %S.
+
+# LOCALIZATION NOTE (msgevent.rcvdmsg_malformed): %S is the name of a chat contact
+msgevent.rcvdmsg_malformed=We received a malformed data message from %S.
+
+# LOCALIZATION NOTE (msgevent.log_heartbeat_rcvd): %S is the name of a chat contact. A Heartbeat is a technical message used to keep a connection alive.
+msgevent.log_heartbeat_rcvd=Heartbeat received from %S.
+
+# LOCALIZATION NOTE (msgevent.log_heartbeat_sent): %S is the name of a chat contact. A Heartbeat is a technical message used to keep a connection alive.
+msgevent.log_heartbeat_sent=Heartbeat sent to %S.
+
+# LOCALIZATION NOTE (msgevent.rcvdmsg_general_err): do not translate OTR which is the name of an encryption protocol
+msgevent.rcvdmsg_general_err=An OTR error occured.
+
+# LOCALIZATION NOTE (msgevent.rcvdmsg_unencrypted): 1st %S is the name of a chat contact. 2nd %S is the message that was received.
+msgevent.rcvdmsg_unencrypted=The following message received from %S was not encrypted: %S
+
+# LOCALIZATION NOTE (msgevent.rcvdmsg_unrecognized): do not translate OTR which is the name of an encryption protocol. %S is the name of a chat contact.
+msgevent.rcvdmsg_unrecognized=We received an unrecognized OTR message from %S.
+
+# LOCALIZATION NOTE (msgevent.rcvdmsg_for_other_instance): %S is the name of a chat contact
+msgevent.rcvdmsg_for_other_instance=%S has sent a message intended for a different session. If you are logged in multiple times, another session may have received the message.
+
+# LOCALIZATION NOTE (context.gone_secure_private): %S is the name of a chat contact
+context.gone_secure_private=Private conversation with %S started.
+
+# LOCALIZATION NOTE (context.gone_secure_unverified): %S is the name of a chat contact
+context.gone_secure_unverified=Private conversation with %S started. However, their identity has not been verified.
+
+# LOCALIZATION NOTE (context.still_secure): %S is the name of a chat contact
+context.still_secure=Successfully refreshed the private conversation with %S.
+
+error.enc=Error occurred encrypting message.
+
+# LOCALIZATION NOTE (error.not_priv): %S is the name of a chat contact
+error.not_priv=You sent encrypted data to %S, who wasn't expecting it.
+error.unreadable=You transmitted an unreadable encrypted message.
+error.malformed=You transmitted a malformed data message.
+resent=[resent]
+# LOCALIZATION NOTE (tlv.disconnected): %S is the name of a chat contact
+tlv.disconnected=%S has ended their private conversation with you; you should do the same.
+# LOCALIZATION NOTE (query.msg): %S is the name of a chat contact. Do not translate "Off-the-Record" and "OTR" which is the name of an encryption protocol
+query.msg=%S has requested an Off-the-Record (OTR) private conversation. However, you do not have a plugin to support that. See https://en.wikipedia.org/wiki/Off-the-Record_Messaging for more information.
+trust.unused=Unused
+trust.not_private=Not Private
+trust.unverified=Unverified
+trust.private=Private
+trust.finished=Finished
diff --git a/chat/content/otrUI.properties b/chat/content/otrUI.properties
new file mode 100644
index 0000000000..84cb178afa
--- /dev/null
+++ b/chat/content/otrUI.properties
@@ -0,0 +1,59 @@
+# 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/.
+
+start.label=Start private conversation
+refresh.label=Refresh private conversation
+auth.label=Verify your contact's identity
+auth.cancel=Cancel
+auth.cancelAccessKey=C
+auth.error=An error occurred while verifying your contact's identity.
+auth.success=Verifying your contact's identity completed successfully.
+auth.successThem=Your contact has successfully verified your identity. You may want to verify their identity as well by asking your own question.
+auth.fail=Failed to verify your contact's identity.
+auth.waiting=Waiting for contact to complete verification …
+reauth.label=Reverify your contact's identity
+finger.verify=Verify
+verify.accessKey=V
+
+# LOCALIZATION NOTE (buddycontextmenu.label): do not translate OTR which is the name of an encryption protocol
+buddycontextmenu.label=Add Contact's OTR Fingerprint
+
+# LOCALIZATION NOTE (alert.start): %S is the name of a chat contact
+alert.start=Attempting to start a private conversation with %S.
+# LOCALIZATION NOTE (alert.refresh): %S is the name of a chat contact
+alert.refresh=Attempting to refresh the private conversation with %S.
+# LOCALIZATION NOTE (alert.gone_insecure): %S is the name of a chat contact
+alert.gone_insecure=Private conversation with %S ended.
+
+# LOCALIZATION NOTE (finger.unseen): %S is the name of a chat contact
+finger.unseen=The identity of %S has not been verified yet. Casual eavesdropping is not possible, but with some effort someone could be listening in. You should verify this contact's identity.
+
+state.not_private=The current conversation is not private.
+
+# LOCALIZATION NOTE (state.unverified): %S is the name of a chat contact
+state.unverified=The current conversation is private but the identity of %S has not been verified.
+
+# LOCALIZATION NOTE (state.private): %S is the name of a chat contact
+state.private=The current conversation is private and the identity of %S has been verified.
+
+# LOCALIZATION NOTE (state.finished): %S is the name of a chat contact
+state.finished=%S has ended their private conversation with you; you should do the same.
+
+state.not_private.label=Insecure
+state.unverified.label=Unverified
+state.private.label=Private
+state.finished.label=Finished
+
+# LOCALIZATION NOTE (afterauth.private): %S is the name of a chat contact
+afterauth.private=You have verified the identity of %S.
+
+# LOCALIZATION NOTE (afterauth.unverified): %S is the name of a chat contact
+afterauth.unverified=The identity of %S has not been verified.
+
+verify.title=Verify your contact's identity
+error.title=Error
+success.title=End to End Encryption
+successThem.title=Verify your contact's identity
+fail.title=Unable to verify
+waiting.title=Verification request sent
diff --git a/chat/content/otrWorker.js b/chat/content/otrWorker.js
new file mode 100644
index 0000000000..12c68b39b0
--- /dev/null
+++ b/chat/content/otrWorker.js
@@ -0,0 +1,54 @@
+/* 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/. */
+
+/* eslint-env mozilla/chrome-worker, node */
+importScripts("resource://gre/modules/workers/require.js");
+var PromiseWorker = require("resource://gre/modules/workers/PromiseWorker.js");
+var Funcs = {};
+
+// Only what we need from libotr.js
+Funcs.generateKey = function(path, otrl_version, newkeySource) {
+ // eslint-disable-next-line no-eval
+ let newkey = eval(newkeySource); // jshint ignore:line
+ let libotr = ctypes.open(path);
+
+ let abi = ctypes.default_abi;
+ let gcry_error_t = ctypes.unsigned_int;
+
+ // Initialize the OTR library. Pass the version of the API you are using.
+ let otrl_init = libotr.declare(
+ "otrl_init", abi, gcry_error_t,
+ ctypes.unsigned_int, ctypes.unsigned_int, ctypes.unsigned_int
+ );
+
+ // Do the private key generation calculation. You may call this from a
+ // background thread. When it completes, call
+ // otrl_privkey_generate_finish from the _main_ thread.
+ let otrl_privkey_generate_calculate = libotr.declare(
+ "otrl_privkey_generate_calculate", abi, gcry_error_t,
+ ctypes.void_t.ptr
+ );
+
+ otrl_init.apply(libotr, otrl_version);
+ let err = otrl_privkey_generate_calculate(newkey);
+ libotr.close();
+ if (err)
+ throw new Error("otrl_privkey_generate_calculate (" + err + ")");
+};
+
+var worker = new PromiseWorker.AbstractWorker();
+
+worker.dispatch = function(method, args = []) {
+ return Funcs[method](...args);
+};
+
+worker.postMessage = function(res, ...args) {
+ self.postMessage(res, ...args);
+};
+
+worker.close = function() {
+ self.close();
+};
+
+self.addEventListener("message", msg => worker.handleMessage(msg));
diff --git a/chat/locales/jar.mn b/chat/locales/jar.mn
index bc8f2795ab..c85244453e 100644
--- a/chat/locales/jar.mn
+++ b/chat/locales/jar.mn
@@ -22,3 +22,11 @@
locale/@AB_CD@/chat/twitter.properties (%twitter.properties)
locale/@AB_CD@/chat/xmpp.properties (%xmpp.properties)
locale/@AB_CD@/chat/yahoo.properties (%yahoo.properties)
+# locale/@AB_CD@/chat/otr-auth.dtd (%otr-auth.dtd)
+# locale/@AB_CD@/chat/otr-auth.properties (%otr-auth.properties)
+# locale/@AB_CD@/chat/otr-add-finger.dtd (%otr-add-finger.dtd)
+# locale/@AB_CD@/chat/otr-add-finger.properties (%otr-add-finger.properties)
+# locale/@AB_CD@/chat/otr.properties (%otr.properties)
+# locale/@AB_CD@/chat/otr-generate-key.dtd (%otr-generate-key.dtd)
+# locale/@AB_CD@/chat/otr-generate-key.properties (%otr-generate-key.properties)
+# locale/@AB_CD@/chat/otrUI.properties (%otrUI.properties)
diff --git a/chat/modules/CLib.jsm b/chat/modules/CLib.jsm
new file mode 100644
index 0000000000..2155b215db
--- /dev/null
+++ b/chat/modules/CLib.jsm
@@ -0,0 +1,74 @@
+/* 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/. */
+
+const {ctypes} = ChromeUtils.import("resource://gre/modules/ctypes.jsm");
+const {Services} = ChromeUtils.import("resource:///modules/imServices.jsm");
+
+var OS = Services.appinfo.OS.toLowerCase();
+
+// type defs
+
+var FILE = ctypes.StructType("FILE");
+var fname_t = ctypes.char.ptr;
+var wchar_t = ctypes.char16_t;
+
+// Set the abi and path to CLib based on the OS.
+var libcAbi, libcPath;
+var strdup = "strdup";
+var fopen = "fopen";
+
+switch (OS) {
+case "win32":
+case "winnt":
+ libcAbi = ctypes.winapi_abi;
+ libcPath = ctypes.libraryName("msvcrt");
+ strdup = "_strdup";
+ fopen = "_wfopen";
+ fname_t = wchar_t.ptr;
+ break;
+case "darwin":
+ libcAbi = ctypes.default_abi;
+ libcPath = ctypes.libraryName("c");
+ break;
+case "linux":
+ libcAbi = ctypes.default_abi;
+ libcPath = "libc.so.6";
+ break;
+default:
+ throw new Error("Unknown OS");
+}
+
+var libc = ctypes.open(libcPath);
+
+var CLib = {
+ FILE,
+ memcmp: libc.declare(
+ "memcmp", libcAbi, ctypes.int,
+ ctypes.void_t.ptr,
+ ctypes.void_t.ptr,
+ ctypes.size_t
+ ),
+ free: libc.declare(
+ "free", libcAbi, ctypes.void_t,
+ ctypes.void_t.ptr
+ ),
+ strdup: libc.declare(
+ strdup, libcAbi, ctypes.char.ptr,
+ ctypes.char.ptr
+ ),
+ fclose: libc.declare(
+ "fclose", libcAbi, ctypes.int,
+ FILE.ptr
+ ),
+ fopen: libc.declare(
+ fopen, libcAbi, FILE.ptr,
+ fname_t,
+ fname_t
+ ),
+};
+
+
+// exports
+
+this.EXPORTED_SYMBOLS = ["CLib"];
diff --git a/chat/modules/OTR.jsm b/chat/modules/OTR.jsm
new file mode 100644
index 0000000000..d0904a463c
--- /dev/null
+++ b/chat/modules/OTR.jsm
@@ -0,0 +1,1110 @@
+/* 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/. */
+
+const {BasePromiseWorker} = ChromeUtils.import("resource://gre/modules/PromiseWorker.jsm");
+const {OS} = ChromeUtils.import("resource://gre/modules/osfile.jsm");
+const {ctypes} = ChromeUtils.import("resource://gre/modules/ctypes.jsm");
+const {Services} = ChromeUtils.import("resource:///modules/imServices.jsm");
+const {
+ XPCOMUtils,
+ l10nHelper,
+} = ChromeUtils.import("resource:///modules/imXPCOMUtils.jsm");
+const {CLib} = ChromeUtils.import("resource:///modules/CLib.jsm");
+const {OTRLib} = ChromeUtils.import("resource:///modules/OTRLib.jsm");
+var workerPath = "chrome://chat/content/otrWorker.js";
+const {OTRHelpers} = ChromeUtils.import("resource:///modules/OTRHelpers.jsm");
+
+XPCOMUtils.defineLazyGetter(this, "_", () =>
+ l10nHelper("chrome://chat/content/otr.properties")
+);
+
+// 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;
+}
+
+// Use the protocol name in user facing strings. See trac #16490
+var names;
+function protocolName(aNormalizedName) {
+ if (!names) {
+ names = new Map();
+ let protocols = Services.core.getProtocols();
+ while (protocols.hasMoreElements()) {
+ let protocol = protocols.getNext();
+ names.set(protocol.normalizedName, protocol.name);
+ }
+ }
+ return names.get(aNormalizedName) || aNormalizedName;
+}
+
+
+// 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 OTR = {
+
+ hasRan: false,
+ libLoaded: false,
+ once() {
+ this.hasRan = true;
+ try {
+ if (OTRLib && OTRLib.init()) {
+ this.initUiOps();
+ OTR.libLoaded = true;
+ }
+ } catch (e) {
+ console.log(e);
+ }
+ },
+
+ privateKeyPath: OTRHelpers.profilePath("otr.private_key"),
+ fingerprintsPath: OTRHelpers.profilePath("otr.fingerprints"),
+ instanceTagsPath: OTRHelpers.profilePath("otr.instance_tags"),
+
+ init(opts) {
+ opts = opts || {};
+
+ if (!this.hasRan)
+ this.once();
+
+ if (!OTR.libLoaded)
+ return;
+
+ this.verifyNudge = !!opts.verifyNudge;
+ this.setPolicy(opts.requireEncryption);
+ this.userstate = OTRLib.otrl_userstate_create();
+
+ // A map of UIConvs, keyed on the target.id
+ this._convos = new Map();
+ this._observers = [];
+ this._buffer = [];
+ 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;
+ for (let i = 0; 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;
+ }
+ }
+ }, 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");
+ },
+
+ protocolName,
+
+ setPolicy(requireEncryption) {
+ if (!OTR.libLoaded)
+ return;
+ this.policy = requireEncryption
+ ? OTRLib.OTRL_POLICY_ALWAYS
+ : OTRLib.OTRL_POLICY_OPPORTUNISTIC;
+ },
+
+ // load stored files from my profile
+ loadFiles() {
+ return Promise.all([
+ OS.File.exists(this.privateKeyPath).then((exists) => {
+ if (exists && OTRLib.otrl_privkey_read(
+ this.userstate, this.privateKeyPath
+ )) throw new Error("Failed to read private keys.");
+ }),
+ OS.File.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.");
+ }),
+ OS.File.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 worker = new BasePromiseWorker(workerPath);
+ return worker.post("generateKey", [
+ OTRLib.path, OTRLib.otrl_version, newkey.toSource(),
+ ]).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;
+ });
+ },
+
+ // 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 = fingerprint.contents.fingerprint;
+ if (hash.isNull())
+ throw 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;
+ },
+
+ getTrustLevel(context) {
+ let level = OTR.trust(context);
+ if (level === OTR.trustState.TRUST_PRIVATE) {
+ return OTR.trustState.TRUST_PRIVATE;
+ } else if (level === OTR.trustState.TRUST_UNVERIFIED) {
+ return OTR.trustState.TRUST_UNVERIFIED;
+ } else if (level === OTR.trustState.TRUST_FINISHED) {
+ return OTR.trustState.TRUST_FINISHED;
+ }
+ return OTR.trustState.TRUST_NOT_PRIVATE;
+ },
+
+ getStatus(level) {
+ switch (level) {
+ case OTR.trustState.TRUST_NOT_PRIVATE:
+ return _("trust.not_private");
+ case OTR.trustState.TRUST_UNVERIFIED:
+ return _("trust.unverified");
+ case OTR.trustState.TRUST_PRIVATE:
+ return _("trust.private");
+ case OTR.trustState.TRUST_FINISHED:
+ return _("trust.finished");
+ }
+ throw new Error("unknown level");
+ },
+
+ // get list of known fingerprints
+ knownFingerprints() {
+ 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);
+ for (
+ let fingerprint = context.contents.fingerprint_root.next;
+ !fingerprint.isNull();
+ fingerprint = fingerprint.contents.next
+ ) {
+ let trust = trustFingerprint(fingerprint);
+ let used = false;
+ let best_level = OTR.trustState.TRUST_NOT_PRIVATE;
+ for (
+ let context_itr = context;
+ !context_itr.isNull() &&
+ comparePointers(context_itr.contents.m_context, context);
+ context_itr = context_itr.contents.next
+ ) {
+ if (comparePointers(
+ context_itr.contents.active_fingerprint, fingerprint
+ )) {
+ used = true;
+ best_level = OTR.getTrustLevel(new Context(context_itr));
+ }
+ }
+ fps.push({
+ fpointer: fingerprint.contents.address(),
+ fingerprint: OTR.hashToHuman(fingerprint),
+ screenname: wContext.username,
+ account: wContext.account,
+ protocol: wContext.protocol,
+ trust,
+ status: used ? OTR.getStatus(best_level) : _("trust.unused"),
+ purge: false,
+ });
+ }
+ }
+ return fps;
+ },
+
+ forgetFingerprints(fps) {
+ 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 do anything 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)
+ ) return;
+ }
+ write = true;
+ OTRLib.otrl_context_forget_fingerprint(fingerprint, 1);
+ fps[i] = null; // null out removed fps
+ });
+ if (write)
+ OTR.writeFingerprints();
+ },
+
+ 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 Services.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");
+ },
+
+ sendQueryMsg(conv) {
+ let query = OTRLib.otrl_proto_default_query_msg(
+ conv.account.normalizedName,
+ this.policy
+ );
+ if (query.isNull()) {
+ Cu.reportError(new Error("Sending query message failed."));
+ return;
+ }
+ // Use the default msg to format the version.
+ // We don't supprt v1 of the protocol so this should be fine.
+ let queryMsg = /^\?OTR.*?\?/.exec(query.readString())[0] + "\n";
+ queryMsg += _("query.msg", conv.account.normalizedName);
+ conv.sendMsg(queryMsg);
+ OTRLib.otrl_message_free(query);
+ },
+
+ 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;
+ },
+
+ // uiOps callbacks
+
+ // Return the OTR policy for the given context.
+ policy_cb(opdata, context) {
+ return this.policy;
+ },
+
+ // 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.getUIConvForRecipient(
+ accountname.readString(),
+ protocol.readString(),
+ recipient.readString()
+ ).target.sendMsg(aMsg);
+ },
+
+ // A 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;
+ }
+
+ // Only nudge on new fingerprint, as opposed to always.
+ if (!this.verifyNudge)
+ this.notifyObservers(new Context(context), "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) {
+ context = new Context(context);
+ let str = "context.gone_secure_" + (context.trust ? "private" : "unverified");
+ this.notifyObservers(context, "otr:msg-state");
+ this.sendAlert(context, _(str, context.username));
+ if (this.verifyNudge && !context.trust)
+ this.notifyObservers(context, "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.
+ // 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, _("context.still_secure", 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 = _("error.enc");
+ break;
+ case OTRLib.errorCode.OTRL_ERRCODE_MSG_NOT_IN_PRIVATE:
+ msg = _("error.not_priv", context.username);
+ break;
+ case OTRLib.errorCode.OTRL_ERRCODE_MSG_UNREADABLE:
+ msg = _("error.unreadable");
+ break;
+ case OTRLib.errorCode.OTRL_ERRCODE_MSG_MALFORMED:
+ msg = _("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(_("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, _("msgevent.encryption_required_part1", context.username));
+ this.sendAlert(context, _("msgevent.encryption_required_part2"));
+ break;
+ case OTRLib.messageEvent.OTRL_MSGEVENT_ENCRYPTION_ERROR:
+ this.sendAlert(context, _("msgevent.encryption_error"));
+ break;
+ case OTRLib.messageEvent.OTRL_MSGEVENT_CONNECTION_ENDED:
+ this.sendAlert(context, _("msgevent.connection_ended", context.username));
+ break;
+ case OTRLib.messageEvent.OTRL_MSGEVENT_SETUP_ERROR:
+ this.sendAlert(context, _("msgevent.setup_error", context.username));
+ break;
+ case OTRLib.messageEvent.OTRL_MSGEVENT_MSG_REFLECTED:
+ this.sendAlert(context, _("msgevent.msg_reflected"));
+ break;
+ case OTRLib.messageEvent.OTRL_MSGEVENT_MSG_RESENT:
+ this.sendAlert(context, _("msgevent.msg_resent", context.username));
+ break;
+ case OTRLib.messageEvent.OTRL_MSGEVENT_RCVDMSG_NOT_IN_PRIVATE:
+ this.sendAlert(context, _("msgevent.rcvdmsg_not_private", context.username));
+ break;
+ case OTRLib.messageEvent.OTRL_MSGEVENT_RCVDMSG_UNREADABLE:
+ this.sendAlert(context, _("msgevent.rcvdmsg_unreadable", context.username));
+ break;
+ case OTRLib.messageEvent.OTRL_MSGEVENT_RCVDMSG_MALFORMED:
+ this.sendAlert(context, _("msgevent.rcvdmsg_malformed", context.username));
+ break;
+ case OTRLib.messageEvent.OTRL_MSGEVENT_LOG_HEARTBEAT_RCVD:
+ this.log(_("msgevent.log_heartbeat_rcvd", context.username));
+ break;
+ case OTRLib.messageEvent.OTRL_MSGEVENT_LOG_HEARTBEAT_SENT:
+ this.log(_("msgevent.log_heartbeat_sent", context.username));
+ break;
+ case OTRLib.messageEvent.OTRL_MSGEVENT_RCVDMSG_GENERAL_ERR:
+ this.sendAlert(context, _("msgevent.rcvdmsg_general_err"));
+ break;
+ case OTRLib.messageEvent.OTRL_MSGEVENT_RCVDMSG_UNENCRYPTED:
+ this.sendAlert(context, _("msgevent.rcvdmsg_unencrypted", context.username, message.isNull() ? "" : message.readString()));
+ break;
+ case OTRLib.messageEvent.OTRL_MSGEVENT_RCVDMSG_UNRECOGNIZED:
+ this.sendAlert(context, _("msgevent.rcvdmsg_unrecognized", context.username));
+ break;
+ case OTRLib.messageEvent.OTRL_MSGEVENT_RCVDMSG_FOR_OTHER_INSTANCE:
+ this.log(_("msgevent.rcvdmsg_for_other_instance", 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);
+ }
+ },
+
+ // 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);
+ },
+
+ 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;
+ }
+ },
+
+ 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;
+
+ // check for irc action messages
+ if (om.action) {
+ om.cancelled = true;
+ let uiConv = this.getUIConvFromConv(conv);
+ if (uiConv)
+ uiConv.sendMsg("/me " + om.message);
+ 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;
+ Cu.reportError(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 ||
+ // Twitter trims tweets.
+ conv.account.protocol.normalizedName === "twitter") {
+ 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);
+ },
+
+ onReceive(im) {
+ if (im.cancelled || im.system)
+ return;
+
+ let conv = im.conversation;
+ if (conv.isChat)
+ return;
+
+ 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();
+
+ this.log("pre receiving: " + im.displayMessage);
+
+ 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
+ );
+
+ if (!newMessage.isNull()) {
+ im.displayMessage = newMessage.readString();
+ }
+
+ // 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, _("tlv.disconnected", conv.normalizedName));
+ }
+
+ if (err) {
+ this.log("error (" + err + ") ignoring: " + im.displayMessage);
+ im.cancelled = true; // ignore
+ } else {
+ this.log("post receiving: " + im.displayMessage);
+ }
+
+ 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
+
+ clearMsgs(convId) {
+ this._buffer = this._buffer.filter((msg) => msg.convId !== convId);
+ },
+
+ bufferMsg(convId, display, sent) {
+ this._buffer.push({
+ convId,
+ display,
+ sent,
+ time: Date.now(),
+ });
+ },
+
+ pluckMsg(im) {
+ let buf = this._buffer;
+ for (let i = 0; i < buf.length; i++) {
+ let b = buf[i];
+ if (b.convId === im.conversation.id && b.sent === im.displayMessage) {
+ im.displayMessage = b.display;
+ buf.splice(i, 1);
+ this.log("displaying: " + b.display);
+ return;
+ }
+ }
+ // don't display if it wasn't buffered
+ im.cancelled = true;
+ this.log("not displaying: " + im.displayMessage);
+ },
+
+};
+
+// exports
+
+this.EXPORTED_SYMBOLS = ["OTR"];
diff --git a/chat/modules/OTRHelpers.jsm b/chat/modules/OTRHelpers.jsm
new file mode 100644
index 0000000000..5870cb8951
--- /dev/null
+++ b/chat/modules/OTRHelpers.jsm
@@ -0,0 +1,38 @@
+/* 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/. */
+
+const {OS} = ChromeUtils.import("resource://gre/modules/osfile.jsm");
+const {Services} = ChromeUtils.import("resource:///modules/imServices.jsm");
+
+var OTRHelpers = {
+
+ profilePath(filename) {
+ return OS.Path.join(OS.Constants.Path.profileDir, filename);
+ },
+
+ * getAccounts() {
+ let accounts = Services.accounts.getAccounts();
+ while (accounts.hasMoreElements())
+ yield accounts.getNext();
+ },
+
+ readTextFile(filename) {
+ let decoder = new TextDecoder();
+ return OS.File.read(filename).then(function(array) {
+ return decoder.decode(array);
+ });
+ },
+
+ writeTextFile(filename, data) {
+ let encoder = new TextEncoder();
+ let array = encoder.encode(data);
+ // https://dutherenverseauborddelatable.wordpress.com/2014/02/05/is-my-data-on-the-disk-safety-properties-of-os-file-writeatomic/
+ return OS.File.writeAtomic(filename, array, { tmpPath: `${filename}.tmp` });
+ },
+
+};
+
+// exports
+
+this.EXPORTED_SYMBOLS = ["OTRHelpers"];
diff --git a/chat/modules/OTRLib.jsm b/chat/modules/OTRLib.jsm
new file mode 100644
index 0000000000..0d17d82cef
--- /dev/null
+++ b/chat/modules/OTRLib.jsm
@@ -0,0 +1,918 @@
+/* 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/. */
+
+const otrl_version = [4, 1, 1];
+
+const {CLib} = ChromeUtils.import("resource:///modules/CLib.jsm");
+const {ctypes} = ChromeUtils.import("resource://gre/modules/ctypes.jsm");
+const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
+var systemOS = Services.appinfo.OS.toLowerCase();
+
+const {OS} = ChromeUtils.import("resource://gre/modules/osfile.jsm");
+
+var abi = ctypes.default_abi;
+
+// Open libotr. Determine the path to the chrome directory and look for it
+// there first. If not, fallback to searching the standard locations.
+var libotr, libotrPath;
+
+function tryLoadOTR(name, suffix) {
+ let filename = ctypes.libraryName(name) + suffix;
+ let binPath = Services.dirsvc.get("XpcomLib", Ci.nsIFile).path;
+ let binDir = OS.Path.dirname(binPath);
+ libotrPath = OS.Path.join(binDir, filename);
+
+ try {
+ console.log("===> trying to load " + libotrPath);
+ libotr = ctypes.open(libotrPath);
+ } catch (e) {}
+
+ if (!libotr) {
+ try {
+ // look in standard locations
+ libotrPath = filename;
+ console.log("===> trying to load " +
+ libotrPath + " from system's standard locations");
+ libotr = ctypes.open(libotrPath);
+ } catch (e) {}
+ }
+}
+
+if (!libotr && (systemOS === "winnt" || systemOS === "darwin")) {
+ // otr.5.dll or otr.5.dylib
+ tryLoadOTR("otr.5", "");
+}
+
+if (!libotr && systemOS === "winnt") {
+ // otr-5.dll
+ tryLoadOTR("otr-5", "");
+}
+
+if (!libotr && systemOS === "winnt") {
+ // libotr-5.dll
+ tryLoadOTR("libotr-5", "");
+}
+
+if (!libotr && !(systemOS === "winnt") && !(systemOS === "darwin")) {
+ // libotr.so.5
+ tryLoadOTR("otr", ".5");
+}
+
+if (!libotr) {
+ tryLoadOTR("otr", "");
+}
+
+// Helper function to open files with the path properly encoded.
+var callWithFILEp = function() {
+ // Windows filenames are in UTF-16.
+ let charType = (systemOS === "winnt") ? "jschar" : "char";
+
+ let args = Array.from(arguments);
+ let func = args.shift() + "_FILEp";
+ let mode = ctypes[charType].array()(args.shift());
+ let ind = args.shift();
+ let filename = ctypes[charType].array()(args[ind]);
+
+ let file = CLib.fopen(filename, mode);
+ if (file.isNull())
+ return 1;
+
+ // Swap filename with file.
+ args[ind] = file;
+
+ let ret = OTRLib[func].apply(OTRLib, args);
+ CLib.fclose(file);
+ return ret;
+};
+
+// type defs
+
+const FILE = CLib.FILE;
+
+const time_t = ctypes.long;
+const gcry_error_t = ctypes.unsigned_int;
+const gcry_cipher_hd_t = ctypes.StructType("gcry_cipher_handle").ptr;
+const gcry_md_hd_t = ctypes.StructType("gcry_md_handle").ptr;
+const gcry_mpi_t = ctypes.StructType("gcry_mpi").ptr;
+
+const otrl_instag_t = ctypes.unsigned_int;
+const OtrlPolicy = ctypes.unsigned_int;
+const OtrlTLV = ctypes.StructType("s_OtrlTLV");
+const ConnContext = ctypes.StructType("context");
+const ConnContextPriv = ctypes.StructType("context_priv");
+const OtrlMessageAppOps = ctypes.StructType("s_OtrlMessageAppOps");
+const OtrlAuthInfo = ctypes.StructType("OtrlAuthInfo");
+const Fingerprint = ctypes.StructType("s_fingerprint");
+const s_OtrlUserState = ctypes.StructType("s_OtrlUserState");
+const OtrlUserState = s_OtrlUserState.ptr;
+const OtrlSMState = ctypes.StructType("OtrlSMState");
+const DH_keypair = ctypes.StructType("DH_keypair");
+const OtrlPrivKey = ctypes.StructType("s_OtrlPrivKey");
+const OtrlInsTag = ctypes.StructType("s_OtrlInsTag");
+const OtrlPendingPrivKey = ctypes.StructType("s_OtrlPendingPrivKey");
+
+const OTRL_PRIVKEY_FPRINT_HUMAN_LEN = 45;
+const fingerprint_t = ctypes.char.array(OTRL_PRIVKEY_FPRINT_HUMAN_LEN);
+const hash_t = ctypes.unsigned_char.array(20);
+
+const app_data_free_t = ctypes.FunctionType(abi, ctypes.void_t, [
+ ctypes.void_t.ptr,
+]).ptr;
+
+// enums
+
+const OtrlErrorCode = ctypes.int;
+const OtrlSMPEvent = ctypes.int;
+const OtrlMessageEvent = ctypes.int;
+const OtrlFragmentPolicy = ctypes.int;
+const OtrlConvertType = ctypes.int;
+const OtrlMessageState = ctypes.int;
+const OtrlAuthState = ctypes.int;
+const OtrlSessionIdHalf = ctypes.int;
+const OtrlSMProgState = ctypes.int;
+const NextExpectedSMP = ctypes.int;
+
+// callback signatures
+
+const policy_cb_t = ctypes.FunctionType(abi, OtrlPolicy, [
+ ctypes.void_t.ptr, ConnContext.ptr,
+]).ptr;
+
+const create_privkey_cb_t = ctypes.FunctionType(abi, ctypes.void_t, [
+ ctypes.void_t.ptr, ctypes.char.ptr, ctypes.char.ptr,
+]).ptr;
+
+const is_logged_in_cb_t = ctypes.FunctionType(abi, ctypes.int, [
+ ctypes.void_t.ptr, ctypes.char.ptr, ctypes.char.ptr, ctypes.char.ptr,
+]).ptr;
+
+const inject_message_cb_t = ctypes.FunctionType(abi, ctypes.void_t, [
+ ctypes.void_t.ptr, ctypes.char.ptr, ctypes.char.ptr, ctypes.char.ptr,
+ ctypes.char.ptr,
+]).ptr;
+
+const update_context_list_cb_t = ctypes.FunctionType(abi, ctypes.void_t, [
+ ctypes.void_t.ptr,
+]).ptr;
+
+const new_fingerprint_cb_t = ctypes.FunctionType(abi, ctypes.void_t, [
+ ctypes.void_t.ptr, OtrlUserState, ctypes.char.ptr, ctypes.char.ptr,
+ ctypes.char.ptr, ctypes.unsigned_char.array(20),
+]).ptr;
+
+const write_fingerprint_cb_t = ctypes.FunctionType(abi, ctypes.void_t, [
+ ctypes.void_t.ptr,
+]).ptr;
+
+const gone_secure_cb_t = ctypes.FunctionType(abi, ctypes.void_t, [
+ ctypes.void_t.ptr, ConnContext.ptr,
+]).ptr;
+
+const gone_insecure_cb_t = ctypes.FunctionType(abi, ctypes.void_t, [
+ ctypes.void_t.ptr, ConnContext.ptr,
+]).ptr;
+
+const still_secure_cb_t = ctypes.FunctionType(abi, ctypes.void_t, [
+ ctypes.void_t.ptr, ConnContext.ptr, ctypes.int,
+]).ptr;
+
+const max_message_size_cb_t = ctypes.FunctionType(abi, ctypes.int, [
+ ctypes.void_t.ptr, ConnContext.ptr,
+]).ptr;
+
+const account_name_cb_t = ctypes.FunctionType(abi, ctypes.char.ptr, [
+ ctypes.void_t.ptr, ctypes.char.ptr, ctypes.char.ptr,
+]).ptr;
+
+const account_name_free_cb_t = ctypes.FunctionType(abi, ctypes.void_t, [
+ ctypes.void_t.ptr, ctypes.char.ptr,
+]).ptr;
+
+const received_symkey_cb_t = ctypes.FunctionType(abi, ctypes.void_t, [
+ ctypes.void_t.ptr, ConnContext.ptr, ctypes.unsigned_int,
+ ctypes.unsigned_char.ptr, ctypes.size_t, ctypes.unsigned_char.ptr,
+]).ptr;
+
+const otr_error_message_cb_t = ctypes.FunctionType(abi, ctypes.char.ptr, [
+ ctypes.void_t.ptr, ConnContext.ptr, OtrlErrorCode,
+]).ptr;
+
+const otr_error_message_free_cb_t = ctypes.FunctionType(abi, ctypes.void_t, [
+ ctypes.void_t.ptr, ctypes.char.ptr,
+]).ptr;
+
+const resent_msg_prefix_cb_t = ctypes.FunctionType(abi, ctypes.char.ptr, [
+ ctypes.void_t.ptr, ConnContext.ptr,
+]).ptr;
+
+const resent_msg_prefix_free_cb_t = ctypes.FunctionType(abi, ctypes.void_t, [
+ ctypes.void_t.ptr, ctypes.char.ptr,
+]).ptr;
+
+const handle_smp_event_cb_t = ctypes.FunctionType(abi, ctypes.void_t, [
+ ctypes.void_t.ptr, OtrlSMPEvent, ConnContext.ptr, ctypes.unsigned_short,
+ ctypes.char.ptr,
+]).ptr;
+
+const handle_msg_event_cb_t = ctypes.FunctionType(abi, ctypes.void_t, [
+ ctypes.void_t.ptr, OtrlMessageEvent, ConnContext.ptr, ctypes.char.ptr,
+ gcry_error_t,
+]).ptr;
+
+const create_instag_cb_t = ctypes.FunctionType(abi, ctypes.void_t, [
+ ctypes.void_t.ptr, ctypes.char.ptr, ctypes.char.ptr,
+]).ptr;
+
+const convert_msg_cb_t = ctypes.FunctionType(abi, ctypes.void_t, [
+ ctypes.void_t.ptr, ConnContext.ptr, OtrlConvertType, ctypes.char.ptr.ptr,
+ ctypes.char.ptr,
+]).ptr;
+
+const convert_free_cb_t = ctypes.FunctionType(abi, ctypes.void_t, [
+ ctypes.void_t.ptr, ConnContext.ptr, ctypes.char.ptr,
+]).ptr;
+
+const timer_control_cb_t = ctypes.FunctionType(abi, ctypes.void_t, [
+ ctypes.void_t.ptr, ctypes.unsigned_int,
+]).ptr;
+
+// defines
+
+s_OtrlUserState.define([
+ { context_root: ConnContext.ptr },
+ { privkey_root: OtrlPrivKey.ptr },
+ { instag_root: OtrlInsTag.ptr },
+ { pending_root: OtrlPendingPrivKey.ptr },
+ { timer_running: ctypes.int },
+]);
+
+Fingerprint.define([
+ { next: Fingerprint.ptr },
+ { tous: Fingerprint.ptr.ptr },
+ { fingerprint: ctypes.unsigned_char.ptr },
+ { context: ConnContext.ptr },
+ { trust: ctypes.char.ptr },
+]);
+
+DH_keypair.define([
+ { groupid: ctypes.unsigned_int },
+ { priv: gcry_mpi_t },
+ { pub: gcry_mpi_t },
+]);
+
+OtrlSMState.define([
+ { secret: gcry_mpi_t },
+ { x2: gcry_mpi_t },
+ { x3: gcry_mpi_t },
+ { g1: gcry_mpi_t },
+ { g2: gcry_mpi_t },
+ { g3: gcry_mpi_t },
+ { g3o: gcry_mpi_t },
+ { p: gcry_mpi_t },
+ { q: gcry_mpi_t },
+ { pab: gcry_mpi_t },
+ { qab: gcry_mpi_t },
+ { nextExpected: NextExpectedSMP },
+ { received_question: ctypes.int },
+ { sm_prog_state: OtrlSMProgState },
+]);
+
+OtrlAuthInfo.define([
+ { authstate: OtrlAuthState },
+ { context: ConnContext.ptr },
+ { our_dh: DH_keypair },
+ { our_keyid: ctypes.unsigned_int },
+ { encgx: ctypes.unsigned_char.ptr },
+ { encgx_len: ctypes.size_t },
+ { r: ctypes.unsigned_char.array(16) },
+ { hashgx: ctypes.unsigned_char.array(32) },
+ { their_pub: gcry_mpi_t },
+ { their_keyid: ctypes.unsigned_int },
+ { enc_c: gcry_cipher_hd_t },
+ { enc_cp: gcry_cipher_hd_t },
+ { mac_m1: gcry_md_hd_t },
+ { mac_m1p: gcry_md_hd_t },
+ { mac_m2: gcry_md_hd_t },
+ { mac_m2p: gcry_md_hd_t },
+ { their_fingerprint: ctypes.unsigned_char.array(20) },
+ { initiated: ctypes.int },
+ { protocol_version: ctypes.unsigned_int },
+ { secure_session_id: ctypes.unsigned_char.array(20) },
+ { secure_session_id_len: ctypes.size_t },
+ { session_id_half: OtrlSessionIdHalf },
+ { lastauthmsg: ctypes.char.ptr },
+ { commit_sent_time: time_t },
+]);
+
+ConnContext.define([
+ { next: ConnContext.ptr },
+ { tous: ConnContext.ptr.ptr },
+ { context_priv: ConnContextPriv.ptr },
+ { username: ctypes.char.ptr },
+ { accountname: ctypes.char.ptr },
+ { protocol: ctypes.char.ptr },
+ { m_context: ConnContext.ptr },
+ { recent_rcvd_child: ConnContext.ptr },
+ { recent_sent_child: ConnContext.ptr },
+ { recent_child: ConnContext.ptr },
+ { our_instance: otrl_instag_t },
+ { their_instance: otrl_instag_t },
+ { msgstate: OtrlMessageState },
+ { auth: OtrlAuthInfo },
+ { fingerprint_root: Fingerprint },
+ { active_fingerprint: Fingerprint.ptr },
+ { sessionid: ctypes.unsigned_char.array(20) },
+ { sessionid_len: ctypes.size_t },
+ { sessionid_half: OtrlSessionIdHalf },
+ { protocol_version: ctypes.unsigned_int },
+ { otr_offer: ctypes.int },
+ { app_data: ctypes.void_t.ptr },
+ { app_data_free: app_data_free_t },
+ { smstate: OtrlSMState.ptr },
+]);
+
+OtrlMessageAppOps.define([
+ { policy: policy_cb_t },
+ { create_privkey: create_privkey_cb_t },
+ { is_logged_in: is_logged_in_cb_t },
+ { inject_message: inject_message_cb_t },
+ { update_context_list: update_context_list_cb_t },
+ { new_fingerprint: new_fingerprint_cb_t },
+ { write_fingerprint: write_fingerprint_cb_t },
+ { gone_secure: gone_secure_cb_t },
+ { gone_insecure: gone_insecure_cb_t },
+ { still_secure: still_secure_cb_t },
+ { max_message_size: max_message_size_cb_t },
+ { account_name: account_name_cb_t },
+ { account_name_free: account_name_free_cb_t },
+ { received_symkey: received_symkey_cb_t },
+ { otr_error_message: otr_error_message_cb_t },
+ { otr_error_message_free: otr_error_message_free_cb_t },
+ { resent_msg_prefix: resent_msg_prefix_cb_t },
+ { resent_msg_prefix_free: resent_msg_prefix_free_cb_t },
+ { handle_smp_event: handle_smp_event_cb_t },
+ { handle_msg_event: handle_msg_event_cb_t },
+ { create_instag: create_instag_cb_t },
+ { convert_msg: convert_msg_cb_t },
+ { convert_free: convert_free_cb_t },
+ { timer_control: timer_control_cb_t },
+]);
+
+OtrlTLV.define([
+ { type: ctypes.unsigned_short },
+ { len: ctypes.unsigned_short },
+ { data: ctypes.unsigned_char.ptr },
+ { next: OtrlTLV.ptr },
+]);
+
+// policies
+
+// const OTRL_POLICY_ALLOW_V1 = 0x01;
+const OTRL_POLICY_ALLOW_V2 = 0x02;
+
+// const OTRL_POLICY_ALLOW_V3 = 0x04;
+// See https://bugzilla.mozilla.org/show_bug.cgi?id=1550474 re v3.
+
+const OTRL_POLICY_REQUIRE_ENCRYPTION = 0x08;
+const OTRL_POLICY_SEND_WHITESPACE_TAG = 0x10;
+const OTRL_POLICY_WHITESPACE_START_AKE = 0x20;
+
+// const OTRL_POLICY_ERROR_START_AKE = 0x40;
+// Disabled to avoid automatic resend and MITM, as explained in
+// https://github.com/arlolra/ctypes-otr/issues/55
+
+var OTRLib;
+
+if (libotr) OTRLib = {
+
+ path: libotrPath,
+
+ // libotr API version
+ otrl_version,
+
+ init() {
+ // console.log("===> OTRLib.init()\n");
+ // apply version array as arguments to the init function
+ if (this.otrl_init.apply(this, this.otrl_version)) {
+ throw new Error("Couldn't initialize libotr.");
+ }
+ return true;
+ },
+
+ // proto.h
+
+ // If we ever see this sequence in a plaintext message, we'll assume the
+ // other side speaks OTR, and try to establish a connection.
+ OTRL_MESSAGE_TAG_BASE: " \t \t\t\t\t \t \t \t ",
+
+ OTRL_POLICY_OPPORTUNISTIC: new ctypes.unsigned_int(
+ OTRL_POLICY_ALLOW_V2 |
+ // OTRL_POLICY_ALLOW_V3 |
+ OTRL_POLICY_SEND_WHITESPACE_TAG |
+ OTRL_POLICY_WHITESPACE_START_AKE |
+ // OTRL_POLICY_ERROR_START_AKE |
+ 0
+ ),
+
+ OTRL_POLICY_ALWAYS: new ctypes.unsigned_int(
+ OTRL_POLICY_ALLOW_V2 |
+ // OTRL_POLICY_ALLOW_V3 |
+ OTRL_POLICY_REQUIRE_ENCRYPTION |
+ OTRL_POLICY_WHITESPACE_START_AKE |
+ // OTRL_POLICY_ERROR_START_AKE |
+ 0
+ ),
+
+ fragPolicy: {
+ OTRL_FRAGMENT_SEND_SKIP: 0,
+ OTRL_FRAGMENT_SEND_ALL: 1,
+ OTRL_FRAGMENT_SEND_ALL_BUT_FIRST: 2,
+ OTRL_FRAGMENT_SEND_ALL_BUT_LAST: 3,
+ },
+
+ // Return a pointer to a newly-allocated OTR query message, customized
+ // with our name. The caller should free() the result when he's done
+ // with it.
+ otrl_proto_default_query_msg: libotr.declare(
+ "otrl_proto_default_query_msg", abi, ctypes.char.ptr,
+ ctypes.char.ptr, OtrlPolicy
+ ),
+
+ // Initialize the OTR library. Pass the version of the API you are using.
+ otrl_init: libotr.declare(
+ "otrl_init", abi, gcry_error_t,
+ ctypes.unsigned_int, ctypes.unsigned_int, ctypes.unsigned_int
+ ),
+
+ // instag.h
+
+ instag: {
+ OTRL_INSTAG_MASTER: new ctypes.unsigned_int(0),
+ OTRL_INSTAG_BEST: new ctypes.unsigned_int(1),
+ OTRL_INSTAG_RECENT: new ctypes.unsigned_int(2),
+ OTRL_INSTAG_RECENT_RECEIVED: new ctypes.unsigned_int(3),
+ OTRL_INSTAG_RECENT_SENT: new ctypes.unsigned_int(4),
+ OTRL_MIN_VALID_INSTAG: new ctypes.unsigned_int(0x100),
+ },
+
+ // Get a new instance tag for the given account and write to file. The FILE*
+ // must be open for writing.
+ otrl_instag_generate: callWithFILEp.bind(null, "otrl_instag_generate", "wb", 1),
+ otrl_instag_generate_FILEp: libotr.declare(
+ "otrl_instag_generate_FILEp", abi, gcry_error_t,
+ OtrlUserState, FILE.ptr, ctypes.char.ptr, ctypes.char.ptr
+ ),
+
+ // Read our instance tag from a file on disk into the given OtrlUserState.
+ // The FILE* must be open for reading.
+ otrl_instag_read: callWithFILEp.bind(null, "otrl_instag_read", "rb", 1),
+ otrl_instag_read_FILEp: libotr.declare(
+ "otrl_instag_read_FILEp", abi, gcry_error_t,
+ OtrlUserState, FILE.ptr
+ ),
+
+ // Write our instance tags to a file on disk. The FILE* must be open for
+ // writing.
+ otrl_instag_write: callWithFILEp.bind(null, "otrl_instag_write", "wb", 1),
+ otrl_instag_write_FILEp: libotr.declare(
+ "otrl_instag_write_FILEp", abi, gcry_error_t,
+ OtrlUserState, FILE.ptr
+ ),
+
+ // auth.h
+
+ authState: {
+ OTRL_AUTHSTATE_NONE: 0,
+ OTRL_AUTHSTATE_AWAITING_DHKEY: 1,
+ OTRL_AUTHSTATE_AWAITING_REVEALSIG: 2,
+ OTRL_AUTHSTATE_AWAITING_SIG: 3,
+ OTRL_AUTHSTATE_V1_SETUP: 4,
+ },
+
+ // b64.h
+
+ // base64 encode data. Insert no linebreaks or whitespace.
+ // The buffer base64data must contain at least ((datalen+2)/3)*4 bytes of
+ // space. This function will return the number of bytes actually used.
+ otrl_base64_encode: libotr.declare(
+ "otrl_base64_encode", abi, ctypes.size_t,
+ ctypes.char.ptr, ctypes.unsigned_char.ptr, ctypes.size_t
+ ),
+
+ // base64 decode data. Skip non-base64 chars, and terminate at the
+ // first '=', or the end of the buffer.
+ // The buffer data must contain at least ((base64len+3) / 4) * 3 bytes
+ // of space. This function will return the number of bytes actually
+ // used.
+ otrl_base64_decode: libotr.declare(
+ "otrl_base64_decode", abi, ctypes.size_t,
+ ctypes.unsigned_char.ptr, ctypes.char.ptr, ctypes.size_t
+ ),
+
+ // context.h
+
+ otr_offer: {
+ OFFER_NOT: 0,
+ OFFER_SENT: 1,
+ OFFER_REJECTED: 2,
+ OFFER_ACCEPTED: 3,
+ },
+
+ messageState: {
+ OTRL_MSGSTATE_PLAINTEXT: 0,
+ OTRL_MSGSTATE_ENCRYPTED: 1,
+ OTRL_MSGSTATE_FINISHED: 2,
+ },
+
+ // Look up a connection context by name/account/protocol/instance from the
+ // given OtrlUserState.
+ otrl_context_find: libotr.declare(
+ "otrl_context_find", abi, ConnContext.ptr,
+ OtrlUserState,
+ ctypes.char.ptr,
+ ctypes.char.ptr,
+ ctypes.char.ptr,
+ otrl_instag_t,
+ ctypes.int,
+ ctypes.int.ptr,
+ ctypes.void_t.ptr,
+ ctypes.void_t.ptr
+ ),
+
+ // Set the trust level for a given fingerprint.
+ otrl_context_set_trust: libotr.declare(
+ "otrl_context_set_trust", abi, ctypes.void_t,
+ Fingerprint.ptr, ctypes.char.ptr
+ ),
+
+ // Find a fingerprint in a given context, perhaps adding it if not present.
+ otrl_context_find_fingerprint: libotr.declare(
+ "otrl_context_find_fingerprint", abi, Fingerprint.ptr,
+ ConnContext.ptr, hash_t, ctypes.int, ctypes.int.ptr
+ ),
+
+ // Forget a fingerprint (and maybe the whole context).
+ otrl_context_forget_fingerprint: libotr.declare(
+ "otrl_context_forget_fingerprint", abi, ctypes.void_t,
+ Fingerprint.ptr, ctypes.int
+ ),
+
+ // Return true iff the given fingerprint is marked as trusted.
+ otrl_context_is_fingerprint_trusted: libotr.declare(
+ "otrl_context_is_fingerprint_trusted", abi, ctypes.int,
+ Fingerprint.ptr
+ ),
+
+ // dh.h
+
+ sessionIdHalf: {
+ OTRL_SESSIONID_FIRST_HALF_BOLD: 0,
+ OTRL_SESSIONID_SECOND_HALF_BOLD: 1,
+ },
+
+ // sm.h
+
+ nextExpectedSMP: {
+ OTRL_SMP_EXPECT1: 0,
+ OTRL_SMP_EXPECT2: 1,
+ OTRL_SMP_EXPECT3: 2,
+ OTRL_SMP_EXPECT4: 3,
+ OTRL_SMP_EXPECT5: 4,
+ },
+
+ smProgState: {
+ OTRL_SMP_PROG_OK: 0,
+ OTRL_SMP_PROG_CHEATED: -2,
+ OTRL_SMP_PROG_FAILED: -1,
+ OTRL_SMP_PROG_SUCCEEDED: 1,
+ },
+
+ // userstate.h
+
+ // Create a new OtrlUserState.
+ otrl_userstate_create: libotr.declare(
+ "otrl_userstate_create", abi, OtrlUserState
+ ),
+
+ // privkey.h
+
+ // Generate a private DSA key for a given account, storing it into a file on
+ // disk, and loading it into the given OtrlUserState. Overwrite any
+ // previously generated keys for that account in that OtrlUserState.
+ otrl_privkey_generate: callWithFILEp.bind(null, "otrl_privkey_generate", "w+b", 1),
+ otrl_privkey_generate_FILEp: libotr.declare(
+ "otrl_privkey_generate_FILEp", abi, gcry_error_t,
+ OtrlUserState, FILE.ptr, ctypes.char.ptr, ctypes.char.ptr
+ ),
+
+ // Begin a private key generation that will potentially take place in
+ // a background thread. This routine must be called from the main
+ // thread. It will set *newkeyp, which you can pass to
+ // otrl_privkey_generate_calculate in a background thread. If it
+ // returns gcry_error(GPG_ERR_EEXIST), then a privkey creation for
+ // this accountname/protocol is already in progress, and *newkeyp will
+ // be set to NULL.
+ otrl_privkey_generate_start: libotr.declare(
+ "otrl_privkey_generate_start", abi, gcry_error_t,
+ OtrlUserState, ctypes.char.ptr, ctypes.char.ptr, ctypes.void_t.ptr.ptr
+ ),
+
+ // Do the private key generation calculation. You may call this from a
+ // background thread. When it completes, call
+ // otrl_privkey_generate_finish from the _main_ thread.
+ otrl_privkey_generate_calculate: libotr.declare(
+ "otrl_privkey_generate_calculate", abi, gcry_error_t,
+ ctypes.void_t.ptr
+ ),
+
+ // Call this from the main thread only. It will write the newly created
+ // private key into the given file and store it in the OtrlUserState.
+ otrl_privkey_generate_finish: callWithFILEp.bind(null, "otrl_privkey_generate_finish", "w+b", 2),
+ otrl_privkey_generate_finish_FILEp: libotr.declare(
+ "otrl_privkey_generate_finish_FILEp", abi, gcry_error_t,
+ OtrlUserState, ctypes.void_t.ptr, FILE.ptr
+ ),
+
+ // Call this from the main thread only, in the event that the background
+ // thread generating the key is cancelled. The newkey is deallocated,
+ // and must not be used further.
+ otrl_privkey_generate_cancelled: libotr.declare(
+ "otrl_privkey_generate_cancelled", abi, gcry_error_t,
+ OtrlUserState, ctypes.void_t.ptr
+ ),
+
+ // Read a sets of private DSA keys from a file on disk into the given
+ // OtrlUserState.
+ otrl_privkey_read: callWithFILEp.bind(null, "otrl_privkey_read", "rb", 1),
+ otrl_privkey_read_FILEp: libotr.declare(
+ "otrl_privkey_read_FILEp", abi, gcry_error_t,
+ OtrlUserState, FILE.ptr
+ ),
+
+ // Read the fingerprint store from a file on disk into the given
+ // OtrlUserState.
+ otrl_privkey_read_fingerprints: callWithFILEp.bind(null, "otrl_privkey_read_fingerprints", "rb", 1),
+ otrl_privkey_read_fingerprints_FILEp: libotr.declare(
+ "otrl_privkey_read_fingerprints_FILEp", abi, gcry_error_t,
+ OtrlUserState, FILE.ptr, ctypes.void_t.ptr, ctypes.void_t.ptr
+ ),
+
+ // Write the fingerprint store from a given OtrlUserState to a file on disk.
+ otrl_privkey_write_fingerprints: callWithFILEp.bind(null, "otrl_privkey_write_fingerprints", "wb", 1),
+ otrl_privkey_write_fingerprints_FILEp: libotr.declare(
+ "otrl_privkey_write_fingerprints_FILEp", abi, gcry_error_t,
+ OtrlUserState, FILE.ptr
+ ),
+
+ // The length of a string representing a human-readable version of a
+ // fingerprint (including the trailing NUL).
+ OTRL_PRIVKEY_FPRINT_HUMAN_LEN,
+
+ // Human readable fingerprint type
+ fingerprint_t,
+
+ // fingerprint value
+ hash_t,
+
+ // Calculate a human-readable hash of our DSA public key. Return it in the
+ // passed fingerprint buffer. Return NULL on error, or a pointer to the given
+ // buffer on success.
+ otrl_privkey_fingerprint: libotr.declare(
+ "otrl_privkey_fingerprint", abi, ctypes.char.ptr,
+ OtrlUserState, fingerprint_t, ctypes.char.ptr, ctypes.char.ptr
+ ),
+
+ // Convert a 20-byte hash value to a 45-byte human-readable value.
+ otrl_privkey_hash_to_human: libotr.declare(
+ "otrl_privkey_hash_to_human", abi, ctypes.void_t,
+ fingerprint_t, hash_t
+ ),
+
+ // Calculate a raw hash of our DSA public key. Return it in the passed
+ // fingerprint buffer. Return NULL on error, or a pointer to the given
+ // buffer on success.
+ otrl_privkey_fingerprint_raw: libotr.declare(
+ "otrl_privkey_fingerprint_raw", abi, ctypes.unsigned_char.ptr,
+ OtrlUserState, hash_t, ctypes.char.ptr, ctypes.char.ptr
+ ),
+
+ // uiOps callbacks
+ policy_cb_t,
+ create_privkey_cb_t,
+ is_logged_in_cb_t,
+ inject_message_cb_t,
+ update_context_list_cb_t,
+ new_fingerprint_cb_t,
+ write_fingerprint_cb_t,
+ gone_secure_cb_t,
+ gone_insecure_cb_t,
+ still_secure_cb_t,
+ max_message_size_cb_t,
+ account_name_cb_t,
+ account_name_free_cb_t,
+ received_symkey_cb_t,
+ otr_error_message_cb_t,
+ otr_error_message_free_cb_t,
+ resent_msg_prefix_cb_t,
+ resent_msg_prefix_free_cb_t,
+ handle_smp_event_cb_t,
+ handle_msg_event_cb_t,
+ create_instag_cb_t,
+ convert_msg_cb_t,
+ convert_free_cb_t,
+ timer_control_cb_t,
+
+ // message.h
+
+ OtrlMessageAppOps,
+
+ errorCode: {
+ OTRL_ERRCODE_NONE: 0,
+ OTRL_ERRCODE_ENCRYPTION_ERROR: 1,
+ OTRL_ERRCODE_MSG_NOT_IN_PRIVATE: 2,
+ OTRL_ERRCODE_MSG_UNREADABLE: 3,
+ OTRL_ERRCODE_MSG_MALFORMED: 4,
+ },
+
+ smpEvent: {
+ OTRL_SMPEVENT_NONE: 0,
+ OTRL_SMPEVENT_ERROR: 1,
+ OTRL_SMPEVENT_ABORT: 2,
+ OTRL_SMPEVENT_CHEATED: 3,
+ OTRL_SMPEVENT_ASK_FOR_ANSWER: 4,
+ OTRL_SMPEVENT_ASK_FOR_SECRET: 5,
+ OTRL_SMPEVENT_IN_PROGRESS: 6,
+ OTRL_SMPEVENT_SUCCESS: 7,
+ OTRL_SMPEVENT_FAILURE: 8,
+ },
+
+ messageEvent: {
+ OTRL_MSGEVENT_NONE: 0,
+ OTRL_MSGEVENT_ENCRYPTION_REQUIRED: 1,
+ OTRL_MSGEVENT_ENCRYPTION_ERROR: 2,
+ OTRL_MSGEVENT_CONNECTION_ENDED: 3,
+ OTRL_MSGEVENT_SETUP_ERROR: 4,
+ OTRL_MSGEVENT_MSG_REFLECTED: 5,
+ OTRL_MSGEVENT_MSG_RESENT: 6,
+ OTRL_MSGEVENT_RCVDMSG_NOT_IN_PRIVATE: 7,
+ OTRL_MSGEVENT_RCVDMSG_UNREADABLE: 8,
+ OTRL_MSGEVENT_RCVDMSG_MALFORMED: 9,
+ OTRL_MSGEVENT_LOG_HEARTBEAT_RCVD: 10,
+ OTRL_MSGEVENT_LOG_HEARTBEAT_SENT: 11,
+ OTRL_MSGEVENT_RCVDMSG_GENERAL_ERR: 12,
+ OTRL_MSGEVENT_RCVDMSG_UNENCRYPTED: 13,
+ OTRL_MSGEVENT_RCVDMSG_UNRECOGNIZED: 14,
+ OTRL_MSGEVENT_RCVDMSG_FOR_OTHER_INSTANCE: 15,
+ },
+
+ convertType: {
+ OTRL_CONVERT_SENDING: 0,
+ OTRL_CONVERT_RECEIVING: 1,
+ },
+
+ // Deallocate a message allocated by other otrl_message_* routines.
+ otrl_message_free: libotr.declare(
+ "otrl_message_free", abi, ctypes.void_t,
+ ctypes.char.ptr
+ ),
+
+ // Handle a message about to be sent to the network.
+ otrl_message_sending: libotr.declare(
+ "otrl_message_sending", abi, gcry_error_t,
+ OtrlUserState,
+ OtrlMessageAppOps.ptr,
+ ctypes.void_t.ptr,
+ ctypes.char.ptr,
+ ctypes.char.ptr,
+ ctypes.char.ptr,
+ otrl_instag_t,
+ ctypes.char.ptr,
+ OtrlTLV.ptr,
+ ctypes.char.ptr.ptr,
+ OtrlFragmentPolicy,
+ ConnContext.ptr.ptr,
+ ctypes.void_t.ptr,
+ ctypes.void_t.ptr
+ ),
+
+ // Handle a message just received from the network.
+ otrl_message_receiving: libotr.declare(
+ "otrl_message_receiving", abi, ctypes.int,
+ OtrlUserState,
+ OtrlMessageAppOps.ptr,
+ ctypes.void_t.ptr,
+ ctypes.char.ptr,
+ ctypes.char.ptr,
+ ctypes.char.ptr,
+ ctypes.char.ptr,
+ ctypes.char.ptr.ptr,
+ OtrlTLV.ptr.ptr,
+ ConnContext.ptr.ptr,
+ ctypes.void_t.ptr,
+ ctypes.void_t.ptr
+ ),
+
+ // Put a connection into the PLAINTEXT state, first sending the
+ // other side a notice that we're doing so if we're currently ENCRYPTED,
+ // and we think he's logged in. Affects only the specified instance.
+ otrl_message_disconnect: libotr.declare(
+ "otrl_message_disconnect", abi, ctypes.void_t,
+ OtrlUserState,
+ OtrlMessageAppOps.ptr,
+ ctypes.void_t.ptr,
+ ctypes.char.ptr,
+ ctypes.char.ptr,
+ ctypes.char.ptr,
+ otrl_instag_t
+ ),
+
+ // Call this function every so often, to clean up stale private state that
+ // may otherwise stick around in memory.
+ otrl_message_poll: libotr.declare(
+ "otrl_message_poll", abi, ctypes.void_t,
+ OtrlUserState,
+ OtrlMessageAppOps.ptr,
+ ctypes.void_t.ptr
+ ),
+
+ // Initiate the Socialist Millionaires' Protocol.
+ otrl_message_initiate_smp: libotr.declare(
+ "otrl_message_initiate_smp", abi, ctypes.void_t,
+ OtrlUserState,
+ OtrlMessageAppOps.ptr,
+ ctypes.void_t.ptr,
+ ConnContext.ptr,
+ ctypes.char.ptr,
+ ctypes.size_t
+ ),
+
+ // Initiate the Socialist Millionaires' Protocol and send a prompt
+ // question to the buddy.
+ otrl_message_initiate_smp_q: libotr.declare(
+ "otrl_message_initiate_smp_q", abi, ctypes.void_t,
+ OtrlUserState,
+ OtrlMessageAppOps.ptr,
+ ctypes.void_t.ptr,
+ ConnContext.ptr,
+ ctypes.char.ptr,
+ ctypes.char.ptr,
+ ctypes.size_t
+ ),
+
+ // Respond to a buddy initiating the Socialist Millionaires' Protocol.
+ otrl_message_respond_smp: libotr.declare(
+ "otrl_message_respond_smp", abi, ctypes.void_t,
+ OtrlUserState,
+ OtrlMessageAppOps.ptr,
+ ctypes.void_t.ptr,
+ ConnContext.ptr,
+ ctypes.char.ptr,
+ ctypes.size_t
+ ),
+
+ // Abort the SMP. Called when an unexpected SMP message breaks the
+ // normal flow.
+ otrl_message_abort_smp: libotr.declare(
+ "otrl_message_abort_smp", abi, ctypes.void_t,
+ OtrlUserState,
+ OtrlMessageAppOps.ptr,
+ ctypes.void_t.ptr,
+ ConnContext.ptr
+ ),
+
+ // tlv.h
+
+ tlvs: {
+ OTRL_TLV_PADDING: new ctypes.unsigned_short(0x0000),
+ OTRL_TLV_DISCONNECTED: new ctypes.unsigned_short(0x0001),
+ OTRL_TLV_SMP1: new ctypes.unsigned_short(0x0002),
+ OTRL_TLV_SMP2: new ctypes.unsigned_short(0x0003),
+ OTRL_TLV_SMP3: new ctypes.unsigned_short(0x0004),
+ OTRL_TLV_SMP4: new ctypes.unsigned_short(0x0005),
+ OTRL_TLV_SMP_ABORT: new ctypes.unsigned_short(0x0006),
+ OTRL_TLV_SMP1Q: new ctypes.unsigned_short(0x0007),
+ OTRL_TLV_SYMKEY: new ctypes.unsigned_short(0x0008),
+ },
+
+ OtrlTLV,
+
+ // Return the first TLV with the given type in the chain, or NULL if one
+ // isn't found.
+ otrl_tlv_find: libotr.declare(
+ "otrl_tlv_find", abi, OtrlTLV.ptr,
+ OtrlTLV.ptr, ctypes.unsigned_short
+ ),
+
+ // Deallocate a chain of TLVs.
+ otrl_tlv_free: libotr.declare(
+ "otrl_tlv_free", abi, ctypes.void_t,
+ OtrlTLV.ptr
+ ),
+
+};
+
+
+// exports
+
+this.EXPORTED_SYMBOLS = ["OTRLib"];
diff --git a/chat/modules/OTRUI.jsm b/chat/modules/OTRUI.jsm
new file mode 100644
index 0000000000..5a211ccf34
--- /dev/null
+++ b/chat/modules/OTRUI.jsm
@@ -0,0 +1,711 @@
+/* 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/. */
+
+this.EXPORTED_SYMBOLS = ["OTRUI"];
+
+const {Services} = ChromeUtils.import("resource:///modules/imServices.jsm");
+const {
+ XPCOMUtils,
+ l10nHelper,
+} = ChromeUtils.import("resource:///modules/imXPCOMUtils.jsm");
+
+const {OTR} = ChromeUtils.import("resource:///modules/OTR.jsm");
+
+const privDialog = "chrome://chat/content/otr-generate-key.xul";
+const authDialog = "chrome://chat/content/otr-auth.xul";
+const addFingerDialog = "chrome://chat/content/otr-add-fingerprint.xul";
+
+XPCOMUtils.defineLazyGetter(this, "_", () =>
+ l10nHelper("chrome://chat/content/otrUI.properties")
+);
+
+const authVerify = "otr-auth-unverified";
+
+var authLabelMap = new Map([
+ ["otr:auth-error", _("auth.error")],
+ ["otr:auth-success", _("auth.success")],
+ ["otr:auth-successThem", _("auth.successThem")],
+ ["otr:auth-fail", _("auth.fail")],
+ ["otr:auth-waiting", _("auth.waiting")],
+]);
+
+var authTitleMap = new Map([
+ ["otr:auth-error", "error"],
+ ["otr:auth-success", "success"],
+ ["otr:auth-successThem", "successThem"],
+ ["otr:auth-fail", "fail"],
+ ["otr:auth-waiting", "waiting"],
+]);
+
+var trustMap = new Map([
+ [OTR.trustState.TRUST_NOT_PRIVATE, {
+ startLabel: _("start.label"),
+ authLabel: _("auth.label"),
+ disableStart: false,
+ disableEnd: true,
+ disableAuth: true,
+ class: "not_private",
+ }],
+ [OTR.trustState.TRUST_UNVERIFIED, {
+ startLabel: _("refresh.label"),
+ authLabel: _("auth.label"),
+ disableStart: false,
+ disableEnd: false,
+ disableAuth: false,
+ class: "unverified",
+ }],
+ [OTR.trustState.TRUST_PRIVATE, {
+ startLabel: _("refresh.label"),
+ authLabel: _("reauth.label"),
+ disableStart: false,
+ disableEnd: false,
+ disableAuth: false,
+ class: "private",
+ }],
+ [OTR.trustState.TRUST_FINISHED, {
+ startLabel: _("start.label"),
+ authLabel: _("auth.label"),
+ disableStart: false,
+ disableEnd: false,
+ disableAuth: true,
+ class: "finished",
+ }],
+]);
+
+var windowRefs = new Map();
+
+var OTRUI = {
+ globalDoc: null,
+ visibleConv: null,
+
+ debug: true,
+ logMsg(msg) {
+ if (!OTRUI.debug)
+ return;
+ Services.console.logStringMessage(msg);
+ },
+
+ prefs: null,
+ setPrefs() {
+ let branch = "chat.otr.";
+ let prefs = {
+ requireEncryption: false,
+ verifyNudge: true,
+ };
+ let defaults = Services.prefs.getDefaultBranch(branch);
+ Object.keys(prefs).forEach(function(key) {
+ defaults.setBoolPref(key, prefs[key]);
+ });
+ OTRUI.prefs = Services.prefs.getBranch(branch);
+ },
+
+ addMenuObserver() {
+ let iter = Services.ww.getWindowEnumerator();
+ while (iter.hasMoreElements())
+ OTRUI.addMenus(iter.getNext());
+ Services.obs.addObserver(OTRUI, "domwindowopened");
+ },
+
+ removeMenuObserver() {
+ let iter = Services.ww.getWindowEnumerator();
+ while (iter.hasMoreElements())
+ OTRUI.removeMenus(iter.getNext());
+ Services.obs.removeObserver(OTRUI, "domwindowopened");
+ },
+
+ addMenus(win) {
+ let doc = win.document;
+ // Account for unready windows
+ if (doc.readyState !== "complete") {
+ let listen = function() {
+ win.removeEventListener("load", listen);
+ OTRUI.addMenus(win);
+ };
+ win.addEventListener("load", listen);
+ }
+ },
+
+ removeMenus(win) {
+ let doc = win.document;
+ OTRUI.removeBuddyContextMenu(doc);
+ },
+
+ addBuddyContextMenu(buddyContextMenu, doc) {
+ if (!buddyContextMenu || !OTR.libLoaded) {
+ return; // Not the buddy list context menu
+ }
+ OTRUI.removeBuddyContextMenu(doc);
+
+ let sep = doc.createElement("menuseparator");
+ sep.setAttribute("id", "otrsep");
+ let menuitem = doc.createElement("menuitem");
+ menuitem.setAttribute("label", _("buddycontextmenu.label"));
+ menuitem.setAttribute("id", "otrcont");
+ menuitem.addEventListener("command", () => {
+ let target = buddyContextMenu.triggerNode;
+ if (target.localName == "richlistitem") {
+ let contact = target.contact;
+ let args = OTRUI.contactWrapper(contact);
+ args.wrappedJSObject = args;
+ let features = "chrome,modal,centerscreen,resizable=no,minimizable=no";
+ Services.ww.openWindow(null, addFingerDialog, "", features, args);
+ }
+ });
+
+ buddyContextMenu.addEventListener("popupshowing", (e) => {
+ let target = e.target.triggerNode;
+ if (target.localName == "richlistitem") {
+ menuitem.hidden = false;
+ sep.hidden = false;
+ } else { /* probably imconv */
+ menuitem.hidden = true;
+ sep.hidden = true;
+ }
+ });
+
+ buddyContextMenu.appendChild(sep);
+ buddyContextMenu.appendChild(menuitem);
+ },
+
+ removeBuddyContextMenu(doc) {
+ let s = doc.getElementById("otrsep");
+ if (s) {
+ s.remove();
+ }
+ let p = doc.getElementById("otrcont");
+ if (p) {
+ p.remove();
+ }
+ },
+
+ init() {
+ // console.log("====> OTRUI init\n");
+ OTRUI.setPrefs();
+ OTR.init({
+ requireEncryption: OTRUI.prefs.getBoolPref("requireEncryption"),
+ verifyNudge: OTRUI.prefs.getBoolPref("verifyNudge"),
+ });
+ if (!OTR.libLoaded) {
+ return;
+ }
+ OTR.addObserver(OTRUI);
+ OTR.loadFiles().then(function() {
+ Services.obs.addObserver(OTR, "new-ui-conversation");
+ // Disabled until #76 is resolved.
+ // Services.obs.addObserver(OTRUI, "contact-added", false);
+ Services.obs.addObserver(OTRUI, "account-added");
+ // Services.obs.addObserver(OTRUI, "contact-signed-off", false);
+ Services.obs.addObserver(OTRUI, "conversation-loaded");
+ Services.obs.addObserver(OTRUI, "conversation-closed");
+ Services.obs.addObserver(OTRUI, "prpl-quit");
+
+ OTRUI.prefs.addObserver("", OTRUI);
+ let conversations = Services.conversations.getConversations();
+ while (conversations.hasMoreElements()) {
+ let aConv = conversations.getNext();
+ OTRUI.initConv(aConv);
+ }
+ OTRUI.addMenuObserver();
+ }).catch(function(err) {
+ // console.log("===> " + err + "\n");
+ throw err;
+ });
+ },
+
+ disconnect(aConv) {
+ if (aConv)
+ return OTR.disconnect(aConv, true);
+ let allGood = true;
+ let conversations = Services.conversations.getConversations();
+ while (conversations.hasMoreElements()) {
+ let conv = conversations.getNext();
+ if (conv.isChat)
+ continue;
+ if (!OTR.disconnect(conv, true)) {
+ allGood = false;
+ }
+ }
+ return allGood;
+ },
+
+ changePref(aMsg) {
+ switch (aMsg) {
+ case "requireEncryption":
+ OTR.setPolicy(OTRUI.prefs.getBoolPref("requireEncryption"));
+ break;
+ case "verifyNudge":
+ OTR.verifyNudge = OTRUI.prefs.getBoolPref("verifyNudge");
+ break;
+ default:
+ OTRUI.logMsg(aMsg);
+ }
+ },
+
+ openAuth(window, name, mode, uiConv, contactInfo) {
+ let otrAuth = this.globalDoc.querySelector(".otr-auth");
+ otrAuth.disabled = true;
+ let win = window.openDialog(
+ authDialog,
+ "auth=" + name,
+ "centerscreen,resizable=no,minimizable=no",
+ mode,
+ uiConv,
+ contactInfo
+ );
+ windowRefs.set(name, win);
+ window.addEventListener("beforeunload", function() {
+ otrAuth.disabled = false;
+ windowRefs.delete(name);
+ });
+ },
+
+ closeAuth(context) {
+ let win = windowRefs.get(context.username);
+ if (win)
+ win.close();
+ },
+
+ noOtrPossible(otrContainer, context) {
+ otrContainer.hidden = true;
+
+ if (context) {
+ OTRUI.hideUserNotifications(context);
+ } else {
+ OTRUI.hideAllNotifications();
+ }
+ },
+
+ sendSystemAlert(uiConv, conv, bundleId) {
+ uiConv.systemMessage(_(bundleId, conv.normalizedName));
+ },
+
+ setNotificationBox(notificationbox) {
+ this.globalBox = notificationbox;
+ },
+
+/*
+ * possible states:
+ * tab isn't a 1:1, isChat == true
+ * then OTR isn't possible, hide the button
+ * tab is a 1:1, isChat == false
+ * no conversation active, uiConv cannot be found
+ * then OTR isn't possible YET, hide the button
+ * conversation active, uiConv found
+ * disconnected?
+ * could the other side come back? should we keep the button?
+ * set the state based on the OTR library state
+ */
+
+ addButton(aObject) {
+ this.globalDoc = aObject.ownerDocument;
+ let _conv = aObject._conv;
+ OTRUI.setMsgState(_conv, null, this.globalDoc, true);
+ },
+
+ hideOTRButton() {
+ if (!OTR.libLoaded)
+ return;
+ if (!this.globalDoc)
+ return;
+ OTRUI.visibleConv = null;
+ let otrContainer = this.globalDoc.querySelector(".otr-container");
+ OTRUI.noOtrPossible(otrContainer);
+ },
+
+ updateOTRButton(_conv) {
+ if (!OTR.libLoaded)
+ return;
+ if (!this.globalDoc)
+ return;
+ OTRUI.visibleConv = _conv;
+ let convBinding =
+ this.globalDoc.getElementById("conversationsDeck").selectedPanel;
+ if (convBinding && convBinding._conv && convBinding._conv.target) {
+ OTRUI.setMsgState(_conv, null, this.globalDoc, false);
+ } else {
+ this.hideOTRButton();
+ }
+ },
+
+ // set msg state on toolbar button
+ setMsgState(_conv, context, doc, addSystemMessage) {
+ if (!this.visibleConv) {
+ return;
+ }
+ if (_conv != null && !(_conv === this.visibleConv)) {
+ return;
+ }
+
+ let otrContainer = doc.querySelector(".otr-container");
+ let otrButton = doc.querySelector(".otr-button");
+ if (_conv != null && _conv.isChat) {
+ OTRUI.noOtrPossible(otrContainer, context);
+ return;
+ }
+
+ if (!context && _conv != null) {
+ context = OTR.getContext(_conv);
+ if (!context) {
+ OTRUI.noOtrPossible(otrContainer, null);
+ }
+ }
+
+ try {
+ let uiConv = OTR.getUIConvFromContext(context);
+ if (uiConv != null && !(uiConv === this.visibleConv)) {
+ return;
+ }
+
+ if (uiConv.isChat) {
+ OTRUI.noOtrPossible(otrContainer, context);
+ return;
+ }
+ if (addSystemMessage) {
+ let trust = OTRUI.getTrustSettings(context);
+ uiConv.systemMessage(_("state." + trust.class, context.username));
+ }
+ } catch (e) {
+ OTRUI.noOtrPossible(otrContainer, context);
+ return;
+ }
+
+ otrContainer.hidden = false;
+ let otrStart = doc.querySelector(".otr-start");
+ let otrEnd = doc.querySelector(".otr-end");
+ let otrAuth = doc.querySelector(".otr-auth");
+ let trust = OTRUI.getTrustSettings(context);
+ otrButton.setAttribute("tooltiptext", _("state." + trust.class, context.username));
+ otrButton.setAttribute("label", _("state." + trust.class + ".label"));
+ otrButton.className = "otr-button otr-" + trust.class;
+ otrStart.setAttribute("label", trust.startLabel);
+ otrStart.setAttribute("disabled", trust.disableStart);
+ otrEnd.setAttribute("disabled", trust.disableEnd);
+ otrAuth.setAttribute("label", trust.authLabel);
+ otrAuth.setAttribute("disabled", trust.disableAuth);
+ OTRUI.hideAllNotifications();
+ OTRUI.showUserNotifications(context);
+ },
+
+ alertTrust(context) {
+ let uiConv = OTR.getUIConvFromContext(context);
+ let trust = OTRUI.getTrustSettings(context);
+ uiConv.systemMessage(_("afterauth." + trust.class, context.username));
+ },
+
+ getTrustSettings(context) {
+ let result = trustMap.get(OTR.trust(context));
+ return result;
+ },
+
+ askAuth(aObject) {
+ let uiConv = OTR.getUIConvFromContext(aObject.context);
+ if (!uiConv) return;
+
+ let window = this.globalDoc.defaultView;
+ let name = uiConv.target.normalizedName;
+ OTRUI.openAuth(window, name, "ask", uiConv, aObject);
+ },
+
+ closeUnverified(context) {
+ let uiConv = OTR.getUIConvFromContext(context);
+ if (!uiConv) return;
+
+ let notification = this.globalBox.getNotificationWithValue(authVerify);
+ if (notification)
+ notification.close();
+ },
+
+ hideUserNotifications(context) {
+ let notifications = this.globalBox.allNotifications;
+ for (let i = notifications.length - 1; i >= 0; i--) {
+ if (context.username == notifications[i].getAttribute("user")) {
+ notifications[i].setAttribute("hidden", "true");
+ }
+ }
+ },
+
+ hideAllNotifications() {
+ let notifications = this.globalBox.allNotifications;
+ for (let i = notifications.length - 1; i >= 0; i--) {
+ notifications[i].setAttribute("hidden", "true");
+ }
+ },
+
+ showUserNotifications(context) {
+ let notifications = this.globalBox.allNotifications;
+ for (let i = notifications.length - 1; i >= 0; i--) {
+ if (context.username == notifications[i].getAttribute("user"))
+ notifications[i].removeAttribute("hidden");
+ }
+ },
+
+ notifyUnverified(context, seen) {
+ let uiConv = OTR.getUIConvFromContext(context);
+ if (!uiConv) return;
+
+ if (this.globalBox.getNotificationWithValue(authVerify))
+ return;
+
+ let window = this.globalDoc.defaultView;
+
+ let msg = _("finger." + seen, context.username);
+ let buttons = [{
+ label: _("finger.verify"),
+ accessKey: _("verify.accessKey"),
+ callback() {
+ let name = uiConv.target.normalizedName;
+ OTRUI.openAuth(window, name, "start", uiConv);
+ // prevent closing of notification bar when the button is hit
+ return true;
+ },
+ }];
+
+ let priority = this.globalBox.PRIORITY_WARNING_MEDIUM;
+ this.globalBox.appendNotification(msg, authVerify, null, priority, buttons, null);
+
+ this.updateNotificationUI(context, "verify", authVerify);
+ },
+
+ updateNotificationUI(context, type, value) {
+ let notification = this.globalBox.getNotificationWithValue(value);
+ notification.setAttribute("user", context.username);
+ notification.setAttribute("orient", "vertical");
+ notification.messageDetails.setAttribute("orient", "vertical");
+ notification.messageDetails.removeAttribute("oncommand");
+ notification.messageDetails.removeAttribute("align");
+
+ let title = this.globalDoc.createElement("title");
+ title.setAttribute("flex", "1");
+ title.setAttribute("crop", "end");
+ title.textContent = _(type + ".title");
+
+ let close = notification.querySelector("toolbarbutton");
+ close.setAttribute("oncommand", "this.parentNode.parentNode.dismiss();");
+
+ let top = this.globalDoc.createElement("hbox");
+ top.setAttribute("flex", "1");
+ top.setAttribute("align", "center");
+ top.classList.add("otr-notification-header");
+ top.appendChild(notification.messageImage);
+ top.appendChild(title);
+ top.appendChild(close);
+ notification.insertBefore(top, notification.messageDetails);
+
+ let bottom = this.globalDoc.createElement("hbox");
+ bottom.setAttribute("flex", "1");
+ bottom.setAttribute("oncommand", "this.parentNode._doButtonCommand(event);");
+ bottom.classList.add("otr-notification-footer");
+
+ notification.querySelectorAll("button").forEach((e) => {
+ bottom.appendChild(e);
+ });
+
+ notification.appendChild(bottom);
+ },
+
+ closeVerification(context) {
+ let uiConv = OTR.getUIConvFromContext(context);
+ if (!uiConv) return;
+
+ authLabelMap.forEach(function(_, key) {
+ let prevNotification = OTRUI.globalBox.getNotificationWithValue(key);
+ if (prevNotification)
+ prevNotification.close();
+ });
+ },
+
+ notifyVerification(context, key, cancelable) {
+ let uiConv = OTR.getUIConvFromContext(context);
+ if (!uiConv) return;
+
+ // TODO: maybe update the .label property on the notification instead
+ // of closing it ... although, buttons need to be updated too.
+ OTRUI.closeVerification(context);
+
+ let msg = authLabelMap.get(key);
+ let type = authTitleMap.get(key);
+ let buttons = [];
+ if (cancelable) {
+ buttons = [{
+ label: _("auth.cancel"),
+ accessKey: _("auth.cancelAccessKey"),
+ callback() {
+ let context = OTR.getContext(uiConv.target);
+ OTR.abortSMP(context);
+ },
+ }];
+ }
+
+ // higher priority to overlay the current notifyUnverified
+ let priority = this.globalBox.PRIORITY_WARNING_HIGH;
+ OTRUI.closeUnverified(context);
+ this.globalBox.appendNotification(msg, key, null, priority, buttons, null);
+
+ this.updateNotificationUI(context, type, key);
+ },
+
+ updateAuth(aObj) {
+ // let uiConv = OTR.getUIConvFromContext(aObj.context);
+ if (!aObj.progress) {
+ OTRUI.closeAuth(aObj.context);
+ OTRUI.notifyVerification(aObj.context, "otr:auth-error", false);
+ } else if (aObj.progress === 100) {
+ let key;
+ if (aObj.success) {
+ if (aObj.context.trust) {
+ key = "otr:auth-success";
+ OTR.notifyTrust(aObj.context);
+ } else {
+ key = "otr:auth-successThem";
+ }
+ } else {
+ key = "otr:auth-fail";
+ if (!aObj.context.trust)
+ OTR.notifyTrust(aObj.context);
+ }
+ OTRUI.notifyVerification(aObj.context, key, false);
+ } else {
+ // TODO: show the aObj.progress to the user with a
+ //
+ OTRUI.notifyVerification(aObj.context, "otr:auth-waiting", true);
+ }
+ },
+
+ generate(args) {
+ let features = "chrome,modal,centerscreen,resizable=no,minimizable=no";
+ args.wrappedJSObject = args;
+ Services.ww.openWindow(null, privDialog, "", features, args);
+ },
+
+ onAccountCreated(acc) {
+ let account = acc.normalizedName;
+ let protocol = acc.protocol.normalizedName;
+ Promise.resolve();
+ if (OTR.privateKeyFingerprint(account, protocol) === null)
+ OTR.generatePrivateKey(account, protocol);
+ },
+
+ contactWrapper(contact) {
+ let wrapper = {
+ account: contact.preferredBuddy.preferredAccountBuddy.account.normalizedName,
+ protocol: contact.preferredBuddy.protocol.normalizedName,
+ screenname: contact.preferredBuddy.preferredAccountBuddy.userName,
+ };
+ return wrapper;
+ },
+
+ onContactAdded(contact) {
+ let args = OTRUI.contactWrapper(contact);
+ if (OTR.getFingerprintsForRecipient(args.account, args.protocol, args.screenname).length > 0)
+ return;
+ args.wrappedJSObject = args;
+ let features = "chrome,modal,centerscreen,resizable=no,minimizable=no";
+ Services.ww.openWindow(null, addFingerDialog, "", features, args);
+ },
+
+ observe(aObject, aTopic, aMsg) {
+ let doc;
+ // console.log("====> observing topic: " + aTopic + " with msg: " + aMsg);
+ // console.log(aObject);
+
+ switch (aTopic) {
+ case "nsPref:changed":
+ OTRUI.changePref(aMsg);
+ break;
+ case "conversation-loaded":
+ doc = aObject.ownerDocument;
+ let windowtype = doc.documentElement.getAttribute("windowtype");
+ if (windowtype !== "mail:3pane") {
+ return;
+ }
+ OTRUI.addButton(aObject);
+ break;
+ case "conversation-closed":
+ if (aObject.isChat)
+ return;
+ this.globalBox.removeAllNotifications();
+ OTRUI.closeAuth(OTR.getContext(aObject));
+ OTRUI.disconnect(aObject);
+ break;
+ // case "contact-signed-off":
+ // break;
+ case "prpl-quit":
+ OTRUI.disconnect(null);
+ break;
+ case "domwindowopened":
+ OTRUI.addMenus(aObject);
+ break;
+ case "otr:generate":
+ OTRUI.generate(aObject);
+ break;
+ case "otr:disconnected":
+ case "otr:msg-state":
+ if (aTopic === "otr:disconnected" ||
+ OTR.trust(aObject) !== OTR.trustState.TRUST_UNVERIFIED) {
+ OTRUI.closeAuth(aObject);
+ OTRUI.closeUnverified(aObject);
+ OTRUI.closeVerification(aObject);
+ }
+ OTRUI.setMsgState(null, aObject, this.globalDoc, false);
+ break;
+ case "otr:unverified":
+ OTRUI.notifyUnverified(aObject, aMsg);
+ break;
+ case "otr:trust-state":
+ OTRUI.alertTrust(aObject);
+ break;
+ case "otr:log":
+ OTRUI.logMsg("otr: " + aObject);
+ break;
+ case "account-added":
+ OTRUI.onAccountCreated(aObject);
+ break;
+ case "contact-added":
+ OTRUI.onContactAdded(aObject);
+ break;
+ case "otr:auth-ask":
+ OTRUI.askAuth(aObject);
+ break;
+ case "otr:auth-update":
+ OTRUI.updateAuth(aObject);
+ break;
+ }
+ },
+
+ initConv(binding) {
+ OTR.addConversation(binding._conv);
+ OTRUI.addButton(binding);
+ },
+
+ resetConv(binding) {
+ OTR.removeConversation(binding._conv);
+ let otrButton = this.globalDoc.querySelector(".otr-button");
+ if (!otrButton)
+ return;
+ otrButton.remove();
+ },
+
+ destroy() {
+ if (!OTR.libLoaded)
+ return;
+ OTRUI.disconnect(null);
+ Services.obs.removeObserver(OTR, "new-ui-conversation");
+ // Services.obs.removeObserver(OTRUI, "contact-added");
+ // Services.obs.removeObserver(OTRUI, "contact-signed-off");
+ Services.obs.removeObserver(OTRUI, "account-added");
+ Services.obs.removeObserver(OTRUI, "conversation-loaded");
+ Services.obs.removeObserver(OTRUI, "conversation-closed");
+ Services.obs.removeObserver(OTRUI, "prpl-quit");
+
+ let conversations = Services.conversations.getConversations();
+ while (conversations.hasMoreElements()) {
+ OTRUI.resetConv(conversations.getNext());
+ }
+ OTRUI.prefs.removeObserver("", OTRUI);
+ OTR.removeObserver(OTRUI);
+ OTR.close();
+ OTRUI.removeMenuObserver();
+ },
+
+};
diff --git a/chat/modules/moz.build b/chat/modules/moz.build
index 420ece11d9..35347e734d 100644
--- a/chat/modules/moz.build
+++ b/chat/modules/moz.build
@@ -8,6 +8,7 @@ XPCSHELL_TESTS_MANIFESTS += ['test/xpcshell.ini']
EXTRA_JS_MODULES += [
'ArrayBufferUtils.jsm',
'BigInteger.jsm',
+ 'CLib.jsm',
'DNS.jsm',
'hiddenWindow.jsm',
'imContentSink.jsm',
@@ -19,6 +20,10 @@ EXTRA_JS_MODULES += [
'imXPCOMUtils.jsm',
'jsProtoHelper.jsm',
'NormalizedMap.jsm',
+ 'OTR.jsm',
+ 'OTRHelpers.jsm',
+ 'OTRLib.jsm',
+ 'OTRUI.jsm',
'socket.jsm',
'ToLocaleFormat.jsm',
]
diff --git a/chat/themes/icons/otr-connection-encrypted.svg b/chat/themes/icons/otr-connection-encrypted.svg
new file mode 100644
index 0000000000..a086b143b2
--- /dev/null
+++ b/chat/themes/icons/otr-connection-encrypted.svg
@@ -0,0 +1,7 @@
+
+
diff --git a/chat/themes/icons/otr-connection-finished.svg b/chat/themes/icons/otr-connection-finished.svg
new file mode 100644
index 0000000000..d98610a6cf
--- /dev/null
+++ b/chat/themes/icons/otr-connection-finished.svg
@@ -0,0 +1,7 @@
+
+
diff --git a/chat/themes/jar.mn b/chat/themes/jar.mn
index f0b34ff0a8..2d9c96308e 100644
--- a/chat/themes/jar.mn
+++ b/chat/themes/jar.mn
@@ -20,9 +20,12 @@ chat.jar:
skin/classic/chat/browserRequest.css
skin/classic/chat/imtooltip.css
skin/classic/chat/status.css
+ skin/classic/chat/otr.css
skin/classic/chat/prpl-generic/icon32.png (icons/prpl-generic-32.png)
skin/classic/chat/prpl-generic/icon48.png (icons/prpl-generic-48.png)
skin/classic/chat/prpl-generic/icon.png (icons/prpl-generic.png)
skin/classic/chat/prpl-unknown/icon32.png (icons/prpl-unknown-32.png)
skin/classic/chat/prpl-unknown/icon48.png (icons/prpl-unknown-48.png)
skin/classic/chat/prpl-unknown/icon.png (icons/prpl-unknown.png)
+ skin/classic/chat/otr-connection-encrypted.svg (icons/otr-connection-encrypted.svg)
+ skin/classic/chat/otr-connection-finished.svg (icons/otr-connection-finished.svg)
diff --git a/chat/themes/otr.css b/chat/themes/otr.css
new file mode 100644
index 0000000000..e0dbde07d8
--- /dev/null
+++ b/chat/themes/otr.css
@@ -0,0 +1,142 @@
+/* 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/. */
+
+.otr-container {
+ border-top: 1px solid var(--splitter-color);
+ min-height: 32px;
+ padding: 4px;
+}
+
+.otr-label {
+ font-weight: 600;
+}
+
+.otr-not_private > image {
+ list-style-image: url("chrome://messenger/skin/icons/connection-insecure.svg");
+}
+
+.otr-unverified > image {
+ list-style-image: url("chrome://messenger/skin/icons/connection-mixed.svg");
+}
+
+.otr-finished > image {
+ list-style-image: url("chrome://chat/skin/otr-connection-finished.svg");
+}
+
+.otr-private > image {
+ list-style-image: url("chrome://chat/skin/otr-connection-encrypted.svg");
+}
+
+toolbarbutton.otr-button {
+ -moz-appearance: button !important;
+ padding: 1px !important;
+}
+
+.otr-button > image {
+ margin-inline-end: 3px;
+ width: 14px;
+}
+
+.otr-button .toolbarbutton-menu-dropmarker {
+ -moz-appearance: none !important;
+ list-style-image: none;
+ margin-left: 0;
+ margin-right: 0;
+ margin-inline-start: 3px;
+ width: 9px;
+}
+
+.otr-button .toolbarbutton-menu-dropmarker > .dropmarker-icon {
+ width: 17px;
+ height: 7px;
+ background-image: url("chrome://messenger/skin/icons/toolbarbutton-arrow.svg");
+ background-position: center;
+ background-repeat: no-repeat;
+ background-size: 9px 7px;
+}
+
+/* otr botificationbox tweaks */
+
+#otr-notification-box notification[type="warning"] {
+ background: #FFF2BE !important;
+}
+
+#otr-notification-box notification {
+ padding-inline-start: 6px !important;
+ padding: 6px !important;
+ border-top: 1px solid var(--splitter-color) !important;
+ border-bottom: none !important;
+}
+
+#otr-notification-box .messageImage {
+ margin-inline-end: 6px !important;
+ color: #9E650C;
+}
+
+#otr-notification-box .messageText {
+ margin-inline-start: 6px !important;
+ margin-bottom: 6px !important;
+}
+
+.otr-notification-header {
+ display: inherit;
+ padding: 3px 6px;
+}
+
+.otr-notification-header title {
+ font-weight: bold;
+ color: #9E650C;
+ flex-grow: 1;
+}
+
+.otr-notification-header .messageCloseButton {
+ min-height: 20px;
+}
+
+.otr-notification-header .messageCloseButton > .toolbarbutton-icon {
+ margin-inline-end: 0px !important;
+}
+
+.otr-notification-footer {
+ display: flex;
+ justify-content: end;
+}
+
+/* waiting */
+#otr-notification-box notification[type="warning"][value="otr:auth-waiting"] >
+ hbox > .messageImage {
+ list-style-image: url("chrome://global/skin/icons/help.svg") !important;
+}
+
+/* fail */
+#otr-notification-box notification[type="warning"][value="otr:auth-fail"] {
+ background: #ffc9d5 !important;
+}
+
+#otr-notification-box notification[type="warning"][value="otr:auth-fail"] >
+ hbox > .messageImage {
+ list-style-image: url("chrome://global/skin/icons/error.svg") !important;
+ color: #c93434 !important;
+}
+
+#otr-notification-box notification[type="warning"][value="otr:auth-fail"]
+ .otr-notification-header title {
+ color: #c93434 !important;
+}
+
+/* success */
+#otr-notification-box notification[type="warning"][value="otr:auth-success"] {
+ background: #D3F4AF !important;
+}
+
+#otr-notification-box notification[type="warning"][value="otr:auth-success"] >
+ hbox > .messageImage {
+ list-style-image: url("chrome://global/skin/icons/check.svg") !important;
+ color: #407501 !important;
+}
+
+#otr-notification-box notification[type="warning"][value="otr:auth-success"]
+ .otr-notification-header title {
+ color: #407501 !important;
+}
diff --git a/mail/app/profile/all-thunderbird.js b/mail/app/profile/all-thunderbird.js
index 673ed4634e..72ef6f3578 100644
--- a/mail/app/profile/all-thunderbird.js
+++ b/mail/app/profile/all-thunderbird.js
@@ -719,6 +719,8 @@ pref("mail.chat.play_sound", true);
pref("mail.chat.play_sound.type", 0);
// if sound is user specified, this needs to be a file url
pref("mail.chat.play_sound.url", "");
+// Enable/Disable support for OTR chat encryption.
+pref("chat.otr.enable", false);
// BigFiles
pref("mail.cloud_files.enabled", true);
diff --git a/mail/components/im/content/chat-conversation-info.js b/mail/components/im/content/chat-conversation-info.js
index 0a2b84b290..fa048cbca4 100644
--- a/mail/components/im/content/chat-conversation-info.js
+++ b/mail/components/im/content/chat-conversation-info.js
@@ -4,7 +4,21 @@
"use strict";
-/* global MozXULElement */
+var {Services} = ChromeUtils.import("resource:///modules/imServices.jsm");
+var {XPCOMUtils} = ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
+
+ChromeUtils.defineModuleGetter(this, "OTR", "resource:///modules/OTR.jsm");
+ChromeUtils.defineModuleGetter(this, "OTRUI", "resource:///modules/OTRUI.jsm");
+
+/* globals MozElements MozXULElement */
+
+const gNotification = {};
+XPCOMUtils.defineLazyGetter(gNotification, "notificationbox", () => {
+ return new MozElements.NotificationBox(element => {
+ element.setAttribute("flex", "1");
+ document.getElementById("otr-notification-box").append(element);
+ });
+});
/**
* The MozChatConversationInfo widget displays information about a chat:
@@ -29,24 +43,54 @@ class MozChatConversationInfo extends MozXULElement {
if (this.hasChildNodes() || this.delayConnectedCallback()) {
return;
}
+ this.setAttribute("orient", "vertical");
+
this.appendChild(MozXULElement.parseXULToFragment(`
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
- `));
+
+
+
+
+
+
+
+
+
+ `, ["chrome://chat/content/otr-chat.dtd"]));
+
this.topic.addEventListener("click", this.startEditTopic.bind(this));
+
+ if (Services.prefs.getBoolPref("chat.otr.enable")) {
+ let otrButton = this.querySelector(".otr-button");
+ otrButton.addEventListener("command", this.otrButtonClicked);
+ OTRUI.setNotificationBox(gNotification.notificationbox);
+ }
this.initializeAttributeInheritance();
}
@@ -116,5 +160,40 @@ class MozChatConversationInfo extends MozXULElement {
}
elt.select();
}
+
+ otrButtonClicked(aEvent) {
+ aEvent.preventDefault();
+ let otrMenu = this.querySelector(".otr-menu-popup");
+ otrMenu.openPopup(otrMenu.parentNode, "after_start");
+ }
+
+ onOtrStartClicked() {
+ // check if start-menu-command is disabled, if yes exit
+ let convBinding = document.getElementById("conversationsDeck").selectedPanel;
+ let uiConv = convBinding._conv;
+ let conv = uiConv.target;
+ let context = OTR.getContext(conv);
+ let bundleId = "alert." + (
+ context.msgstate === OTR.getMessageState().OTRL_MSGSTATE_ENCRYPTED ?
+ "refresh" : "start");
+ OTRUI.sendSystemAlert(uiConv, conv, bundleId);
+ OTR.sendQueryMsg(conv);
+ }
+
+ onOtrEndClicked() {
+ let convBinding = document.getElementById("conversationsDeck").selectedPanel;
+ let uiConv = convBinding._conv;
+ let conv = uiConv.target;
+ OTR.disconnect(conv, false);
+ let bundleId = "alert.gone_insecure";
+ OTRUI.sendSystemAlert(uiConv, conv, bundleId);
+ }
+
+ onOtrAuthClicked() {
+ let convBinding = document.getElementById("conversationsDeck").selectedPanel;
+ let uiConv = convBinding._conv;
+ let conv = uiConv.target;
+ OTRUI.openAuth(window, conv.normalizedName, "start", uiConv);
+ }
}
customElements.define("chat-conversation-info", MozChatConversationInfo);
diff --git a/mail/components/im/content/chat-messenger.inc.xul b/mail/components/im/content/chat-messenger.inc.xul
index 7ed467e07d..aa7136b0ca 100644
--- a/mail/components/im/content/chat-messenger.inc.xul
+++ b/mail/components/im/content/chat-messenger.inc.xul
@@ -93,8 +93,7 @@
-
-
+
-
-
-
-
-
- &chat.noConv.description;
-
-
-
- &chat.noAccount.description;
-
-
-
-
-
-
-
- &chat.noConnectedAccount.description;
-
-
-
-
-
-
-
-
-
-
-
-
- &chat.noPreviousConv.description;
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+ &chat.noConv.description;
+
+
+
+ &chat.noAccount.description;
+
+
+
+
+
+
+
+ &chat.noConnectedAccount.description;
+
+
+
+
+
+
+
+
+
+
+
+
+
+ &chat.noPreviousConv.description;
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/mail/components/im/content/chat-messenger.js b/mail/components/im/content/chat-messenger.js
index be8bf6a8ad..87d7ddc39d 100644
--- a/mail/components/im/content/chat-messenger.js
+++ b/mail/components/im/content/chat-messenger.js
@@ -13,7 +13,9 @@ var { Services: imServices } = ChromeUtils.import("resource:///modules/imService
var {AppConstants} = ChromeUtils.import("resource://gre/modules/AppConstants.jsm");
ChromeUtils.defineModuleGetter(this, "OS", "resource://gre/modules/osfile.jsm");
+ChromeUtils.defineModuleGetter(this, "OTRUI", "resource:///modules/OTRUI.jsm");
+var gOtrEnabled = false;
var gBuddyListContextMenu = null;
function buddyListContextMenu(aXulMenu) {
@@ -35,7 +37,12 @@ function buddyListContextMenu(aXulMenu) {
document.getElementById("context-close-conversation").hidden = !this.onConv;
document.getElementById("context-openconversation").disabled =
!hide && !this.target.canOpenConversation();
+
+ if (gOtrEnabled) {
+ OTRUI.addBuddyContextMenu(this.menu, document);
+ }
}
+
buddyListContextMenu.prototype = {
openConversation() {
if (this.onContact || this.onConv)
@@ -629,6 +636,9 @@ var chatHandler = {
document.getElementById("noConvScreen");
this.updateTitle();
this.observedContact = null;
+ if (gOtrEnabled) {
+ OTRUI.hideOTRButton();
+ }
return;
}
@@ -648,6 +658,9 @@ var chatHandler = {
cti.removeAttribute("topicEditable");
cti.removeAttribute("noTopic");
this.observedContact = null;
+ if (gOtrEnabled) {
+ OTRUI.hideOTRButton();
+ }
let path = "logs/" + item.log.path;
path = OS.Path.join(OS.Constants.Path.profileDir, ...path.split("/"));
@@ -680,6 +693,10 @@ var chatHandler = {
item.convView.updateConvStatus();
item.update();
+ if (gOtrEnabled) {
+ OTRUI.updateOTRButton(item.conv);
+ }
+
imServices.logs.getLogsForConversation(item.conv, true).then(aLogs => {
if (contactlistbox.selectedItem != item)
return;
@@ -700,6 +717,9 @@ var chatHandler = {
button.disabled = false;
this.observedContact = null;
} else if (item.localName == "richlistitem" && item.getAttribute("is") == "chat-contact") {
+ if (gOtrEnabled) {
+ OTRUI.hideOTRButton();
+ }
let contact = item.contact;
if (this.observedContact && contact &&
this.observedContact.id == contact.id) {
@@ -1214,6 +1234,28 @@ var chatHandler = {
this.ChatCore.init();
this._addObserver("chat-core-initialized");
}
+
+ gOtrEnabled =
+ Services.prefs.getBoolPref("chat.otr.enable");
+
+ if (gOtrEnabled) {
+ new Promise(resolve => {
+ if (Services.core.initialized) {
+ resolve();
+ return;
+ }
+ function initObserver() {
+ Services.obs.removeObserver(initObserver, "prpl-init");
+ resolve();
+ }
+ Services.obs.addObserver(initObserver, "prpl-init");
+ }).then(() => {
+ let sss = Cc["@mozilla.org/content/style-sheet-service;1"].getService(Ci.nsIStyleSheetService);
+ let uri = Services.io.newURI("chrome://chat/skin/otr.css");
+ sss.loadAndRegisterSheet(uri, sss.USER_SHEET);
+ OTRUI.init();
+ });
+ }
},
};
diff --git a/mail/components/im/themes/chat.css b/mail/components/im/themes/chat.css
index 61c53522d8..ee71752e13 100644
--- a/mail/components/im/themes/chat.css
+++ b/mail/components/im/themes/chat.css
@@ -292,7 +292,6 @@ richlistitem[is="chat-imconv"]:not(:hover) > .closeConversationButton {
.conv-top-info {
margin: 0;
- padding: 0.6ex;
border-style: none;
-moz-appearance: none;
-moz-window-dragging: no-drag;
@@ -480,6 +479,11 @@ richlistitem[state="disconnected"] .accountStateIcon {
}
/* corresponds to im/themes/conversation.css @media all and (min-height: 251px) */
+.displayUserAccount {
+ padding: 4px;
+ background-color: -moz-OddTreeRow;
+}
+
.statusImageStack,
.displayNameAndstatusMessageStack {
margin: 2px 2px;