Prechádzať zdrojové kódy

Updated so that own device is properly created and published

Based on live testing. Updated tests accordingly.
JC Brand 6 rokov pred
rodič
commit
fa0e7aeff5
6 zmenil súbory, kde vykonal 417 pridanie a 325 odobranie
  1. 173 175
      dist/converse.js
  2. 3 3
      spec/chatbox.js
  3. 147 41
      spec/omemo.js
  4. 5 5
      spec/presence.js
  5. 88 100
      src/converse-omemo.js
  6. 1 1
      tests/utils.js

+ 173 - 175
dist/converse.js

@@ -63067,9 +63067,7 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_
            *      that contains the message stanza, if it was
            *      contained, otherwise it's the message stanza itself.
            */
-          const _converse = this.__super__._converse,
-                __ = _converse.__,
-                archive = sizzle(`result[xmlns="${Strophe.NS.MAM}"]`, original_stanza).pop(),
+          const archive = sizzle(`result[xmlns="${Strophe.NS.MAM}"]`, original_stanza).pop(),
                 spoiler = sizzle(`spoiler[xmlns="${Strophe.NS.SPOILER}"]`, original_stanza).pop(),
                 delay = sizzle(`delay[xmlns="${Strophe.NS.DELAY}"]`, original_stanza).pop(),
                 chat_state = stanza.getElementsByTagName(_converse.COMPOSING).length && _converse.COMPOSING || stanza.getElementsByTagName(_converse.PAUSED).length && _converse.PAUSED || stanza.getElementsByTagName(_converse.INACTIVE).length && _converse.INACTIVE || stanza.getElementsByTagName(_converse.ACTIVE).length && _converse.ACTIVE || stanza.getElementsByTagName(_converse.GONE).length && _converse.GONE;
@@ -63775,9 +63773,11 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_
       emojione.ascii = true;
 
       function onWindowStateChanged(data) {
-        _converse.chatboxviews.each(function (chatboxview) {
-          chatboxview.onWindowStateChanged(data.state);
-        });
+        if (_converse.chatboxviews) {
+          _converse.chatboxviews.each(chatboxview => {
+            chatboxview.onWindowStateChanged(data.state);
+          });
+        }
       }
 
       _converse.api.listen.on('windowStateChanged', onWindowStateChanged);
@@ -66011,11 +66011,11 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_
 
       _converse.connection.reset();
 
-      _converse.off();
-
       _converse.stopListening();
 
       _converse.tearDown();
+
+      _converse.off();
     }
 
     if ('onpagehide' in window) {
@@ -69986,6 +69986,10 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_
       });
 
       _converse.api.listen.on('afterTearDown', () => {
+        if (!_converse.chatboxviews) {
+          return;
+        }
+
         const container = _converse.chatboxviews.el.querySelector("#converse-modals");
 
         if (container) {
@@ -74075,6 +74079,7 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_
           this.devicelist = _converse.devicelists.get(_converse.bare_jid);
           this.devicelist.devices.on('change:bundle', this.debouncedRender, this);
           this.devicelist.devices.on('reset', this.debouncedRender, this);
+          this.devicelist.devices.on('remove', this.debouncedRender, this);
           return this.__super__.initialize.apply(this, arguments);
         },
 
@@ -74131,6 +74136,8 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_
           });
           this.devicelist.devices.on('change:bundle', this.render, this);
           this.devicelist.devices.on('change:trusted', this.render, this);
+          this.devicelist.devices.on('remove', this.render, this);
+          this.devicelist.devices.on('reset', this.render, this);
           return this.__super__.initialize.apply(this, arguments);
         },
 
@@ -74162,21 +74169,30 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_
                 address = new libsignal.SignalProtocolAddress(device.get('jid'), device.get('id')),
                 sessionBuilder = new libsignal.SessionBuilder(_converse.omemo_store, address),
                 prekey = device.getRandomPreKey();
