Forráskód Böngészése

Wait on `OMEMOInitialized` promise...

before parsing message stanza for encryption parameters.

Otherwise we might not know what our own device-id/sid is, and therefore
can't decrypt the incoming message.

Fixes #2733
JC Brand 3 éve
szülő
commit
9b1a7c70a3

+ 1 - 0
CHANGES.md

@@ -16,6 +16,7 @@
 - #2704: Send button doesn't work in a multi-user chat
 - #2725: Send new presence status to all connected MUCs
 - #2728: Not sending headers with upload request
+- #2733: OMEMO Messages received while client closed not decrypted
 
 - Emit a `change` event when a configuration setting changes
 - 3 New configuration settings:

+ 4 - 28
src/headless/shared/parsers.js

@@ -56,40 +56,16 @@ export function getStanzaIDs (stanza, original_stanza) {
     return attrs;
 }
 
-export function getEncryptionAttributes (stanza, _converse) {
+export function getEncryptionAttributes (stanza) {
     const eme_tag = sizzle(`encryption[xmlns="${Strophe.NS.EME}"]`, stanza).pop();
     const namespace = eme_tag?.getAttribute('namespace');
     const attrs = {};
-
     if (namespace) {
         attrs.is_encrypted = true;
         attrs.encryption_namespace = namespace;
-        if (namespace !== Strophe.NS.OMEMO) {
-            // Found an encrypted message, but it's not OMEMO
-            return attrs;
-        }
-    }
-
-    const encrypted = sizzle(`encrypted[xmlns="${Strophe.NS.OMEMO}"]`, stanza).pop();
-    if (!eme_tag) {
-        attrs.is_encrypted = !!encrypted;
-    }
-
-    if (!encrypted || api.settings.get('clear_cache_on_logout')) {
-        return attrs;
-    }
-    const header = encrypted.querySelector('header');
-    attrs.encrypted = { 'device_id': header.getAttribute('sid') };
-
-    const device_id = _converse.omemo_store?.get('device_id');
-    const key = device_id && sizzle(`key[rid="${device_id}"]`, encrypted).pop();
-    if (key) {
-        Object.assign(attrs.encrypted, {
-            'iv': header.querySelector('iv').textContent,
-            'key': key.textContent,
-            'payload': encrypted.querySelector('payload')?.textContent || null,
-            'prekey': ['true', '1'].includes(key.getAttribute('prekey'))
-        });
+    } else if (sizzle(`encrypted[xmlns="${Strophe.NS.OMEMO}"]`, stanza).pop()) {
+        attrs.is_encrypted = true;
+        attrs.encryption_namespace = Strophe.NS.OMEMO;
     }
     return attrs;
 }

+ 10 - 1
src/plugins/omemo/api.js

@@ -1,4 +1,4 @@
-import { _converse } from '@converse/headless/core';
+import { _converse, api } from '@converse/headless/core';
 import { generateFingerprint } from './utils.js';
 
 export default {
@@ -10,6 +10,14 @@ export default {
      * @memberOf _converse.api
      */
     'omemo': {
+        /**
+         * Returns the device ID of the current device.
+         */
+        async getDeviceID () {
+            await api.waitUntil('OMEMOInitialized');
+            return _converse.omemo_store.get('device_id');
+        },
+
         /**
          * The "bundle" namespace groups methods relevant to the user's
          * OMEMO bundle.
@@ -25,6 +33,7 @@ export default {
              * @returns {promise} Promise which resolves once we have a result from the server.
              */
             'generate': async () => {
+                await api.waitUntil('OMEMOInitialized');
                 // Remove current device
                 const devicelist = _converse.devicelists.get(_converse.bare_jid);
                 const device_id = _converse.omemo_store.get('device_id');

+ 5 - 7
src/plugins/omemo/store.js

@@ -154,9 +154,7 @@ const OMEMOStore = Model.extend({
             key.startsWith('session' + identifier) ? key : false
         );
         const attrs = {};
-        keys.forEach(key => {
-            attrs[key] = undefined;
-        });
+        keys.forEach(key => { attrs[key] = undefined; });
         this.save(attrs);
         return Promise.resolve();
     },
@@ -208,7 +206,7 @@ const OMEMOStore = Model.extend({
     },
 
     /**
-     * Generate a the data used by the X3DH key agreement protocol
+     * Generate the data used by the X3DH key agreement protocol
      * that can be used to build a session with a device.
      */
     async generateBundle () {
@@ -234,7 +232,7 @@ const OMEMOStore = Model.extend({
         });
         const signed_prekey = await libsignal.KeyHelper.generateSignedPreKey(identity_keypair, 0);
 
-        _converse.omemo_store.storeSignedPreKey(signed_prekey);
+        this.storeSignedPreKey(signed_prekey);
         bundle['signed_prekey'] = {
             'id': signed_prekey.keyId,
             'public_key': u.arrayBufferToBase64(signed_prekey.keyPair.pubKey),
@@ -243,7 +241,7 @@ const OMEMOStore = Model.extend({
         const keys = await Promise.all(
             range(0, _converse.NUM_PREKEYS).map(id => libsignal.KeyHelper.generatePreKey(id))
         );
-        keys.forEach(k => _converse.omemo_store.storePreKey(k.keyId, k.keyPair));
+        keys.forEach(k => this.storePreKey(k.keyId, k.keyPair));
         const devicelist = _converse.devicelists.get(_converse.bare_jid);
         const device = await devicelist.devices.create(
             { 'id': bundle.device_id, 'jid': _converse.bare_jid },
@@ -262,7 +260,7 @@ const OMEMOStore = Model.extend({
             this._setup_promise = new Promise((resolve, reject) => {
                 this.fetch({
                     'success': () => {
-                        if (!_converse.omemo_store.get('device_id')) {
+                        if (!this.get('device_id')) {
                             this.generateBundle().then(resolve).catch(reject);
                         } else {
                             resolve();

+ 54 - 25
src/plugins/omemo/utils.js

@@ -203,27 +203,48 @@ export function handleEncryptedFiles (richtext) {
     richtext.addAnnotations((text, offset) => addEncryptedFiles(text, offset, richtext));
 }
 
-export function parseEncryptedMessage (stanza, attrs) {
-    if (attrs.is_encrypted) {
-        if (!attrs.encrypted.key) {
-            return Object.assign(attrs, {
-                'error_condition': 'not-encrypted-for-this-device',
-                'error_type': 'Decryption',
-                'is_ephemeral': true,
-                'is_error': true,
-                'type': 'error'
-            });
-        } else {
-            // https://xmpp.org/extensions/xep-0384.html#usecases-receiving
-            if (attrs.encrypted.prekey === true) {
-                return decryptPrekeyWhisperMessage(attrs);
-            } else {
-                return decryptWhisperMessage(attrs);
-            }
-        }
-    } else {
+/**
+ * Hook handler for { @link parseMessage } and { @link parseMUCMessage }, which
+ * parses the passed in `message` stanza for OMEMO attributes and then sets
+ * them on the attrs object.
+ * @param { XMLElement } stanza - The message stanza
+ * @param { (MUCMessageAttributes|MessageAttributes) } attrs
+ * @returns (MUCMessageAttributes|MessageAttributes)
+ */
+export async function parseEncryptedMessage (stanza, attrs) {
+    if (api.settings.get('clear_cache_on_logout') ||
+            !attrs.is_encrypted ||
+            attrs.encryption_namespace !== Strophe.NS.OMEMO) {
         return attrs;
     }
+    const encrypted_el = sizzle(`encrypted[xmlns="${Strophe.NS.OMEMO}"]`, stanza).pop();
+    const header = encrypted_el.querySelector('header');
+    attrs.encrypted = { 'device_id': header.getAttribute('sid') };
+
+    const device_id = await api.omemo?.getDeviceID();
+    const key = device_id && sizzle(`key[rid="${device_id}"]`, encrypted_el).pop();
+    if (key) {
+        Object.assign(attrs.encrypted, {
+            'iv': header.querySelector('iv').textContent,
+            'key': key.textContent,
+            'payload': encrypted_el.querySelector('payload')?.textContent || null,
+            'prekey': ['true', '1'].includes(key.getAttribute('prekey'))
+        });
+    } else {
+        return Object.assign(attrs, {
+            'error_condition': 'not-encrypted-for-this-device',
+            'error_type': 'Decryption',
+            'is_ephemeral': true,
+            'is_error': true,
+            'type': 'error'
+        });
+    }
+    // https://xmpp.org/extensions/xep-0384.html#usecases-receiving
+    if (attrs.encrypted.prekey === true) {
+        return decryptPrekeyWhisperMessage(attrs);
+    } else {
+        return decryptWhisperMessage(attrs);
+    }
 }
 
 export function onChatBoxesInitialized () {
@@ -499,7 +520,7 @@ function updateBundleFromStanza (stanza) {
     const jid = stanza.getAttribute('from');
     const bundle_el = sizzle(`item > bundle`, items_el).pop();
     const devicelist = _converse.devicelists.getDeviceList(jid);
-    const device = devicelist.devices.get(device_id) || devicelist.devices.create({ 'id': device_id, 'jid': jid });
+    const device = devicelist.devices.get(device_id) || devicelist.devices.create({ 'id': device_id, jid });
     device.save({ 'bundle': parseBundle(bundle_el) });
 }
 
@@ -566,11 +587,21 @@ export function restoreOMEMOSession () {
 }
 
 function fetchDeviceLists () {
-    return new Promise((success, error) => _converse.devicelists.fetch({ success, 'error': (m, e) => error(e) }));
+    _converse.devicelists = new _converse.DeviceLists();
+    const id = `converse.devicelists-${_converse.bare_jid}`;
+    initStorage(_converse.devicelists, id);
+    return new Promise(resolve => {
+        _converse.devicelists.fetch({
+            'success': resolve,
+            'error': (m, e) => {
+                log.error(e);
+                resolve();
+            }
+        })
+    });
 }
 
 async function fetchOwnDevices () {
-    await fetchDeviceLists();
     let own_devicelist = _converse.devicelists.get(_converse.bare_jid);
     if (own_devicelist) {
         own_devicelist.fetchDevices();
@@ -585,10 +616,8 @@ export async function initOMEMO () {
         log.warn('Not initializing OMEMO, since this browser is not trusted or clear_cache_on_logout is set to true');
         return;
     }
-    _converse.devicelists = new _converse.DeviceLists();
-    const id = `converse.devicelists-${_converse.bare_jid}`;
-    initStorage(_converse.devicelists, id);
     try {
+        await fetchDeviceLists();
         await fetchOwnDevices();
         await restoreOMEMOSession();
         await _converse.omemo_store.publishBundle();