Przeglądaj źródła

Render the OMEMO lock icon in MUC toolbars as well

updates #1180
JC Brand 6 lat temu
rodzic
commit
f64fdb8088
4 zmienionych plików z 358 dodań i 25 usunięć
  1. 89 9
      dist/converse.js
  2. 183 7
      spec/omemo.js
  3. 69 9
      src/converse-omemo.js
  4. 17 0
      src/headless/converse-muc.js

+ 89 - 9
dist/converse.js

@@ -56433,13 +56433,6 @@ _converse_headless_converse_core__WEBPACK_IMPORTED_MODULE_0__["default"].plugins
 
         this.model.on('change:omemo_active', this.renderOMEMOToolbarButton, this);
         this.model.on('change:omemo_supported', this.onOMEMOSupportedDetermined, this);
-        this.checkOMEMOSupported();
-      },
-
-      async checkOMEMOSupported() {
-        const _converse = this.__super__._converse;
-        const supported = await _converse.contactHasOMEMOSupport(this.model.get('jid'));
-        this.model.set('omemo_supported', supported);
       },
 
       showMessage(message) {
@@ -56486,6 +56479,33 @@ _converse_headless_converse_core__WEBPACK_IMPORTED_MODULE_0__["default"].plugins
         });
       }
 
+    },
+    ChatRoomView: {
+      events: {
+        'click .toggle-omemo': 'toggleOMEMO'
+      },
+
+      initialize() {
+        this.__super__.initialize.apply(this, arguments);
+
+        this.model.on('change:omemo_active', this.renderOMEMOToolbarButton, this);
+        this.model.on('change:omemo_supported', this.onOMEMOSupportedDetermined, this);
+      },
+
+      toggleOMEMO(ev) {
+        const _converse = this.__super__._converse,
+              __ = _converse.__;
+
+        if (!this.model.get('omemo_supported')) {
+          return _converse.api.alert.show(Strophe.LogLevel.ERROR, __('Error'), [__('Cannot use end-to-end encryption in this groupchat, ' + 'either the groupchat has some anonymity or not all participants support OMEMO.')]);
+        }
+
+        ev.preventDefault();
+        this.model.save({
+          'omemo_active': !this.model.get('omemo_active')
+        });
+      }
+
     }
   },
 
@@ -56493,7 +56513,8 @@ _converse_headless_converse_core__WEBPACK_IMPORTED_MODULE_0__["default"].plugins
     /* The initialize function gets called as soon as the plugin is
      * loaded by Converse.js's plugin machinery.
      */
-    const _converse = this._converse;
+    const _converse = this._converse,
+          __ = _converse.__;
 
     _converse.api.promises.add(['OMEMOInitialized']);
 
@@ -57134,6 +57155,49 @@ _converse_headless_converse_core__WEBPACK_IMPORTED_MODULE_0__["default"].plugins
       fetchOwnDevices().then(() => restoreOMEMOSession()).then(() => _converse.omemo_store.publishBundle()).then(() => _converse.emit('OMEMOInitialized')).catch(_.partial(_converse.log, _, Strophe.LogLevel.ERROR));
     }
 
