소스 검색

Handle errors when sending encrypted groupchat messages

updates #1180
JC Brand 6 년 전
부모
커밋
9aca32ad97
3개의 변경된 파일237개의 추가작업 그리고 46개의 파일을 삭제
  1. 57 24
      dist/converse.js
  2. 128 0
      spec/omemo.js
  3. 52 22
      src/converse-omemo.js

+ 57 - 24
dist/converse.js

@@ -55980,6 +55980,15 @@ const KEY_ALGO = {
   'length': 128
 };
 
+class IQError extends Error {
+  constructor(message, iq) {
+    super(message, iq);
+    this.name = 'IQError';
+    this.iq = iq;
+  }
+
+}
+
 function parseBundle(bundle_el) {
   /* Given an XML element representing a user's OMEMO bundle, parse it
    * and return a map.
@@ -56011,7 +56020,7 @@ _converse_headless_converse_core__WEBPACK_IMPORTED_MODULE_0__["default"].plugins
     return !_.isNil(window.libsignal) && !f.includes('converse-omemo', _converse.blacklisted_plugins);
   },
 
-  dependencies: ["converse-chatview"],
+  dependencies: ["converse-chatview", "converse-pubsub"],
   overrides: {
     ProfileModal: {
       events: {
@@ -56267,6 +56276,31 @@ _converse_headless_converse_core__WEBPACK_IMPORTED_MODULE_0__["default"].plugins
         }));
       },
 
+      handleMessageSendError(e) {
+        const _converse = this.__super__._converse,
+              __ = _converse.__;
+
+        if (e.name === 'IQError') {
+          this.save('omemo_supported', false);
+          const err_msgs = [];
+
+          if (sizzle(`presence-subscription-required[xmlns="${Strophe.NS.PUBSUB_ERROR}"]`, e.iq).length) {
+            err_msgs.push(__("Sorry, we're unable to send an encrypted message because %1$s " + "requires you to be subscribed to their presence in order to see their OMEMO information", e.iq.getAttribute('from')));
+          } else if (sizzle(`remote-server-not-found[xmlns="urn:ietf:params:xml:ns:xmpp-stanzas"]`, e.iq).length) {
+            err_msgs.push(__("Sorry, we're unable to send an encrypted message because the remote server for %1$s could not be found", e.iq.getAttribute('from')));
+          } else {
+            err_msgs.push(__("Unable to send an encrypted message due to an unexpected error."));
+            err_msgs.push(e.iq.outerHTML);
+          }
+
+          _converse.api.alert.show(Strophe.LogLevel.ERROR, __('Error'), err_msgs);
+
+          _converse.log(e, Strophe.LogLevel.ERROR);
+        } else {
+          throw e;
+        }
+      },
+
       async sendMessage(attrs) {
         const _converse = this.__super__._converse,
               __ = _converse.__;
@@ -56274,19 +56308,13 @@ _converse_headless_converse_core__WEBPACK_IMPORTED_MODULE_0__["default"].plugins
         if (this.get('omemo_active') && attrs.message) {
           attrs['is_encrypted'] = true;
           attrs['plaintext'] = attrs.message;
-          const devices = await _converse.getBundlesAndBuildSessions(this);
-          const stanza = await _converse.createOMEMOMessageStanza(this, this.messages.create(attrs), devices);
 
           try {
+            const devices = await _converse.getBundlesAndBuildSessions(this);
+            const stanza = await _converse.createOMEMOMessageStanza(this, this.messages.create(attrs), devices);
             this.sendMessageStanza(stanza);
           } catch (e) {
-            this.messages.create({
-              'message': __("Sorry, could not send the message due to an error.") + ` ${e.message}`,
-              'type': 'error'
-            });
-
-            _converse.log(e, Strophe.LogLevel.ERROR);
-
+            this.handleMessageSendError(e);
             return false;
           }
 
@@ -56770,7 +56798,7 @@ _converse_headless_converse_core__WEBPACK_IMPORTED_MODULE_0__["default"].plugins
         return _converse.api.pubsub.publish(null, node, item, options);
       },
 
-      generateMissingPreKeys() {
+      async generateMissingPreKeys() {
         const current_keys = this.getPreKeys(),
               missing_keys = _.difference(_.invokeMap(_.range(0, _converse.NUM_PREKEYS), Number.prototype.toString), _.keys(current_keys));
 
@@ -56780,20 +56808,21 @@ _converse_headless_converse_core__WEBPACK_IMPORTED_MODULE_0__["default"].plugins
           return Promise.resolve();
         }
 
-        return Promise.all(_.map(missing_keys, id => libsignal.KeyHelper.generatePreKey(parseInt(id, 10)))).then(keys => {
-          _.forEach(keys, k => this.storePreKey(k.keyId, k.keyPair));
+        const keys = await Promise.all(_.map(missing_keys, id => libsignal.KeyHelper.generatePreKey(parseInt(id, 10))));
 
-          const marshalled_keys = _.map(this.getPreKeys(), k => ({
-            'id': k.keyId,
-            'key': u.arrayBufferToBase64(k.pubKey)
-          })),
-                devicelist = _converse.devicelists.get(_converse.bare_jid),
-                device = devicelist.devices.get(this.get('device_id'));
+        _.forEach(keys, k => this.storePreKey(k.keyId, k.keyPair));
 
-          return device.getBundle().then(bundle => device.save('bundle', _.extend(bundle, {
-            'prekeys': marshalled_keys
-          })));
-        });
+        const marshalled_keys = _.map(this.getPreKeys(), k => ({
+          'id': k.keyId,
+          'key': u.arrayBufferToBase64(k.pubKey)
+        })),
+              devicelist = _converse.devicelists.get(_converse.bare_jid),
+              device = devicelist.devices.get(this.get('device_id'));
+
+        const bundle = await device.getBundle();
+        device.save('bundle', _.extend(bundle, {
+          'prekeys': marshalled_keys
+        }));
       },
 
       async generateBundle() {
@@ -56892,7 +56921,11 @@ _converse_headless_converse_core__WEBPACK_IMPORTED_MODULE_0__["default"].plugins
         try {
           iq = await _converse.api.sendIQ(stanza);
         } catch (iq) {
-          return _converse.log(iq.outerHTML, Strophe.LogLevel.ERROR);
+          throw new IQError("Could not fetch bundle", iq);
+        }
+
+        if (iq.querySelector('error')) {
+          throw new IQError("Could not fetch bundle", iq);
         }
 
         const publish_el = sizzle(`items[node="${Strophe.NS.OMEMO_BUNDLES}:${this.get('id')}"]`, iq).pop(),

+ 128 - 0
spec/omemo.js

@@ -371,6 +371,134 @@
             done();
         }));
 
+        it("gracefully handles auth errors when trying to send encrypted groupchat messages",
+            mock.initConverseWithPromises(
+                null, ['rosterGroupsFetched', 'chatBoxesFetched'], {},
+                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 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));
+
+            const toolbar = view.el.querySelector('.chat-toolbar');
+            const toggle = toolbar.querySelector('.toggle-omemo');
+            toggle.click();
+            expect(view.model.get('omemo_active')).toBe(true);
+            expect(view.model.get('omemo_supported')).toBe(true);
+
+            const textarea = view.el.querySelector('.chat-textarea');
+            textarea.value = 'This message will be encrypted';
+            view.keyPressed({
+                target: textarea,
+                preventDefault: _.noop,
+                keyCode: 13 // Enter
+            });
+            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()
+
+            _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(1);
+            expect(devicelist.devices.at(0).get('id')).toBe('4e30f35051b7b8b42abe083742187228');
+
+            iq_stanza = await test_utils.waitUntil(() => bundleFetched(_converse, _converse.bare_jid, '482886413b977930064a5888b92134fe'));
+            stanza = $iq({
+                'from': _converse.bare_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.bundles:482886413b977930064a5888b92134fe"})
+                    .c('item')
+                        .c('bundle', {'xmlns': 'eu.siacs.conversations.axolotl'})
+                            .c('signedPreKeyPublic', {'signedPreKeyId': '4223'}).t(btoa('100000')).up()
+                            .c('signedPreKeySignature').t(btoa('200000')).up()
+                            .c('identityKey').t(btoa('300000')).up()
+                            .c('prekeys')
+                                .c('preKeyPublic', {'preKeyId': '1'}).t(btoa('1991')).up()
+                                .c('preKeyPublic', {'preKeyId': '2'}).t(btoa('1992')).up()
+                                .c('preKeyPublic', {'preKeyId': '3'}).t(btoa('1993'));
+            iq_stanza = await test_utils.waitUntil(() => bundleFetched(_converse, contact_jid, '4e30f35051b7b8b42abe083742187228'));
+
+            /* <iq xmlns="jabber:client" to="jc@opkode.com/converse.js-34183907" type="error" id="945c8ab3-b561-4d8a-92da-77c226bb1689:sendIQ" from="joris@konuro.net">
+             *     <pubsub xmlns="http://jabber.org/protocol/pubsub">
+             *         <items node="eu.siacs.conversations.axolotl.bundles:7580"/>
+             *     </pubsub>
+             *     <error code="401" type="auth">
+             *         <presence-subscription-required xmlns="http://jabber.org/protocol/pubsub#errors"/>
+             *         <not-authorized xmlns="urn:ietf:params:xml:ns:xmpp-stanzas"/>
+             *     </error>
+             * </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.bundles:4e30f35051b7b8b42abe083742187228"}).up().up()
+            .c('error', {'code': '401', 'type': 'auth'})
+                .c('presence-subscription-required', {'xmlns':"http://jabber.org/protocol/pubsub#errors" }).up()
+                .c('not-authorized', {'xmlns': "urn:ietf:params:xml:ns:xmpp-stanzas"});
+            _converse.connection._dataRecv(test_utils.createRequest(stanza));
+
+            await test_utils.waitUntil(() => document.querySelectorAll('.alert-danger').length, 2000);
+            const header = document.querySelector('.alert-danger .modal-title');
+            expect(header.textContent).toBe("Error");
+            expect(u.ancestor(header, '.modal-content').querySelector('.modal-body p').textContent.trim())
+                .toBe("Sorry, we're unable to send an encrypted message because newguy@localhost requires you "+
+                      "to be subscribed to their presence in order to see their OMEMO information");
+
+            expect(view.model.get('omemo_supported')).toBe(false);
+            expect(view.el.querySelector('.chat-textarea').value).toBe('This message will be encrypted');
+            done();
+        }));
+
         it("can receive a PreKeySignalMessage",
             mock.initConverseWithPromises(
                 null, ['rosterGroupsFetched', 'chatBoxesFetched'], {},

+ 52 - 22
src/converse-omemo.js

@@ -27,6 +27,15 @@ const KEY_ALGO = {
 };
 
 
+class IQError extends Error {
+    constructor (message, iq) {
+        super(message, iq);
+        this.name = 'IQError';
+        this.iq = iq;
+    }
+}
+
+
 function parseBundle (bundle_el) {
     /* Given an XML element representing a user's OMEMO bundle, parse it
      * and return a map.
@@ -61,7 +70,7 @@ converse.plugins.add('converse-omemo', {
         return !_.isNil(window.libsignal) && !f.includes('converse-omemo', _converse.blacklisted_plugins);
     },
 
-    dependencies: ["converse-chatview"],
+    dependencies: ["converse-chatview", "converse-pubsub"],
 
     overrides: {
 
@@ -309,6 +318,31 @@ converse.plugins.add('converse-omemo', {
                     .then(payload => ({'payload': payload, 'device': device}));
             },
 
+            handleMessageSendError (e) {
+                const { _converse } = this.__super__,
+                      { __ } = _converse;
+                if (e.name === 'IQError') {
+                    this.save('omemo_supported', false);
+
+                    const err_msgs = [];
+                    if (sizzle(`presence-subscription-required[xmlns="${Strophe.NS.PUBSUB_ERROR}"]`, e.iq).length) {
+                        err_msgs.push(__("Sorry, we're unable to send an encrypted message because %1$s "+
+                                        "requires you to be subscribed to their presence in order to see their OMEMO information",
+                                        e.iq.getAttribute('from')));
+                    } else if (sizzle(`remote-server-not-found[xmlns="urn:ietf:params:xml:ns:xmpp-stanzas"]`, e.iq).length) {
+                        err_msgs.push(__("Sorry, we're unable to send an encrypted message because the remote server for %1$s could not be found",
+                                        e.iq.getAttribute('from')));
+                    } else {
+                        err_msgs.push(__("Unable to send an encrypted message due to an unexpected error."));
+                        err_msgs.push(e.iq.outerHTML);
+                    }
+                    _converse.api.alert.show(Strophe.LogLevel.ERROR, __('Error'), err_msgs);
+                    _converse.log(e, Strophe.LogLevel.ERROR);
+                } else {
+                    throw e;
+                }
+            },
+
             async sendMessage (attrs) {
                 const { _converse } = this.__super__,
                       { __ } = _converse;
@@ -316,16 +350,12 @@ converse.plugins.add('converse-omemo', {
                 if (this.get('omemo_active') && attrs.message) {
                     attrs['is_encrypted'] = true;
                     attrs['plaintext'] = attrs.message;
-                    const devices = await _converse.getBundlesAndBuildSessions(this);
-                    const stanza = await _converse.createOMEMOMessageStanza(this, this.messages.create(attrs), devices);
                     try {
+                        const devices = await _converse.getBundlesAndBuildSessions(this);
+                        const stanza = await _converse.createOMEMOMessageStanza(this, this.messages.create(attrs), devices);
                         this.sendMessageStanza(stanza);
                     } catch (e) {
-                        this.messages.create({
-                            'message': __("Sorry, could not send the message due to an error.") + ` ${e.message}`,
-                            'type': 'error',
-                        });
-                        _converse.log(e, Strophe.LogLevel.ERROR);
+                        this.handleMessageSendError(e);
                         return false;
                     }
                     return true;
@@ -346,7 +376,6 @@ converse.plugins.add('converse-omemo', {
                 this.model.on('change:omemo_supported', this.onOMEMOSupportedDetermined, this);
             },
 
-
             showMessage (message) {
                 // We don't show a message if it's only keying material
                 if (!message.get('is_only_key')) {
@@ -768,7 +797,7 @@ converse.plugins.add('converse-omemo', {
                 return _converse.api.pubsub.publish(null, node, item, options);
             },
 
-            generateMissingPreKeys () {
+            async generateMissingPreKeys () {
                 const current_keys = this.getPreKeys(),
                       missing_keys = _.difference(_.invokeMap(_.range(0, _converse.NUM_PREKEYS), Number.prototype.toString), _.keys(current_keys));
 
@@ -776,16 +805,14 @@ converse.plugins.add('converse-omemo', {
                     _converse.log("No missing prekeys to generate for our own device", Strophe.LogLevel.WARN);
                     return Promise.resolve();
                 }
-                return Promise.all(_.map(missing_keys, id => libsignal.KeyHelper.generatePreKey(parseInt(id, 10))))
-                    .then(keys => {
-                        _.forEach(keys, k => this.storePreKey(k.keyId, k.keyPair));
-                        const marshalled_keys = _.map(this.getPreKeys(), k => ({'id': k.keyId, 'key': u.arrayBufferToBase64(k.pubKey)})),
-                              devicelist = _converse.devicelists.get(_converse.bare_jid),
-                              device = devicelist.devices.get(this.get('device_id'));
-
-                        return device.getBundle()
-                            .then(bundle => device.save('bundle', _.extend(bundle, {'prekeys': marshalled_keys})));
-                    });
+                const keys = await Promise.all(_.map(missing_keys, id => libsignal.KeyHelper.generatePreKey(parseInt(id, 10))));
+                _.forEach(keys, k => this.storePreKey(k.keyId, k.keyPair));
+                const marshalled_keys = _.map(this.getPreKeys(), k => ({'id': k.keyId, 'key': u.arrayBufferToBase64(k.pubKey)})),
+                        devicelist = _converse.devicelists.get(_converse.bare_jid),
+                        device = devicelist.devices.get(this.get('device_id'));
+
+                const bundle = await device.getBundle();
+                device.save('bundle', _.extend(bundle, {'prekeys': marshalled_keys}));
             },
 
             async generateBundle () {
@@ -871,8 +898,11 @@ converse.plugins.add('converse-omemo', {
                 let iq;
                 try {
                     iq = await _converse.api.sendIQ(stanza)
-                } catch(iq) {
-                    return _converse.log(iq.outerHTML, Strophe.LogLevel.ERROR);
+                } catch (iq) {
+                    throw new IQError("Could not fetch bundle", iq);
+                }
+                if (iq.querySelector('error')) {
+                    throw new IQError("Could not fetch bundle", iq);
                 }
                 const publish_el = sizzle(`items[node="${Strophe.NS.OMEMO_BUNDLES}:${this.get('id')}"]`, iq).pop(),
                         bundle_el = sizzle(`bundle[xmlns="${Strophe.NS.OMEMO}"]`, publish_el).pop(),