-          return sessionBuilder.processPreKey({
-            'registrationId': _converse.omemo_store.get('registration_id'),
-            'identityKey': _converse.omemo_store.get('identity_keypair'),
-            'signedPreKey': {
-              'keyId': bundle.signed_prekey.id,
-              // <Number>
-              'publicKey': u.base64ToArrayBuffer(bundle.signed_prekey.public_key),
-              'signature': u.base64ToArrayBuffer(bundle.signed_prekey.signature)
-            },
-            'preKey': {
-              'keyId': prekey.id,
-              // <Number>
-              'publicKey': u.base64ToArrayBuffer(prekey.key)
-            }
-          });
+
+          try {
+            return sessionBuilder.processPreKey({
+              'registrationId': parseInt(_converse.omemo_store.get('device_id'), 10),
+              'identityKey': _converse.omemo_store.get('identity_key'),
+              'signedPreKey': {
+                'keyId': bundle.signed_prekey.id,
+                // <Number>
+                'publicKey': u.base64ToArrayBuffer(bundle.signed_prekey.public_key),
+                'signature': u.base64ToArrayBuffer(bundle.signed_prekey.signature)
+              },
+              'preKey': {
+                'keyId': prekey.id,
+                // <Number>
+                'publicKey': u.base64ToArrayBuffer(prekey.key)
+              }
+            });
+          } catch (e) {
+            _converse.log(`Error: could not build session for device ${device.get('id')}`, Strophe.LogLevel.ERROR);
+
+            _converse.log(e.message, Strophe.LogLevel.ERROR);
+
+            return Promise.resolve();
+          }
         },
 
         getKeyAndTag(string) {
@@ -74447,21 +74463,14 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_
       _converse.NUM_PREKEYS = 100; // Set here so that tests can override
 
       function generateFingerprint(device) {
-        return new Promise((resolve, reject) => {
-          device.getBundle().then(bundle => {
-            if (_.isNil(bundle)) {
-              resolve();
-            } // TODO: only generate fingerprints when necessary
-
-
-            crypto.subtle.digest('SHA-1', u.base64ToArrayBuffer(bundle['identity_key'])).then(fp => {
-              bundle['fingerprint'] = u.arrayBufferToHex(fp);
-              device.save('bundle', bundle);
-              device.trigger('change:bundle'); // Doesn't get triggered automatically due to pass-by-reference
-
-              resolve();
-            }).catch(reject);
-          });
+        let bundle;
+        return device.getBundle().then(b => {
+          bundle = b;
+          return crypto.subtle.digest('SHA-1', u.base64ToArrayBuffer(bundle['identity_key']));
+        }).then(fp => {
+          bundle['fingerprint'] = u.arrayBufferToHex(fp);
+          device.save('bundle', bundle);
+          device.trigger('change:bundle'); // Doesn't get triggered automatically due to pass-by-reference
         });
       }
 
@@ -74470,19 +74479,13 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_
       };
 
       _converse.getDevicesForContact = function (jid) {
-        return new Promise((resolve, reject) => {
-          _converse.api.waitUntil('OMEMOInitialized').then(() => {
-            let devicelist = _converse.devicelists.get(jid);
-
-            if (_.isNil(devicelist)) {
-              devicelist = _converse.devicelists.create({
-                'jid': jid
-              });
-            }
-
-            devicelist.fetchDevices().then(() => resolve(devicelist.devices));
-          }).catch(_.partial(_converse.log, _, Strophe.LogLevel.ERROR));
-        });
+        let devicelist;
+        return _converse.api.waitUntil('OMEMOInitialized').then(() => {
+          devicelist = _converse.devicelists.get(jid) || _converse.devicelists.create({
+            'jid': jid
+          });
+          return devicelist.fetchDevices();
+        }).then(() => devicelist.devices);
       };
 
       _converse.contactHasOMEMOSupport = function (jid) {
@@ -74508,35 +74511,7 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_
           }
         }
 
-        return device_id;
-      }
-
-      function generateBundle() {
-        /* The first thing that needs to happen if a client wants to
-         * start using OMEMO is they need to generate an IdentityKey
-         * and a Device ID. The IdentityKey is a Curve25519 [6]
-         * public/private Key pair. The Device ID is a randomly
-         * generated integer between 1 and 2^31 - 1.
-         */
-        return new Promise((resolve, reject) => {
-          libsignal.KeyHelper.generateIdentityKeyPair().then(identity_keypair => {
-            const data = {
-              'device_id': generateDeviceID(),
-              'identity_keypair': identity_keypair,
-              'prekeys': {}
-            };
-            libsignal.KeyHelper.generateSignedPreKey(identity_keypair, 0).then(signed_prekey => {
-              data['signed_prekey'] = signed_prekey;
-
-              const key_promises = _.map(_.range(0, _converse.NUM_PREKEYS), id => libsignal.KeyHelper.generatePreKey(id));
-
-              Promise.all(key_promises).then(keys => {
-                data['prekeys'] = keys;
-                resolve(data);
-              });
-            }).catch(_.partial(_converse.log, _, Strophe.LogLevel.ERROR));
-          });
-        }).catch(_.partial(_converse.log, _, Strophe.LogLevel.ERROR));
+        return device_id.toString();
       }
 
       _converse.OMEMOStore = Backbone.Model.extend({
@@ -74666,11 +74641,39 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_
           return Promise.resolve();
         },
 
-        createNewDeviceBundle() {
-          return generateBundle().then(data => {
-            // TODO: should storeSession be used here?
-            _converse.omemo_store.save(data);
-          }).catch(_.partial(_converse.log, _, Strophe.LogLevel.ERROR));
+        generateBundle() {
+          /* The first thing that needs to happen if a client wants to
+           * start using OMEMO is they need to generate an IdentityKey
+           * and a Device ID. The IdentityKey is a Curve25519 [6]
+           * public/private Key pair. The Device ID is a randomly
+           * generated integer between 1 and 2^31 - 1.
+           */
+          const data = {
+            'device_id': generateDeviceID()
+          };
+          return libsignal.KeyHelper.generateIdentityKeyPair().then(identity_keypair => {
+            data['identity_keypair'] = identity_keypair;
+            data['identity_key'] = identity_keypair.pubKey;
+            return libsignal.KeyHelper.generateSignedPreKey(identity_keypair, 1);
+          }).then(signed_prekey => {
+            _converse.omemo_store.storeSignedPreKey(signed_prekey.keyId, signed_prekey.keyPair);
+
+            data['signed_prekey'] = signed_prekey;
+            return Promise.all(_.map(_.range(0, _converse.NUM_PREKEYS), id => libsignal.KeyHelper.generatePreKey(id)));
+          }).then(keys => {
+            _.forEach(keys, k => _converse.omemo_store.storePreKey(k.keyId, k.keyPair));
+
+            data['prekeys'] = keys;
+            this.save(data); // Save the bundle to the device
+
+            const devicelist = _converse.devicelists.get(_converse.bare_jid),
+                  device = devicelist.devices.create({
+              'id': data.device_id,
+              'jid': _converse.bare_jid
+            });
+
+            device.save('bundle', data);
+          });
         },
 
         fetchSession() {
@@ -74679,13 +74682,13 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_
               this.fetch({
                 'success': () => {
                   if (!_converse.omemo_store.get('device_id')) {
-                    this.createNewDeviceBundle().then(resolve).catch(resolve);
+                    this.generateBundle().then(resolve).catch(resolve);
                   } else {
                     resolve();
                   }
                 },
                 'error': () => {
-                  this.createNewDeviceBundle().then(resolve).catch(resolve);
+                  this.generateBundle().then(resolve).catch(resolve);
                 }
               });
             });
@@ -74760,13 +74763,13 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_
               this.devices.fetch({
                 'success': collection => {
                   if (collection.length === 0) {
-                    this.fetchDevicesFromServer().then(resolve).catch(reject);
+                    this.fetchDevicesFromServer().then(ids => this.publishCurrentDevice(ids)).then(resolve).catch(resolve);
                   } else {
                     resolve();
                   }
                 },
                 'error': () => {
-                  this.fetchDevicesFromServer().then(resolve).catch(reject);
+                  this.fetchDevicesFromServer().then(ids => this.publishCurrentDevice(ids)).then(resolve).catch(resolve);
                 }
               });
             });
@@ -74775,26 +74778,51 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_
           return this._devices_promise;
         },
 
