From ca02bdcb61e351f42713841b7286bdcf992e1190 Mon Sep 17 00:00:00 2001 From: JC Brand Date: Sat, 30 Oct 2021 20:53:26 +0200 Subject: [PATCH] Bugfix. Use real JID when setting up a device session in a MUC Thanks to @orbitz, see: https://github.com/conversejs/converse.js/issues/1481#issuecomment-509183431 Updates #1481 --- CHANGES.md | 3 +- karma.conf.js | 1 + src/plugins/omemo/tests/muc.js | 478 +++++++++++++++++++++++++++++++ src/plugins/omemo/tests/omemo.js | 446 +--------------------------- src/plugins/omemo/utils.js | 39 +-- 5 files changed, 504 insertions(+), 463 deletions(-) create mode 100644 src/plugins/omemo/tests/muc.js diff --git a/CHANGES.md b/CHANGES.md index 44176df27..c14fc6392 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -2,8 +2,10 @@ ## 9.0.0 (Unreleased) +- Use more specific types for form fields based on XEP-0122 - Fix trimming of chats in overlayed view mode - #2647: Singleton mode doesn't work +- OMEMO bugfix: Always create device session based on real JID. - Emit a `change` event when a configuration setting changes - 3 New configuration settings: @@ -18,7 +20,6 @@ Three config settings have been obsoleted: - show_images_inline - muc_show_ogp_unfurls -- Use more specific types for form fields based on XEP-0122 ### Breaking Changes diff --git a/karma.conf.js b/karma.conf.js index 40701141f..a2d59f99c 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -93,6 +93,7 @@ module.exports = function(config) { { pattern: "src/plugins/notifications/tests/notification.js", type: 'module' }, { pattern: "src/plugins/omemo/tests/media-sharing.js", type: 'module' }, { pattern: "src/plugins/omemo/tests/omemo.js", type: 'module' }, + { pattern: "src/plugins/omemo/tests/muc.js", type: 'module' }, { pattern: "src/plugins/push/tests/push.js", type: 'module' }, { pattern: "src/plugins/register/tests/register.js", type: 'module' }, { pattern: "src/plugins/rootview/tests/root.js", type: 'module' }, diff --git a/src/plugins/omemo/tests/muc.js b/src/plugins/omemo/tests/muc.js new file mode 100644 index 000000000..4b258fd66 --- /dev/null +++ b/src/plugins/omemo/tests/muc.js @@ -0,0 +1,478 @@ +/*global mock, converse */ + +const { $iq, $msg, $pres, Strophe, omemo } = converse.env; +const u = converse.env.utils; + +describe("The OMEMO module", function() { + + it("enables encrypted groupchat messages to be sent and received", + mock.initConverse(['chatBoxesFetched'], {}, async function (_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' + ]; + const muc_jid = 'lounge@montague.lit'; + await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo', features); + const view = _converse.chatboxviews.get('lounge@montague.lit'); + await u.waitUntil(() => mock.initializedOMEMO(_converse)); + + const toolbar = await u.waitUntil(() => view.querySelector('.chat-toolbar')); + const el = await u.waitUntil(() => toolbar.querySelector('.toggle-omemo')); + el.click(); + expect(view.model.get('omemo_active')).toBe(true); + + // newguy enters the room + const contact_jid = 'newguy@montague.lit'; + let stanza = $pres({ + 'to': 'romeo@montague.lit/orchard', + 'from': 'lounge@montague.lit/newguy' + }) + .c('x', {xmlns: Strophe.NS.MUC_USER}) + .c('item', { + 'affiliation': 'none', + 'jid': 'newguy@montague.lit/_converse.js-290929789', + 'role': 'participant' + }).tree(); + _converse.connection._dataRecv(mock.createRequest(stanza)); + + // Wait for Converse to fetch newguy's device list + let iq_stanza = await u.waitUntil(() => mock.deviceListFetched(_converse, contact_jid)); + expect(Strophe.serialize(iq_stanza)).toBe( + ``+ + ``+ + ``+ + ``+ + ``); + + // The server returns his device list + stanza = $iq({ + 'from': contact_jid, + 'id': iq_stanza.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(mock.createRequest(stanza)); + await u.waitUntil(() => _converse.omemo_store); + expect(_converse.devicelists.length).toBe(2); + + await u.waitUntil(() => mock.deviceListFetched(_converse, contact_jid)); + const devicelist = _converse.devicelists.get(contact_jid); + expect(devicelist.devices.length).toBe(1); + expect(devicelist.devices.at(0).get('id')).toBe('4e30f35051b7b8b42abe083742187228'); + expect(view.model.get('omemo_active')).toBe(true); + + const icon = toolbar.querySelector('.toggle-omemo converse-icon'); + expect(u.hasClass('fa-unlock', icon)).toBe(false); + expect(u.hasClass('fa-lock', icon)).toBe(true); + + const textarea = view.querySelector('.chat-textarea'); + textarea.value = 'This message will be encrypted'; + const message_form = view.querySelector('converse-muc-message-form'); + message_form.onKeyDown({ + target: textarea, + preventDefault: function preventDefault () {}, + keyCode: 13 // Enter + }); + iq_stanza = await u.waitUntil(() => mock.bundleFetched(_converse, contact_jid, '4e30f35051b7b8b42abe083742187228'), 1000); + console.log("Bundle fetched 4e30f35051b7b8b42abe083742187228"); + stanza = $iq({ + 'from': contact_jid, + 'id': iq_stanza.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"}) + .c('item') + .c('bundle', {'xmlns': 'eu.siacs.conversations.axolotl'}) + .c('signedPreKeyPublic', {'signedPreKeyId': '4223'}).t(btoa('1111')).up() + .c('signedPreKeySignature').t(btoa('2222')).up() + .c('identityKey').t(btoa('3333')).up() + .c('prekeys') + .c('preKeyPublic', {'preKeyId': '1'}).t(btoa('1001')).up() + .c('preKeyPublic', {'preKeyId': '2'}).t(btoa('1002')).up() + .c('preKeyPublic', {'preKeyId': '3'}).t(btoa('1003')); + _converse.connection._dataRecv(mock.createRequest(stanza)); + + iq_stanza = await u.waitUntil(() => mock.bundleFetched(_converse, _converse.bare_jid, '482886413b977930064a5888b92134fe'), 1000); + console.log("Bundle fetched 482886413b977930064a5888b92134fe"); + stanza = $iq({ + 'from': _converse.bare_jid, + 'id': iq_stanza.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')); + + spyOn(_converse.connection, 'send'); + _converse.connection._dataRecv(mock.createRequest(stanza)); + await u.waitUntil(() => _converse.connection.send.calls.count(), 1000); + const sent_stanza = _converse.connection.send.calls.all()[0].args[0]; + + expect(Strophe.serialize(sent_stanza)).toBe( + ``+ + `This is an OMEMO encrypted message which your client doesn’t seem to support. Find more information on https://conversations.im/omemo`+ + ``+ + `
`+ + `YzFwaDNSNzNYNw==`+ + `YzFwaDNSNzNYNw==`+ + `${sent_stanza.querySelector("iv").textContent}`+ + `
`+ + `${sent_stanza.querySelector("payload").textContent}`+ + `
`+ + ``+ + ``+ + `
`); + + // Test reception of an encrypted message + const obj = await omemo.encryptMessage('This is an encrypted message from the contact') + // XXX: Normally the key will be encrypted via libsignal. + // However, we're mocking libsignal in the tests, so we include it as plaintext in the message. + stanza = $msg({ + 'from': `${muc_jid}/newguy`, + 'to': _converse.connection.jid, + 'type': 'groupchat', + 'id': _converse.connection.getUniqueId() + }).c('body').t('This is a fallback message').up() + .c('encrypted', {'xmlns': Strophe.NS.OMEMO}) + .c('header', {'sid': '555'}) + .c('key', {'rid': _converse.omemo_store.get('device_id')}).t(u.arrayBufferToBase64(obj.key_and_tag)).up() + .c('iv').t(obj.iv) + .up().up() + .c('payload').t(obj.payload); + _converse.connection._dataRecv(mock.createRequest(stanza)); + await new Promise(resolve => view.model.messages.once('rendered', resolve)); + expect(view.model.messages.length).toBe(2); + expect(view.querySelectorAll('.chat-msg__body')[1].textContent.trim()) + .toBe('This is an encrypted message from the contact'); + + expect(_converse.devicelists.length).toBe(2); + expect(_converse.devicelists.at(0).get('jid')).toBe(_converse.bare_jid); + expect(_converse.devicelists.at(1).get('jid')).toBe(contact_jid); + })); + + it("gracefully handles auth errors when trying to send encrypted groupchat messages", + mock.initConverse(['chatBoxesFetched'], {}, async function (_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 mock.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'romeo', features); + const view = _converse.chatboxviews.get('lounge@montague.lit'); + await u.waitUntil(() => mock.initializedOMEMO(_converse)); + + const contact_jid = 'newguy@montague.lit'; + let stanza = $pres({ + 'to': 'romeo@montague.lit/orchard', + 'from': 'lounge@montague.lit/newguy' + }) + .c('x', {xmlns: Strophe.NS.MUC_USER}) + .c('item', { + 'affiliation': 'none', + 'jid': 'newguy@montague.lit/_converse.js-290929789', + 'role': 'participant' + }).tree(); + _converse.connection._dataRecv(mock.createRequest(stanza)); + + const toolbar = await u.waitUntil(() => view.querySelector('.chat-toolbar')); + const toggle = await u.waitUntil(() => toolbar.querySelector('.toggle-omemo')); + toggle.click(); + expect(view.model.get('omemo_active')).toBe(true); + expect(view.model.get('omemo_supported')).toBe(true); + + const textarea = await u.waitUntil(() => view.querySelector('.chat-textarea')); + textarea.value = 'This message will be encrypted'; + const message_form = view.querySelector('converse-muc-message-form'); + message_form.onKeyDown({ + target: textarea, + preventDefault: function preventDefault () {}, + keyCode: 13 // Enter + }); + let iq_stanza = await u.waitUntil(() => mock.deviceListFetched(_converse, contact_jid)); + expect(Strophe.serialize(iq_stanza)).toBe( + ``+ + ``+ + ``+ + ``+ + ``); + + stanza = $iq({ + 'from': contact_jid, + 'id': iq_stanza.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(mock.createRequest(stanza)); + await u.waitUntil(() => _converse.omemo_store); + expect(_converse.devicelists.length).toBe(2); + + const devicelist = _converse.devicelists.get(contact_jid); + await u.waitUntil(() => mock.deviceListFetched(_converse, contact_jid)); + expect(devicelist.devices.length).toBe(1); + expect(devicelist.devices.at(0).get('id')).toBe('4e30f35051b7b8b42abe083742187228'); + + iq_stanza = await u.waitUntil(() => mock.bundleFetched(_converse, _converse.bare_jid, '482886413b977930064a5888b92134fe')); + stanza = $iq({ + 'from': _converse.bare_jid, + 'id': iq_stanza.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 u.waitUntil(() => mock.bundleFetched(_converse, contact_jid, '4e30f35051b7b8b42abe083742187228')); + + /* + * + * + * + * + * + * + * + * + */ + stanza = $iq({ + 'from': contact_jid, + 'id': iq_stanza.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(mock.createRequest(stanza)); + + await u.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@montague.lit 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.querySelector('.chat-textarea').value).toBe('This message will be encrypted'); + })); + + + it("adds a toolbar button for starting an encrypted groupchat session", + mock.initConverse(['chatBoxesFetched'], {}, async function (_converse) { + + await mock.waitForRoster(_converse, 'current', 0); + await mock.waitUntilDiscoConfirmed( + _converse, _converse.bare_jid, + [{'category': 'pubsub', 'type': 'pep'}], + ['http://jabber.org/protocol/pubsub#publish-options'] + ); + + // 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 mock.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'romeo', features); + const view = _converse.chatboxviews.get('lounge@montague.lit'); + await u.waitUntil(() => mock.initializedOMEMO(_converse)); + + const toolbar = await u.waitUntil(() => view.querySelector('.chat-toolbar')); + let toggle = await u.waitUntil(() => toolbar.querySelector('.toggle-omemo')); + expect(view.model.get('omemo_active')).toBe(undefined); + expect(view.model.get('omemo_supported')).toBe(true); + await u.waitUntil(() => toggle.dataset.disabled === "false"); + + let icon = toolbar.querySelector('.toggle-omemo converse-icon'); + expect(u.hasClass('fa-unlock', icon)).toBe(true); + expect(u.hasClass('fa-lock', icon)).toBe(false); + + toggle.click(); + toggle = toolbar.querySelector('.toggle-omemo'); + expect(toggle.dataset.disabled).toBe("false"); + expect(view.model.get('omemo_active')).toBe(true); + expect(view.model.get('omemo_supported')).toBe(true); + + await u.waitUntil(() => !u.hasClass('fa-unlock', toolbar.querySelector('.toggle-omemo converse-icon'))); + expect(u.hasClass('fa-lock', toolbar.querySelector('.toggle-omemo converse-icon'))).toBe(true); + + let contact_jid = 'newguy@montague.lit'; + let stanza = $pres({ + to: 'romeo@montague.lit/orchard', + from: 'lounge@montague.lit/newguy' + }) + .c('x', {xmlns: Strophe.NS.MUC_USER}) + .c('item', { + 'affiliation': 'none', + 'jid': 'newguy@montague.lit/_converse.js-290929789', + 'role': 'participant' + }).tree(); + _converse.connection._dataRecv(mock.createRequest(stanza)); + + let iq_stanza = await u.waitUntil(() => mock.deviceListFetched(_converse, contact_jid)); + expect(Strophe.serialize(iq_stanza)).toBe( + ``+ + ``+ + ``+ + ``+ + ``); + + stanza = $iq({ + 'from': contact_jid, + 'id': iq_stanza.getAttribute('id'), + 'to': _converse.bare_jid, + 'type': 'result', + }).c('pubsub', {'xmlns': "http://jabber.org/protocol/pubsub"}) + .c('items', {'node': "eu.siacs.conversations.axolotl.devicelist"}) + .c('item', {'xmlns': "http://jabber.org/protocol/pubsub"}) // TODO: must have an id attribute + .c('list', {'xmlns': "eu.siacs.conversations.axolotl"}) + .c('device', {'id': '4e30f35051b7b8b42abe083742187228'}).up() + .c('device', {'id': 'ae890ac52d0df67ed7cfdf51b644e901'}); + _converse.connection._dataRecv(mock.createRequest(stanza)); + await u.waitUntil(() => _converse.omemo_store); + expect(_converse.devicelists.length).toBe(2); + + await u.waitUntil(() => mock.deviceListFetched(_converse, contact_jid)); + const devicelist = _converse.devicelists.get(contact_jid); + expect(devicelist.devices.length).toBe(2); + expect(devicelist.devices.at(0).get('id')).toBe('4e30f35051b7b8b42abe083742187228'); + expect(devicelist.devices.at(1).get('id')).toBe('ae890ac52d0df67ed7cfdf51b644e901'); + + expect(view.model.get('omemo_active')).toBe(true); + toggle = toolbar.querySelector('.toggle-omemo'); + expect(toggle === null).toBe(false); + expect(toggle.dataset.disabled).toBe("false"); + expect(view.model.get('omemo_supported')).toBe(true); + + await u.waitUntil(() => !u.hasClass('fa-unlock', toolbar.querySelector('.toggle-omemo converse-icon'))); + expect(u.hasClass('fa-lock', toolbar.querySelector('.toggle-omemo converse-icon'))).toBe(true); + + // Test that the button gets disabled when the room becomes + // anonymous or semi-anonymous + view.model.features.save({'nonanonymous': false, 'semianonymous': true}); + await u.waitUntil(() => !view.model.get('omemo_supported')); + await u.waitUntil(() => view.querySelector('.toggle-omemo').dataset.disabled === "true"); + + view.model.features.save({'nonanonymous': true, 'semianonymous': false}); + await u.waitUntil(() => view.model.get('omemo_supported')); + await u.waitUntil(() => view.querySelector('.toggle-omemo') !== null); + expect(u.hasClass('fa-unlock', toolbar.querySelector('.toggle-omemo converse-icon'))).toBe(true); + expect(u.hasClass('fa-lock', toolbar.querySelector('.toggle-omemo converse-icon'))).toBe(false); + expect(view.querySelector('.toggle-omemo').dataset.disabled).toBe("false"); + + // Test that the button gets disabled when the room becomes open + view.model.features.save({'membersonly': false, 'open': true}); + await u.waitUntil(() => !view.model.get('omemo_supported')); + await u.waitUntil(() => view.querySelector('.toggle-omemo').dataset.disabled === "true"); + + view.model.features.save({'membersonly': true, 'open': false}); + await u.waitUntil(() => view.model.get('omemo_supported')); + await u.waitUntil(() => view.querySelector('.toggle-omemo').dataset.disabled === "false"); + + expect(u.hasClass('fa-unlock', view.querySelector('.toggle-omemo converse-icon'))).toBe(true); + expect(u.hasClass('fa-lock', view.querySelector('.toggle-omemo converse-icon'))).toBe(false); + + expect(view.model.get('omemo_supported')).toBe(true); + expect(view.model.get('omemo_active')).toBe(false); + + view.querySelector('.toggle-omemo').click(); + expect(view.model.get('omemo_active')).toBe(true); + + // Someone enters the room who doesn't have OMEMO support, while we + // have OMEMO activated... + contact_jid = 'oldguy@montague.lit'; + stanza = $pres({ + to: 'romeo@montague.lit/orchard', + from: 'lounge@montague.lit/oldguy' + }) + .c('x', {xmlns: Strophe.NS.MUC_USER}) + .c('item', { + 'affiliation': 'none', + 'jid': `${contact_jid}/_converse.js-290929788`, + 'role': 'participant' + }).tree(); + _converse.connection._dataRecv(mock.createRequest(stanza)); + iq_stanza = await u.waitUntil(() => mock.deviceListFetched(_converse, contact_jid)); + expect(Strophe.serialize(iq_stanza)).toBe( + ``+ + ``+ + ``+ + ``+ + ``); + + stanza = $iq({ + 'from': contact_jid, + 'id': iq_stanza.getAttribute('id'), + 'to': _converse.bare_jid, + 'type': 'error' + }).c('error', {'type': 'cancel'}) + .c('item-not-found', {'xmlns': "urn:ietf:params:xml:ns:xmpp-stanzas"}); + _converse.connection._dataRecv(mock.createRequest(stanza)); + + await u.waitUntil(() => !view.model.get('omemo_supported')); + await u.waitUntil(() => view.querySelector('.chat-error .chat-info__message')?.textContent.trim() === + "oldguy doesn't appear to have a client that supports OMEMO. "+ + "Encrypted chat will no longer be possible in this grouchat." + ); + + await u.waitUntil(() => toolbar.querySelector('.toggle-omemo').dataset.disabled === "true"); + icon = view.querySelector('.toggle-omemo converse-icon'); + expect(u.hasClass('fa-unlock', icon)).toBe(true); + expect(u.hasClass('fa-lock', icon)).toBe(false); + expect(toolbar.querySelector('.toggle-omemo').title).toBe('This groupchat needs to be members-only and non-anonymous in order to support OMEMO encrypted messages'); + })); +}); diff --git a/src/plugins/omemo/tests/omemo.js b/src/plugins/omemo/tests/omemo.js index 7ab8d3998..e25ace1bc 100644 --- a/src/plugins/omemo/tests/omemo.js +++ b/src/plugins/omemo/tests/omemo.js @@ -1,6 +1,6 @@ /*global mock, converse */ -const { $iq, $pres, $msg, omemo, Strophe } = converse.env; +const { $iq, $msg, omemo, Strophe } = converse.env; const u = converse.env.utils; describe("The OMEMO module", function() { @@ -152,152 +152,6 @@ describe("The OMEMO module", function() { .toBe('Another received encrypted message without fallback'); })); - it("enables encrypted groupchat messages to be sent and received", - mock.initConverse(['chatBoxesFetched'], {}, async function (_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 mock.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'romeo', features); - const view = _converse.chatboxviews.get('lounge@montague.lit'); - await u.waitUntil(() => mock.initializedOMEMO(_converse)); - - const toolbar = await u.waitUntil(() => view.querySelector('.chat-toolbar')); - const el = await u.waitUntil(() => toolbar.querySelector('.toggle-omemo')); - el.click(); - expect(view.model.get('omemo_active')).toBe(true); - - // newguy enters the room - const contact_jid = 'newguy@montague.lit'; - let stanza = $pres({ - 'to': 'romeo@montague.lit/orchard', - 'from': 'lounge@montague.lit/newguy' - }) - .c('x', {xmlns: Strophe.NS.MUC_USER}) - .c('item', { - 'affiliation': 'none', - 'jid': 'newguy@montague.lit/_converse.js-290929789', - 'role': 'participant' - }).tree(); - _converse.connection._dataRecv(mock.createRequest(stanza)); - - // Wait for Converse to fetch newguy's device list - let iq_stanza = await u.waitUntil(() => mock.deviceListFetched(_converse, contact_jid)); - expect(Strophe.serialize(iq_stanza)).toBe( - ``+ - ``+ - ``+ - ``+ - ``); - - // The server returns his device list - stanza = $iq({ - 'from': contact_jid, - 'id': iq_stanza.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(mock.createRequest(stanza)); - await u.waitUntil(() => _converse.omemo_store); - expect(_converse.devicelists.length).toBe(2); - - await u.waitUntil(() => mock.deviceListFetched(_converse, contact_jid)); - const devicelist = _converse.devicelists.get(contact_jid); - expect(devicelist.devices.length).toBe(1); - expect(devicelist.devices.at(0).get('id')).toBe('4e30f35051b7b8b42abe083742187228'); - expect(view.model.get('omemo_active')).toBe(true); - - const icon = toolbar.querySelector('.toggle-omemo converse-icon'); - expect(u.hasClass('fa-unlock', icon)).toBe(false); - expect(u.hasClass('fa-lock', icon)).toBe(true); - - const textarea = view.querySelector('.chat-textarea'); - textarea.value = 'This message will be encrypted'; - const message_form = view.querySelector('converse-muc-message-form'); - message_form.onKeyDown({ - target: textarea, - preventDefault: function preventDefault () {}, - keyCode: 13 // Enter - }); - iq_stanza = await u.waitUntil(() => mock.bundleFetched(_converse, contact_jid, '4e30f35051b7b8b42abe083742187228'), 1000); - console.log("Bundle fetched 4e30f35051b7b8b42abe083742187228"); - stanza = $iq({ - 'from': contact_jid, - 'id': iq_stanza.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"}) - .c('item') - .c('bundle', {'xmlns': 'eu.siacs.conversations.axolotl'}) - .c('signedPreKeyPublic', {'signedPreKeyId': '4223'}).t(btoa('1111')).up() - .c('signedPreKeySignature').t(btoa('2222')).up() - .c('identityKey').t(btoa('3333')).up() - .c('prekeys') - .c('preKeyPublic', {'preKeyId': '1'}).t(btoa('1001')).up() - .c('preKeyPublic', {'preKeyId': '2'}).t(btoa('1002')).up() - .c('preKeyPublic', {'preKeyId': '3'}).t(btoa('1003')); - _converse.connection._dataRecv(mock.createRequest(stanza)); - - iq_stanza = await u.waitUntil(() => mock.bundleFetched(_converse, _converse.bare_jid, '482886413b977930064a5888b92134fe'), 1000); - console.log("Bundle fetched 482886413b977930064a5888b92134fe"); - stanza = $iq({ - 'from': _converse.bare_jid, - 'id': iq_stanza.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')); - - spyOn(_converse.connection, 'send'); - _converse.connection._dataRecv(mock.createRequest(stanza)); - await u.waitUntil(() => _converse.connection.send.calls.count(), 1000); - const sent_stanza = _converse.connection.send.calls.all()[0].args[0]; - - expect(Strophe.serialize(sent_stanza)).toBe( - ``+ - `This is an OMEMO encrypted message which your client doesn’t seem to support. Find more information on https://conversations.im/omemo`+ - ``+ - `
`+ - `YzFwaDNSNzNYNw==`+ - `YzFwaDNSNzNYNw==`+ - `${sent_stanza.querySelector("iv").textContent}`+ - `
`+ - `${sent_stanza.querySelector("payload").textContent}`+ - `
`+ - ``+ - ``+ - `
`); - })); - it("will create a new device based on a received carbon message", mock.initConverse(['chatBoxesFetched'], {}, async function (_converse) { @@ -406,133 +260,6 @@ describe("The OMEMO module", function() { ``); })); - it("gracefully handles auth errors when trying to send encrypted groupchat messages", - mock.initConverse(['chatBoxesFetched'], {}, async function (_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 mock.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'romeo', features); - const view = _converse.chatboxviews.get('lounge@montague.lit'); - await u.waitUntil(() => mock.initializedOMEMO(_converse)); - - const contact_jid = 'newguy@montague.lit'; - let stanza = $pres({ - 'to': 'romeo@montague.lit/orchard', - 'from': 'lounge@montague.lit/newguy' - }) - .c('x', {xmlns: Strophe.NS.MUC_USER}) - .c('item', { - 'affiliation': 'none', - 'jid': 'newguy@montague.lit/_converse.js-290929789', - 'role': 'participant' - }).tree(); - _converse.connection._dataRecv(mock.createRequest(stanza)); - - const toolbar = await u.waitUntil(() => view.querySelector('.chat-toolbar')); - const toggle = await u.waitUntil(() => toolbar.querySelector('.toggle-omemo')); - toggle.click(); - expect(view.model.get('omemo_active')).toBe(true); - expect(view.model.get('omemo_supported')).toBe(true); - - const textarea = await u.waitUntil(() => view.querySelector('.chat-textarea')); - textarea.value = 'This message will be encrypted'; - const message_form = view.querySelector('converse-muc-message-form'); - message_form.onKeyDown({ - target: textarea, - preventDefault: function preventDefault () {}, - keyCode: 13 // Enter - }); - let iq_stanza = await u.waitUntil(() => mock.deviceListFetched(_converse, contact_jid)); - expect(Strophe.serialize(iq_stanza)).toBe( - ``+ - ``+ - ``+ - ``+ - ``); - - stanza = $iq({ - 'from': contact_jid, - 'id': iq_stanza.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(mock.createRequest(stanza)); - await u.waitUntil(() => _converse.omemo_store); - expect(_converse.devicelists.length).toBe(2); - - const devicelist = _converse.devicelists.get(contact_jid); - await u.waitUntil(() => mock.deviceListFetched(_converse, contact_jid)); - expect(devicelist.devices.length).toBe(1); - expect(devicelist.devices.at(0).get('id')).toBe('4e30f35051b7b8b42abe083742187228'); - - iq_stanza = await u.waitUntil(() => mock.bundleFetched(_converse, _converse.bare_jid, '482886413b977930064a5888b92134fe')); - stanza = $iq({ - 'from': _converse.bare_jid, - 'id': iq_stanza.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 u.waitUntil(() => mock.bundleFetched(_converse, contact_jid, '4e30f35051b7b8b42abe083742187228')); - - /* - * - * - * - * - * - * - * - * - */ - stanza = $iq({ - 'from': contact_jid, - 'id': iq_stanza.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(mock.createRequest(stanza)); - - await u.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@montague.lit 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.querySelector('.chat-textarea').value).toBe('This message will be encrypted'); - })); - it("can receive a PreKeySignalMessage", mock.initConverse(['chatBoxesFetched'], {}, async function (_converse) { @@ -1178,177 +905,6 @@ describe("The OMEMO module", function() { expect(u.hasClass('fa-unlock', icon)).toBe(true); })); - it("adds a toolbar button for starting an encrypted groupchat session", - mock.initConverse(['chatBoxesFetched'], {}, async function (_converse) { - - await mock.waitForRoster(_converse, 'current', 0); - await mock.waitUntilDiscoConfirmed( - _converse, _converse.bare_jid, - [{'category': 'pubsub', 'type': 'pep'}], - ['http://jabber.org/protocol/pubsub#publish-options'] - ); - - // 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 mock.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'romeo', features); - const view = _converse.chatboxviews.get('lounge@montague.lit'); - await u.waitUntil(() => mock.initializedOMEMO(_converse)); - - const toolbar = await u.waitUntil(() => view.querySelector('.chat-toolbar')); - let toggle = await u.waitUntil(() => toolbar.querySelector('.toggle-omemo')); - expect(view.model.get('omemo_active')).toBe(undefined); - expect(view.model.get('omemo_supported')).toBe(true); - await u.waitUntil(() => toggle.dataset.disabled === "false"); - - let icon = toolbar.querySelector('.toggle-omemo converse-icon'); - expect(u.hasClass('fa-unlock', icon)).toBe(true); - expect(u.hasClass('fa-lock', icon)).toBe(false); - - toggle.click(); - toggle = toolbar.querySelector('.toggle-omemo'); - expect(toggle.dataset.disabled).toBe("false"); - expect(view.model.get('omemo_active')).toBe(true); - expect(view.model.get('omemo_supported')).toBe(true); - - await u.waitUntil(() => !u.hasClass('fa-unlock', toolbar.querySelector('.toggle-omemo converse-icon'))); - expect(u.hasClass('fa-lock', toolbar.querySelector('.toggle-omemo converse-icon'))).toBe(true); - - let contact_jid = 'newguy@montague.lit'; - let stanza = $pres({ - to: 'romeo@montague.lit/orchard', - from: 'lounge@montague.lit/newguy' - }) - .c('x', {xmlns: Strophe.NS.MUC_USER}) - .c('item', { - 'affiliation': 'none', - 'jid': 'newguy@montague.lit/_converse.js-290929789', - 'role': 'participant' - }).tree(); - _converse.connection._dataRecv(mock.createRequest(stanza)); - - let iq_stanza = await u.waitUntil(() => mock.deviceListFetched(_converse, contact_jid)); - expect(Strophe.serialize(iq_stanza)).toBe( - ``+ - ``+ - ``+ - ``+ - ``); - - stanza = $iq({ - 'from': contact_jid, - 'id': iq_stanza.getAttribute('id'), - 'to': _converse.bare_jid, - 'type': 'result', - }).c('pubsub', {'xmlns': "http://jabber.org/protocol/pubsub"}) - .c('items', {'node': "eu.siacs.conversations.axolotl.devicelist"}) - .c('item', {'xmlns': "http://jabber.org/protocol/pubsub"}) // TODO: must have an id attribute - .c('list', {'xmlns': "eu.siacs.conversations.axolotl"}) - .c('device', {'id': '4e30f35051b7b8b42abe083742187228'}).up() - .c('device', {'id': 'ae890ac52d0df67ed7cfdf51b644e901'}); - _converse.connection._dataRecv(mock.createRequest(stanza)); - await u.waitUntil(() => _converse.omemo_store); - expect(_converse.devicelists.length).toBe(2); - - await u.waitUntil(() => mock.deviceListFetched(_converse, contact_jid)); - const devicelist = _converse.devicelists.get(contact_jid); - expect(devicelist.devices.length).toBe(2); - expect(devicelist.devices.at(0).get('id')).toBe('4e30f35051b7b8b42abe083742187228'); - expect(devicelist.devices.at(1).get('id')).toBe('ae890ac52d0df67ed7cfdf51b644e901'); - - expect(view.model.get('omemo_active')).toBe(true); - toggle = toolbar.querySelector('.toggle-omemo'); - expect(toggle === null).toBe(false); - expect(toggle.dataset.disabled).toBe("false"); - expect(view.model.get('omemo_supported')).toBe(true); - - await u.waitUntil(() => !u.hasClass('fa-unlock', toolbar.querySelector('.toggle-omemo converse-icon'))); - expect(u.hasClass('fa-lock', toolbar.querySelector('.toggle-omemo converse-icon'))).toBe(true); - - // Test that the button gets disabled when the room becomes - // anonymous or semi-anonymous - view.model.features.save({'nonanonymous': false, 'semianonymous': true}); - await u.waitUntil(() => !view.model.get('omemo_supported')); - await u.waitUntil(() => view.querySelector('.toggle-omemo').dataset.disabled === "true"); - - view.model.features.save({'nonanonymous': true, 'semianonymous': false}); - await u.waitUntil(() => view.model.get('omemo_supported')); - await u.waitUntil(() => view.querySelector('.toggle-omemo') !== null); - expect(u.hasClass('fa-unlock', toolbar.querySelector('.toggle-omemo converse-icon'))).toBe(true); - expect(u.hasClass('fa-lock', toolbar.querySelector('.toggle-omemo converse-icon'))).toBe(false); - expect(view.querySelector('.toggle-omemo').dataset.disabled).toBe("false"); - - // Test that the button gets disabled when the room becomes open - view.model.features.save({'membersonly': false, 'open': true}); - await u.waitUntil(() => !view.model.get('omemo_supported')); - await u.waitUntil(() => view.querySelector('.toggle-omemo').dataset.disabled === "true"); - - view.model.features.save({'membersonly': true, 'open': false}); - await u.waitUntil(() => view.model.get('omemo_supported')); - await u.waitUntil(() => view.querySelector('.toggle-omemo').dataset.disabled === "false"); - - expect(u.hasClass('fa-unlock', view.querySelector('.toggle-omemo converse-icon'))).toBe(true); - expect(u.hasClass('fa-lock', view.querySelector('.toggle-omemo converse-icon'))).toBe(false); - - expect(view.model.get('omemo_supported')).toBe(true); - expect(view.model.get('omemo_active')).toBe(false); - - view.querySelector('.toggle-omemo').click(); - expect(view.model.get('omemo_active')).toBe(true); - - // Someone enters the room who doesn't have OMEMO support, while we - // have OMEMO activated... - contact_jid = 'oldguy@montague.lit'; - stanza = $pres({ - to: 'romeo@montague.lit/orchard', - from: 'lounge@montague.lit/oldguy' - }) - .c('x', {xmlns: Strophe.NS.MUC_USER}) - .c('item', { - 'affiliation': 'none', - 'jid': `${contact_jid}/_converse.js-290929788`, - 'role': 'participant' - }).tree(); - _converse.connection._dataRecv(mock.createRequest(stanza)); - iq_stanza = await u.waitUntil(() => mock.deviceListFetched(_converse, contact_jid)); - expect(Strophe.serialize(iq_stanza)).toBe( - ``+ - ``+ - ``+ - ``+ - ``); - - stanza = $iq({ - 'from': contact_jid, - 'id': iq_stanza.getAttribute('id'), - 'to': _converse.bare_jid, - 'type': 'error' - }).c('error', {'type': 'cancel'}) - .c('item-not-found', {'xmlns': "urn:ietf:params:xml:ns:xmpp-stanzas"}); - _converse.connection._dataRecv(mock.createRequest(stanza)); - - await u.waitUntil(() => !view.model.get('omemo_supported')); - await u.waitUntil(() => view.querySelector('.chat-error .chat-info__message')?.textContent.trim() === - "oldguy doesn't appear to have a client that supports OMEMO. "+ - "Encrypted chat will no longer be possible in this grouchat." - ); - - await u.waitUntil(() => toolbar.querySelector('.toggle-omemo').dataset.disabled === "true"); - icon = view.querySelector('.toggle-omemo converse-icon'); - expect(u.hasClass('fa-unlock', icon)).toBe(true); - expect(u.hasClass('fa-lock', icon)).toBe(false); - expect(toolbar.querySelector('.toggle-omemo').title).toBe('This groupchat needs to be members-only and non-anonymous in order to support OMEMO encrypted messages'); - })); - - it("shows OMEMO device fingerprints in the user details modal", mock.initConverse(['chatBoxesFetched'], {}, async function (_converse) { diff --git a/src/plugins/omemo/utils.js b/src/plugins/omemo/utils.js index a000753d5..727532753 100644 --- a/src/plugins/omemo/utils.js +++ b/src/plugins/omemo/utils.js @@ -251,14 +251,30 @@ export function getSessionCipher (jid, id) { return new window.libsignal.SessionCipher(_converse.omemo_store, address); } +function getJIDForDecryption (attrs) { + const from_jid = attrs.from_muc ? attrs.from_real_jid : attrs.from; + if (!from_jid) { + Object.assign(attrs, { + 'error_text': __("Sorry, could not decrypt a received OMEMO message because we don't have the XMPP address for that user."), + 'error_type': 'Decryption', + 'is_ephemeral': false, + 'is_error': true, + 'type': 'error' + }); + throw new Error("Could not find JID to decrypt OMEMO message for"); + } + return from_jid; +} + async function handleDecryptedWhisperMessage (attrs, key_and_tag) { - const encrypted = attrs.encrypted; - const devicelist = _converse.devicelists.getDeviceList(attrs.from); + const from_jid = getJIDForDecryption(attrs); + const devicelist = _converse.devicelists.getDeviceList(from_jid); await devicelist._devices_promise; + const encrypted = attrs.encrypted; let device = devicelist.get(encrypted.device_id); if (!device) { - device = await devicelist.devices.create({ 'id': encrypted.device_id, 'jid': attrs.from }, { 'promise': true }); + device = await devicelist.devices.create({ 'id': encrypted.device_id, 'jid': from_jid }, { 'promise': true }); } if (encrypted.payload) { const key = key_and_tag.slice(0, 16); @@ -285,7 +301,8 @@ function getDecryptionErrorAttributes (e) { } async function decryptPrekeyWhisperMessage (attrs) { - const session_cipher = getSessionCipher(attrs.from, parseInt(attrs.encrypted.device_id, 10)); + const from_jid = getJIDForDecryption(attrs); + const session_cipher = getSessionCipher(from_jid, parseInt(attrs.encrypted.device_id, 10)); const key = base64ToArrayBuffer(attrs.encrypted.key); let key_and_tag; try { @@ -332,16 +349,7 @@ async function decryptPrekeyWhisperMessage (attrs) { } async function decryptWhisperMessage (attrs) { - const from_jid = attrs.from_muc ? attrs.from_real_jid : attrs.from; - if (!from_jid) { - Object.assign(attrs, { - 'error_text': __("Sorry, could not decrypt a received OMEMO message because we don't have the XMPP address for that user."), - 'error_type': 'Decryption', - 'is_ephemeral': false, - 'is_error': true, - 'type': 'error' - }); - } + const from_jid = getJIDForDecryption(attrs); const session_cipher = getSessionCipher(from_jid, parseInt(attrs.encrypted.device_id, 10)); const key = base64ToArrayBuffer(attrs.encrypted.key); try { @@ -432,9 +440,6 @@ export function generateDeviceID () { } async function buildSession (device) { - // TODO: check device-get('jid') versus the 'from' attribute which is used - // to build a session when receiving an encrypted message in a MUC. - // https://github.com/conversejs/converse.js/issues/1481#issuecomment-509183431 const address = new libsignal.SignalProtocolAddress(device.get('jid'), device.get('id')); const sessionBuilder = new libsignal.SessionBuilder(_converse.omemo_store, address); const prekey = device.getRandomPreKey();