+    async function onOccupantAdded(chatroom, occupant) {
+      if (occupant.isSelf() || !chatroom.get('nonanonymous')) {
+        return;
+      }
+
+      if (chatroom.get('omemo_active')) {
+        const supported = await _converse.contactHasOMEMOSupport(occupant.get('jid'));
+
+        if (!supported) {
+          chatroom.messages.create({
+            'message': __("%1$s doesn't appear to have a client that supports OMEMO. " + "Encrypted chat will no longer be possible in this grouchat.", occupant.get('nick')),
+            'type': 'error'
+          });
+          chatroom.save({
+            'omemo_active': false,
+            'omemo_supported': false
+          });
+        }
+      }
+    }
+
+    async function checkOMEMOSupported(chatbox) {
+      let supported;
+
+      if (chatbox.get('type') === _converse.CHATROOMS_TYPE) {
+        supported = chatbox.get('nonanonymous') && chatbox.get('membersonly');
+      } else if (chatbox.get('type') === _converse.PRIVATE_CHAT_TYPE) {
+        supported = await _converse.contactHasOMEMOSupport(chatbox.get('jid'));
+      }
+
+      chatbox.set('omemo_supported', supported);
+    }
+
+    _converse.api.waitUntil('chatBoxesInitialized').then(() => _converse.chatboxes.on('add', chatbox => {
+      checkOMEMOSupported(chatbox);
+
+      if (chatbox.get('type') === _converse.CHATROOMS_TYPE) {
+        chatbox.occupants.on('add', o => onOccupantAdded(chatbox, o));
+        chatbox.on('change:nonanonymous', checkOMEMOSupported);
+        chatbox.on('change:membersonly', checkOMEMOSupported);
+      }
+    }));
+
     _converse.api.listen.on('afterTearDown', () => {
       if (_converse.devicelists) {
         _converse.devicelists.reset();
@@ -65885,7 +65949,23 @@ Strophe.addNamespace('MUC_REGISTER', "jabber:iq:register");
 Strophe.addNamespace('MUC_ROOMCONF', Strophe.NS.MUC + "#roomconfig");
 Strophe.addNamespace('MUC_USER', Strophe.NS.MUC + "#user");
 _converse_core__WEBPACK_IMPORTED_MODULE_6__["default"].MUC_NICK_CHANGED_CODE = "303";
-_converse_core__WEBPACK_IMPORTED_MODULE_6__["default"].ROOM_FEATURES = ['passwordprotected', 'unsecured', 'hidden', 'publicroom', 'membersonly', 'open', 'persistent', 'temporary', 'nonanonymous', 'semianonymous', 'moderated', 'unmoderated', 'mam_enabled'];
+_converse_core__WEBPACK_IMPORTED_MODULE_6__["default"].ROOM_FEATURES = ['passwordprotected', 'unsecured', 'hidden', 'publicroom', 'membersonly', 'open', 'persistent', 'temporary', 'nonanonymous', 'semianonymous', 'moderated', 'unmoderated', 'mam_enabled']; // No longer used in code, but useful as reference.
+//
+// const ROOM_FEATURES_MAP = {
+//     'passwordprotected': 'unsecured',
+//     'unsecured': 'passwordprotected',
+//     'hidden': 'publicroom',
+//     'publicroom': 'hidden',
+//     'membersonly': 'open',
+//     'open': 'membersonly',
+//     'persistent': 'temporary',
+//     'temporary': 'persistent',
+//     'nonanonymous': 'semianonymous',
+//     'semianonymous': 'nonanonymous',
+//     'moderated': 'unmoderated',
+//     'unmoderated': 'moderated'
+// };
+
 _converse_core__WEBPACK_IMPORTED_MODULE_6__["default"].ROOMSTATUS = {
   CONNECTED: 0,
   CONNECTING: 1,

+ 183 - 7
spec/omemo.js

@@ -1,12 +1,8 @@
 (function (root, factory) {
     define(["jasmine", "mock", "test-utils"], factory);
 } (this, function (jasmine, mock, test_utils) {
-    var Strophe = converse.env.Strophe;
-    var b64_sha1 = converse.env.b64_sha1;
-    var $iq = converse.env.$iq;
-    var $msg = converse.env.$msg;
-    var _ = converse.env._;
-    var u = converse.env.utils;
+    const { $iq, $pres, $msg, _, Strophe } = converse.env;
+    const u = converse.env.utils;
 
 
     function deviceListFetched (_converse, jid) {
@@ -229,7 +225,6 @@
             done();
         }));
 
-
         it("can receive a PreKeySignalMessage",
             mock.initConverseWithPromises(
                 null, ['rosterGroupsFetched', 'chatBoxesFetched'], {},
@@ -824,6 +819,187 @@
             done();
         }));
 
+        it("adds a toolbar button for starting an encrypted groupchat session",
+            mock.initConverseWithPromises(
+                null, ['rosterGroupsFetched', 'chatBoxesFetched'], {'view_mode': 'fullscreen'},
+                async function (done, _converse) {
+
+            // MEMO encryption works only in members-only conferences that are non-anonymous. 
+            const features = [
+                'http://jabber.org/protocol/muc',
+                'jabber:iq:register',
+                'muc_passwordprotected',
+                'muc_hidden',
+                'muc_temporary',
+                'muc_membersonly',
+                'muc_unmoderated',
+                'muc_nonanonymous'
+            ];
+            await test_utils.openAndEnterChatRoom(_converse, 'lounge', 'localhost', 'dummy', features);
+            const view = _converse.chatboxviews.get('lounge@localhost');
+            await test_utils.waitUntil(() => initializedOMEMO(_converse));
+
+            const toolbar = view.el.querySelector('.chat-toolbar');
+            let toggle = toolbar.querySelector('.toggle-omemo');
+            expect(view.model.get('omemo_active')).toBe(undefined);
+            expect(_.isNull(toggle)).toBe(false);
+            expect(u.hasClass('fa-unlock', toggle)).toBe(true);
+            expect(u.hasClass('fa-lock', toggle)).toBe(false);
+            expect(u.hasClass('disabled', toggle)).toBe(false);
+            expect(view.model.get('omemo_supported')).toBe(true);
+
+            toggle.click();
+            toggle = toolbar.querySelector('.toggle-omemo');
+            expect(view.model.get('omemo_active')).toBe(true);
+            expect(u.hasClass('fa-unlock', toggle)).toBe(false);
+            expect(u.hasClass('fa-lock', toggle)).toBe(true);
+            expect(u.hasClass('disabled', toggle)).toBe(false);
+            expect(view.model.get('omemo_supported')).toBe(true);
+
+            let contact_jid = 'newguy@localhost';
+            let stanza = $pres({
+                    to: 'dummy@localhost/resource',
+                    from: 'lounge@localhost/newguy'
+                })
+                .c('x', {xmlns: Strophe.NS.MUC_USER})
+                .c('item', {
+                    'affiliation': 'none',
+                    'jid': 'newguy@localhost/_converse.js-290929789',
+                    'role': 'participant'
+                }).tree();
+            _converse.connection._dataRecv(test_utils.createRequest(stanza));
+
+            let iq_stanza = await test_utils.waitUntil(() => deviceListFetched(_converse, contact_jid));
+            expect(iq_stanza.toLocaleString()).toBe(
+                `<iq from="dummy@localhost" id="${iq_stanza.nodeTree.getAttribute("id")}" to="${contact_jid}" type="get" xmlns="jabber:client">`+
+                    `<pubsub xmlns="http://jabber.org/protocol/pubsub">`+
+                        `<items node="eu.siacs.conversations.axolotl.devicelist"/>`+
+                    `</pubsub>`+
+                `</iq>`);
+
+            stanza = $iq({
+                'from': contact_jid,
+                'id': iq_stanza.nodeTree.getAttribute('id'),
+                'to': _converse.bare_jid,
+                'type': 'result',
+            }).c('pubsub', {'xmlns': "http://jabber.org/protocol/pubsub"})
+                .c('items', {'node': "eu.siacs.conversations.axolotl.devicelist"})
+                    .c('item', {'xmlns': "http://jabber.org/protocol/pubsub"}) // TODO: must have an id attribute
+                        .c('list', {'xmlns': "eu.siacs.conversations.axolotl"})
+                            .c('device', {'id': '4e30f35051b7b8b42abe083742187228'}).up()
+                            .c('device', {'id': 'ae890ac52d0df67ed7cfdf51b644e901'});
+            _converse.connection._dataRecv(test_utils.createRequest(stanza));
+            await test_utils.waitUntil(() => _converse.omemo_store);
+            expect(_converse.devicelists.length).toBe(2);
+
+            const devicelist = _converse.devicelists.get(contact_jid);
+            expect(devicelist.devices.length).toBe(2);
+            expect(devicelist.devices.at(0).get('id')).toBe('4e30f35051b7b8b42abe083742187228');
+            expect(devicelist.devices.at(1).get('id')).toBe('ae890ac52d0df67ed7cfdf51b644e901');
+
+            expect(view.model.get('omemo_active')).toBe(true);
+            toggle = toolbar.querySelector('.toggle-omemo');
+            expect(_.isNull(toggle)).toBe(false);
+            expect(u.hasClass('fa-unlock', toggle)).toBe(false);
+            expect(u.hasClass('fa-lock', toggle)).toBe(true);
+            expect(u.hasClass('disabled', toggle)).toBe(false);
+            expect(view.model.get('omemo_supported')).toBe(true);
+
+            // Test that the button gets disabled when the room becomes
+            // anonymous or semi-anonymous
+            view.model.save({'nonanonymous': false, 'semianonymous': true});
+            await test_utils.waitUntil(() => !view.model.get('omemo_supported'));
+            toggle = toolbar.querySelector('.toggle-omemo');
+            expect(_.isNull(toggle)).toBe(false);
+            expect(u.hasClass('fa-unlock', toggle)).toBe(true);
+            expect(u.hasClass('fa-lock', toggle)).toBe(false);
+            expect(u.hasClass('disabled', toggle)).toBe(true);
+            expect(view.model.get('omemo_supported')).toBe(false);
+
+            view.model.save({'nonanonymous': true, 'semianonymous': false});
+            await test_utils.waitUntil(() => view.model.get('omemo_supported'));
+            toggle = toolbar.querySelector('.toggle-omemo');
+            expect(_.isNull(toggle)).toBe(false);
+            expect(u.hasClass('fa-unlock', toggle)).toBe(true);
+            expect(u.hasClass('fa-lock', toggle)).toBe(false);
+            expect(u.hasClass('disabled', toggle)).toBe(false);
+
+            // Test that the button gets disabled when the room becomes open
+            view.model.save({'membersonly': false, 'open': true});
+            await test_utils.waitUntil(() => !view.model.get('omemo_supported'));
+            toggle = toolbar.querySelector('.toggle-omemo');
+            expect(_.isNull(toggle)).toBe(false);
+            expect(u.hasClass('fa-unlock', toggle)).toBe(true);
+            expect(u.hasClass('fa-lock', toggle)).toBe(false);
+            expect(u.hasClass('disabled', toggle)).toBe(true);
+            expect(view.model.get('omemo_supported')).toBe(false);
+
+            view.model.save({'membersonly': true, 'open': false});
+            await test_utils.waitUntil(() => view.model.get('omemo_supported'));
+            toggle = toolbar.querySelector('.toggle-omemo');
+            expect(_.isNull(toggle)).toBe(false);
+            expect(u.hasClass('fa-unlock', toggle)).toBe(true);
+            expect(u.hasClass('fa-lock', toggle)).toBe(false);
+            expect(u.hasClass('disabled', toggle)).toBe(false);
+            expect(view.model.get('omemo_supported')).toBe(true);
+            expect(view.model.get('omemo_active')).toBe(false);
+
+            toggle.click();
+            expect(view.model.get('omemo_active')).toBe(true);
+
+            // Someone enters the room who doesn't have OMEMO support, while we
+            // have OMEMO activated...
+            contact_jid = 'oldguy@localhost';
+            stanza = $pres({
+                    to: 'dummy@localhost/resource',
+                    from: 'lounge@localhost/oldguy'
+                })
+                .c('x', {xmlns: Strophe.NS.MUC_USER})
+                .c('item', {
+                    'affiliation': 'none',
+                    'jid': `${contact_jid}/_converse.js-290929788`,
+                    'role': 'participant'
+                }).tree();
+            _converse.connection._dataRecv(test_utils.createRequest(stanza));
+            iq_stanza = await test_utils.waitUntil(() => deviceListFetched(_converse, contact_jid));
+            expect(iq_stanza.toLocaleString()).toBe(
+                `<iq from="dummy@localhost" id="${iq_stanza.nodeTree.getAttribute("id")}" to="${contact_jid}" type="get" xmlns="jabber:client">`+
+                    `<pubsub xmlns="http://jabber.org/protocol/pubsub">`+
+                        `<items node="eu.siacs.conversations.axolotl.devicelist"/>`+
+                    `</pubsub>`+
+                `</iq>`);
+
+            stanza = $iq({
+                'from': contact_jid,
+                'id': iq_stanza.nodeTree.getAttribute('id'),
+                'to': _converse.bare_jid,
+                'type': 'error'
+            }).c('error', {'type': 'cancel'})
+                .c('item-not-found', {'xmlns': "urn:ietf:params:xml:ns:xmpp-stanzas"});
+            _converse.connection._dataRecv(test_utils.createRequest(stanza));
+
+            await test_utils.waitUntil(() => !view.model.get('omemo_supported'));
+
+            expect(view.el.querySelector('.chat-error').textContent).toBe(
+                "oldguy doesn't appear to have a client that supports OMEMO. "+
+                "Encrypted chat will no longer be possible in this grouchat."
+            );
+
+            toggle = toolbar.querySelector('.toggle-omemo');
+            expect(_.isNull(toggle)).toBe(false);
+            expect(u.hasClass('fa-unlock', toggle)).toBe(true);
+            expect(u.hasClass('fa-lock', toggle)).toBe(false);
+            expect(u.hasClass('disabled', toggle)).toBe(true);
+
+            expect( _converse.chatboxviews.el.querySelector('.modal-body p')).toBe(null);
+            toggle.click();
+            const msg = _converse.chatboxviews.el.querySelector('.modal-body p');
+            expect(msg.textContent).toBe(
+                'Cannot use end-to-end encryption in this groupchat, '+
+                'either the groupchat has some anonymity or not all participants support OMEMO.');
+            done();
+        }));
+
 
         it("shows OMEMO device fingerprints in the user details modal",
             mock.initConverseWithPromises(

+ 69 - 9
src/converse-omemo.js

@@ -87,7 +87,7 @@ converse.plugins.add('converse-omemo', {
             beforeRender () {
                 const { _converse } = this.__super__,
                       device_id = _converse.omemo_store.get('device_id');
-                
+
                 if (device_id) {
                     this.current_device = this.devicelist.devices.get(device_id);
                 }
@@ -458,14 +458,8 @@ converse.plugins.add('converse-omemo', {
                 this.__super__.initialize.apply(this, arguments);
                 this.model.on('change:omemo_active', this.renderOMEMOToolbarButton, this);
                 this.model.on('change:omemo_supported', this.onOMEMOSupportedDetermined, this);
-                this.checkOMEMOSupported();
             },
 
-            async checkOMEMOSupported () {
-                const { _converse } = this.__super__;
-                const supported = await _converse.contactHasOMEMOSupport(this.model.get('jid'));
-                this.model.set('omemo_supported', supported);
-            },
 
             showMessage (message) {
                 // We don't show a message if it's only keying material
@@ -503,12 +497,38 @@ converse.plugins.add('converse-omemo', {
                         __('Error'),
                         [__("Cannot use end-to-end encryption because %1$s uses a client that doesn't support OMEMO.",
                             this.model.contact.getDisplayName()
-                           )] 
+                           )]
                     )
                 }
                 ev.preventDefault();
                 this.model.save({'omemo_active': !this.model.get('omemo_active')});
             }
+        },
+
+        ChatRoomView: {
+            events: {
+                'click .toggle-omemo': 'toggleOMEMO'
+            },
+
+            initialize () {
+                this.__super__.initialize.apply(this, arguments);
+                this.model.on('change:omemo_active', this.renderOMEMOToolbarButton, this);
+                this.model.on('change:omemo_supported', this.onOMEMOSupportedDetermined, this);
+            },
+
+            toggleOMEMO (ev) {
+                const { _converse } = this.__super__, { __ } = _converse;
+                if (!this.model.get('omemo_supported')) {
+                    return _converse.api.alert.show(
+                        Strophe.LogLevel.ERROR,
+                        __('Error'),
+                        [__('Cannot use end-to-end encryption in this groupchat, '+
+                            'either the groupchat has some anonymity or not all participants support OMEMO.')]
+                    );
+                }
+                ev.preventDefault();
+                this.model.save({'omemo_active': !this.model.get('omemo_active')});
+            }
         }
     },
 
@@ -516,7 +536,8 @@ converse.plugins.add('converse-omemo', {
         /* The initialize function gets called as soon as the plugin is
          * loaded by Converse.js's plugin machinery.
          */
-        const { _converse } = this;
+        const { _converse } = this,
+              { __ } = _converse;
 
         _converse.api.promises.add(['OMEMOInitialized']);
 
@@ -1070,6 +1091,45 @@ converse.plugins.add('converse-omemo', {
                 .catch(_.partial(_converse.log, _, Strophe.LogLevel.ERROR));
         }
 
+        async function onOccupantAdded (chatroom, occupant) {
+            if (occupant.isSelf() || !chatroom.get('nonanonymous')) {
+                return;
+            }
+            if (chatroom.get('omemo_active')) {
+                const supported = await _converse.contactHasOMEMOSupport(occupant.get('jid'));
+                if (!supported) {
+                        chatroom.messages.create({
+                            'message': __("%1$s doesn't appear to have a client that supports OMEMO. " +
+                                          "Encrypted chat will no longer be possible in this grouchat.", occupant.get('nick')),
+                            'type': 'error'
+                        });
+                    chatroom.save({'omemo_active': false, 'omemo_supported': false});
+                }
+            }
+        }
+
+        async function checkOMEMOSupported (chatbox) {
+            let supported;
+            if (chatbox.get('type') === _converse.CHATROOMS_TYPE) {
+                supported = chatbox.get('nonanonymous') && chatbox.get('membersonly');
+            } else if (chatbox.get('type') === _converse.PRIVATE_CHAT_TYPE) {
+                supported = await _converse.contactHasOMEMOSupport(chatbox.get('jid'));
+            }
+            chatbox.set('omemo_supported', supported);
+        }
+
+        _converse.api.waitUntil('chatBoxesInitialized').then(() =>
+            _converse.chatboxes.on('add', chatbox => {
+                checkOMEMOSupported(chatbox);
+                if (chatbox.get('type') === _converse.CHATROOMS_TYPE) {
+                    chatbox.occupants.on('add', o => onOccupantAdded(chatbox, o));
+                    chatbox.on('change:nonanonymous', checkOMEMOSupported);
+                    chatbox.on('change:membersonly', checkOMEMOSupported);
+                }
+            })
+        );
+
+
         _converse.api.listen.on('afterTearDown', () => {
             if (_converse.devicelists) {
                 _converse.devicelists.reset();

+ 17 - 0
src/headless/converse-muc.js

@@ -38,6 +38,23 @@ converse.ROOM_FEATURES = [
     'moderated', 'unmoderated', 'mam_enabled'
 ];
 
+// No longer used in code, but useful as reference.
+//
+// const ROOM_FEATURES_MAP = {
+//     'passwordprotected': 'unsecured',
+//     'unsecured': 'passwordprotected',
+//     'hidden': 'publicroom',
+//     'publicroom': 'hidden',
+//     'membersonly': 'open',
+//     'open': 'membersonly',
+//     'persistent': 'temporary',
+//     'temporary': 'persistent',
+//     'nonanonymous': 'semianonymous',
+//     'semianonymous': 'nonanonymous',
+//     'moderated': 'unmoderated',
+//     'unmoderated': 'moderated'
+// };
+
 converse.ROOMSTATUS = {
     CONNECTED: 0,
     CONNECTING: 1,