+        publishCurrentDevice(device_ids) {
+          if (this.get('jid') !== _converse.bare_jid) {
+            // We only publish for ourselves.
+            return Promise.resolve();
+          }
+
+          return restoreOMEMOSession().then(() => {
+            const device_id = _converse.omemo_store.get('device_id');
+
+            if (!_.includes(device_ids, device_id)) {
+              return this.publishDevices();
+            } else {
+              const own_device = this.devices.findWhere({
+                'id': device_id
+              });
+
+              if (!own_device.get('active')) {
+                own_device.set('active', true, {
+                  'silent': true
+                });
+                return this.publishDevices();
+              }
+            }
+          });
+        },
+
         fetchDevicesFromServer() {
-          return new Promise((resolve, reject) => {
-            const stanza = $iq({
-              'type': 'get',
-              'from': _converse.bare_jid,
-              'to': this.get('jid')
-            }).c('pubsub', {
-              'xmlns': Strophe.NS.PUBSUB
-            }).c('items', {
-              'node': Strophe.NS.OMEMO_DEVICELIST
-            });
+          const stanza = $iq({
+            'type': 'get',
+            'from': _converse.bare_jid,
+            'to': this.get('jid')
+          }).c('pubsub', {
+            'xmlns': Strophe.NS.PUBSUB
+          }).c('items', {
+            'node': Strophe.NS.OMEMO_DEVICELIST
+          });
+          return _converse.api.sendIQ(stanza).then(iq => {
+            const device_ids = _.map(sizzle(`list[xmlns="${Strophe.NS.OMEMO}"] device`, iq), dev => dev.getAttribute('id'));
 
-            _converse.connection.sendIQ(stanza, iq => {
-              _.forEach(sizzle(`list[xmlns="${Strophe.NS.OMEMO}"] device`, iq), dev => this.devices.create({
-                'id': dev.getAttribute('id'),
-                'jid': this.get('jid')
-              }));
+            _.forEach(device_ids, id => this.devices.create({
+              'id': id,
+              'jid': this.get('jid')
+            }));
 
-              resolve();
-            }, reject, _converse.IQ_TIMEOUT);
+            return device_ids;
           });
         },
 
@@ -74821,22 +74849,6 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_
           return _converse.api.sendIQ(stanza);
         },
 
-        addOwnDevice(device_id) {
-          /* Add this device to our list of devices stored on the
-           * server.
-           * https://xmpp.org/extensions/xep-0384.html#usecases-announcing
-           */
-          if (this.get('jid') !== _converse.bare_jid) {
-            throw new Error("Cannot add device to someone else's device list");
-          }
-
-          this.devices.create({
-            'id': device_id.toString(),
-            'jid': this.get('jid')
-          });
-          return this.publishDevices();
-        },
-
         removeOwnDevices(device_ids) {
           if (this.get('jid') !== _converse.bare_jid) {
             throw new Error("Cannot remove devices from someone else's device list");
@@ -74854,28 +74866,26 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_
         publishBundle() {
           const store = _converse.omemo_store,
                 signed_prekey = store.get('signed_prekey');
-          return new Promise((resolve, reject) => {
-            const stanza = $iq({
-              'from': _converse.bare_jid,
-              'type': 'set'
-            }).c('pubsub', {
-              'xmlns': Strophe.NS.PUBSUB
-            }).c('publish', {
-              'node': `${Strophe.NS.OMEMO_BUNDLES}:${store.get('device_id')}`
-            }).c('item').c('bundle', {
-              'xmlns': Strophe.NS.OMEMO
-            }).c('signedPreKeyPublic', {
-              'signedPreKeyId': signed_prekey.keyId
-            }).t(u.arrayBufferToBase64(signed_prekey.keyPair.pubKey)).up().c('signedPreKeySignature').t(u.arrayBufferToBase64(signed_prekey.signature)).up().c('identityKey').t(u.arrayBufferToBase64(store.get('identity_keypair').pubKey)).up().c('prekeys');
-
-            _.forEach(store.get('prekeys').slice(0, _converse.NUM_PREKEYS), prekey => {
-              stanza.c('preKeyPublic', {
-                'preKeyId': prekey.keyId
-              }).t(u.arrayBufferToBase64(prekey.keyPair.pubKey)).up();
-            });
-
-            _converse.connection.sendIQ(stanza, resolve, reject, _converse.IQ_TIMEOUT);
+          const stanza = $iq({
+            'from': _converse.bare_jid,
+            'type': 'set'
+          }).c('pubsub', {
+            'xmlns': Strophe.NS.PUBSUB
+          }).c('publish', {
+            'node': `${Strophe.NS.OMEMO_BUNDLES}:${store.get('device_id')}`
+          }).c('item').c('bundle', {
+            'xmlns': Strophe.NS.OMEMO
+          }).c('signedPreKeyPublic', {
+            'signedPreKeyId': signed_prekey.keyId
+          }).t(u.arrayBufferToBase64(signed_prekey.keyPair.pubKey)).up().c('signedPreKeySignature').t(u.arrayBufferToBase64(signed_prekey.signature)).up().c('identityKey').t(u.arrayBufferToBase64(store.get('identity_keypair').pubKey)).up().c('prekeys');
+
+          _.forEach(store.get('prekeys').slice(0, _converse.NUM_PREKEYS), prekey => {
+            stanza.c('preKeyPublic', {
+              'preKeyId': prekey.keyId
+            }).t(u.arrayBufferToBase64(prekey.keyPair.pubKey)).up();
           });
+
+          return _converse.api.sendIQ(stanza);
         }
 
       };
@@ -74901,28 +74911,6 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_
         });
       }
 
-      function updateOwnDeviceList() {
-        /* If our own device is not on the list, add it.
-         * Also, deduplicate devices if necessary.
-         */
-        const devicelist = _converse.devicelists.get(_converse.bare_jid),
-              device_id = _converse.omemo_store.get('device_id').toString(),
-              own_device = devicelist.devices.findWhere({
-          'id': device_id
-        });
-
-        if (!own_device) {
-          return devicelist.addOwnDevice(device_id);
-        } else if (!own_device.get('active')) {
-          own_device.set('active', true, {
-            'silent': true
-          });
-          return devicelist.addOwnDevice(device_id);
-        } else {
-          return Promise.resolve();
-        }
-      }
-
       function updateBundleFromStanza(stanza) {
         const items_el = sizzle(`items`, stanza).pop();
 
@@ -74977,19 +74965,25 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_
               'jid': jid
             });
           }
-        }); // Make sure our own device is on the list (i.e. if it was
-        // removed, add it again.
-
+        });
 
