/*global mock, converse */ const { $iq, $pres, $msg, _, omemo, Strophe } = converse.env; const u = converse.env.utils; async function deviceListFetched (_converse, jid) { const selector = `iq[to="${jid}"] items[node="eu.siacs.conversations.axolotl.devicelist"]`; const stanza = await u.waitUntil( () => Array.from(_converse.connection.IQ_stanzas).filter(iq => iq.querySelector(selector)).pop() ); await u.waitUntil(() => _converse.devicelists.get(jid)); return stanza; } function ownDeviceHasBeenPublished (_converse) { return _.filter( Array.from(_converse.connection.IQ_stanzas), iq => iq.querySelector('iq[from="'+_converse.bare_jid+'"] publish[node="eu.siacs.conversations.axolotl.devicelist"]') ).pop(); } function bundleHasBeenPublished (_converse) { const selector = 'publish[node="eu.siacs.conversations.axolotl.bundles:123456789"]'; return Array.from(_converse.connection.IQ_stanzas).filter(iq => iq.querySelector(selector)).pop(); } function bundleFetched (_converse, jid, device_id) { return _.filter( Array.from(_converse.connection.IQ_stanzas), iq => iq.querySelector(`iq[to="${jid}"] items[node="eu.siacs.conversations.axolotl.bundles:${device_id}"]`) ).pop(); } async function initializedOMEMO (_converse) { await mock.waitUntilDiscoConfirmed( _converse, _converse.bare_jid, [{'category': 'pubsub', 'type': 'pep'}], ['http://jabber.org/protocol/pubsub#publish-options'] ); let iq_stanza = await u.waitUntil(() => deviceListFetched(_converse, _converse.bare_jid)); let 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.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': '482886413b977930064a5888b92134fe'}); _converse.connection._dataRecv(mock.createRequest(stanza)); iq_stanza = await u.waitUntil(() => ownDeviceHasBeenPublished(_converse)) stanza = $iq({ 'from': _converse.bare_jid, 'id': iq_stanza.getAttribute('id'), 'to': _converse.bare_jid, 'type': 'result'}); _converse.connection._dataRecv(mock.createRequest(stanza)); iq_stanza = await u.waitUntil(() => bundleHasBeenPublished(_converse)) stanza = $iq({ 'from': _converse.bare_jid, 'id': iq_stanza.getAttribute('id'), 'to': _converse.bare_jid, 'type': 'result'}); _converse.connection._dataRecv(mock.createRequest(stanza)); await _converse.api.waitUntil('OMEMOInitialized'); } describe("The OMEMO module", function() { it("adds methods for encrypting and decrypting messages via AES GCM", mock.initConverse( ['rosterGroupsFetched', 'chatBoxesFetched'], {}, async function (done, _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); done(); })); it("enables encrypted messages to be sent and received", mock.initConverse( ['rosterGroupsFetched', 'chatBoxesFetched'], {}, async function (done, _converse) { let sent_stanza; await mock.waitForRoster(_converse, 'current', 1); const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; await u.waitUntil(() => initializedOMEMO(_converse)); await mock.openChatBoxFor(_converse, contact_jid); let iq_stanza = await u.waitUntil(() => deviceListFetched(_converse, contact_jid)); let stanza = $iq({ 'from': contact_jid, 'id': iq_stanza.getAttribute('id'), 'to': _converse.connection.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': '555'}); _converse.connection._dataRecv(mock.createRequest(stanza)); await u.waitUntil(() => _converse.omemo_store); const devicelist = _converse.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.el.querySelector('.chat-textarea'); textarea.value = 'This message will be encrypted'; view.onKeyDown({ target: textarea, preventDefault: function preventDefault () {}, keyCode: 13 // Enter }); iq_stanza = await u.waitUntil(() => bundleFetched(_converse, contact_jid, '555')); 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:555"}) .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(() => 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')); spyOn(_converse.connection, 'send').and.callFake(stanza => { sent_stanza = stanza }); _converse.connection._dataRecv(mock.createRequest(stanza)); await u.waitUntil(() => sent_stanza); expect(sent_stanza.toLocaleString()).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.nodeTree.querySelector("iv").textContent}`+ `
`+ `${sent_stanza.nodeTree.querySelector("payload").textContent}`+ `
`+ ``+ `
`); // 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. stanza = $msg({ 'from': contact_jid, 'to': _converse.connection.jid, 'type': 'chat', '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.el.querySelectorAll('.chat-msg__body')[1].textContent.trim()) .toBe('This is an encrypted message from the contact'); // #1193 Check for a received message without tag obj = await omemo.encryptMessage('Another received encrypted message without fallback') stanza = $msg({ 'from': contact_jid, 'to': _converse.connection.jid, 'type': 'chat', 'id': _converse.connection.getUniqueId() }).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)); await u.waitUntil(() => view.model.messages.length > 1); expect(view.model.messages.length).toBe(3); expect(view.el.querySelectorAll('.chat-msg__body')[2].textContent.trim()) .toBe('Another received encrypted message without fallback'); done(); })); it("enables encrypted groupchat messages to be sent and received", mock.initConverse( ['rosterGroupsFetched', 'chatBoxesFetched'], {}, async function (done, _converse) { // MEMO encryption works only in members only conferences // that are non-anonymous. const features = [ 'http://jabber.org/protocol/muc', 'jabber:iq:register', 'muc_passwordprotected', 'muc_hidden', 'muc_temporary', 'muc_membersonly', 'muc_unmoderated', 'muc_nonanonymous' ]; await mock.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'romeo', features); const view = _converse.chatboxviews.get('lounge@montague.lit'); await u.waitUntil(() => initializedOMEMO(_converse)); const toolbar = view.el.querySelector('.chat-toolbar'); toolbar.querySelector('.toggle-omemo').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(() => 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(() => 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.el.querySelector('.chat-textarea'); textarea.value = 'This message will be encrypted'; view.onKeyDown({ target: textarea, preventDefault: function preventDefault () {}, keyCode: 13 // Enter }); iq_stanza = await u.waitUntil(() => 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(() => 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.nodeTree.querySelector("iv").textContent}`+ `
`+ `${sent_stanza.nodeTree.querySelector("payload").textContent}`+ `
`+ ``+ `
`); done(); })); it("will create a new device based on a received carbon message", mock.initConverse( ['rosterGroupsFetched', 'chatBoxesFetched'], {}, async function (done, _converse) { await mock.waitUntilDiscoConfirmed(_converse, _converse.bare_jid, [], [Strophe.NS.SID]); await mock.waitForRoster(_converse, 'current', 1); const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; await u.waitUntil(() => initializedOMEMO(_converse)); await mock.openChatBoxFor(_converse, contact_jid); let iq_stanza = await u.waitUntil(() => deviceListFetched(_converse, contact_jid)); const my_devicelist = _converse.devicelists.get({'jid': _converse.bare_jid}); expect(my_devicelist.devices.length).toBe(2); const stanza = $iq({ 'from': contact_jid, 'id': iq_stanza.getAttribute('id'), 'to': _converse.connection.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': '555'}); _converse.connection._dataRecv(mock.createRequest(stanza)); await u.waitUntil(() => _converse.omemo_store); const contact_devicelist = _converse.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 = u.toStanza(`
${u.arrayBufferToBase64(obj.key_and_tag)} ${obj.iv}
${obj.payload}
`); _converse.connection._dataRecv(mock.createRequest(carbon)); await new Promise(resolve => view.model.messages.once('rendered', resolve)); expect(view.model.messages.length).toBe(1); expect(view.el.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.el.querySelector('.chat-textarea'); textarea.value = 'This is an encrypted message from this device'; view.onKeyDown({ target: textarea, preventDefault: function preventDefault () {}, keyCode: 13 // Enter }); iq_stanza = await u.waitUntil(() => bundleFetched(_converse, _converse.bare_jid, '988349631')); expect(Strophe.serialize(iq_stanza)).toBe( ``+ ``+ ``+ ``+ ``); done(); })); it("gracefully handles auth errors when trying to send encrypted groupchat messages", mock.initConverse( ['rosterGroupsFetched', 'chatBoxesFetched'], {}, async function (done, _converse) { // MEMO encryption works only in members only conferences // that are non-anonymous. const features = [ 'http://jabber.org/protocol/muc', 'jabber:iq:register', 'muc_passwordprotected', 'muc_hidden', 'muc_temporary', 'muc_membersonly', 'muc_unmoderated', 'muc_nonanonymous' ]; await mock.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'romeo', features); const view = _converse.chatboxviews.get('lounge@montague.lit'); await u.waitUntil(() => 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 = view.el.querySelector('.chat-toolbar'); const toggle = toolbar.querySelector('.toggle-omemo'); toggle.click(); expect(view.model.get('omemo_active')).toBe(true); expect(view.model.get('omemo_supported')).toBe(true); const textarea = view.el.querySelector('.chat-textarea'); textarea.value = 'This message will be encrypted'; view.onKeyDown({ target: textarea, preventDefault: function preventDefault () {}, keyCode: 13 // Enter }); let iq_stanza = await u.waitUntil(() => 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(() => deviceListFetched(_converse, contact_jid)); expect(devicelist.devices.length).toBe(1); expect(devicelist.devices.at(0).get('id')).toBe('4e30f35051b7b8b42abe083742187228'); iq_stanza = await u.waitUntil(() => 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(() => 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.el.querySelector('.chat-textarea').value).toBe('This message will be encrypted'); done(); })); it("can receive a PreKeySignalMessage", mock.initConverse( ['rosterGroupsFetched', 'chatBoxesFetched'], {}, async function (done, _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(() => 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 = $msg({ 'from': contact_jid, 'to': _converse.connection.jid, 'type': 'chat', 'id': 'qwerty' }).c('body').t('This is a fallback message').up() .c('encrypted', {'xmlns': Strophe.NS.OMEMO}) .c('header', {'sid': '555'}) .c('key', { 'prekey': 'true', '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); const generateMissingPreKeys = _converse.omemo_store.generateMissingPreKeys; spyOn(_converse.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.omemo_store.removePreKey(1); return generateMissingPreKeys.apply(_converse.omemo_store, arguments); }); _converse.connection._dataRecv(mock.createRequest(stanza)); let iq_stanza = await deviceListFetched(_converse, contact_jid); stanza = $iq({ 'from': contact_jid, 'id': iq_stanza.getAttribute('id'), 'to': _converse.connection.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': '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.connection.IQ_stanzas = []; _converse.connection._dataRecv(mock.createRequest(stanza)); await u.waitUntil(() => _converse.omemo_store); iq_stanza = await u.waitUntil(() => bundleHasBeenPublished(_converse), 1000); expect(Strophe.serialize(iq_stanza)).toBe( ``+ ``+ ``+ ``+ ``+ `${btoa("1234")}`+ `${btoa("11112222333344445555")}`+ `${btoa("1234")}`+ ``+ `${btoa("1234")}`+ `${btoa("1234")}`+ `${btoa("1234")}`+ `${btoa("1234")}`+ `${btoa("1234")}`+ ``+ ``+ ``+ ``+ ``+ ``+ ``+ `http://jabber.org/protocol/pubsub#publish-options`+ ``+ ``+ `open`+ ``+ ``+ ``+ ``+ ``) const own_device = _converse.devicelists.get(_converse.bare_jid).devices.get(_converse.omemo_store.get('device_id')); expect(own_device.get('bundle').prekeys.length).toBe(5); expect(_converse.omemo_store.generateMissingPreKeys).toHaveBeenCalled(); done(); })); it("updates device lists based on PEP messages", mock.initConverse( ['rosterGroupsFetched'], {'allow_non_roster_messaging': true}, async function (done, _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'; // Wait until own devices are fetched let iq_stanza = await u.waitUntil(() => deviceListFetched(_converse, _converse.bare_jid)); expect(Strophe.serialize(iq_stanza)).toBe( ``+ ``+ ``+ ``+ ``); let 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.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': '555'}); _converse.connection._dataRecv(mock.createRequest(stanza)); await u.waitUntil(() => _converse.omemo_store); expect(_converse.chatboxes.length).toBe(1); expect(_converse.devicelists.length).toBe(1); 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'); iq_stanza = await u.waitUntil(() => ownDeviceHasBeenPublished(_converse)); stanza = $iq({ 'from': _converse.bare_jid, 'id': iq_stanza.getAttribute('id'), 'to': _converse.bare_jid, 'type': 'result'}); _converse.connection._dataRecv(mock.createRequest(stanza)); iq_stanza = await u.waitUntil(() => bundleHasBeenPublished(_converse)); stanza = $iq({ 'from': _converse.bare_jid, 'id': iq_stanza.getAttribute('id'), 'to': _converse.bare_jid, 'type': 'result'}); _converse.connection._dataRecv(mock.createRequest(stanza)); await _converse.api.waitUntil('OMEMOInitialized'); stanza = $msg({ 'from': contact_jid, 'to': _converse.bare_jid, 'type': 'headline', 'id': 'update_01', }).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': '1234'}) .c('device', {'id': '4223'}) _converse.connection._dataRecv(mock.createRequest(stanza)); expect(_converse.devicelists.length).toBe(2); let devices = _converse.devicelists.get(contact_jid).devices; expect(devices.length).toBe(2); expect(_.map(devices.models, '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'}) .c('device', {'id': '4224'}) _converse.connection._dataRecv(mock.createRequest(stanza)); expect(_converse.devicelists.length).toBe(2); expect(devices.length).toBe(3); expect(_.map(devices.models, '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.connection._dataRecv(mock.createRequest(stanza)); expect(_converse.devicelists.length).toBe(2); devices = _converse.devicelists.get(_converse.bare_jid).devices; expect(devices.length).toBe(3); expect(_.map(devices.models, '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.connection.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.connection._dataRecv(mock.createRequest(stanza)); iq_stanza = await u.waitUntil(() => ownDeviceHasBeenPublished(_converse)); // Check that our own device is added again, but that removed // devices are not added. expect(Strophe.serialize(iq_stanza)).toBe( ``+ ``+ ``+ ``+ ``+ ``+ ``+ ``+ ``+ ``+ ``+ ``+ ``+ `http://jabber.org/protocol/pubsub#publish-options`+ ``+ ``+ `open`+ ``+ ``+ ``+ ``+ ``); expect(_converse.devicelists.length).toBe(2); devices = _converse.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(_.map(devices.models, '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); done(); })); it("updates device bundles based on PEP messages", mock.initConverse( ['rosterGroupsFetched'], {}, async function (done, _converse) { await mock.waitUntilDiscoConfirmed( _converse, _converse.bare_jid, [{'category': 'pubsub', 'type': 'pep'}], ['http://jabber.org/protocol/pubsub#publish-options'] ); await mock.waitForRoster(_converse, 'current'); const contact_jid = mock.cur_names[3].replace(/ /g,'.').toLowerCase() + '@montague.lit'; let iq_stanza = await u.waitUntil(() => deviceListFetched(_converse, _converse.bare_jid)); expect(Strophe.serialize(iq_stanza)).toBe( ``+ ``+ ``+ ``+ ``); let 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': '555'}); _converse.connection._dataRecv(mock.createRequest(stanza)); await await u.waitUntil(() => _converse.omemo_store); expect(_converse.devicelists.length).toBe(1); let 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'); iq_stanza = await u.waitUntil(() => ownDeviceHasBeenPublished(_converse)); stanza = $iq({ 'from': _converse.bare_jid, 'id': iq_stanza.getAttribute('id'), 'to': _converse.bare_jid, 'type': 'result'}); _converse.connection._dataRecv(mock.createRequest(stanza)); iq_stanza = await u.waitUntil(() => bundleHasBeenPublished(_converse)); stanza = $iq({ 'from': _converse.bare_jid, 'id': iq_stanza.getAttribute('id'), 'to': _converse.bare_jid, 'type': 'result'}); _converse.connection._dataRecv(mock.createRequest(stanza)); await _converse.api.waitUntil('OMEMOInitialized'); stanza = $msg({ 'from': contact_jid, 'to': _converse.bare_jid, 'type': 'headline', 'id': 'update_01', }).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': '4223'}).t('1111').up() .c('signedPreKeySignature').t('2222').up() .c('identityKey').t('3333').up() .c('prekeys') .c('preKeyPublic', {'preKeyId': '1001'}).up() .c('preKeyPublic', {'preKeyId': '1002'}).up() .c('preKeyPublic', {'preKeyId': '1003'}); _converse.connection._dataRecv(mock.createRequest(stanza)); expect(_converse.devicelists.length).toBe(2); 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'); 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:555'}) .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.connection._dataRecv(mock.createRequest(stanza)); expect(_converse.devicelists.length).toBe(2); devicelist = _converse.devicelists.get(contact_jid); expect(devicelist.devices.length).toBe(1); device = devicelist.devices.at(0); expect(device.get('bundle').identity_key).toBe('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); 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.bundles:123456789'}) .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'}); _converse.connection._dataRecv(mock.createRequest(stanza)); expect(_converse.devicelists.length).toBe(2); 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'); device = devicelist.devices.at(1); expect(device.get('bundle').identity_key).toBe('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); done(); })); it("publishes a bundle with which an encrypted session can be created", mock.initConverse( ['rosterGroupsFetched', 'chatBoxesFetched'], {}, async function (done, _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'; let iq_stanza = await u.waitUntil(() => deviceListFetched(_converse, _converse.bare_jid)); let 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': '482886413b977930064a5888b92134fe'}); _converse.connection._dataRecv(mock.createRequest(stanza)); expect(_converse.devicelists.length).toBe(1); await mock.openChatBoxFor(_converse, contact_jid); iq_stanza = await ownDeviceHasBeenPublished(_converse); stanza = $iq({ 'from': _converse.bare_jid, 'id': iq_stanza.getAttribute('id'), 'to': _converse.bare_jid, 'type': 'result'}); _converse.connection._dataRecv(mock.createRequest(stanza)); iq_stanza = await u.waitUntil(() => bundleHasBeenPublished(_converse)); expect(Strophe.serialize(iq_stanza)).toBe( ``+ ``+ ``+ ``+ ``+ `${btoa("1234")}`+ `${btoa("11112222333344445555")}`+ `${btoa("1234")}`+ ``+ `${btoa("1234")}`+ `${btoa("1234")}`+ ``+ ``+ ``+ ``+ ``+ ``+ ``+ `http://jabber.org/protocol/pubsub#publish-options`+ ``+ ``+ `open`+ ``+ ``+ ``+ ``+ ``) stanza = $iq({ 'from': _converse.bare_jid, 'id': iq_stanza.getAttribute('id'), 'to': _converse.bare_jid, 'type': 'result'}); _converse.connection._dataRecv(mock.createRequest(stanza)); await _converse.api.waitUntil('OMEMOInitialized'); done(); })); it("adds a toolbar button for starting an encrypted chat session", mock.initConverse( ['rosterGroupsFetched', 'chatBoxesFetched'], {}, async function (done, _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(() => deviceListFetched(_converse, _converse.bare_jid)); expect(Strophe.serialize(iq_stanza)).toBe( ``+ ``+ ``+ ``+ ``); let 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.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': '482886413b977930064a5888b92134fe'}); _converse.connection._dataRecv(mock.createRequest(stanza)); await u.waitUntil(() => _converse.omemo_store); expect(_converse.devicelists.length).toBe(1); let devicelist = _converse.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(() => ownDeviceHasBeenPublished(_converse)); expect(Strophe.serialize(iq_stanza)).toBe( ``+ ``+ ``+ ``+ ``+ ``+ ``+ ``+ ``+ ``+ ``+ ``+ ``+ `http://jabber.org/protocol/pubsub#publish-options`+ ``+ ``+ `open`+ ``+ ``+ ``+ ``+ ``); stanza = $iq({ 'from': _converse.bare_jid, 'id': iq_stanza.getAttribute('id'), 'to': _converse.bare_jid, 'type': 'result'}); _converse.connection._dataRecv(mock.createRequest(stanza)); const iq_el = await u.waitUntil(() => bundleHasBeenPublished(_converse)); expect(iq_el.getAttributeNames().sort().join()).toBe(["from", "type", "xmlns", "id"].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 = $iq({ 'from': _converse.bare_jid, 'id': iq_el.getAttribute('id'), 'to': _converse.bare_jid, 'type': 'result'}); _converse.connection._dataRecv(mock.createRequest(stanza)); await _converse.api.waitUntil('OMEMOInitialized', 1000); await mock.openChatBoxFor(_converse, contact_jid); iq_stanza = await u.waitUntil(() => 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': '368866411b877c30064a5f62b917cffe'}).up() .c('device', {'id': '3300659945416e274474e469a1f0154c'}).up() .c('device', {'id': '4e30f35051b7b8b42abe083742187228'}).up() .c('device', {'id': 'ae890ac52d0df67ed7cfdf51b644e901'}); _converse.connection._dataRecv(mock.createRequest(stanza)); devicelist = _converse.devicelists.get(contact_jid); await u.waitUntil(() => devicelist.devices.length); expect(_converse.devicelists.length).toBe(2); devicelist = _converse.devicelists.get(contact_jid); expect(devicelist.devices.length).toBe(4); expect(devicelist.devices.at(0).get('id')).toBe('368866411b877c30064a5f62b917cffe'); expect(devicelist.devices.at(1).get('id')).toBe('3300659945416e274474e469a1f0154c'); expect(devicelist.devices.at(2).get('id')).toBe('4e30f35051b7b8b42abe083742187228'); expect(devicelist.devices.at(3).get('id')).toBe('ae890ac52d0df67ed7cfdf51b644e901'); await u.waitUntil(() => _converse.chatboxviews.get(contact_jid).el.querySelector('.chat-toolbar')); const view = _converse.chatboxviews.get(contact_jid); const toolbar = view.el.querySelector('.chat-toolbar'); expect(view.model.get('omemo_active')).toBe(undefined); const toggle = toolbar.querySelector('.toggle-omemo'); expect(toggle === null).toBe(false); expect(u.hasClass('fa-unlock', toggle.querySelector('converse-icon'))).toBe(true); expect(u.hasClass('fa-lock', toggle.querySelector('.converse-icon'))).toBe(false); view.delegateEvents(); // We need to rebind all events otherwise our spy won't be called toolbar.querySelector('.toggle-omemo').click(); expect(view.model.get('omemo_active')).toBe(true); await u.waitUntil(() => u.hasClass('fa-lock', toolbar.querySelector('.toggle-omemo converse-icon'))); let 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.el.querySelector('.chat-textarea'); textarea.value = 'This message will be sent encrypted'; view.onKeyDown({ target: textarea, preventDefault: function preventDefault () {}, keyCode: 13 }); view.model.save({'omemo_supported': false}); await u.waitUntil(() => toolbar.querySelector('.toggle-omemo').disabled); icon = toolbar.querySelector('.toggle-omemo converse-icon'); expect(u.hasClass('fa-lock', icon)).toBe(false); expect(u.hasClass('fa-unlock', icon)).toBe(true); view.model.save({'omemo_supported': true}); await u.waitUntil(() => !toolbar.querySelector('.toggle-omemo').disabled); icon = toolbar.querySelector('.toggle-omemo converse-icon'); expect(u.hasClass('fa-lock', icon)).toBe(false); expect(u.hasClass('fa-unlock', icon)).toBe(true); done(); })); it("adds a toolbar button for starting an encrypted groupchat session", mock.initConverse( ['rosterGroupsFetched', 'chatBoxesFetched'], {}, async function (done, _converse) { 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(() => initializedOMEMO(_converse)); const toolbar = view.el.querySelector('.chat-toolbar'); let toggle = toolbar.querySelector('.toggle-omemo'); expect(view.model.get('omemo_active')).toBe(undefined); expect(view.model.get('omemo_supported')).toBe(true); await u.waitUntil(() => !toggle.disabled); 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.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(() => 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(() => 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.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.el.querySelector('.toggle-omemo').disabled); view.model.features.save({'nonanonymous': true, 'semianonymous': false}); await u.waitUntil(() => view.model.get('omemo_supported')); await u.waitUntil(() => view.el.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.el.querySelector('.toggle-omemo').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.el.querySelector('.toggle-omemo').disabled); view.model.features.save({'membersonly': true, 'open': false}); await u.waitUntil(() => view.model.get('omemo_supported')); await u.waitUntil(() => !view.el.querySelector('.toggle-omemo').disabled); expect(u.hasClass('fa-unlock', view.el.querySelector('.toggle-omemo converse-icon'))).toBe(true); expect(u.hasClass('fa-lock', view.el.querySelector('.toggle-omemo converse-icon'))).toBe(false); expect(view.model.get('omemo_supported')).toBe(true); expect(view.model.get('omemo_active')).toBe(false); view.el.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(() => 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.el.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').disabled); icon = view.el.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'); done(); })); it("shows OMEMO device fingerprints in the user details modal", mock.initConverse( ['rosterGroupsFetched', 'chatBoxesFetched'], {}, async function (done, _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'; await mock.openChatBoxFor(_converse, contact_jid) // We simply emit, to avoid doing all the setup work _converse.api.trigger('OMEMOInitialized'); const view = _converse.chatboxviews.get(contact_jid); const show_modal_button = view.el.querySelector('.show-user-details-modal'); show_modal_button.click(); const modal = view.user_details_modal; await u.waitUntil(() => u.isVisible(modal.el), 1000); let iq_stanza = await u.waitUntil(() => deviceListFetched(_converse, contact_jid)); expect(Strophe.serialize(iq_stanza)).toBe( ``+ ``+ ``); let 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': '555'}); _converse.connection._dataRecv(mock.createRequest(stanza)); await u.waitUntil(() => u.isVisible(modal.el), 1000); iq_stanza = await u.waitUntil(() => bundleFetched(_converse, contact_jid, '555')); 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.bundles:555"}) .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('BQmHEOHjsYm3w5M8VqxAtqJmLCi7CaxxsdZz6G0YpuMI').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)); await u.waitUntil(() => modal.el.querySelectorAll('.fingerprints .fingerprint').length); expect(modal.el.querySelectorAll('.fingerprints .fingerprint').length).toBe(1); const el = modal.el.querySelector('.fingerprints .fingerprint'); expect(el.textContent.trim()).toBe( u.formatFingerprint(u.arrayBufferToHex(u.base64ToArrayBuffer('BQmHEOHjsYm3w5M8VqxAtqJmLCi7CaxxsdZz6G0YpuMI'))) ); expect(modal.el.querySelectorAll('input[type="radio"]').length).toBe(2); const devicelist = _converse.devicelists.get(contact_jid); expect(devicelist.devices.get('555').get('trusted')).toBe(0); let trusted_radio = modal.el.querySelector('input[type="radio"][name="555"][value="1"]'); expect(trusted_radio.checked).toBe(true); let untrusted_radio = modal.el.querySelector('input[type="radio"][name="555"][value="-1"]'); expect(untrusted_radio.checked).toBe(false); // Test that the device can be set to untrusted untrusted_radio.click(); trusted_radio = document.querySelector('input[type="radio"][name="555"][value="1"]'); expect(trusted_radio.hasAttribute('checked')).toBe(false); expect(devicelist.devices.get('555').get('trusted')).toBe(-1); untrusted_radio = document.querySelector('input[type="radio"][name="555"][value="-1"]'); expect(untrusted_radio.hasAttribute('checked')).toBe(true); trusted_radio.click(); expect(devicelist.devices.get('555').get('trusted')).toBe(1); done(); })); });