浏览代码

OTR stuff has been moved to a component/plugin in src/converse-otr.js

Also fixed errors with converse obj not being defined in the MUC plugin.
JC Brand 9 年之前
父节点
当前提交
9eba9989dc
共有 7 个文件被更改,包括 673 次插入474 次删除
  1. 1 0
      converse.js
  2. 1 0
      main.js
  3. 57 466
      src/converse-core.js
  4. 70 5
      src/converse-muc.js
  5. 512 0
      src/converse-otr.js
  6. 1 3
      src/deps-full.js
  7. 31 0
      src/templates/toolbar_otr.html

+ 1 - 0
converse.js

@@ -9,6 +9,7 @@ define("converse", [
      * Any of the following components can be removed if they're not needed.
      * Any of the following components can be removed if they're not needed.
      */
      */
     "converse-muc", // XEP-0045 Multi-user chat
     "converse-muc", // XEP-0045 Multi-user chat
+    "converse-otr", // Off-the-record encryption for one-on-one messages
     /* End: Removable components */
     /* End: Removable components */
 
 
     "converse-core"
     "converse-core"

+ 1 - 0
main.js

@@ -46,6 +46,7 @@ require.config({
         // Converse
         // Converse
         "converse-core":            "src/converse-core",
         "converse-core":            "src/converse-core",
         "converse-muc":             "src/converse-muc",
         "converse-muc":             "src/converse-muc",
+        "converse-otr":             "src/converse-otr",
         "converse-dependencies":    "src/deps-full",
         "converse-dependencies":    "src/deps-full",
         "converse-templates":       "src/templates",
         "converse-templates":       "src/templates",
 
 

+ 57 - 466
src/converse-core.js

@@ -4,8 +4,8 @@
 // Copyright (c) 2012-2016, Jan-Carel Brand <jc@opkode.com>
 // Copyright (c) 2012-2016, Jan-Carel Brand <jc@opkode.com>
 // Licensed under the Mozilla Public License (MPLv2)
 // Licensed under the Mozilla Public License (MPLv2)
 //
 //
-/*global Backbone, CryptoJS, crypto, define, window, jQuery, setTimeout, clearTimeout, document, templates, _,
-  $iq, $msg, $pres, $build, DSA, OTR, Strophe, moment, utils, b64_sha1, locales */
+/*global Backbone, define, window, jQuery, setTimeout, clearTimeout, document, templates, _,
+  $iq, $msg, $pres, $build, Strophe, moment, utils, b64_sha1, locales */
 
 
 (function (root, factory) {
 (function (root, factory) {
     if (typeof define === 'function' && define.amd) {
     if (typeof define === 'function' && define.amd) {
@@ -30,8 +30,6 @@
                     dependencies.$msg,
                     dependencies.$msg,
                     dependencies.$pres,
                     dependencies.$pres,
                     dependencies.$build,
                     dependencies.$build,
-                    dependencies.otr ? dependencies.otr.DSA : undefined,
-                    dependencies.otr ? dependencies.otr.OTR : undefined,
                     dependencies.Strophe,
                     dependencies.Strophe,
                     dependencies.underscore,
                     dependencies.underscore,
                     dependencies.moment,
                     dependencies.moment,
@@ -46,9 +44,9 @@
         // In this case, the dependencies need to be available already as
         // In this case, the dependencies need to be available already as
         // global variables, and should be loaded separately via *script* tags.
         // global variables, and should be loaded separately via *script* tags.
         // See the file **non_amd.html** for an example of this usecase.
         // See the file **non_amd.html** for an example of this usecase.
-        root.converse = factory(templates, jQuery, $iq, $msg, $pres, $build, DSA, OTR, Strophe, _, moment, utils, b64_sha1);
+        root.converse = factory(templates, jQuery, $iq, $msg, $pres, $build, Strophe, _, moment, utils, b64_sha1);
     }
     }
-}(this, function (templates, $, $iq, $msg, $pres, $build, DSA, OTR, Strophe, _, moment, utils, b64_sha1) {
+}(this, function (templates, $, $iq, $msg, $pres, $build, Strophe, _, moment, utils, b64_sha1) {
     /* "use strict";
     /* "use strict";
      * Cannot use this due to Safari bug.
      * Cannot use this due to Safari bug.
      * See https://github.com/jcbrand/converse.js/issues/196
      * See https://github.com/jcbrand/converse.js/issues/196
@@ -175,10 +173,6 @@
         var ANONYMOUS  = "anonymous";
         var ANONYMOUS  = "anonymous";
         var PREBIND = "prebind";
         var PREBIND = "prebind";
 
 
-        var UNENCRYPTED = 0;
-        var UNVERIFIED= 1;
-        var VERIFIED= 2;
-        var FINISHED = 3;
         var KEY = {
         var KEY = {
             ENTER: 13,
             ENTER: 13,
             FORWARD_SLASH: 47
             FORWARD_SLASH: 47
@@ -208,15 +202,6 @@
             'PAUSED':     20000,
             'PAUSED':     20000,
             'INACTIVE':   90000
             'INACTIVE':   90000
         };
         };
-        var HAS_CSPRNG = ((typeof crypto !== 'undefined') &&
-            ((typeof crypto.randomBytes === 'function') ||
-                (typeof crypto.getRandomValues === 'function')
-        ));
-        var HAS_CRYPTO = HAS_CSPRNG && (
-            (typeof CryptoJS !== "undefined") &&
-            (typeof OTR !== "undefined") &&
-            (typeof DSA !== "undefined")
-        );
         var OPENED = 'opened';
         var OPENED = 'opened';
         var CLOSED = 'closed';
         var CLOSED = 'closed';
 
 
@@ -235,10 +220,8 @@
             var view = converse.chatboxviews.get(chatbox.get('jid'));
             var view = converse.chatboxviews.get(chatbox.get('jid'));
             return {
             return {
                 'close': view.close.bind(view),
                 'close': view.close.bind(view),
-                'endOTR': chatbox.endOTR.bind(chatbox),
                 'focus': view.focus.bind(view),
                 'focus': view.focus.bind(view),
                 'get': chatbox.get.bind(chatbox),
                 'get': chatbox.get.bind(chatbox),
-                'initiateOTR': chatbox.initiateOTR.bind(chatbox),
                 // FIXME: leaky abstraction from MUC
                 // FIXME: leaky abstraction from MUC
                 'is_chatroom': view.is_chatroom,
                 'is_chatroom': view.is_chatroom,
                 'maximize': chatbox.maximize.bind(chatbox),
                 'maximize': chatbox.maximize.bind(chatbox),
@@ -311,8 +294,6 @@
             allow_contact_requests: true,
             allow_contact_requests: true,
             allow_dragresize: true,
             allow_dragresize: true,
             allow_logout: true,
             allow_logout: true,
-            allow_muc: true,
-            allow_otr: true,
             allow_registration: true,
             allow_registration: true,
             animate: true,
             animate: true,
             archived_messages_page_size: '20',
             archived_messages_page_size: '20',
@@ -324,7 +305,6 @@
             auto_subscribe: false,
             auto_subscribe: false,
             auto_xa: 0, // Seconds after which user status is set to 'xa'
             auto_xa: 0, // Seconds after which user status is set to 'xa'
             bosh_service_url: undefined, // The BOSH connection manager URL.
             bosh_service_url: undefined, // The BOSH connection manager URL.
-            cache_otr_key: false,
             csi_waiting_time: 0, // Support for XEP-0352. Seconds before client is considered idle and CSI is sent out.
             csi_waiting_time: 0, // Support for XEP-0352. Seconds before client is considered idle and CSI is sent out.
             debug: false,
             debug: false,
             default_domain: undefined,
             default_domain: undefined,
@@ -355,7 +335,6 @@
             sid: undefined,
             sid: undefined,
             sounds_path: '/sounds/',
             sounds_path: '/sounds/',
             storage: 'session',
             storage: 'session',
-            use_otr_by_default: false,
             use_vcards: true,
             use_vcards: true,
             visible_toolbar_buttons: {
             visible_toolbar_buttons: {
                 'emoticons': true,
                 'emoticons': true,
@@ -394,26 +373,6 @@
         }
         }
         $.fx.off = !this.animate;
         $.fx.off = !this.animate;
 
 
-        // Only allow OTR if we have the capability
-        this.allow_otr = this.allow_otr && HAS_CRYPTO;
-
-        // Only use OTR by default if allow OTR is enabled to begin with
-        this.use_otr_by_default = this.use_otr_by_default && this.allow_otr;
-
-        // Translation aware constants
-        // ---------------------------
-        var OTR_CLASS_MAPPING = {};
-        OTR_CLASS_MAPPING[UNENCRYPTED] = 'unencrypted';
-        OTR_CLASS_MAPPING[UNVERIFIED] = 'unverified';
-        OTR_CLASS_MAPPING[VERIFIED] = 'verified';
-        OTR_CLASS_MAPPING[FINISHED] = 'finished';
-
-        var OTR_TRANSLATED_MAPPING  = {};
-        OTR_TRANSLATED_MAPPING[UNENCRYPTED] = __('unencrypted');
-        OTR_TRANSLATED_MAPPING[UNVERIFIED] = __('unverified');
-        OTR_TRANSLATED_MAPPING[VERIFIED] = __('verified');
-        OTR_TRANSLATED_MAPPING[FINISHED] = __('finished');
-
         var STATUSES = {
         var STATUSES = {
             'dnd': __('This contact is busy'),
             'dnd': __('This contact is busy'),
             'online': __('This contact is online'),
             'online': __('This contact is online'),
@@ -777,15 +736,6 @@
         };
         };
 
 
         this.registerGlobalEventHandlers = function () {
         this.registerGlobalEventHandlers = function () {
-            $(document).click(function () {
-                if ($('.toggle-otr ul').is(':visible')) {
-                    $('.toggle-otr ul', this).slideUp();
-                }
-                if ($('.toggle-smiley ul').is(':visible')) {
-                    $('.toggle-smiley ul', this).slideUp();
-                }
-            });
-
             $(document).on('mousemove', function (ev) {
             $(document).on('mousemove', function (ev) {
                 if (!this.resizing || !this.allow_dragresize) { return true; }
                 if (!this.resizing || !this.allow_dragresize) { return true; }
                 ev.preventDefault();
                 ev.preventDefault();
@@ -948,43 +898,6 @@
             converse.emit('ready');
             converse.emit('ready');
         };
         };
 
 
-        // Backbone Models and Views
-        // -------------------------
-        this.OTR = Backbone.Model.extend({
-            // A model for managing OTR settings.
-            getSessionPassphrase: function () {
-                if (converse.authentication === 'prebind') {
-                    var key = b64_sha1(converse.connection.jid),
-                        pass = window.sessionStorage[key];
-                    if (typeof pass === 'undefined') {
-                        pass = Math.floor(Math.random()*4294967295).toString();
-                        window.sessionStorage[key] = pass;
-                    }
-                    return pass;
-                } else {
-                    return converse.connection.pass;
-                }
-            },
-
-            generatePrivateKey: function (instance_tag) {
-                var key = new DSA();
-                var jid = converse.connection.jid;
-                if (converse.cache_otr_key) {
-                    var cipher = CryptoJS.lib.PasswordBasedCipher;
-                    var pass = this.getSessionPassphrase();
-                    if (typeof pass !== "undefined") {
-                        // Encrypt the key and set in sessionStorage. Also store instance tag.
-                        window.sessionStorage[b64_sha1(jid+'priv_key')] =
-                            cipher.encrypt(CryptoJS.algo.AES, key.packPrivate(), pass).toString();
-                        window.sessionStorage[b64_sha1(jid+'instance_tag')] = instance_tag;
-                        window.sessionStorage[b64_sha1(jid+'pass_check')] =
-                            cipher.encrypt(CryptoJS.algo.AES, 'match', pass).toString();
-                    }
-                }
-                return key;
-            }
-        });
-
         this.Message = Backbone.Model.extend({
         this.Message = Backbone.Model.extend({
             idAttribute: 'msgid',
             idAttribute: 'msgid',
             defaults: function(){
             defaults: function(){
@@ -1004,10 +917,10 @@
                 var height = this.get('height'),
                 var height = this.get('height'),
                     width = this.get('width'),
                     width = this.get('width'),
                     settings = {
                     settings = {
-                    'height': converse.applyDragResistance(height, this.get('default_height')),
-                    'width': converse.applyDragResistance(width, this.get('default_width')),
-                    'num_unread': this.get('num_unread') || 0
-                };
+                        'height': converse.applyDragResistance(height, this.get('default_height')),
+                        'width': converse.applyDragResistance(width, this.get('default_width')),
+                        'num_unread': this.get('num_unread') || 0
+                    };
                 if (this.get('box_id') !== 'controlbox') {
                 if (this.get('box_id') !== 'controlbox') {
                     this.messages = new converse.Messages();
                     this.messages = new converse.Messages();
                     this.messages.browserStorage = new Backbone.BrowserStorage[converse.storage](
                     this.messages.browserStorage = new Backbone.BrowserStorage[converse.storage](
@@ -1018,7 +931,6 @@
                         'chat_state': undefined,
                         'chat_state': undefined,
                         'box_id' : b64_sha1(this.get('jid')),
                         'box_id' : b64_sha1(this.get('jid')),
                         'minimized': this.get('minimized') || false,
                         'minimized': this.get('minimized') || false,
-                        'otr_status': this.get('otr_status') || UNENCRYPTED,
                         'time_minimized': this.get('time_minimized') || moment(),
                         'time_minimized': this.get('time_minimized') || moment(),
                         'time_opened': this.get('time_opened') || moment().valueOf(),
                         'time_opened': this.get('time_opened') || moment().valueOf(),
                         'url': '',
                         'url': '',
@@ -1043,133 +955,6 @@
                 });
                 });
             },
             },
 
 
-            getSession: function (callback) {
-                var cipher = CryptoJS.lib.PasswordBasedCipher;
-                var pass, instance_tag, saved_key, pass_check;
-                if (converse.cache_otr_key) {
-                    pass = converse.otr.getSessionPassphrase();
-                    if (typeof pass !== "undefined") {
-                        instance_tag = window.sessionStorage[b64_sha1(this.id+'instance_tag')];
-                        saved_key = window.sessionStorage[b64_sha1(this.id+'priv_key')];
-                        pass_check = window.sessionStorage[b64_sha1(this.connection.jid+'pass_check')];
-                        if (saved_key && instance_tag && typeof pass_check !== 'undefined') {
-                            var decrypted = cipher.decrypt(CryptoJS.algo.AES, saved_key, pass);
-                            var key = DSA.parsePrivate(decrypted.toString(CryptoJS.enc.Latin1));
-                            if (cipher.decrypt(CryptoJS.algo.AES, pass_check, pass).toString(CryptoJS.enc.Latin1) === 'match') {
-                                // Verified that the passphrase is still the same
-                                this.trigger('showHelpMessages', [__('Re-establishing encrypted session')]);
-                                callback({
-                                    'key': key,
-                                    'instance_tag': instance_tag
-                                });
-                                return; // Our work is done here
-                            }
-                        }
-                    }
-                }
-                // We need to generate a new key and instance tag
-                this.trigger('showHelpMessages', [
-                    __('Generating private key.'),
-                    __('Your browser might become unresponsive.')],
-                    null,
-                    true // show spinner
-                );
-                setTimeout(function () {
-                    var instance_tag = OTR.makeInstanceTag();
-                    callback({
-                        'key': converse.otr.generatePrivateKey.call(this, instance_tag),
-                        'instance_tag': instance_tag
-                    });
-                }, 500);
-            },
-
-            updateOTRStatus: function (state) {
-                switch (state) {
-                    case OTR.CONST.STATUS_AKE_SUCCESS:
-                        if (this.otr.msgstate === OTR.CONST.MSGSTATE_ENCRYPTED) {
-                            this.save({'otr_status': UNVERIFIED});
-                        }
-                        break;
-                    case OTR.CONST.STATUS_END_OTR:
-                        if (this.otr.msgstate === OTR.CONST.MSGSTATE_FINISHED) {
-                            this.save({'otr_status': FINISHED});
-                        } else if (this.otr.msgstate === OTR.CONST.MSGSTATE_PLAINTEXT) {
-                            this.save({'otr_status': UNENCRYPTED});
-                        }
-                        break;
-                }
-            },
-
-            onSMP: function (type, data) {
-                // Event handler for SMP (Socialist's Millionaire Protocol)
-                // used by OTR (off-the-record).
-                switch (type) {
-                    case 'question':
-                        this.otr.smpSecret(prompt(__(
-                            'Authentication request from %1$s\n\nYour chat contact is attempting to verify your identity, by asking you the question below.\n\n%2$s',
-                            [this.get('fullname'), data])));
-                        break;
-                    case 'trust':
-                        if (data === true) {
-                            this.save({'otr_status': VERIFIED});
-                        } else {
-                            this.trigger(
-                                'showHelpMessages',
-                                [__("Could not verify this user's identify.")],
-                                'error');
-                            this.save({'otr_status': UNVERIFIED});
-                        }
-                        break;
-                    default:
-                        throw new TypeError('ChatBox.onSMP: Unknown type for SMP');
-                }
-            },
-
-            initiateOTR: function (query_msg) {
-                // Sets up an OTR object through which we can send and receive
-                // encrypted messages.
-                //
-                // If 'query_msg' is passed in, it means there is an alread incoming
-                // query message from our contact. Otherwise, it is us who will
-                // send the query message to them.
-                this.save({'otr_status': UNENCRYPTED});
-                this.getSession(function (session) {
-                    this.otr = new OTR({
-                        fragment_size: 140,
-                        send_interval: 200,
-                        priv: session.key,
-                        instance_tag: session.instance_tag,
-                        debug: this.debug
-                    });
-                    this.otr.on('status', this.updateOTRStatus.bind(this));
-                    this.otr.on('smp', this.onSMP.bind(this));
-
-                    this.otr.on('ui', function (msg) {
-                        this.trigger('showReceivedOTRMessage', msg);
-                    }.bind(this));
-                    this.otr.on('io', function (msg) {
-                        this.trigger('sendMessage', new converse.Message({ message: msg }));
-                    }.bind(this));
-                    this.otr.on('error', function (msg) {
-                        this.trigger('showOTRError', msg);
-                    }.bind(this));
-
-                    this.trigger('showHelpMessages', [__('Exchanging private key with contact.')]);
-                    if (query_msg) {
-                        this.otr.receiveMsg(query_msg);
-                    } else {
-                        this.otr.sendQueryMsg();
-                    }
-                }.bind(this));
-            },
-
-            endOTR: function () {
-                if (this.otr) {
-                    this.otr.endOtr();
-                }
-                this.save({'otr_status': UNENCRYPTED});
-            },
-
             createMessage: function ($message, $delay, archive_id) {
             createMessage: function ($message, $delay, archive_id) {
                 $delay = $delay || $message.find('delay');
                 $delay = $delay || $message.find('delay');
                 var body = $message.children('body').text(),
                 var body = $message.children('body').text(),
@@ -1211,32 +996,6 @@
                     time: time,
                     time: time,
                     archive_id: archive_id
                     archive_id: archive_id
                 });
                 });
-            },
-
-            receiveMessage: function ($message, $delay, archive_id) {
-                var $body = $message.children('body');
-                var text = ($body.length > 0 ? $body.text() : undefined);
-                if ((!text) || (!converse.allow_otr)) {
-                    return this.createMessage($message, $delay, archive_id);
-                }
-                if (text.match(/^\?OTRv23?/)) {
-                    this.initiateOTR(text);
-                } else {
-                    if (_.contains([UNVERIFIED, VERIFIED], this.get('otr_status'))) {
-                        this.otr.receiveMsg(text);
-                    } else {
-                        if (text.match(/^\?OTR/)) {
-                            if (!this.otr) {
-                                this.initiateOTR(text);
-                            } else {
-                                this.otr.receiveMsg(text);
-                            }
-                        } else {
-                            // Normal unencrypted message.
-                            this.createMessage($message, $delay, archive_id);
-                        }
-                    }
-                }
             }
             }
         });
         });
 
 
@@ -1253,10 +1012,6 @@
                 'click .toggle-smiley': 'toggleEmoticonMenu',
                 'click .toggle-smiley': 'toggleEmoticonMenu',
                 'click .toggle-smiley ul li': 'insertEmoticon',
                 'click .toggle-smiley ul li': 'insertEmoticon',
                 'click .toggle-clear': 'clearMessages',
                 'click .toggle-clear': 'clearMessages',
-                'click .toggle-otr': 'toggleOTRMenu',
-                'click .start-otr': 'startOTRFromToolbar',
-                'click .end-otr': 'endOTR',
-                'click .auth-otr': 'authOTR',
                 'click .toggle-call': 'toggleCall',
                 'click .toggle-call': 'toggleCall',
                 'mousedown .dragresize-top': 'onStartVerticalResize',
                 'mousedown .dragresize-top': 'onStartVerticalResize',
                 'mousedown .dragresize-left': 'onStartHorizontalResize',
                 'mousedown .dragresize-left': 'onStartHorizontalResize',
@@ -1272,23 +1027,11 @@
                 this.model.on('change:chat_state', this.sendChatState, this);
                 this.model.on('change:chat_state', this.sendChatState, this);
                 this.model.on('change:chat_status', this.onChatStatusChanged, this);
                 this.model.on('change:chat_status', this.onChatStatusChanged, this);
                 this.model.on('change:image', this.renderAvatar, this);
                 this.model.on('change:image', this.renderAvatar, this);
-                this.model.on('change:otr_status', this.onOTRStatusChanged, this);
                 this.model.on('change:minimized', this.onMinimizedChanged, this);
                 this.model.on('change:minimized', this.onMinimizedChanged, this);
                 this.model.on('change:status', this.onStatusChanged, this);
                 this.model.on('change:status', this.onStatusChanged, this);
-                this.model.on('showOTRError', this.showOTRError, this);
                 this.model.on('showHelpMessages', this.showHelpMessages, this);
                 this.model.on('showHelpMessages', this.showHelpMessages, this);
                 this.model.on('sendMessage', this.sendMessage, this);
                 this.model.on('sendMessage', this.sendMessage, this);
-                this.model.on('showSentOTRMessage', function (text) {
-                    this.showMessage({'message': text, 'sender': 'me'});
-                }, this);
-                this.model.on('showReceivedOTRMessage', function (text) {
-                    this.showMessage({'message': text, 'sender': 'them'});
-                }, this);
                 this.updateVCard().render().fetchMessages().insertIntoPage().hide();
                 this.updateVCard().render().fetchMessages().insertIntoPage().hide();
-
-                if ((_.contains([UNVERIFIED, VERIFIED], this.model.get('otr_status'))) || converse.use_otr_by_default) {
-                    this.model.initiateOTR();
-                }
             },
             },
 
 
             render: function () {
             render: function () {
@@ -1668,22 +1411,25 @@
                 }
                 }
             },
             },
 
 
+            createMessageStanza: function (message) {
+                return $msg({
+                            from: converse.connection.jid,
+                            to: this.model.get('jid'),
+                            type: 'chat',
+                            id: message.get('msgid')
+                       }).c('body').t(message.get('message')).up()
+                         .c(ACTIVE, {'xmlns': Strophe.NS.CHATSTATES}).up();
+            },
+
             sendMessage: function (message) {
             sendMessage: function (message) {
                 /* Responsible for sending off a text message.
                 /* Responsible for sending off a text message.
                  *
                  *
                  *  Parameters:
                  *  Parameters:
                  *    (Message) message - The chat message
                  *    (Message) message - The chat message
                  */
                  */
-                // TODO: We might want to send to specfic resources. Especially in the OTR case.
-                var bare_jid = this.model.get('jid');
-                var messageStanza = $msg({from: converse.connection.jid, to: bare_jid, type: 'chat', id: message.get('msgid')})
-                    .c('body').t(message.get('message')).up()
-                    .c(ACTIVE, {'xmlns': Strophe.NS.CHATSTATES}).up();
-
-                if (this.model.get('otr_status') !== UNENCRYPTED) {
-                    // OTR messages aren't carbon copied
-                    messageStanza.c('private', {'xmlns': Strophe.NS.CARBONS});
-                }
+                // TODO: We might want to send to specfic resources.
+                // Especially in the OTR case.
+                var messageStanza = this.createMessageStanza(message);
                 converse.connection.send(messageStanza);
                 converse.connection.send(messageStanza);
                 if (converse.forward_messages) {
                 if (converse.forward_messages) {
                     // Forward the message, so that other connected resources are also aware of it.
                     // Forward the message, so that other connected resources are also aware of it.
@@ -1704,7 +1450,11 @@
                  *    (string) text - The chat message text.
                  *    (string) text - The chat message text.
                  */
                  */
                 if (!converse.connection.authenticated) {
                 if (!converse.connection.authenticated) {
-                    return this.showHelpMessages(['Sorry, the connection has been lost, and your message could not be sent'], 'error');
+                    return this.showHelpMessages(
+                        ['Sorry, the connection has been lost, '+
+                            'and your message could not be sent'],
+                        'error'
+                    );
                 }
                 }
                 var match = text.replace(/^\s*/, "").match(/^\/(.*)\s*$/), msgs;
                 var match = text.replace(/^\s*/, "").match(/^\/(.*)\s*$/), msgs;
                 if (match) {
                 if (match) {
@@ -1719,28 +1469,17 @@
                             ];
                             ];
                         this.showHelpMessages(msgs);
                         this.showHelpMessages(msgs);
                         return;
                         return;
-                    } else if ((converse.allow_otr) && (match[1] === "endotr")) {
-                        return this.endOTR();
-                    } else if ((converse.allow_otr) && (match[1] === "otr")) {
-                        return this.model.initiateOTR();
                     }
                     }
                 }
                 }
-                if (_.contains([UNVERIFIED, VERIFIED], this.model.get('otr_status'))) {
-                    // Off-the-record encryption is active
-                    this.model.otr.sendMsg(text);
-                    this.model.trigger('showSentOTRMessage', text);
-                } else {
-                    // We only save unencrypted messages.
-                    var fullname = converse.xmppstatus.get('fullname');
-                    fullname = _.isEmpty(fullname)? converse.bare_jid: fullname;
-                    var message = this.model.messages.create({
-                        fullname: fullname,
-                        sender: 'me',
-                        time: moment().format(),
-                        message: text
-                    });
-                    this.sendMessage(message);
-                }
+                var fullname = converse.xmppstatus.get('fullname');
+                fullname = _.isEmpty(fullname)? converse.bare_jid: fullname;
+                var message = this.model.messages.create({
+                    fullname: fullname,
+                    sender: 'me',
+                    time: moment().format(),
+                    message: text
+                });
+                this.sendMessage(message);
             },
             },
 
 
             sendChatState: function () {
             sendChatState: function () {
@@ -1906,70 +1645,6 @@
                 this.$el.find('.toggle-smiley ul').slideToggle(200);
                 this.$el.find('.toggle-smiley ul').slideToggle(200);
             },
             },
 
 
-            toggleOTRMenu: function (ev) {
-                ev.stopPropagation();
-                this.$el.find('.toggle-otr ul').slideToggle(200);
-            },
-
-            showOTRError: function (msg) {
-                if (msg === 'Message cannot be sent at this time.') {
-                    this.showHelpMessages(
-                        [__('Your message could not be sent')], 'error');
-                } else if (msg === 'Received an unencrypted message.') {
-                    this.showHelpMessages(
-                        [__('We received an unencrypted message')], 'error');
-                } else if (msg === 'Received an unreadable encrypted message.') {
-                    this.showHelpMessages(
-                        [__('We received an unreadable encrypted message')],
-                        'error');
-                } else {
-                    this.showHelpMessages(['Encryption error occured: '+msg], 'error');
-                }
-                converse.log("OTR ERROR:"+msg);
-            },
-
-            startOTRFromToolbar: function (ev) {
-                $(ev.target).parent().parent().slideUp();
-                ev.stopPropagation();
-                this.model.initiateOTR();
-            },
-
-            endOTR: function (ev) {
-                if (typeof ev !== "undefined") {
-                    ev.preventDefault();
-                    ev.stopPropagation();
-                }
-                this.model.endOTR();
-            },
-
-            authOTR: function (ev) {
-                var scheme = $(ev.target).data().scheme;
-                var result, question, answer;
-                if (scheme === 'fingerprint') {
-                    result = confirm(__('Here are the fingerprints, please confirm them with %1$s, outside of this chat.\n\nFingerprint for you, %2$s: %3$s\n\nFingerprint for %1$s: %4$s\n\nIf you have confirmed that the fingerprints match, click OK, otherwise click Cancel.', [
-                            this.model.get('fullname'),
-                            converse.xmppstatus.get('fullname')||converse.bare_jid,
-                            this.model.otr.priv.fingerprint(),
-                            this.model.otr.their_priv_pk.fingerprint()
-                        ]
-                    ));
-                    if (result === true) {
-                        this.model.save({'otr_status': VERIFIED});
-                    } else {
-                        this.model.save({'otr_status': UNVERIFIED});
-                    }
-                } else if (scheme === 'smp') {
-                    alert(__('You will be prompted to provide a security question and then an answer to that question.\n\nYour contact will then be prompted the same question and if they type the exact same answer (case sensitive), their identity will be verified.'));
-                    question = prompt(__('What is your security question?'));
-                    if (question) {
-                        answer = prompt(__('What is the answer to the security question?'));
-                        this.model.otr.smpSecret(answer, question);
-                    }
-                } else {
-                    this.showHelpMessages([__('Invalid authentication scheme provided')], 'error');
-                }
-            },
-
             toggleCall: function (ev) {
             toggleCall: function (ev) {
                 ev.stopPropagation();
                 ev.stopPropagation();
                 converse.emit('callButtonClicked', {
                 converse.emit('callButtonClicked', {
@@ -2001,10 +1676,6 @@
                 converse.emit('contactStatusMessageChanged', item.attributes, item.get('status'));
                 converse.emit('contactStatusMessageChanged', item.attributes, item.get('status'));
             },
             },
 
 
-            onOTRStatusChanged: function () {
-                this.renderToolbar().informOTRChange();
-            },
-
             onMinimizedChanged: function (item) {
             onMinimizedChanged: function (item) {
                 if (item.get('minimized')) {
                 if (item.get('minimized')) {
                     this.hide();
                     this.hide();
@@ -2078,68 +1749,22 @@
                 return this;
                 return this;
             },
             },
 
 
-            informOTRChange: function () {
-                var data = this.model.toJSON();
-                var msgs = [];
-                if (data.otr_status === UNENCRYPTED) {
-                    msgs.push(__("Your messages are not encrypted anymore"));
-                } else if (data.otr_status === UNVERIFIED) {
-                    msgs.push(__("Your messages are now encrypted but your contact's identity has not been verified."));
-                } else if (data.otr_status === VERIFIED) {
-                    msgs.push(__("Your contact's identify has been verified."));
-                } else if (data.otr_status === FINISHED) {
-                    msgs.push(__("Your contact has ended encryption on their end, you should do the same."));
-                }
-                return this.showHelpMessages(msgs, 'info', false);
-            },
-
-            getOTRTooltip: function () {
-                var data = this.model.toJSON();
-                if (data.otr_status === UNENCRYPTED) {
-                    return __('Your messages are not encrypted. Click here to enable OTR encryption.');
-                } else if (data.otr_status === UNVERIFIED) {
-                    return __('Your messages are encrypted, but your contact has not been verified.');
-                } else if (data.otr_status === VERIFIED) {
-                    return __('Your messages are encrypted and your contact verified.');
-                } else if (data.otr_status === FINISHED) {
-                    return __('Your contact has closed their end of the private session, you should do the same');
-                }
-            },
-
-            renderToolbar: function () {
-                // FIXME: leaky abstractions from plugins, particularly
-                // "toggle_occupants" from MUC.
-                if (converse.show_toolbar) {
-                    var data = this.model.toJSON();
-                    this.$el.find('.chat-toolbar').html(
-                        converse.templates.toolbar(
-                            _.extend(data, {
-                                FINISHED: FINISHED,
-                                UNENCRYPTED: UNENCRYPTED,
-                                UNVERIFIED: UNVERIFIED,
-                                VERIFIED: VERIFIED,
-                                allow_otr: converse.allow_otr && !this.is_chatroom,
-                                otr_tooltip: this.getOTRTooltip(),
-                                label_clear: __('Clear all messages'),
-                                label_end_encrypted_conversation: __('End encrypted conversation'),
-                                label_insert_smiley: __('Insert a smiley'),
-                                label_hide_occupants: __('Hide the list of occupants'),
-                                label_refresh_encrypted_conversation: __('Refresh encrypted conversation'),
-                                label_start_call: __('Start a call'),
-                                label_start_encrypted_conversation: __('Start encrypted conversation'),
-                                label_verify_with_fingerprints: __('Verify with fingerprints'),
-                                label_verify_with_smp: __('Verify with SMP'),
-                                label_whats_this: __("What\'s this?"),
-                                otr_status_class: OTR_CLASS_MAPPING[data.otr_status],
-                                otr_translated_status: OTR_TRANSLATED_MAPPING[data.otr_status],
-                                show_call_button: converse.visible_toolbar_buttons.call,
-                                show_clear_button: converse.visible_toolbar_buttons.clear,
-                                show_emoticons: converse.visible_toolbar_buttons.emoticons,
-                                show_occupants_toggle: this.is_chatroom && converse.visible_toolbar_buttons.toggle_occupants
-                            })
-                        )
-                    );
+            renderToolbar: function (options) {
+                if (!converse.show_toolbar) {
+                    return;
                 }
                 }
+                options = _.extend(options || {}, {
+                    label_clear: __('Clear all messages'),
+                    label_hide_occupants: __('Hide the list of occupants'),
+                    label_insert_smiley: __('Insert a smiley'),
+                    label_start_call: __('Start a call'),
+                    show_call_button: converse.visible_toolbar_buttons.call,
+                    show_clear_button: converse.visible_toolbar_buttons.clear,
+                    show_emoticons: converse.visible_toolbar_buttons.emoticons,
+                    // FIXME Leaky abstraction MUC
+                    show_occupants_toggle: this.is_chatroom && converse.visible_toolbar_buttons.toggle_occupants
+                });
+                this.$el.find('.chat-toolbar').html(converse.templates.toolbar(_.extend(this.model.toJSON(), options || {})));
                 return this;
                 return this;
             },
             },
 
 
@@ -2379,14 +2004,6 @@
             onConnected: function () {
             onConnected: function () {
                 if (this.model.get('connected')) {
                 if (this.model.get('connected')) {
                     this.render().initRoster();
                     this.render().initRoster();
-                    converse.features.off('add', this.featureAdded, this);
-                    converse.features.on('add', this.featureAdded, this);
-                    // Features could have been added before the controlbox was
-                    // initialized. Currently we're only interested in MUC
-                    var feature = converse.features.findWhere({'var': Strophe.NS.MUC});
-                    if (feature) {
-                        this.featureAdded(feature);
-                    }
                 }
                 }
             },
             },
 
 
@@ -2434,24 +2051,14 @@
 
 
             renderContactsPanel: function () {
             renderContactsPanel: function () {
                 this.$el.html(converse.templates.controlbox(this.model.toJSON()));
                 this.$el.html(converse.templates.controlbox(this.model.toJSON()));
-                this.contactspanel = new converse.ContactsPanel({'$parent': this.$el.find('.controlbox-panes')});
+                this.contactspanel = new converse.ContactsPanel({
+                    '$parent': this.$el.find('.controlbox-panes')
+                });
                 this.contactspanel.render();
                 this.contactspanel.render();
-                converse.xmppstatusview = new converse.XMPPStatusView({'model': converse.xmppstatus});
+                converse.xmppstatusview = new converse.XMPPStatusView({
+                    'model': converse.xmppstatus
+                });
                 converse.xmppstatusview.render();
                 converse.xmppstatusview.render();
-                if (converse.allow_muc) {
-                    this.roomspanel = new converse.RoomsPanel({
-                        '$parent': this.$el.find('.controlbox-panes'),
-                        'model': new (Backbone.Model.extend({
-                            id: b64_sha1('converse.roomspanel'+converse.bare_jid), // Required by sessionStorage
-                            browserStorage: new Backbone.BrowserStorage[converse.storage](
-                                b64_sha1('converse.roomspanel'+converse.bare_jid))
-                        }))()
-                    });
-                    this.roomspanel.render().model.fetch();
-                    if (!this.roomspanel.model.get('nick')) {
-                        this.roomspanel.model.save({nick: Strophe.getNodeFromJid(converse.bare_jid)});
-                    }
-                }
                 this.initDragResize().setDimensions();
                 this.initDragResize().setDimensions();
             },
             },
 
 
@@ -2500,16 +2107,6 @@
                 return this;
                 return this;
             },
             },
 
 
-            featureAdded: function (feature) {
-                if ((feature.get('var') === Strophe.NS.MUC) && (converse.allow_muc)) {
-                    this.roomspanel.model.save({muc_domain: feature.get('from')});
-                    var $server= this.$el.find('input.new-chatroom-server');
-                    if (! $server.is(':focus')) {
-                        $server.val(this.roomspanel.model.get('muc_domain'));
-                    }
-                }
-            },
-
             switchTab: function (ev) {
             switchTab: function (ev) {
                 // TODO: automatically focus the relevant input
                 // TODO: automatically focus the relevant input
                 if (ev && ev.preventDefault) { ev.preventDefault(); }
                 if (ev && ev.preventDefault) { ev.preventDefault(); }
@@ -2780,7 +2377,7 @@
                 if (!this.isOnlyChatStateNotification($message) && !is_me && !$forwarded.length) {
                 if (!this.isOnlyChatStateNotification($message) && !is_me && !$forwarded.length) {
                     converse.playNotification();
                     converse.playNotification();
                 }
                 }
-                chatbox.receiveMessage($message, $delay, archive_id);
+                chatbox.createMessage($message, $delay, archive_id);
                 converse.roster.addResource(contact_jid, resource);
                 converse.roster.addResource(contact_jid, resource);
                 converse.emit('message', message);
                 converse.emit('message', message);
                 return true;
                 return true;
@@ -2974,8 +2571,6 @@
                     }
                     }
                 }, this);
                 }, this);
                 this.model.on('change:minimized', this.clearUnreadMessagesCounter, this);
                 this.model.on('change:minimized', this.clearUnreadMessagesCounter, this);
-                this.model.on('showReceivedOTRMessage', this.updateUnreadMessagesCounter, this);
-                this.model.on('showSentOTRMessage', this.updateUnreadMessagesCounter, this);
             },
             },
 
 
             render: function () {
             render: function () {
@@ -4590,9 +4185,6 @@
                 if (converse.use_vcards) {
                 if (converse.use_vcards) {
                     converse.connection.disco.addFeature(Strophe.NS.VCARD);
                     converse.connection.disco.addFeature(Strophe.NS.VCARD);
                 }
                 }
-                if (converse.allow_muc) {
-                    converse.connection.disco.addFeature(Strophe.NS.MUC);
-                }
                 if (converse.message_carbons) {
                 if (converse.message_carbons) {
                     converse.connection.disco.addFeature(Strophe.NS.CARBONS);
                     converse.connection.disco.addFeature(Strophe.NS.CARBONS);
                 }
                 }
@@ -5353,7 +4945,6 @@
             this.chatboxes = new this.ChatBoxes();
             this.chatboxes = new this.ChatBoxes();
             this.chatboxviews = new this.ChatBoxViews({model: this.chatboxes});
             this.chatboxviews = new this.ChatBoxViews({model: this.chatboxes});
             this.controlboxtoggle = new this.ControlBoxToggle();
             this.controlboxtoggle = new this.ControlBoxToggle();
-            this.otr = new this.OTR();
             this.initSession();
             this.initSession();
             this.initConnection();
             this.initConnection();
             if (this.connection) {
             if (this.connection) {

+ 70 - 5
src/converse-muc.js

@@ -6,6 +6,9 @@
 //
 //
 /*global converse, utils, Backbone, define, window, setTimeout */
 /*global converse, utils, Backbone, define, window, setTimeout */
 
 
+/* This is a Converse.js plugin which add support for multi-user chat rooms, as
+ * specified in XEP-0045 Multi-user chat.
+ */
 (function (root, factory) {
 (function (root, factory) {
     if (typeof define === 'function' && define.amd) {
     if (typeof define === 'function' && define.amd) {
         // AMD module loading
         // AMD module loading
@@ -44,8 +47,6 @@
     Strophe.addNamespace('MUC_USER', Strophe.NS.MUC + "#user");
     Strophe.addNamespace('MUC_USER', Strophe.NS.MUC + "#user");
 
 
     converse_api.plugins.add('muc', {
     converse_api.plugins.add('muc', {
-        /* This plugin adds support for XEP-0045 Multi-user chat
-         */
 
 
         overrides: {
         overrides: {
             // Overrides mentioned here will be picked up by converse.js's
             // Overrides mentioned here will be picked up by converse.js's
@@ -53,6 +54,69 @@
             // relevant objects or classes.
             // relevant objects or classes.
             //
             //
             // New functions which don't exist yet can also be added.
             // New functions which don't exist yet can also be added.
+
+            Features: {
+                addClientFeatures: function () {
+                    var converse = this._super.converse;
+                    this._super.addClientFeatures.apply(this, arguments);
+                    if (converse.allow_muc) {
+                        converse.connection.disco.addFeature(Strophe.NS.MUC);
+                    }
+                }
+            },
+
+            ControlBoxView: {
+                renderContactsPanel: function () {
+                    var converse = this._super.converse;
+                    this._super.renderContactsPanel.apply(this, arguments);
+                    if (converse.allow_muc) {
+                        this.roomspanel = new converse.RoomsPanel({
+                            '$parent': this.$el.find('.controlbox-panes'),
+                            'model': new (Backbone.Model.extend({
+                                id: b64_sha1('converse.roomspanel'+converse.bare_jid), // Required by sessionStorage
+                                browserStorage: new Backbone.BrowserStorage[converse.storage](
+                                    b64_sha1('converse.roomspanel'+converse.bare_jid))
+                            }))()
+                        });
+                        this.roomspanel.render().model.fetch();
+                        if (!this.roomspanel.model.get('nick')) {
+                            this.roomspanel.model.save({
+                                nick: Strophe.getNodeFromJid(converse.bare_jid)
+                            });
+                        }
+                    }
+                },
+
+                onConnected: function () {
+                    var converse = this._super.converse;
+                    this._super.onConnected.apply(this, arguments);
+
+                    if (this.model.get('connected')) {
+                        converse.features.off('add', this.featureAdded, this);
+                        converse.features.on('add', this.featureAdded, this);
+                        // Features could have been added before the controlbox was
+                        // initialized. We're only interested in MUC
+                        var feature = converse.features.findWhere({
+                            'var': Strophe.NS.MUC
+                        });
+                        if (feature) {
+                            this.featureAdded(feature);
+                        }
+                    }
+                },
+
+                featureAdded: function (feature) {
+                    var converse = this._super.converse;
+                    if ((feature.get('var') === Strophe.NS.MUC) && (converse.allow_muc)) {
+                        this.roomspanel.model.save({muc_domain: feature.get('from')});
+                        var $server= this.$el.find('input.new-chatroom-server');
+                        if (! $server.is(':focus')) {
+                            $server.val(this.roomspanel.model.get('muc_domain'));
+                        }
+                    }
+                }
+            },
+
             ChatBoxView: {
             ChatBoxView: {
                 clearChatRoomMessages: function (ev) {
                 clearChatRoomMessages: function (ev) {
                     /* New method added to the ChatBox model which allows all
                     /* New method added to the ChatBox model which allows all
@@ -72,7 +136,7 @@
                     /* Override so that we can register a handler
                     /* Override so that we can register a handler
                      * for chat room invites.
                      * for chat room invites.
                      */
                      */
-                    this._super.registerMessageHandler(); // First call the original
+                    this._super.registerMessageHandler.apply(this, arguments); // First call the original
                     this._super.converse.connection.addHandler(
                     this._super.converse.connection.addHandler(
                         function (message) {
                         function (message) {
                             this.onInvite(message);
                             this.onInvite(message);
@@ -134,10 +198,11 @@
             var converse = this.converse;
             var converse = this.converse;
             // Configuration values for this plugin
             // Configuration values for this plugin
             var settings = {
             var settings = {
+                allow_muc: true,
                 auto_join_on_invite: false  // Auto-join chatroom on invite
                 auto_join_on_invite: false  // Auto-join chatroom on invite
             };
             };
-            _.extend(this, settings);
-            _.extend(this, _.pick(converse.user_settings, Object.keys(settings)));
+            _.extend(converse, settings);
+            _.extend(converse, _.pick(converse.user_settings, Object.keys(settings)));
 
 
             converse.ChatRoomView = converse.ChatBoxView.extend({
             converse.ChatRoomView = converse.ChatBoxView.extend({
                 /* Backbone View which renders a chat room, based upon the view
                 /* Backbone View which renders a chat room, based upon the view

+ 512 - 0
src/converse-otr.js

@@ -0,0 +1,512 @@
+// Converse.js (A browser based XMPP chat client)
+// http://conversejs.org
+//
+// Copyright (c) 2012-2016, Jan-Carel Brand <jc@opkode.com>
+// Licensed under the Mozilla Public License (MPLv2)
+//
+/*global converse, utils, Backbone, define, window, setTimeout, 
+  crypto, CryptoJS, otr */
+
+/* This is a Converse.js plugin which add support Off-the-record (OTR)
+ * encryption of one-on-one chat messages.
+ */
+(function (root, factory) {
+    if (typeof define === 'function' && define.amd) {
+        // AMD module loading
+        define("converse-otr", ["otr", "converse-core", "utils"], factory);
+    } else {
+        // When not using a module loader
+        // -------------------------------
+        // In this case, the dependencies need to be available already as
+        // global variables, and should be loaded separately via *script* tags.
+        // See the file **non_amd.html** for an example of this usecase.
+        root.converse = factory(otr, converse, utils);
+    }
+}(this, function (otr, converse_api, utils) {
+    // Strophe methods for building stanzas
+    var Strophe = converse_api.env.Strophe,
+        b64_sha1 = converse_api.env.b64_sha1;
+    // Other necessary globals
+    var $ = converse_api.env.jQuery,
+        _ = converse_api.env._;
+
+    // Translation machinery
+    // ---------------------
+    var __ = utils.__.bind(this);
+
+    var HAS_CSPRNG = ((typeof crypto !== 'undefined') &&
+        ((typeof crypto.randomBytes === 'function') ||
+            (typeof crypto.getRandomValues === 'function')
+    ));
+    var HAS_CRYPTO = HAS_CSPRNG && (
+        (typeof CryptoJS !== "undefined") &&
+        (typeof otr.OTR !== "undefined") &&
+        (typeof otr.DSA !== "undefined")
+    );
+
+    var UNENCRYPTED = 0;
+    var UNVERIFIED= 1;
+    var VERIFIED= 2;
+    var FINISHED = 3;
+
+    // Translation aware constants
+    // ---------------------------
+    var OTR_CLASS_MAPPING = {};
+    OTR_CLASS_MAPPING[UNENCRYPTED] = 'unencrypted';
+    OTR_CLASS_MAPPING[UNVERIFIED] = 'unverified';
+    OTR_CLASS_MAPPING[VERIFIED] = 'verified';
+    OTR_CLASS_MAPPING[FINISHED] = 'finished';
+
+    var OTR_TRANSLATED_MAPPING  = {};
+    OTR_TRANSLATED_MAPPING[UNENCRYPTED] = __('unencrypted');
+    OTR_TRANSLATED_MAPPING[UNVERIFIED] = __('unverified');
+    OTR_TRANSLATED_MAPPING[VERIFIED] = __('verified');
+    OTR_TRANSLATED_MAPPING[FINISHED] = __('finished');
+
+    converse_api.plugins.add('otr', {
+
+        overrides: {
+            // Overrides mentioned here will be picked up by converse.js's
+            // plugin architecture they will replace existing methods on the
+            // relevant objects or classes.
+            //
+            // New functions which don't exist yet can also be added.
+ 
+            _initialize: function () {
+                this._super._initialize.apply(this, arguments);
+                this._super.converse.otr = new this._super.converse.OTR();
+            },
+
+            registerGlobalEventHandlers: function () {
+                this._super.registerGlobalEventHandlers();
+
+                $(document).click(function () {
+                    if ($('.toggle-otr ul').is(':visible')) {
+                        $('.toggle-otr ul', this).slideUp();
+                    }
+                    if ($('.toggle-smiley ul').is(':visible')) {
+                        $('.toggle-smiley ul', this).slideUp();
+                    }
+                });
+            },
+
+            wrappedChatBox: function (chatbox) {
+                var wrapped_chatbox = this._super.wrappedChatBox.apply(this, arguments);
+                if (!chatbox) { return; }
+                return _.extend(wrapped_chatbox, {
+                    'endOTR': chatbox.endOTR.bind(chatbox),
+                    'initiateOTR': chatbox.initiateOTR.bind(chatbox),
+                });
+            },
+
+            ChatBox: {
+                initialize: function () {
+                    this._super.initialize.apply(this, arguments);
+                    if (this.get('box_id') !== 'controlbox') {
+                        this.save({
+                            'otr_status': this.get('otr_status') || UNENCRYPTED
+                        });
+                    }
+                },
+
+                createMessage: function ($message, $delay, archive_id) {
+                    var $body = $message.children('body');
+                    var text = ($body.length > 0 ? $body.text() : undefined);
+                    if ((!text) || (!converse.allow_otr)) {
+                        return this._super.createMessage.apply(this, arguments);
+                    }
+                    if (text.match(/^\?OTRv23?/)) {
+                        this.initiateOTR(text);
+                    } else {
+                        if (_.contains([UNVERIFIED, VERIFIED], this.get('otr_status'))) {
+                            this.otr.receiveMsg(text);
+                        } else {
+                            if (text.match(/^\?OTR/)) {
+                                if (!this.otr) {
+                                    this.initiateOTR(text);
+                                } else {
+                                    this.otr.receiveMsg(text);
+                                }
+                            } else {
+                                // Normal unencrypted message.
+                                return this._super.createMessage.apply(this, arguments);
+                            }
+                        }
+                    }
+                },
+                
+                getSession: function (callback) {
+                    var cipher = CryptoJS.lib.PasswordBasedCipher;
+                    var pass, instance_tag, saved_key, pass_check;
+                    if (converse.cache_otr_key) {
+                        pass = converse.otr.getSessionPassphrase();
+                        if (typeof pass !== "undefined") {
+                            instance_tag = window.sessionStorage[b64_sha1(this.id+'instance_tag')];
+                            saved_key = window.sessionStorage[b64_sha1(this.id+'priv_key')];
+                            pass_check = window.sessionStorage[b64_sha1(this.connection.jid+'pass_check')];
+                            if (saved_key && instance_tag && typeof pass_check !== 'undefined') {
+                                var decrypted = cipher.decrypt(CryptoJS.algo.AES, saved_key, pass);
+                                var key = otr.DSA.parsePrivate(decrypted.toString(CryptoJS.enc.Latin1));
+                                if (cipher.decrypt(CryptoJS.algo.AES, pass_check, pass).toString(CryptoJS.enc.Latin1) === 'match') {
+                                    // Verified that the passphrase is still the same
+                                    this.trigger('showHelpMessages', [__('Re-establishing encrypted session')]);
+                                    callback({
+                                        'key': key,
+                                        'instance_tag': instance_tag
+                                    });
+                                    return; // Our work is done here
+                                }
+                            }
+                        }
+                    }
+                    // We need to generate a new key and instance tag
+                    this.trigger('showHelpMessages', [
+                        __('Generating private key.'),
+                        __('Your browser might become unresponsive.')],
+                        null,
+                        true // show spinner
+                    );
+                    setTimeout(function () {
+                        var instance_tag = otr.OTR.makeInstanceTag();
+                        callback({
+                            'key': converse.otr.generatePrivateKey.call(this, instance_tag),
+                            'instance_tag': instance_tag
+                        });
+                    }, 500);
+                },
+
+                updateOTRStatus: function (state) {
+                    switch (state) {
+                        case otr.OTR.CONST.STATUS_AKE_SUCCESS:
+                            if (this.otr.msgstate === otr.OTR.CONST.MSGSTATE_ENCRYPTED) {
+                                this.save({'otr_status': UNVERIFIED});
+                            }
+                            break;
+                        case otr.OTR.CONST.STATUS_END_OTR:
+                            if (this.otr.msgstate === otr.OTR.CONST.MSGSTATE_FINISHED) {
+                                this.save({'otr_status': FINISHED});
+                            } else if (this.otr.msgstate === otr.OTR.CONST.MSGSTATE_PLAINTEXT) {
+                                this.save({'otr_status': UNENCRYPTED});
+                            }
+                            break;
+                    }
+                },
+
+                onSMP: function (type, data) {
+                    // Event handler for SMP (Socialist's Millionaire Protocol)
+                    // used by OTR (off-the-record).
+                    switch (type) {
+                        case 'question':
+                            this.otr.smpSecret(prompt(__(
+                                'Authentication request from %1$s\n\nYour chat contact is attempting to verify your identity, by asking you the question below.\n\n%2$s',
+                                [this.get('fullname'), data])));
+                            break;
+                        case 'trust':
+                            if (data === true) {
+                                this.save({'otr_status': VERIFIED});
+                            } else {
+                                this.trigger(
+                                    'showHelpMessages',
+                                    [__("Could not verify this user's identify.")],
+                                    'error');
+                                this.save({'otr_status': UNVERIFIED});
+                            }
+                            break;
+                        default:
+                            throw new TypeError('ChatBox.onSMP: Unknown type for SMP');
+                    }
+                },
+
+                initiateOTR: function (query_msg) {
+                    // Sets up an OTR object through which we can send and receive
+                    // encrypted messages.
+                    //
+                    // If 'query_msg' is passed in, it means there is an alread incoming
+                    // query message from our contact. Otherwise, it is us who will
+                    // send the query message to them.
+                    this.save({'otr_status': UNENCRYPTED});
+                    this.getSession(function (session) {
+                        this.otr = new otr.OTR({
+                            fragment_size: 140,
+                            send_interval: 200,
+                            priv: session.key,
+                            instance_tag: session.instance_tag,
+                            debug: this.debug
+                        });
+                        this.otr.on('status', this.updateOTRStatus.bind(this));
+                        this.otr.on('smp', this.onSMP.bind(this));
+
+                        this.otr.on('ui', function (msg) {
+                            this.trigger('showReceivedOTRMessage', msg);
+                        }.bind(this));
+                        this.otr.on('io', function (msg) {
+                            this.trigger('sendMessage', new converse.Message({ message: msg }));
+                        }.bind(this));
+                        this.otr.on('error', function (msg) {
+                            this.trigger('showOTRError', msg);
+                        }.bind(this));
+
+                        this.trigger('showHelpMessages', [__('Exchanging private key with contact.')]);
+                        if (query_msg) {
+                            this.otr.receiveMsg(query_msg);
+                        } else {
+                            this.otr.sendQueryMsg();
+                        }
+                    }.bind(this));
+                },
+
+                endOTR: function () {
+                    if (this.otr) {
+                        this.otr.endOtr();
+                    }
+                    this.save({'otr_status': UNENCRYPTED});
+                }
+            },
+
+            ChatBoxView:  {
+                events: {
+                    'click .toggle-otr': 'toggleOTRMenu',
+                    'click .start-otr': 'startOTRFromToolbar',
+                    'click .end-otr': 'endOTR',
+                    'click .auth-otr': 'authOTR'
+                },
+
+                initialize: function () {
+                    this._super.initialize.apply(this, arguments);
+                    this.model.on('change:otr_status', this.onOTRStatusChanged, this);
+                    this.model.on('showOTRError', this.showOTRError, this);
+                    this.model.on('showSentOTRMessage', function (text) {
+                        this.showMessage({'message': text, 'sender': 'me'});
+                    }, this);
+                    this.model.on('showReceivedOTRMessage', function (text) {
+                        this.showMessage({'message': text, 'sender': 'them'});
+                    }, this);
+                    if ((_.contains([UNVERIFIED, VERIFIED], this.model.get('otr_status'))) || converse.use_otr_by_default) {
+                        this.model.initiateOTR();
+                    }
+                },
+
+                createMessageStanza: function () {
+                    var stanza = this._super.createMessageStanza.apply(this, arguments);
+                    if (this.model.get('otr_status') !== UNENCRYPTED) {
+                        // OTR messages aren't carbon copied
+                        stanza.c('private', {'xmlns': Strophe.NS.CARBONS});
+                    }
+                    return stanza;
+                },
+
+                onMessageSubmitted: function (text) {
+                    if (!converse.connection.authenticated) {
+                        return this.showHelpMessages(
+                            ['Sorry, the connection has been lost, '+
+                              'and your message could not be sent'],
+                            'error'
+                        );
+                    }
+                    var match = text.replace(/^\s*/, "").match(/^\/(.*)\s*$/);
+                    if (match) {
+                        if ((converse.allow_otr) && (match[1] === "endotr")) {
+                            return this.endOTR();
+                        } else if ((converse.allow_otr) && (match[1] === "otr")) {
+                            return this.model.initiateOTR();
+                        }
+                    }
+                    if (_.contains([UNVERIFIED, VERIFIED], this.model.get('otr_status'))) {
+                        // Off-the-record encryption is active
+                        this.model.otr.sendMsg(text);
+                        this.model.trigger('showSentOTRMessage', text);
+                    } else {
+                        this._super.onMessageSubmitted.apply(this, arguments);
+                    }
+                },
+
+                onOTRStatusChanged: function () {
+                    this.renderToolbar().informOTRChange();
+                },
+
+                informOTRChange: function () {
+                    var data = this.model.toJSON();
+                    var msgs = [];
+                    if (data.otr_status === UNENCRYPTED) {
+                        msgs.push(__("Your messages are not encrypted anymore"));
+                    } else if (data.otr_status === UNVERIFIED) {
+                        msgs.push(__("Your messages are now encrypted but your contact's identity has not been verified."));
+                    } else if (data.otr_status === VERIFIED) {
+                        msgs.push(__("Your contact's identify has been verified."));
+                    } else if (data.otr_status === FINISHED) {
+                        msgs.push(__("Your contact has ended encryption on their end, you should do the same."));
+                    }
+                    return this.showHelpMessages(msgs, 'info', false);
+                },
+
+                showOTRError: function (msg) {
+                    if (msg === 'Message cannot be sent at this time.') {
+                        this.showHelpMessages(
+                            [__('Your message could not be sent')], 'error');
+                    } else if (msg === 'Received an unencrypted message.') {
+                        this.showHelpMessages(
+                            [__('We received an unencrypted message')], 'error');
+                    } else if (msg === 'Received an unreadable encrypted message.') {
+                        this.showHelpMessages(
+                            [__('We received an unreadable encrypted message')],
+                            'error');
+                    } else {
+                        this.showHelpMessages(['Encryption error occured: '+msg], 'error');
+                    }
+                    converse.log("OTR ERROR:"+msg);
+                },
+
+                startOTRFromToolbar: function (ev) {
+                    $(ev.target).parent().parent().slideUp();
+                    ev.stopPropagation();
+                    this.model.initiateOTR();
+                },
+
+                endOTR: function (ev) {
+                    if (typeof ev !== "undefined") {
+                        ev.preventDefault();
+                        ev.stopPropagation();
+                    }
+                    this.model.endOTR();
+                },
+
+                authOTR: function (ev) {
+                    var scheme = $(ev.target).data().scheme;
+                    var result, question, answer;
+                    if (scheme === 'fingerprint') {
+                        result = confirm(__('Here are the fingerprints, please confirm them with %1$s, outside of this chat.\n\nFingerprint for you, %2$s: %3$s\n\nFingerprint for %1$s: %4$s\n\nIf you have confirmed that the fingerprints match, click OK, otherwise click Cancel.', [
+                                this.model.get('fullname'),
+                                converse.xmppstatus.get('fullname')||converse.bare_jid,
+                                this.model.otr.priv.fingerprint(),
+                                this.model.otr.their_priv_pk.fingerprint()
+                            ]
+                        ));
+                        if (result === true) {
+                            this.model.save({'otr_status': VERIFIED});
+                        } else {
+                            this.model.save({'otr_status': UNVERIFIED});
+                        }
+                    } else if (scheme === 'smp') {
+                        alert(__('You will be prompted to provide a security question and then an answer to that question.\n\nYour contact will then be prompted the same question and if they type the exact same answer (case sensitive), their identity will be verified.'));
+                        question = prompt(__('What is your security question?'));
+                        if (question) {
+                            answer = prompt(__('What is the answer to the security question?'));
+                            this.model.otr.smpSecret(answer, question);
+                        }
+                    } else {
+                        this.showHelpMessages([__('Invalid authentication scheme provided')], 'error');
+                    }
+                },
+
+                toggleOTRMenu: function (ev) {
+                    ev.stopPropagation();
+                    this.$el.find('.toggle-otr ul').slideToggle(200);
+                },
+                
+                getOTRTooltip: function () {
+                    var data = this.model.toJSON();
+                    if (data.otr_status === UNENCRYPTED) {
+                        return __('Your messages are not encrypted. Click here to enable OTR encryption.');
+                    } else if (data.otr_status === UNVERIFIED) {
+                        return __('Your messages are encrypted, but your contact has not been verified.');
+                    } else if (data.otr_status === VERIFIED) {
+                        return __('Your messages are encrypted and your contact verified.');
+                    } else if (data.otr_status === FINISHED) {
+                        return __('Your contact has closed their end of the private session, you should do the same');
+                    }
+                },
+
+                renderToolbar: function (options) {
+                    if (!converse.show_toolbar) {
+                        return;
+                    }
+                    var data = this.model.toJSON();
+                    options = _.extend(options || {}, {
+                        FINISHED: FINISHED,
+                        UNENCRYPTED: UNENCRYPTED,
+                        UNVERIFIED: UNVERIFIED,
+                        VERIFIED: VERIFIED,
+                        // Leaky abstraction
+                        allow_otr: converse.allow_otr && !this.is_chatroom,
+                        label_end_encrypted_conversation: __('End encrypted conversation'),
+                        label_refresh_encrypted_conversation: __('Refresh encrypted conversation'),
+                        label_start_encrypted_conversation: __('Start encrypted conversation'),
+                        label_verify_with_fingerprints: __('Verify with fingerprints'),
+                        label_verify_with_smp: __('Verify with SMP'),
+                        label_whats_this: __("What\'s this?"),
+                        otr_status_class: OTR_CLASS_MAPPING[data.otr_status],
+                        otr_tooltip: this.getOTRTooltip(),
+                        otr_translated_status: OTR_TRANSLATED_MAPPING[data.otr_status],
+                    });
+                    this._super.renderToolbar.call(this, options);
+                    return this;
+                }
+            },
+
+            MinimizedChatBoxView: {
+                initialize: function () {
+                    this._super.initialize.apply(this, arguments);
+                    this.model.on('showReceivedOTRMessage', this.updateUnreadMessagesCounter, this);
+                    this.model.on('showSentOTRMessage', this.updateUnreadMessagesCounter, this);
+                },
+            }
+        },
+
+        initialize: function () {
+            /* The initialize function gets called as soon as the plugin is
+             * loaded by converse.js's plugin machinery.
+             */
+            var converse = this.converse;
+            // Configuration values for this plugin
+            var settings = {
+                allow_otr: true,
+                cache_otr_key: false,
+                use_otr_by_default: false
+            };
+            _.extend(converse, settings);
+            _.extend(converse, _.pick(converse.user_settings, Object.keys(settings)));
+
+            // Only allow OTR if we have the capability
+            converse.allow_otr = converse.allow_otr && HAS_CRYPTO;
+            // Only use OTR by default if allow OTR is enabled to begin with
+            converse.use_otr_by_default = converse.use_otr_by_default && converse.allow_otr;
+
+            // Backbone Models and Views
+            // -------------------------
+            converse.OTR = Backbone.Model.extend({
+                // A model for managing OTR settings.
+                getSessionPassphrase: function () {
+                    if (converse.authentication === 'prebind') {
+                        var key = b64_sha1(converse.connection.jid),
+                            pass = window.sessionStorage[key];
+                        if (typeof pass === 'undefined') {
+                            pass = Math.floor(Math.random()*4294967295).toString();
+                            window.sessionStorage[key] = pass;
+                        }
+                        return pass;
+                    } else {
+                        return converse.connection.pass;
+                    }
+                },
+
+                generatePrivateKey: function (instance_tag) {
+                    var key = new otr.DSA();
+                    var jid = converse.connection.jid;
+                    if (converse.cache_otr_key) {
+                        var cipher = CryptoJS.lib.PasswordBasedCipher;
+                        var pass = this.getSessionPassphrase();
+                        if (typeof pass !== "undefined") {
+                            // Encrypt the key and set in sessionStorage. Also store instance tag.
+                            window.sessionStorage[b64_sha1(jid+'priv_key')] =
+                                cipher.encrypt(CryptoJS.algo.AES, key.packPrivate(), pass).toString();
+                            window.sessionStorage[b64_sha1(jid+'instance_tag')] = instance_tag;
+                            window.sessionStorage[b64_sha1(jid+'pass_check')] =
+                                cipher.encrypt(CryptoJS.algo.AES, 'match', pass).toString();
+                        }
+                    }
+                    return key;
+                }
+            });
+        }
+    });
+}));

+ 1 - 3
src/deps-full.js

@@ -3,7 +3,6 @@ define("converse-dependencies", [
     "underscore",
     "underscore",
     "polyfill",
     "polyfill",
     "utils",
     "utils",
-    "otr",
     "moment_with_locales",
     "moment_with_locales",
     "strophe",
     "strophe",
     "strophe.disco",
     "strophe.disco",
@@ -14,11 +13,10 @@ define("converse-dependencies", [
     "backbone.overview",
     "backbone.overview",
     "jquery.browser",
     "jquery.browser",
     "typeahead"
     "typeahead"
-], function($, _, dummy, utils, otr, moment, Strophe) {
+], function($, _, dummy, utils, moment, Strophe) {
     return _.extend({
     return _.extend({
         'underscore': _,
         'underscore': _,
         'jQuery': $,
         'jQuery': $,
-        'otr': otr,
         'moment': moment,
         'moment': moment,
         'utils': utils
         'utils': utils
     }, Strophe);
     }, Strophe);

+ 31 - 0
src/templates/toolbar_otr.html

@@ -0,0 +1,31 @@
+{[ if (allow_otr)  { ]}
+    <li class="toggle-otr {{otr_status_class}}" title="{{otr_tooltip}}">
+        <span class="chat-toolbar-text">{{otr_translated_status}}</span>
+        {[ if (otr_status == UNENCRYPTED) { ]}
+            <span class="icon-unlocked"></span>
+        {[ } ]}
+        {[ if (otr_status == UNVERIFIED) { ]}
+            <span class="icon-lock"></span>
+        {[ } ]}
+        {[ if (otr_status == VERIFIED) { ]}
+            <span class="icon-lock"></span>
+        {[ } ]}
+        {[ if (otr_status == FINISHED) { ]}
+            <span class="icon-unlocked"></span>
+        {[ } ]}
+        <ul>
+            {[ if (otr_status == UNENCRYPTED) { ]}
+               <li><a class="start-otr" href="#">{{label_start_encrypted_conversation}}</a></li>
+            {[ } ]}
+            {[ if (otr_status != UNENCRYPTED) { ]}
+               <li><a class="start-otr" href="#">{{label_refresh_encrypted_conversation}}</a></li>
+               <li><a class="end-otr" href="#">{{label_end_encrypted_conversation}}</a></li>
+               <li><a class="auth-otr" data-scheme="smp" href="#">{{label_verify_with_smp}}</a></li>
+            {[ } ]}
+            {[ if (otr_status == UNVERIFIED) { ]}
+               <li><a class="auth-otr" data-scheme="fingerprint" href="#">{{label_verify_with_fingerprints}}</a></li>
+            {[ } ]}
+            <li><a href="http://www.cypherpunks.ca/otr/help/3.2.0/levels.php" target="_blank">{{label_whats_this}}</a></li>
+        </ul>
+    </li>
+{[ } ]}