-        updateOwnDeviceList();
+        if (Strophe.getBareJidFromJid(jid) === _converse.bare_jid) {
+          // Make sure our own device is on the list (i.e. if it was
+          // removed, add it again.
+          _converse.devicelists.get(_converse.bare_jid).publishCurrentDevice(device_ids);
+        }
       }
 
       function registerPEPPushHandler() {
         // Add a handler for devices pushed from other connected clients
         _converse.connection.addHandler(message => {
-          if (message.querySelector('event[xmlns="' + Strophe.NS.PUBSUB + '#event"]')) {
-            updateDevicesFromStanza(message);
-            updateBundleFromStanza(message);
+          try {
+            if (sizzle(`event[xmlns="${Strophe.NS.PUBSUB}#event"]`, message).length) {
+              updateDevicesFromStanza(message);
+              updateBundleFromStanza(message);
+            }
+          } catch (e) {
+            _converse.log(e.message, Strophe.LogLevel.ERROR);
           }
 
           return true;
@@ -75012,10 +75006,14 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_
         const id = `converse.devicelists-${_converse.bare_jid}`;
         _converse.devicelists.id = id;
         _converse.devicelists.browserStorage = new Backbone.BrowserStorage[_converse.storage](id);
-        fetchOwnDevices().then(() => restoreOMEMOSession()).then(() => updateOwnDeviceList()).then(() => _converse.omemo.publishBundle()).then(() => _converse.emit('OMEMOInitialized')).catch(_.partial(_converse.log, _, Strophe.LogLevel.ERROR));
+        fetchOwnDevices().then(() => restoreOMEMOSession()).then(() => _converse.omemo.publishBundle()).then(() => _converse.emit('OMEMOInitialized')).catch(_.partial(_converse.log, _, Strophe.LogLevel.ERROR));
       }
 
-      _converse.api.listen.on('afterTearDown', () => _converse.devicelists.reset());
+      _converse.api.listen.on('afterTearDown', () => {
+        _converse.devicelists.reset();
+
+        delete _converse.omemo_store;
+      });
 
       _converse.api.listen.on('connected', registerPEPPushHandler);
 
@@ -75023,7 +75021,7 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_
 
       _converse.api.listen.on('statusInitialized', initOMEMO);
 
-      _converse.api.listen.on('addClientFeatures', () => _converse.api.disco.own.features.add(Strophe.NS.OMEMO_DEVICELIST + "notify"));
+      _converse.api.listen.on('addClientFeatures', () => _converse.api.disco.own.features.add(`${Strophe.NS.OMEMO_DEVICELIST}+notify`));
 
       _converse.api.listen.on('userDetailsModalInitialized', contact => {
         const jid = contact.get('jid');

+ 3 - 3
spec/chatbox.js

@@ -621,10 +621,10 @@
                             expect(view.model.get('chat_state')).toBe('inactive');
                             spyOn(_converse.connection, 'send');
                             view.model.maximize();
-                            return test_utils.waitUntil(() => view.model.get('chat_state') === 'active', 700);
+                            return test_utils.waitUntil(() => view.model.get('chat_state') === 'active', 1000);
                         }).then(() => {
                             expect(_converse.connection.send).toHaveBeenCalled();
-                            var calls = _.filter(_converse.connection.send.calls.all(), function (call) {
+                            const calls = _.filter(_converse.connection.send.calls.all(), function (call) {
                                 return call.args[0] instanceof Strophe.Builder;
                             });
                             expect(calls.length).toBe(1);
@@ -635,7 +635,7 @@
                             expect($stanza.children().get(1).tagName).toBe('no-store');
                             expect($stanza.children().get(2).tagName).toBe('no-permanent-store');
                             done();
-                        });
+                        }).catch(_.partial(_converse.log, _, Strophe.LogLevel.FATAL))
                     }));
                 });
 

+ 147 - 41
spec/omemo.js

@@ -65,10 +65,41 @@
                                 .c('device', {'id': '482886413b977930064a5888b92134fe'});
                 _converse.connection._dataRecv(test_utils.createRequest(stanza));
 
