UNPKG

converse.js

Version:
975 lines (874 loc) 59.5 kB
/*global mock, converse */ const { $iq, $msg, omemo, Strophe, sizzle, stx, u } = converse.env; describe("The OMEMO module", function() { beforeAll(() => jasmine.addMatchers({ toEqualStanza: jasmine.toEqualStanza })); it("adds methods for encrypting and decrypting messages via AES GCM", mock.initConverse(['chatBoxesFetched'], {}, async function (_converse) { const message = 'This message will be encrypted' await mock.waitForRoster(_converse, 'current', 1); const payload = await omemo.encryptMessage(message); const result = await omemo.decryptMessage(payload); expect(result).toBe(message); })); it("enables encrypted messages to be sent and received", mock.initConverse(['chatBoxesFetched'], {}, async function (_converse) { await mock.waitForRoster(_converse, 'current', 1); const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; await mock.initializedOMEMO(_converse); await mock.openChatBoxFor(_converse, contact_jid); await u.waitUntil(() => mock.deviceListFetched(_converse, contact_jid, ['555'])); await u.waitUntil(() => _converse.state.omemo_store); const devicelist = _converse.state.devicelists.get({'jid': contact_jid}); await u.waitUntil(() => devicelist.devices.length === 1); const view = _converse.chatboxviews.get(contact_jid); view.model.set('omemo_active', true); const textarea = view.querySelector('.chat-textarea'); textarea.value = 'This message will be encrypted'; const message_form = view.querySelector('converse-message-form'); message_form.onKeyDown({ target: textarea, preventDefault: function preventDefault () {}, key: "Enter", }); await u.waitUntil(() => mock.bundleFetched(_converse, { jid: contact_jid, device_id: '555', identity_key: '3333', signed_prekey_id: "4223", signed_prekey_public: "1111", signed_prekey_sig: "2222", prekeys: ['1001', '1002', '1003'], })); await u.waitUntil(() => mock.bundleFetched(_converse, { jid: _converse.bare_jid, device_id: "482886413b977930064a5888b92134fe", identity_key: '300000', signed_prekey_id: "4224", signed_prekey_public: "100000", signed_prekey_sig: "200000", prekeys: ["1991", "1992", "1993"], }) ); const sent_stanzas = _converse.api.connection.get().sent_stanzas; const sent_stanza = await u.waitUntil(() => sent_stanzas.filter((s) => sizzle('body', s).length).pop(), 1000); expect(sent_stanza).toEqualStanza( stx`<message from="romeo@montague.lit/orchard" id="${sent_stanza.getAttribute("id")}" to="mercutio@montague.lit" type="chat" xmlns="jabber:client"> <body>This is an OMEMO encrypted message which your client doesn’t seem to support. Find more information on https://conversations.im/omemo</body> <active xmlns="http://jabber.org/protocol/chatstates"/> <request xmlns="urn:xmpp:receipts"/> <origin-id id="${sent_stanza.getAttribute('id')}" xmlns="urn:xmpp:sid:0"/> <encrypted xmlns="eu.siacs.conversations.axolotl"> <header sid="123456789"> <key rid="482886413b977930064a5888b92134fe">YzFwaDNSNzNYNw==</key> <key rid="555">YzFwaDNSNzNYNw==</key> <iv>${sent_stanza.querySelector("iv").textContent}</iv> </header> <payload>${sent_stanza.querySelector("payload").textContent}</payload> </encrypted> <store xmlns="urn:xmpp:hints"/> <encryption namespace="eu.siacs.conversations.axolotl" xmlns="urn:xmpp:eme:0"/> </message>`); // Test reception of an encrypted message let 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. let stanza = stx`<message from="${contact_jid}" to="${_converse.api.connection.get().jid}" type="chat" id="${_converse.api.connection.get().getUniqueId()}" xmlns="jabber:client"> <body>This is a fallback message</body> <encrypted xmlns="${Strophe.NS.OMEMO}"> <header sid="555"> <key rid="${_converse.state.omemo_store.get('device_id')}">${u.arrayBufferToBase64(obj.key_and_tag)}</key> <iv>${obj.iv}</iv> </header> <payload>${obj.payload}</payload> </encrypted> </message>`; _converse.api.connection.get()._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'); // #1193 Check for a received message without <body> tag obj = await omemo.encryptMessage('Another received encrypted message without fallback') stanza = stx`<message from="${contact_jid}" to="${_converse.api.connection.get().jid}" type="chat" xmlns="jabber:client" id="${_converse.api.connection.get().getUniqueId()}"> <encrypted xmlns="${Strophe.NS.OMEMO}"> <header sid="555"> <key rid="${_converse.state.omemo_store.get('device_id')}">${u.arrayBufferToBase64(obj.key_and_tag)}</key> <iv>${obj.iv}</iv> </header> <payload>${obj.payload}</payload> </encrypted> </message>`; _converse.api.connection.get()._dataRecv(mock.createRequest(stanza)); await new Promise(resolve => view.model.messages.once('rendered', resolve)); await u.waitUntil(() => view.model.messages.length > 1); expect(view.model.messages.length).toBe(3); expect(view.querySelectorAll('.chat-msg__body')[2].textContent.trim()) .toBe('Another received encrypted message without fallback'); })); it("properly handles an already decrypted message being received again", mock.initConverse(['chatBoxesFetched'], {}, async function (_converse) { await mock.waitForRoster(_converse, 'current', 1); const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; await mock.initializedOMEMO(_converse); await mock.openChatBoxFor(_converse, contact_jid); await u.waitUntil(() => mock.deviceListFetched(_converse, contact_jid, ["555"])); await u.waitUntil(() => _converse.state.omemo_store); const view = _converse.chatboxviews.get(contact_jid); view.model.set('omemo_active', true); // Test reception of an encrypted message const msg_txt = 'This is an encrypted message from the contact'; const obj = await omemo.encryptMessage(msg_txt) const id = _converse.api.connection.get().getUniqueId(); let stanza = $msg({ 'from': contact_jid, 'to': _converse.api.connection.get().jid, 'type': 'chat', id }).c('body').t('This is a fallback message').up() .c('encrypted', {'xmlns': Strophe.NS.OMEMO}) .c('header', {'sid': '555'}) .c('key', {'rid': _converse.state.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.api.connection.get()._dataRecv(mock.createRequest(stanza)); // Test reception of the same message, but the decryption fails. // The properly decrypted message should still show to the user. // See issue: https://github.com/conversejs/converse.js/issues/2733#issuecomment-1035493594 stanza = $msg({ 'from': contact_jid, 'to': _converse.api.connection.get().jid, 'type': 'chat', id }).c('body').t('This is a fallback message').up() .c('encrypted', {'xmlns': Strophe.NS.OMEMO}) .c('header', {'sid': '555'}) .c('key', {'rid': _converse.state.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+'x'); // Hack to break decryption. _converse.api.connection.get()._dataRecv(mock.createRequest(stanza)); await u.waitUntil(() => view.querySelector('.chat-msg__text')?.textContent.trim() === msg_txt); expect(view.model.messages.length).toBe(1); const msg = view.model.messages.at(0); expect(msg.get('is_ephemeral')).toBe(false) expect(msg.getDisplayName()).toBe('Mercutio'); expect(msg.get('is_error')).toBe(false); })); it("will create a new device based on a received carbon message", mock.initConverse(['chatBoxesFetched'], {}, async function (_converse) { await mock.initializedOMEMO( _converse, [{'category': 'pubsub', 'type': 'pep'}], [ Strophe.NS.SID, 'http://jabber.org/protocol/pubsub#publish-options' ] ); await mock.waitForRoster(_converse, 'current', 1); const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; await mock.openChatBoxFor(_converse, contact_jid); let iq_stanza = await u.waitUntil(() => mock.deviceListFetched(_converse, contact_jid)); const my_devicelist = _converse.state.devicelists.get({'jid': _converse.bare_jid}); expect(my_devicelist.devices.length).toBe(2); const stanza = stx` <iq from="${contact_jid}" id="${iq_stanza.getAttribute('id')}" to="${_converse.api.connection.get().jid}" xmlns="jabber:server" type="result"> <pubsub xmlns="http://jabber.org/protocol/pubsub"> <items node="eu.siacs.conversations.axolotl.devicelist"> <item xmlns="http://jabber.org/protocol/pubsub"> <list xmlns="eu.siacs.conversations.axolotl"> <device id="555"/> </list> </item> </items> </pubsub> </iq>`; _converse.api.connection.get()._dataRecv(mock.createRequest(stanza)); const omemo_store = await u.waitUntil(() => _converse.state.omemo_store); const contact_devicelist = _converse.state.devicelists.get({'jid': contact_jid}); await u.waitUntil(() => contact_devicelist.devices.length === 1); const view = _converse.chatboxviews.get(contact_jid); view.model.set('omemo_active', true); // Test reception of an encrypted carbon message const obj = await omemo.encryptMessage('This is an encrypted carbon message from another device of mine') const carbon = stx` <message xmlns="jabber:client" to="romeo@montague.lit/orchard" from="romeo@montague.lit" type="chat"> <sent xmlns="urn:xmpp:carbons:2"> <forwarded xmlns="urn:xmpp:forward:0"> <message xmlns="jabber:client" from="romeo@montague.lit/gajim.HE02SW1L" xml:lang="en" to="${contact_jid}/gajim.0LATM5V2" type="chat" id="87141781-61d6-4eb3-9a31-429935a61b76"> <archived xmlns="urn:xmpp:mam:tmp" by="romeo@montague.lit" id="1554033877043470"/> <stanza-id xmlns="urn:xmpp:sid:0" by="romeo@montague.lit" id="1554033877043470"/> <request xmlns="urn:xmpp:receipts"/> <active xmlns="http://jabber.org/protocol/chatstates"/> <origin-id xmlns="urn:xmpp:sid:0" id="87141781-61d6-4eb3-9a31-429935a61b76"/> <encrypted xmlns="eu.siacs.conversations.axolotl"> <header sid="988349631"> <key rid="${omemo_store.get('device_id')}" prekey="true">${u.arrayBufferToBase64(obj.key_and_tag)}</key> <iv>${obj.iv}</iv> </header> <payload>${obj.payload}</payload> </encrypted> <encryption xmlns="urn:xmpp:eme:0" namespace="eu.siacs.conversations.axolotl" name="OMEMO"/> <store xmlns="urn:xmpp:hints"/> </message> </forwarded> </sent> </message>`; _converse.api.connection.get().IQ_stanzas = []; _converse.api.connection.get()._dataRecv(mock.createRequest(carbon)); // Remove one pre-key to exercise code that generates new ones. const prekeys = omemo_store.getPreKeys(); omemo_store.removePreKey(Object.keys(prekeys)[9]); let prekey_ids = Object.keys(omemo_store.getPreKeys()); expect(prekey_ids.length).toBe(99); expect(prekey_ids.includes('9')).toBe(false); // The message received is a prekey message, so missing prekeys are // generated and a new bundle published. iq_stanza = await u.waitUntil(() => mock.bundleHasBeenPublished(_converse)); const result_iq = stx` <iq xmlns="jabber:client" from="${_converse.bare_jid}" id="${iq_stanza.getAttribute('id')}" to="${_converse.bare_jid}" type="result"/>`; _converse.api.connection.get()._dataRecv(mock.createRequest(result_iq)); await new Promise(resolve => view.model.messages.once('rendered', resolve)); expect(view.model.messages.length).toBe(1); expect(view.querySelector('.chat-msg__text').textContent.trim()) .toBe('This is an encrypted carbon message from another device of mine'); expect(contact_devicelist.devices.length).toBe(1); // Check that the new device id has been added to my devices expect(my_devicelist.devices.length).toBe(3); expect(my_devicelist.devices.at(0).get('id')).toBe('482886413b977930064a5888b92134fe'); expect(my_devicelist.devices.at(1).get('id')).toBe('123456789'); expect(my_devicelist.devices.at(2).get('id')).toBe('988349631'); expect(my_devicelist.devices.get('988349631').get('active')).toBe(true); const textarea = view.querySelector('.chat-textarea'); textarea.value = 'This is an encrypted message from this device'; const message_form = view.querySelector('converse-message-form'); message_form.onKeyDown({ target: textarea, preventDefault: function preventDefault () {}, key: "Enter", }); iq_stanza = await u.waitUntil(() => mock.bundleIQRequestSent(_converse, _converse.bare_jid, '988349631')); expect(iq_stanza).toEqualStanza( stx`<iq from="romeo@montague.lit" id="${iq_stanza.getAttribute("id")}" to="${_converse.bare_jid}" type="get" xmlns="jabber:client"> <pubsub xmlns="http://jabber.org/protocol/pubsub"> <items node="eu.siacs.conversations.axolotl.bundles:988349631"/> </pubsub> </iq>`); prekey_ids = Object.keys(omemo_store.getPreKeys()); expect(prekey_ids.length).toBe(100); expect(prekey_ids.includes('9')).toBe(true); })); it("can receive a PreKeySignalMessage", mock.initConverse(['chatBoxesFetched'], {}, async function (_converse) { _converse.NUM_PREKEYS = 5; // Restrict to 5, otherwise the resulting stanza is too large to easily test await mock.waitForRoster(_converse, 'current', 1); const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; await u.waitUntil(() => mock.initializedOMEMO(_converse)); 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. let stanza = stx`<message from="${contact_jid}" to="${_converse.api.connection.get().jid}" xmlns="jabber:client" type="chat" id="qwerty"> <body>This is a fallback message</body> <encrypted xmlns="${Strophe.NS.OMEMO}"> <header sid="555"> <key prekey="true" rid="${_converse.state.omemo_store.get('device_id')}"> ${u.arrayBufferToBase64(obj.key_and_tag)} </key> <iv>${obj.iv}</iv> </header> <payload>${obj.payload}</payload> </encrypted> </message>`; const generateMissingPreKeys = _converse.state.omemo_store.generateMissingPreKeys; spyOn(_converse.state.omemo_store, 'generateMissingPreKeys').and.callFake(() => { // Since it's difficult to override // decryptPreKeyWhisperMessage, where a prekey will be // removed from the store, we do it here, before the // missing prekeys are generated. _converse.state.omemo_store.removePreKey(1); return generateMissingPreKeys.apply(_converse.state.omemo_store, arguments); }); _converse.api.connection.get()._dataRecv(mock.createRequest(stanza)); await mock.deviceListFetched(_converse, contact_jid, ["555"]); // XXX: the bundle gets published twice, we want to make sure // that we wait for the 2nd, so we clear all the already sent // stanzas. _converse.api.connection.get().IQ_stanzas = []; _converse.api.connection.get()._dataRecv(mock.createRequest(stanza)); await u.waitUntil(() => _converse.state.omemo_store); let iq_stanza = await u.waitUntil(() => mock.bundleHasBeenPublished(_converse), 1000); expect(iq_stanza).toEqualStanza( stx`<iq to="${_converse.bare_jid}" from="${_converse.bare_jid}" id="${iq_stanza.getAttribute("id")}" type="set" xmlns="jabber:client"> <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> <signedPreKeySignature>${btoa("11112222333344445555")}</signedPreKeySignature> <identityKey>${btoa("1234")}</identityKey> <prekeys> <preKeyPublic preKeyId="0">${btoa("1234")}</preKeyPublic> <preKeyPublic preKeyId="1">${btoa("1234")}</preKeyPublic> <preKeyPublic preKeyId="2">${btoa("1234")}</preKeyPublic> <preKeyPublic preKeyId="3">${btoa("1234")}</preKeyPublic> <preKeyPublic preKeyId="4">${btoa("1234")}</preKeyPublic> </prekeys> </bundle> </item> </publish> <publish-options> <x type="submit" xmlns="jabber:x:data"> <field type="hidden" var="FORM_TYPE"> <value>http://jabber.org/protocol/pubsub#publish-options</value> </field> <field var="pubsub#access_model"> <value>open</value> </field> </x> </publish-options> </pubsub> </iq>`) const own_device = _converse.state.devicelists.get(_converse.bare_jid).devices.get(_converse.state.omemo_store.get('device_id')); expect(own_device.get('bundle').prekeys.length).toBe(5); expect(_converse.state.omemo_store.generateMissingPreKeys).toHaveBeenCalled(); _converse.NUM_PREKEYS = 100; })); it("updates device lists based on PEP messages", mock.initConverse([], {'allow_non_roster_messaging': true}, async function (_converse) { await mock.waitForRoster(_converse, 'current', 1); await mock.waitUntilDiscoConfirmed( _converse, _converse.bare_jid, [{'category': 'pubsub', 'type': 'pep'}], ['http://jabber.org/protocol/pubsub#publish-options'] ); const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; // Wait until own devices are fetched let iq_stanza = await u.waitUntil(() => mock.deviceListFetched(_converse, _converse.bare_jid)); expect(iq_stanza).toEqualStanza(stx` <iq from="romeo@montague.lit" id="${iq_stanza.getAttribute("id")}" to="romeo@montague.lit" type="get" xmlns="jabber:client"> <pubsub xmlns="http://jabber.org/protocol/pubsub"> <items node="eu.siacs.conversations.axolotl.devicelist"/> </pubsub> </iq>`); let stanza = stx` <iq from="${_converse.bare_jid}" id="${iq_stanza.getAttribute('id')}" to="${_converse.bare_jid}" type="result" xmlns="jabber:client"> <pubsub xmlns="http://jabber.org/protocol/pubsub"> <items node="eu.siacs.conversations.axolotl.devicelist"> <item xmlns="http://jabber.org/protocol/pubsub"> <!-- TODO: must have an id attribute --> <list xmlns="eu.siacs.conversations.axolotl"> <device id="555"/> </list> </item> </items> </pubsub> </iq>`; _converse.api.connection.get()._dataRecv(mock.createRequest(stanza)); await u.waitUntil(() => _converse.state.omemo_store); expect(_converse.chatboxes.length).toBe(1); expect(_converse.state.devicelists.length).toBe(1); const devicelist = _converse.state.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'); iq_stanza = await u.waitUntil(() => mock.ownDeviceHasBeenPublished(_converse)); stanza = stx` <iq xmlns="jabber:client" from="${_converse.bare_jid}" id="${iq_stanza.getAttribute('id')}" to="${_converse.bare_jid}" type="result"/>`; _converse.api.connection.get()._dataRecv(mock.createRequest(stanza)); iq_stanza = await u.waitUntil(() => mock.bundleHasBeenPublished(_converse)); stanza = stx` <iq xmlns="jabber:client" from="${_converse.bare_jid}" id="${iq_stanza.getAttribute('id')}" to="${_converse.bare_jid}" type="result"/>`; _converse.api.connection.get()._dataRecv(mock.createRequest(stanza)); await _converse.api.waitUntil('OMEMOInitialized'); // A PEP message is received with a device list. _converse.api.connection.get()._dataRecv(mock.createRequest(stx` <message xmlns="jabber:client" from="${contact_jid}" to="${_converse.bare_jid}" type="headline" id="update_01"> <event xmlns="http://jabber.org/protocol/pubsub#event"> <items node="eu.siacs.conversations.axolotl.devicelist"> <item id="current"> <list xmlns="eu.siacs.conversations.axolotl"> <device id="1234"/> <device id="4223"/> </list> </item> </items> </event> </message>`)); // Since we haven't yet fetched any devices for this user, the // devicelist model for them isn't yet initialized. // It will be created and then automatically the devices will // be requested from the server via IQ stanza. // // This is perhaps a bit wasteful since we're already (AFIAK) getting the info we need // from the PEP headline message, but the code is simpler this way. await u.waitUntil(() => mock.deviceListFetched(_converse, contact_jid, ["1234", "4223"])); await u.waitUntil(() => _converse.state.devicelists.length === 2); const list = _converse.state.devicelists.get(contact_jid); await list.initialized; await u.waitUntil(() => list.devices.length === 2); let devices = list.devices; expect(list.devices.length).toBe(2); expect(list.devices.models.map(d => d.attributes.id).sort().join()).toBe('1234,4223'); stanza = $msg({ 'from': contact_jid, 'to': _converse.bare_jid, 'type': 'headline', 'id': 'update_02', }).c('event', {'xmlns': 'http://jabber.org/protocol/pubsub#event'}) .c('items', {'node': 'eu.siacs.conversations.axolotl.devicelist'}) .c('item') .c('list', {'xmlns': 'eu.siacs.conversations.axolotl'}) .c('device', {'id': '4223'}).up() .c('device', {'id': '4224'}) _converse.api.connection.get()._dataRecv(mock.createRequest(stanza)); expect(_converse.state.devicelists.length).toBe(2); await u.waitUntil(() => list.devices.length === 3); expect(devices.models.map(d => d.attributes.id).sort().join()).toBe('1234,4223,4224'); expect(devices.get('1234').get('active')).toBe(false); expect(devices.get('4223').get('active')).toBe(true); expect(devices.get('4224').get('active')).toBe(true); // Check that own devicelist gets updated stanza = $msg({ 'from': _converse.bare_jid, 'to': _converse.bare_jid, 'type': 'headline', 'id': 'update_03', }).c('event', {'xmlns': 'http://jabber.org/protocol/pubsub#event'}) .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.api.connection.get()._dataRecv(mock.createRequest(stanza)); expect(_converse.state.devicelists.length).toBe(2); devices = _converse.state.devicelists.get(_converse.bare_jid).devices; await u.waitUntil(() => devices.length === 3); expect(devices.models.map(d => d.attributes.id).sort().join()).toBe('123456789,555,777'); expect(devices.get('123456789').get('active')).toBe(true); expect(devices.get('555').get('active')).toBe(true); expect(devices.get('777').get('active')).toBe(true); _converse.api.connection.get().IQ_stanzas = []; // Check that own device gets re-added stanza = $msg({ 'from': _converse.bare_jid, 'to': _converse.bare_jid, 'type': 'headline', 'id': 'update_04', }).c('event', {'xmlns': 'http://jabber.org/protocol/pubsub#event'}) .c('items', {'node': 'eu.siacs.conversations.axolotl.devicelist'}) .c('item') .c('list', {'xmlns': 'eu.siacs.conversations.axolotl'}) .c('device', {'id': '444'}) _converse.api.connection.get()._dataRecv(mock.createRequest(stanza)); iq_stanza = await u.waitUntil(() => mock.ownDeviceHasBeenPublished(_converse)); // Check that our own device is added again, but that removed // devices are not added. expect(iq_stanza).toEqualStanza( stx`<iq from="${_converse.bare_jid}" to="${_converse.bare_jid}" id="${iq_stanza.getAttribute(`id`)}" type="set" xmlns="jabber:client"> <pubsub xmlns="http://jabber.org/protocol/pubsub"> <publish node="eu.siacs.conversations.axolotl.devicelist"> <item id="current"> <list xmlns="eu.siacs.conversations.axolotl"> <device id="123456789"/> <device id="444"/> </list> </item> </publish> <publish-options> <x type="submit" xmlns="jabber:x:data"> <field type="hidden" var="FORM_TYPE"> <value>http://jabber.org/protocol/pubsub#publish-options</value> </field> <field var="pubsub#access_model"> <value>open</value> </field> </x> </publish-options> </pubsub> </iq>`); expect(_converse.state.devicelists.length).toBe(2); devices = _converse.state.devicelists.get(_converse.bare_jid).devices; // The device id for this device (123456789) was also generated and added to the list, // which is why we have 2 devices now. expect(devices.length).toBe(4); expect(devices.models.map(d => d.attributes.id).sort().join()).toBe('123456789,444,555,777'); expect(devices.get('123456789').get('active')).toBe(true); expect(devices.get('444').get('active')).toBe(true); expect(devices.get('555').get('active')).toBe(false); expect(devices.get('777').get('active')).toBe(false); })); it("updates device bundles based on PEP messages", mock.initConverse([], {}, async function (_converse) { await mock.waitForRoster(_converse, 'current'); await mock.waitUntilDiscoConfirmed( _converse, _converse.bare_jid, [{'category': 'pubsub', 'type': 'pep'}], ['http://jabber.org/protocol/pubsub#publish-options'] ); const contact_jid = mock.cur_names[3].replace(/ /g,'.').toLowerCase() + '@montague.lit'; let iq_stanza = await u.waitUntil(() => mock.deviceListFetched(_converse, _converse.bare_jid)); expect(iq_stanza).toEqualStanza(stx` <iq from="romeo@montague.lit" id="${iq_stanza.getAttribute("id")}" to="romeo@montague.lit" type="get" xmlns="jabber:client"> <pubsub xmlns="http://jabber.org/protocol/pubsub"> <items node="eu.siacs.conversations.axolotl.devicelist"/> </pubsub> </iq>`); _converse.api.connection.get()._dataRecv(mock.createRequest(stx` <iq from="${contact_jid}" id="${iq_stanza.getAttribute('id')}" to="${_converse.bare_jid}" xmlns="jabber:client" type="result"> <pubsub xmlns="http://jabber.org/protocol/pubsub"> <items node="eu.siacs.conversations.axolotl.devicelist"> <item xmlns="http://jabber.org/protocol/pubsub"> <!-- TODO: must have an id attribute --> <list xmlns="eu.siacs.conversations.axolotl"> <device id="555"/> </list> </item> </items> </pubsub> </iq>`)); await await u.waitUntil(() => _converse.state.omemo_store); expect(_converse.state.devicelists.length).toBe(1); const own_device_list = _converse.state.devicelists.get(_converse.bare_jid); expect(own_device_list.devices.length).toBe(2); expect(own_device_list.devices.at(0).get('id')).toBe('555'); expect(own_device_list.devices.at(1).get('id')).toBe('123456789'); iq_stanza = await u.waitUntil(() => mock.ownDeviceHasBeenPublished(_converse)); let stanza = $iq({ 'from': _converse.bare_jid, 'id': iq_stanza.getAttribute('id'), 'to': _converse.bare_jid, 'type': 'result'}); _converse.api.connection.get()._dataRecv(mock.createRequest(stanza)); iq_stanza = await u.waitUntil(() => mock.bundleHasBeenPublished(_converse)); stanza = $iq({ 'from': _converse.bare_jid, 'id': iq_stanza.getAttribute('id'), 'to': _converse.bare_jid, 'type': 'result'}); _converse.api.connection.get()._dataRecv(mock.createRequest(stanza)); await _converse.api.waitUntil('OMEMOInitialized'); _converse.api.connection.get()._dataRecv(mock.createRequest(stx` <message from="${contact_jid}" to="${_converse.bare_jid}" type="headline" id="update_01" xmlns="jabber:client"> <event xmlns="http://jabber.org/protocol/pubsub#event"> <items node="eu.siacs.conversations.axolotl.bundles:1234"> <item> <bundle xmlns="eu.siacs.conversations.axolotl"> <signedPreKeyPublic signedPreKeyId="4223">1111</signedPreKeyPublic> <signedPreKeySignature>2222</signedPreKeySignature> <identityKey>3333</identityKey> <prekeys> <preKeyPublic preKeyId="1001"/> <preKeyPublic preKeyId="1002"/> <preKeyPublic preKeyId="1003"/> </prekeys> </bundle> </item> </items> </event> </message>`)); // Since we haven't yet fetched any devices for this user, the // devicelist model for them isn't yet initialized. // It will be created and then automatically the devices will // be requested from the server via IQ stanza. await u.waitUntil(() => mock.deviceListFetched(_converse, contact_jid, ["1234"])); await u.waitUntil(() => _converse.state.devicelists.length === 2); const list = _converse.state.devicelists.get(contact_jid); await list.initialized; await u.waitUntil(() => list.devices.length); let device = list.devices.at(0); expect(device.get('bundle').identity_key).toBe('3333'); expect(device.get('bundle').signed_prekey.public_key).toBe('1111'); expect(device.get('bundle').signed_prekey.id).toBe(4223); expect(device.get('bundle').signed_prekey.signature).toBe('2222'); expect(device.get('bundle').prekeys.length).toBe(3); expect(device.get('bundle').prekeys[0].id).toBe(1001); expect(device.get('bundle').prekeys[1].id).toBe(1002); expect(device.get('bundle').prekeys[2].id).toBe(1003); stanza = $msg({ 'from': contact_jid, 'to': _converse.bare_jid, 'type': 'headline', 'id': 'update_02', }).c('event', {'xmlns': 'http://jabber.org/protocol/pubsub#event'}) .c('items', {'node': 'eu.siacs.conversations.axolotl.bundles:1234'}) .c('item') .c('bundle', {'xmlns': 'eu.siacs.conversations.axolotl'}) .c('signedPreKeyPublic', {'signedPreKeyId': '4223'}).t('5555').up() .c('signedPreKeySignature').t('6666').up() .c('identityKey').t('7777').up() .c('prekeys') .c('preKeyPublic', {'preKeyId': '2001'}).up() .c('preKeyPublic', {'preKeyId': '2002'}).up() .c('preKeyPublic', {'preKeyId': '2003'}); _converse.api.connection.get()._dataRecv(mock.createRequest(stanza)); expect(_converse.state.devicelists.length).toBe(2); expect(list.devices.length).toBe(1); device = list.devices.at(0); await u.waitUntil(() => device.get('bundle').identity_key === '7777'); expect(device.get('bundle').signed_prekey.public_key).toBe('5555'); expect(device.get('bundle').signed_prekey.id).toBe(4223); expect(device.get('bundle').signed_prekey.signature).toBe('6666'); expect(device.get('bundle').prekeys.length).toBe(3); expect(device.get('bundle').prekeys[0].id).toBe(2001); expect(device.get('bundle').prekeys[1].id).toBe(2002); expect(device.get('bundle').prekeys[2].id).toBe(2003); _converse.api.connection.get()._dataRecv(mock.createRequest($msg({ 'from': _converse.bare_jid, 'to': _converse.bare_jid, 'type': 'headline', 'id': 'update_03', }).c('event', {'xmlns': 'http://jabber.org/protocol/pubsub#event'}) .c('items', {'node': 'eu.siacs.conversations.axolotl.bundles:555'}) .c('item') .c('bundle', {'xmlns': 'eu.siacs.conversations.axolotl'}) .c('signedPreKeyPublic', {'signedPreKeyId': '9999'}).t('8888').up() .c('signedPreKeySignature').t('3333').up() .c('identityKey').t('1111').up() .c('prekeys') .c('preKeyPublic', {'preKeyId': '3001'}).up() .c('preKeyPublic', {'preKeyId': '3002'}).up() .c('preKeyPublic', {'preKeyId': '3003'}) )); expect(_converse.state.devicelists.length).toBe(2); expect(own_device_list.devices.length).toBe(2); expect(own_device_list.devices.at(0).get('id')).toBe('555'); expect(own_device_list.devices.at(1).get('id')).toBe('123456789'); device = own_device_list.devices.at(0); await u.waitUntil(() => device.get('bundle')?.identity_key === '1111'); expect(device.get('bundle').signed_prekey.public_key).toBe('8888'); expect(device.get('bundle').signed_prekey.id).toBe(9999); expect(device.get('bundle').signed_prekey.signature).toBe('3333'); expect(device.get('bundle').prekeys.length).toBe(3); expect(device.get('bundle').prekeys[0].id).toBe(3001); expect(device.get('bundle').prekeys[1].id).toBe(3002); expect(device.get('bundle').prekeys[2].id).toBe(3003); })); it("publishes a bundle with which an encrypted session can be created", mock.initConverse(['chatBoxesFetched'], {}, async function (_converse) { await mock.waitUntilDiscoConfirmed( _converse, _converse.bare_jid, [{'category': 'pubsub', 'type': 'pep'}], ['http://jabber.org/protocol/pubsub#publish-options'] ); _converse.NUM_PREKEYS = 2; // Restrict to 2, otherwise the resulting stanza is too large to easily test await mock.waitForRoster(_converse, 'current', 1); const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; await u.waitUntil(() => mock.deviceListFetched(_converse, _converse.bare_jid, ['482886413b977930064a5888b92134fe'])); expect(_converse.state.devicelists.length).toBe(1); await mock.openChatBoxFor(_converse, contact_jid); let iq_stanza = await mock.ownDeviceHasBeenPublished(_converse); let stanza = stx`<iq from="${_converse.bare_jid}" xmlns="jabber:server" id="${iq_stanza.getAttribute('id')}" to="${_converse.bare_jid}" type="result"/>`; _converse.api.connection.get()._dataRecv(mock.createRequest(stanza)); iq_stanza = await u.waitUntil(() => mock.bundleHasBeenPublished(_converse)); expect(iq_stanza).toEqualStanza( stx`<iq from="${_converse.bare_jid}" to="${_converse.bare_jid}" id="${iq_stanza.getAttribute("id")}" type="set" xmlns="jabber:client"> <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> <signedPreKeySignature>${btoa("11112222333344445555")}</signedPreKeySignature> <identityKey>${btoa("1234")}</identityKey> <prekeys> <preKeyPublic preKeyId="0">${btoa("1234")}</preKeyPublic> <preKeyPublic preKeyId="1">${btoa("1234")}</preKeyPublic> </prekeys> </bundle> </item> </publish> <publish-options> <x type="submit" xmlns="jabber:x:data"> <field type="hidden" var="FORM_TYPE"> <value>http://jabber.org/protocol/pubsub#publish-options</value> </field> <field var="pubsub#access_model"> <value>open</value> </field> </x> </publish-options> </pubsub> </iq>`) stanza = $iq({ 'from': _converse.bare_jid, 'id': iq_stanza.getAttribute('id'), 'to': _converse.bare_jid, 'type': 'result'}); _converse.api.connection.get()._dataRecv(mock.createRequest(stanza)); await _converse.api.waitUntil('OMEMOInitialized'); _converse.NUM_PREKEYS = 100; })); it("adds a toolbar button for starting an encrypted chat session", mock.initConverse(['chatBoxesFetched'], {}, async function (_converse) { await mock.waitUntilDiscoConfirmed( _converse, _converse.bare_jid, [{'category': 'pubsub', 'type': 'pep'}], ['http://jabber.org/protocol/pubsub#publish-options'] ); await mock.waitForRoster(_converse, 'current', 1); const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; let iq_stanza = await u.waitUntil(() => mock.deviceListFetched(_converse, _converse.bare_jid)); expect(iq_stanza).toEqualStanza( stx`<iq from="romeo@montague.lit" id="${iq_stanza.getAttribute("id")}" to="romeo@montague.lit" type="get" xmlns="jabber:client"> <pubsub xmlns="http://jabber.org/protocol/pubsub"> <items node="eu.siacs.conversations.axolotl.devicelist"/> </pubsub> </iq>`); let stanza = stx`<iq from="${_converse.bare_jid}" xmlns="jabber:client" id="${iq_stanza.getAttribute('id')}" to="${_converse.bare_jid}" type="result"> <pubsub xmlns="http://jabber.org/protocol/pubsub"> <items node="eu.siacs.conversations.axolotl.devicelist"> <item xmlns="http://jabber.org/protocol/pubsub"> <list xmlns="eu.siacs.conversations.axolotl"> <device id="482886413b977930064a5888b92134fe"/> </list> </item> </items> </pubsub> </iq>`; _converse.api.connection.get()._dataRecv(mock.createRequest(stanza)); await u.waitUntil(() => _converse.state.omemo_store); expect(_converse.state.devicelists.length).toBe(1); let devicelist = _converse.state.devicelists.get(_converse.bare_jid); expect(devicelist.devices.length).toBe(2); expect(devicelist.devices.at(0).get('id')).toBe('482886413b977930064a5888b92134fe'); expect(devicelist.devices.at(1).get('id')).toBe('123456789'); // Check that own device was published iq_stanza = await u.waitUntil(() => mock.ownDeviceHasBeenPublished(_converse)); expect(iq_stanza).toEqualStanza( stx`<iq from="${_converse.bare_jid}" to="${_converse.bare_jid}" id="${iq_stanza.getAttribute(`id`)}" type="set" xmlns="jabber:client"> <pubsub xmlns="http://jabber.org/protocol/pubsub"> <publish node="eu.siacs.conversations.axolotl.devicelist"> <item id="current"> <list xmlns="eu.siacs.conversations.axolotl"> <device id="482886413b977930064a5888b92134fe"/> <device id="123456789"/> </list> </item> </publish> <publish-options> <x type="submit" xmlns="jabber:x:data"> <field type="hidden" var="FORM_TYPE"> <value>http://jabber.org/protocol/pubsub#publish-options</value> </field> <field var="pubsub#access_model"> <value>open</value> </field> </x> </publish-options> </pubsub> </iq>`); stanza = stx`<iq from="${_converse.bare_jid}" id="${iq_stanza.getAttribute('id')}" to="${_converse.bare_jid}" type="result" xmlns="jabber:server"/>`; _converse.api.connection.get()._dataRecv(mock.createRequest(stanza)); const iq_el = await u.waitUntil(() => mock.bundleHasBeenPublished(_converse)); expect(iq_el.getAttributeNames().sort().join()).toBe(["to", "from", "id", "type", "xmlns"].sort().join()); expect(iq_el.querySelector('prekeys').childNodes.length).toBe(100); const signed_prekeys = iq_el.querySelectorAll('signedPreKeyPublic'); expect(signed_prekeys.length).toBe(1); const signed_prekey = signed_prekeys[0]; expect(signed_prekey.getAttribute('signedPreKeyId')).toBe('0') expect(iq_el.querySelectorAll('signedPreKeySignature').length).toBe(1); expect(iq_el.querySelectorAll('identityKey').length).toBe(1); stanza = stx`<iq xmlns="jabber:server" from="${_converse.bare_jid}" id="${iq_el.getAttribute('id')}" to="${_converse.bare_jid}" type="result"/>`; _converse.api.connection.get()._dataRecv(mock.createRequest(stanza)); await _converse.api.waitUntil('OMEMOInitialized', 1000); await mock.openChatBoxFor(_converse, contact_jid); iq_stanza = await u.waitUntil(() => mock.deviceListFetched(_converse, contact_jid)); expect(iq_stanza).toEqualStanza( stx`<iq from="romeo@montague.lit" id="${iq_stanza.getAttribute("id")}" to="${contact_jid}" type="get" xmlns="jabber:client"> <pubsub xmlns="http://jabber.org/protocol/pubsub"> <items node="eu.siacs.conversations.axolotl.devicelist"/> </pubsub> </iq>`); _converse.api.connection.get()._dataRecv(mock.createRequest( stx`<iq from="${contact_jid}" id="${iq_stanza.getAttribute('id')}" to="${_converse.bare_jid}"