-                _converse.emit('OMEMOInitialized');
-                // Check that device list for contact is fetched when chat is opened.
-                return test_utils.openChatBoxFor(_converse, contact_jid);
+                // Check that own device was published
+                return test_utils.waitUntil(() => {
+                    return _.filter(
+                        _converse.connection.IQ_stanzas,
+                        (iq) => {
+                            const node = iq.nodeTree.querySelector('iq[from="'+_converse.bare_jid+'"] publish[node="eu.siacs.conversations.axolotl.devicelist"]');
+                            if (node) { iq_stanza = iq.nodeTree;}
+                            return node;
+                        }).length;
+                });
             }).then(() => {
+                const stanza = $iq({
+                    'from': _converse.bare_jid,
+                    'id': iq_stanza.getAttribute('id'),
+                    'to': _converse.bare_jid,
+                    'type': 'result'});
+                _converse.connection._dataRecv(test_utils.createRequest(stanza));
+
+                return test_utils.waitUntil(() => {
+                    return _.filter(_converse.connection.IQ_stanzas, function (iq) {
+                        const node = iq.nodeTree.querySelector('publish[node="eu.siacs.conversations.axolotl.bundles:123456789"]');
+                        if (node) { iq_stanza = iq.nodeTree; }
+                        return node;
+                    }).length;
+                });
+            }).then(() => {
+                const stanza = $iq({
+                    'from': _converse.bare_jid,
+                    'id': iq_stanza.getAttribute('id'),
+                    'to': _converse.bare_jid,
+                    'type': 'result'});
+                _converse.connection._dataRecv(test_utils.createRequest(stanza));
+                return _converse.api.waitUntil('OMEMOInitialized');
+            }).then(() => test_utils.openChatBoxFor(_converse, contact_jid))
+              .then(() => {
                 return test_utils.waitUntil(() => {
                     return _.filter(
                         _converse.connection.IQ_stanzas,
@@ -90,7 +121,8 @@
                             .c('list', {'xmlns': "eu.siacs.conversations.axolotl"})
                                 .c('device', {'id': '555'});
                 _converse.connection._dataRecv(test_utils.createRequest(stanza));
-
+                return test_utils.waitUntil(() => _converse.omemo_store);
+            }).then(() => {
                 const devicelist = _converse.devicelists.get({'jid': contact_jid});
                 expect(devicelist.devices.length).toBe(1);
 
@@ -233,7 +265,8 @@
             test_utils.createContacts(_converse, 'current', 1);
             const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@localhost';
 
-            test_utils.waitUntil(function () {
+            // Wait until own devices are fetched
+            test_utils.waitUntil(() => {
                 return _.filter(
                     _converse.connection.IQ_stanzas,
                     (iq) => {
@@ -241,7 +274,7 @@
                         if (node) { iq_stanza = iq.nodeTree;}
                         return node;
                     }).length;
-            }).then(function () {
+            }).then(() => {
                 expect(iq_stanza.outerHTML).toBe(
                     '<iq type="get" from="dummy@localhost" to="dummy@localhost" xmlns="jabber:client" id="'+iq_stanza.getAttribute("id")+'">'+
                         '<pubsub xmlns="http://jabber.org/protocol/pubsub">'+
@@ -259,16 +292,48 @@
                             .c('list', {'xmlns': "eu.siacs.conversations.axolotl"})
                                 .c('device', {'id': '555'});
                 _converse.connection._dataRecv(test_utils.createRequest(stanza));
-
+                return test_utils.waitUntil(() => _converse.omemo_store);
+            }).then(() => {
                 expect(_converse.devicelists.length).toBe(1);
                 const devicelist = _converse.devicelists.get(_converse.bare_jid);
-                expect(devicelist.devices.length).toBe(1);
+                expect(devicelist.devices.length).toBe(2);
                 expect(devicelist.devices.at(0).get('id')).toBe('555');
-                return test_utils.waitUntil(() => _converse.devicelists);
-            }).then(function () {
-                // We simply emit, to avoid doing all the setup work
-                _converse.emit('OMEMOInitialized');
+                expect(devicelist.devices.at(1).get('id')).toBe('123456789');
 
+                // Check that own device was published
+                return test_utils.waitUntil(() => {
+                    return _.filter(
+                        _converse.connection.IQ_stanzas,
+                        (iq) => {
+                            const node = iq.nodeTree.querySelector('iq[from="'+_converse.bare_jid+'"] publish[node="eu.siacs.conversations.axolotl.devicelist"]');
+                            if (node) { iq_stanza = iq.nodeTree;}
+                            return node;
+                        }).length;
+                });
+            }).then(() => {
+                const stanza = $iq({
+                    'from': _converse.bare_jid,
+                    'id': iq_stanza.getAttribute('id'),
+                    'to': _converse.bare_jid,
+                    'type': 'result'});
+                _converse.connection._dataRecv(test_utils.createRequest(stanza));
+
+                return test_utils.waitUntil(() => {
+                    return _.filter(_converse.connection.IQ_stanzas, function (iq) {
+                        const node = iq.nodeTree.querySelector('publish[node="eu.siacs.conversations.axolotl.bundles:123456789"]');
+                        if (node) { iq_stanza = iq.nodeTree; }
+                        return node;
+                    }).length;
+                });
+            }).then(() => {
+                const stanza = $iq({
+                    'from': _converse.bare_jid,
+                    'id': iq_stanza.getAttribute('id'),
+                    'to': _converse.bare_jid,
+                    'type': 'result'});
+                _converse.connection._dataRecv(test_utils.createRequest(stanza));
+                return _converse.api.waitUntil('OMEMOInitialized');
+            }).then(() => {
                 let stanza = $msg({
                     'from': contact_jid,
                     'to': _converse.bare_jid,
@@ -319,6 +384,7 @@
                     .c('items', {'node': 'eu.siacs.conversations.axolotl.devicelist'})
                         .c('item')
                             .c('list', {'xmlns': 'eu.siacs.conversations.axolotl'})
+                                .c('device', {'id': '123456789'})
                                 .c('device', {'id': '555'})
                                 .c('device', {'id': '777'})
                 _converse.connection._dataRecv(test_utils.createRequest(stanza));
@@ -346,7 +412,8 @@
                                 .c('device', {'id': '444'})
                 _converse.connection._dataRecv(test_utils.createRequest(stanza));
 
-                return test_utils.waitUntil(function () {
+                // Check that own device was published
+                return test_utils.waitUntil(() => {
                     return _.filter(
                         _converse.connection.IQ_stanzas,
                         (iq) => {
@@ -355,7 +422,7 @@
                             return node;
                         }).length;
                 });
-            }).then(function () {
+            }).then(() => {
                 // Check that our own device is added again, but that removed
                 // devices are not added.
                 expect(iq_stanza.outerHTML).toBe(
@@ -394,7 +461,7 @@
             test_utils.createContacts(_converse, 'current');
             const contact_jid = mock.cur_names[3].replace(/ /g,'.').toLowerCase() + '@localhost';
 
-            test_utils.waitUntil(function () {
+            test_utils.waitUntil(() => {
                 return _.filter(
                     _converse.connection.IQ_stanzas,
                     (iq) => {
@@ -402,7 +469,7 @@
                         if (node) { iq_stanza = iq.nodeTree;}
                         return node;
                     }).length;
-            }).then(function () {
+            }).then(() => {
                 expect(iq_stanza.outerHTML).toBe(
                     '<iq type="get" from="dummy@localhost" to="dummy@localhost" xmlns="jabber:client" id="'+iq_stanza.getAttribute("id")+'">'+
                         '<pubsub xmlns="http://jabber.org/protocol/pubsub">'+
@@ -421,18 +488,49 @@
                             .c('list', {'xmlns': "eu.siacs.conversations.axolotl"})
                                 .c('device', {'id': '555'});
                 _converse.connection._dataRecv(test_utils.createRequest(stanza));
-
-                expect(_converse.devicelists.length).toBe(1);
-                return test_utils.waitUntil(() => _converse.devicelists);
+                return test_utils.waitUntil(() => _converse.omemo_store);
             }).then(function () {
                 // We simply emit, to avoid doing all the setup work
                 expect(_converse.devicelists.length).toBe(1);
-                let devicelist = _converse.devicelists.get(_converse.bare_jid);
+                const devicelist = _converse.devicelists.get(_converse.bare_jid);
                 expect(devicelist.devices.length).toBe(2);
                 expect(devicelist.devices.at(0).get('id')).toBe('555');
                 expect(devicelist.devices.at(1).get('id')).toBe('123456789');
-                _converse.emit('OMEMOInitialized');
+                // Check that own device was published
+                return test_utils.waitUntil(() => {
+                    return _.filter(
+                        _converse.connection.IQ_stanzas,
+                        (iq) => {
+                            const node = iq.nodeTree.querySelector('iq[from="'+_converse.bare_jid+'"] publish[node="eu.siacs.conversations.axolotl.devicelist"]');
+                            if (node) { iq_stanza = iq.nodeTree;}
+                            return node;
+                        }).length;
+                });
+            }).then(() => {
+                const stanza = $iq({
+                    'from': _converse.bare_jid,
+                    'id': iq_stanza.getAttribute('id'),
+                    'to': _converse.bare_jid,
+                    'type': 'result'});
+                _converse.connection._dataRecv(test_utils.createRequest(stanza));
 
+                // Check that own bundle gets published
+                return test_utils.waitUntil(() => {
+                    return _.filter(_converse.connection.IQ_stanzas, (iq) => {
+                        const node = iq.nodeTree.querySelector('publish[node="eu.siacs.conversations.axolotl.bundles:123456789"]');
+                        if (node) { iq_stanza = iq.nodeTree; }
+                        return node;
+                    }).length;
+                });
+            }).then(() => {
+                const stanza = $iq({
+                    'from': _converse.bare_jid,
+                    'id': iq_stanza.getAttribute('id'),
+                    'to': _converse.bare_jid,
+                    'type': 'result'});
+                _converse.connection._dataRecv(test_utils.createRequest(stanza));
+                return _converse.api.waitUntil('OMEMOInitialized');
+            }).then(() => {
                 let stanza = $msg({
                     'from': contact_jid,
                     'to': _converse.bare_jid,
@@ -452,7 +550,7 @@
                 _converse.connection._dataRecv(test_utils.createRequest(stanza));
 
                 expect(_converse.devicelists.length).toBe(2);
-                devicelist = _converse.devicelists.get(contact_jid);
+                let devicelist = _converse.devicelists.get(contact_jid);
                 expect(devicelist.devices.length).toBe(1);
                 let device = devicelist.devices.at(0);
                 expect(device.get('bundle').identity_key).toBe('3333');
@@ -543,7 +641,7 @@
             _converse.emit('rosterContactsFetched');
             const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@localhost';
 
-            test_utils.waitUntil(function () {
+            test_utils.waitUntil(() => {
                 return _.filter(
                     _converse.connection.IQ_stanzas,
                     (iq) => {
@@ -551,7 +649,7 @@
                         if (node) { iq_stanza = iq.nodeTree;}
                         return node;
                     }).length;
-            }).then(function () {
+            }).then(() => {
                 const stanza = $iq({
                     'from': contact_jid,
                     'id': iq_stanza.getAttribute('id'),
@@ -589,14 +687,14 @@
                         return node;
                     }).length;
                 });
-            }).then(function () {
+            }).then(() => {
                 expect(iq_stanza.outerHTML).toBe(
                     `<iq from="dummy@localhost" type="set" xmlns="jabber:client" id="${iq_stanza.getAttribute('id')}">`+
                         `<pubsub xmlns="http://jabber.org/protocol/pubsub">`+
                             `<publish node="eu.siacs.conversations.axolotl.bundles:123456789">`+
                                 `<item>`+
                                     `<bundle xmlns="eu.siacs.conversations.axolotl">`+
-                                        `<signedPreKeyPublic signedPreKeyId="0">${btoa('1234')}</signedPreKeyPublic>`+
+                                        `<signedPreKeyPublic signedPreKeyId="1">${btoa('1234')}</signedPreKeyPublic>`+
                                             `<signedPreKeySignature>${btoa('11112222333344445555')}</signedPreKeySignature>`+
                                             `<identityKey>${btoa('1234')}</identityKey>`+
                                         `<prekeys>`+
@@ -646,7 +744,7 @@
                     '</iq>');
 
                 const stanza = $iq({
-                    'from': contact_jid,
+                    'from': _converse.bare_jid,
                     'id': iq_stanza.getAttribute('id'),
                     'to': _converse.bare_jid,
                     'type': 'result',
@@ -656,20 +754,22 @@
                             .c('list', {'xmlns': "eu.siacs.conversations.axolotl"})
                                 .c('device', {'id': '482886413b977930064a5888b92134fe'});
                 _converse.connection._dataRecv(test_utils.createRequest(stanza));
-
+                return test_utils.waitUntil(() => _converse.omemo_store);
+            }).then(() => {
                 expect(_converse.devicelists.length).toBe(1);
                 const devicelist = _converse.devicelists.get(_converse.bare_jid);
-                expect(devicelist.devices.length).toBe(1);
+                expect(devicelist.devices.length).toBe(2);
                 expect(devicelist.devices.at(0).get('id')).toBe('482886413b977930064a5888b92134fe');
-
-                return test_utils.openChatBoxFor(_converse, contact_jid);
-            }).then(() => {
+                expect(devicelist.devices.at(1).get('id')).toBe('123456789');
+                // Check that own device was published
                 return test_utils.waitUntil(() => {
-                    return _.filter(_converse.connection.IQ_stanzas, function (iq) {
-                        const node = iq.nodeTree.querySelector('publish[node="eu.siacs.conversations.axolotl.devicelist"]');
-                        if (node) { iq_stanza = iq.nodeTree; }
-                        return node;
-                    }).length;
+                    return _.filter(
+                        _converse.connection.IQ_stanzas,
+                        (iq) => {
+                            const node = iq.nodeTree.querySelector('iq[from="'+_converse.bare_jid+'"] publish[node="eu.siacs.conversations.axolotl.devicelist"]');
+                            if (node) { iq_stanza = iq.nodeTree;}
+                            return node;
+                        }).length;
                 });
             }).then(() => {
                 expect(iq_stanza.outerHTML).toBe(
@@ -693,8 +793,9 @@
                     'type': 'result'});
                 _converse.connection._dataRecv(test_utils.createRequest(stanza));
 
+                // Check that own bundle gets published
                 return test_utils.waitUntil(() => {
-                    return _.filter(_converse.connection.IQ_stanzas, function (iq) {
+                    return _.filter(_converse.connection.IQ_stanzas, (iq) => {
                         const node = iq.nodeTree.querySelector('publish[node="eu.siacs.conversations.axolotl.bundles:123456789"]');
                         if (node) { iq_stanza = iq.nodeTree; }
                         return node;
@@ -707,7 +808,7 @@
                 const signed_prekeys = iq_stanza.querySelectorAll('signedPreKeyPublic');
                 expect(signed_prekeys.length).toBe(1);
                 const signed_prekey = signed_prekeys[0];
-                expect(signed_prekey.getAttribute('signedPreKeyId')).toBe('0')
+                expect(signed_prekey.getAttribute('signedPreKeyId')).toBe('1')
                 expect(iq_stanza.querySelectorAll('signedPreKeySignature').length).toBe(1);
                 expect(iq_stanza.querySelectorAll('identityKey').length).toBe(1);
 
@@ -717,7 +818,10 @@
                     'to': _converse.bare_jid,
                     'type': 'result'});
                 _converse.connection._dataRecv(test_utils.createRequest(stanza));
-
+                return _converse.api.waitUntil('OMEMOInitialized', 1000);
+            }).then(() => {
+                return test_utils.openChatBoxFor(_converse, contact_jid);
+            }).then(() => {
                 return test_utils.waitUntil(() => {
                     return _.filter(
                         _converse.connection.IQ_stanzas,
@@ -748,7 +852,9 @@
                                 .c('device', {'id': '4e30f35051b7b8b42abe083742187228'}).up()
                                 .c('device', {'id': 'ae890ac52d0df67ed7cfdf51b644e901'});
                 _converse.connection._dataRecv(test_utils.createRequest(stanza));
-
+                const devicelist = _converse.devicelists.get(contact_jid);
+                return test_utils.waitUntil(() => devicelist.devices.length);
+            }).then(() => {
                 expect(_converse.devicelists.length).toBe(2);
                 const devicelist = _converse.devicelists.get(contact_jid);
                 expect(devicelist.devices.length).toBe(4);

+ 5 - 5
spec/presence.js

@@ -47,7 +47,7 @@
                 "<presence xmlns='jabber:client'>"+
                     "<status>Hello world</status>"+
                     "<priority>0</priority>"+
-                    "<c xmlns='http://jabber.org/protocol/caps' hash='sha-1' node='https://conversejs.org' ver='nE765l4CRVrSUEIPAdtgCw4+5cc='/>"+
+                    "<c xmlns='http://jabber.org/protocol/caps' hash='sha-1' node='https://conversejs.org' ver='ggltNSI5YG/7dFKB57Bk2dRYRU0='/>"+
                 "</presence>"
             );
             _converse.priority = 2;
@@ -57,7 +57,7 @@
                     "<show>away</show>"+
                     "<status>Going jogging</status>"+
                     "<priority>2</priority>"+
-                    "<c xmlns='http://jabber.org/protocol/caps' hash='sha-1' node='https://conversejs.org' ver='nE765l4CRVrSUEIPAdtgCw4+5cc='/>"+
+                    "<c xmlns='http://jabber.org/protocol/caps' hash='sha-1' node='https://conversejs.org' ver='ggltNSI5YG/7dFKB57Bk2dRYRU0='/>"+
                 "</presence>"
             );
 
@@ -68,7 +68,7 @@
                     "<show>dnd</show>"+
                     "<status>Doing taxes</status>"+
                     "<priority>0</priority>"+
-                    "<c xmlns='http://jabber.org/protocol/caps' hash='sha-1' node='https://conversejs.org' ver='nE765l4CRVrSUEIPAdtgCw4+5cc='/>"+
+                    "<c xmlns='http://jabber.org/protocol/caps' hash='sha-1' node='https://conversejs.org' ver='ggltNSI5YG/7dFKB57Bk2dRYRU0='/>"+
                 "</presence>"
             );
         }));
@@ -97,7 +97,7 @@
                     .toBe("<presence xmlns='jabber:client'>"+
                           "<status>My custom status</status>"+
                           "<priority>0</priority>"+
-                          "<c xmlns='http://jabber.org/protocol/caps' hash='sha-1' node='https://conversejs.org' ver='nE765l4CRVrSUEIPAdtgCw4+5cc='/>"+
+                          "<c xmlns='http://jabber.org/protocol/caps' hash='sha-1' node='https://conversejs.org' ver='ggltNSI5YG/7dFKB57Bk2dRYRU0='/>"+
                           "</presence>")
 
                 return test_utils.waitUntil(function () {
@@ -113,7 +113,7 @@
                 modal.el.querySelector('[type="submit"]').click();
                 expect(_converse.connection.send.calls.mostRecent().args[0].toLocaleString())
                     .toBe("<presence xmlns='jabber:client'><show>dnd</show><status>My custom status</status><priority>0</priority>"+
-                          "<c xmlns='http://jabber.org/protocol/caps' hash='sha-1' node='https://conversejs.org' ver='nE765l4CRVrSUEIPAdtgCw4+5cc='/>"+
+                          "<c xmlns='http://jabber.org/protocol/caps' hash='sha-1' node='https://conversejs.org' ver='ggltNSI5YG/7dFKB57Bk2dRYRU0='/>"+
                           "</presence>")
                 done();
             });

+ 88 - 100
src/converse-omemo.js

@@ -177,7 +177,7 @@
                     try {
                         return sessionBuilder.processPreKey({
                             'registrationId': parseInt(_converse.omemo_store.get('device_id'), 10),
-                            'identityKey': _converse.omemo_store.get('identity_keypair').pubKey,
+                            'identityKey': _converse.omemo_store.get('identity_key'),
                             'signedPreKey': {
                                 'keyId': bundle.signed_prekey.id, // <Number>
                                 'publicKey': u.base64ToArrayBuffer(bundle.signed_prekey.public_key),
@@ -478,16 +478,12 @@
             }
 
             _converse.getDevicesForContact = function (jid) {
-                return new Promise((resolve, reject) => {
-                    _converse.api.waitUntil('OMEMOInitialized').then(() => {
-                        let devicelist = _converse.devicelists.get(jid);
-                        if (_.isNil(devicelist)) {
-                            devicelist = _converse.devicelists.create({'jid': jid});
-                        }
-                        devicelist.fetchDevices().then(() => resolve(devicelist.devices));
-
-                    }).catch(_.partial(_converse.log, _, Strophe.LogLevel.ERROR));
-                });
+                let devicelist;
+                return _converse.api.waitUntil('OMEMOInitialized')
+                    .then(() => {
+                        devicelist = _converse.devicelists.get(jid) || _converse.devicelists.create({'jid': jid});
+                        return devicelist.fetchDevices();
+                    }).then(() => devicelist.devices);
             }
 
             _converse.contactHasOMEMOSupport = function (jid) {
@@ -516,36 +512,6 @@
             }
 
 
-            function generateBundle () {
-                /* The first thing that needs to happen if a client wants to
-                 * start using OMEMO is they need to generate an IdentityKey
-                 * and a Device ID. The IdentityKey is a Curve25519 [6]
-                 * public/private Key pair. The Device ID is a randomly
-                 * generated integer between 1 and 2^31 - 1.
-                 */
-                return new Promise((resolve, reject) => {
-                    libsignal.KeyHelper.generateIdentityKeyPair().then((identity_keypair) => {
-                        const data = {
-                            'device_id': generateDeviceID(),
-                            'identity_keypair': identity_keypair,
-                            'prekeys': {}
-                        };
-                        libsignal.KeyHelper.generateSignedPreKey(identity_keypair, 1)
-                        .then((signed_prekey) => {
-                            _converse.omemo_store.storeSignedPreKey(signed_prekey.keyId, signed_prekey.keyPair);
-                            data['signed_prekey'] = signed_prekey;
-                            const key_promises = _.map(_.range(0, _converse.NUM_PREKEYS), id => libsignal.KeyHelper.generatePreKey(id));
-                            Promise.all(key_promises).then(keys => {
-                                _.forEach(keys, k => _converse.omemo_store.storePreKey(k.keyId, k.keyPair));
-                                data['prekeys'] = keys;
-                                resolve(data)
-                            });
-                        }).catch(_.partial(_converse.log, _, Strophe.LogLevel.ERROR));
-                    });
-                }).catch(_.partial(_converse.log, _, Strophe.LogLevel.ERROR));
-            }
-
-
             _converse.OMEMOStore = Backbone.Model.extend({
 
                 Direction: {
@@ -652,19 +618,49 @@
                     return Promise.resolve();
                 },
 
+                generateBundle () {
+                    /* The first thing that needs to happen if a client wants to
+                     * start using OMEMO is they need to generate an IdentityKey
+                     * and a Device ID. The IdentityKey is a Curve25519 [6]
+                     * public/private Key pair. The Device ID is a randomly
+                     * generated integer between 1 and 2^31 - 1.
+                     */
+                    const data = {
+                        'device_id': generateDeviceID()
+                    };
+                    return libsignal.KeyHelper.generateIdentityKeyPair()
+                        .then(identity_keypair => {
+                            data['identity_keypair'] = identity_keypair;
+                            data['identity_key'] = identity_keypair.pubKey;
+                            return libsignal.KeyHelper.generateSignedPreKey(identity_keypair, 1);
+                        }).then(signed_prekey => {
+                            _converse.omemo_store.storeSignedPreKey(signed_prekey.keyId, signed_prekey.keyPair);
+                            data['signed_prekey'] = signed_prekey;
+                            return Promise.all(_.map(_.range(0, _converse.NUM_PREKEYS), id => libsignal.KeyHelper.generatePreKey(id)));
+                        }).then(keys => {
+                            _.forEach(keys, k => _converse.omemo_store.storePreKey(k.keyId, k.keyPair));
+                            data['prekeys'] = keys;
+                            this.save(data)
+                            // Save the bundle to the device
+                            const devicelist = _converse.devicelists.get(_converse.bare_jid),
+                                  device = devicelist.devices.create({'id': data.device_id, 'jid': _converse.bare_jid});
+                            device.save('bundle', data);
+                        });
+                },
+
                 fetchSession () {
                     if (_.isUndefined(this._setup_promise)) {
                         this._setup_promise = new Promise((resolve, reject) => {
                             this.fetch({
                                 'success': () => {
                                     if (!_converse.omemo_store.get('device_id')) {
-                                        generateBundle().then(data => resolve(this.save(data))).catch(resolve);
+                                        this.generateBundle().then(resolve).catch(resolve);
                                     } else {
                                         resolve();
                                     }
                                 },
                                 'error': () => {
-                                    generateBundle().then(data => resolve(this.save(data))).catch(resolve);
+                                    this.generateBundle().then(resolve).catch(resolve);
                                 }
                             });
                         });
@@ -738,13 +734,19 @@
                             this.devices.fetch({
                                 'success': (collection) => {
                                     if (collection.length === 0) {
-                                        this.fetchDevicesFromServer().then(resolve).catch(reject);
+                                        this.fetchDevicesFromServer()
+                                            .then(ids => this.publishCurrentDevice(ids))
+                                            .then(resolve)
+                                            .catch(resolve);
                                     } else {
                                         resolve();
                                     }
                                 },
                                 'error': () => {
-                                    this.fetchDevicesFromServer().then(resolve).catch(reject);
+                                    this.fetchDevicesFromServer()
+                                                .then(ids => this.publishCurrentDevice(ids))
+                                                .then(resolve)
+                                                .catch(resolve)
                                 }
                             });
                         });
@@ -752,26 +754,39 @@
                     return this._devices_promise;
                 },
 
+                publishCurrentDevice (device_ids) {
+                    if (this.get('jid') !== _converse.bare_jid) {
+                        // We only publish for ourselves.
+                        return Promise.resolve();
+                    }
+                    return restoreOMEMOSession()
+                        .then(() => {
+                            const device_id = _converse.omemo_store.get('device_id');
+                            if (!_.includes(device_ids, device_id)) {
+                                return this.publishDevices();
+                            } else {
+                                const own_device = this.devices.findWhere({'id': device_id})
+                                if (!own_device.get('active')) {
+                                    own_device.set('active', true, {'silent': true});
+                                    return this.publishDevices();
+                                }
+                            }
+                        });
+                },
+
                 fetchDevicesFromServer () {
-                    return new Promise((resolve, reject) => {
-                        const stanza = $iq({
-                            'type': 'get',
-                            'from': _converse.bare_jid,
-                            'to': this.get('jid')
-                        }).c('pubsub', {'xmlns': Strophe.NS.PUBSUB})
-                            .c('items', {'node': Strophe.NS.OMEMO_DEVICELIST});
-                        _converse.connection.sendIQ(
-                            stanza,
-                            (iq) => {
-                                _.forEach(
-                                    sizzle(`list[xmlns="${Strophe.NS.OMEMO}"] device`, iq),
-                                    (dev) => this.devices.create({'id': dev.getAttribute('id'), 'jid': this.get('jid')})
-                                );
-                                resolve();
-                            },
-                            reject,
-                            _converse.IQ_TIMEOUT);
-                    });
+                    const stanza = $iq({
+                        'type': 'get',
+                        'from': _converse.bare_jid,
+                        'to': this.get('jid')
+                    }).c('pubsub', {'xmlns': Strophe.NS.PUBSUB})
+                        .c('items', {'node': Strophe.NS.OMEMO_DEVICELIST});
+                    return _converse.api.sendIQ(stanza)
+                        .then(iq => {
+                            const device_ids = _.map(sizzle(`list[xmlns="${Strophe.NS.OMEMO}"] device`, iq), dev => dev.getAttribute('id'));
+                            _.forEach(device_ids, id => this.devices.create({'id': id, 'jid': this.get('jid')}));
+                            return device_ids;
+                        });
                 },
 
                 publishDevices () {
@@ -788,18 +803,6 @@
                     return _converse.api.sendIQ(stanza);
                 },
 
-                addOwnDevice (device_id) {
-                    /* Add this device to our list of devices stored on the
-                     * server.
-                     * https://xmpp.org/extensions/xep-0384.html#usecases-announcing
-                     */
-                    if (this.get('jid') !== _converse.bare_jid) {
-                        throw new Error("Cannot add device to someone else's device list");
-                    }
-                    this.devices.create({'id': device_id.toString(), 'jid': this.get('jid')});
-                    return this.publishDevices();
-                },
-
                 removeOwnDevices (device_ids) {
                     if (this.get('jid') !== _converse.bare_jid) {
                         throw new Error("Cannot remove devices from someone else's device list");
@@ -861,25 +864,6 @@
                 });
             }
 
-            function updateOwnDeviceList () {
-                /* If our own device is not on the list, add it.
-                 * Also, deduplicate devices if necessary.
-                 */
-                const devicelist = _converse.devicelists.get(_converse.bare_jid),
-                      device_id = _converse.omemo_store.get('device_id').toString(),
-                      own_device = devicelist.devices.findWhere({'id': device_id});
-
-                if (!own_device) {
-                    return devicelist.addOwnDevice(device_id);
-                } else if (!own_device.get('active')) {
-                    own_device.set('active', true, {'silent': true});
-                    return devicelist.addOwnDevice(device_id);
-                } else {
-                    return Promise.resolve();
-                }
-            }
-
-
             function updateBundleFromStanza (stanza) {
                 const items_el = sizzle(`items`, stanza).pop();
                 if (!items_el || !items_el.getAttribute('node').startsWith(Strophe.NS.OMEMO_BUNDLES)) {
@@ -916,9 +900,11 @@
                         devices.create({'id': device_id, 'jid': jid})
                     }
                 });
-                // Make sure our own device is on the list (i.e. if it was
-                // removed, add it again.
-                updateOwnDeviceList();
+                if (Strophe.getBareJidFromJid(jid) === _converse.bare_jid) {
+                    // Make sure our own device is on the list (i.e. if it was
+                    // removed, add it again.
+                    _converse.devicelists.get(_converse.bare_jid).publishCurrentDevice(device_ids);
+                }
             }
 
             function registerPEPPushHandler () {
@@ -954,13 +940,15 @@
 
                 fetchOwnDevices()
                     .then(() => restoreOMEMOSession())
-                    .then(() => updateOwnDeviceList())
                     .then(() => _converse.omemo.publishBundle())
                     .then(() => _converse.emit('OMEMOInitialized'))
                     .catch(_.partial(_converse.log, _, Strophe.LogLevel.ERROR));
             }
 
-            _converse.api.listen.on('afterTearDown', () => _converse.devicelists.reset());
+            _converse.api.listen.on('afterTearDown', () => {
+                _converse.devicelists.reset();
+                delete _converse.omemo_store;
+            });
             _converse.api.listen.on('connected', registerPEPPushHandler);
             _converse.api.listen.on('renderToolbar', (view) => view.renderOMEMOToolbarButton());
             _converse.api.listen.on('statusInitialized', initOMEMO);

+ 1 - 1
tests/utils.js

@@ -100,7 +100,7 @@
 
     utils.openChatBoxFor = function (_converse, jid) {
         _converse.roster.get(jid).trigger("open");
-        return utils.waitUntil(() => _converse.chatboxviews.get(jid));
+        return utils.waitUntil(() => _converse.chatboxviews.get(jid), 1000);
     };
 
     utils.openChatRoomViaModal = function (_converse, jid, nick='') {