Bugfix. Use real JID when setting up a device session in a MUC
Thanks to @orbitz, see: https://github.com/conversejs/converse.js/issues/1481#issuecomment-509183431 Updates #1481
This commit is contained in:
parent
90d93b364a
commit
ca02bdcb61
@ -2,8 +2,10 @@
|
||||
|
||||
## 9.0.0 (Unreleased)
|
||||
|
||||
- Use more specific types for form fields based on XEP-0122
|
||||
- Fix trimming of chats in overlayed view mode
|
||||
- #2647: Singleton mode doesn't work
|
||||
- OMEMO bugfix: Always create device session based on real JID.
|
||||
|
||||
- Emit a `change` event when a configuration setting changes
|
||||
- 3 New configuration settings:
|
||||
@ -18,7 +20,6 @@ Three config settings have been obsoleted:
|
||||
- show_images_inline
|
||||
- muc_show_ogp_unfurls
|
||||
|
||||
- Use more specific types for form fields based on XEP-0122
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
|
@ -93,6 +93,7 @@ module.exports = function(config) {
|
||||
{ pattern: "src/plugins/notifications/tests/notification.js", type: 'module' },
|
||||
{ pattern: "src/plugins/omemo/tests/media-sharing.js", type: 'module' },
|
||||
{ pattern: "src/plugins/omemo/tests/omemo.js", type: 'module' },
|
||||
{ pattern: "src/plugins/omemo/tests/muc.js", type: 'module' },
|
||||
{ pattern: "src/plugins/push/tests/push.js", type: 'module' },
|
||||
{ pattern: "src/plugins/register/tests/register.js", type: 'module' },
|
||||
{ pattern: "src/plugins/rootview/tests/root.js", type: 'module' },
|
||||
|
478
src/plugins/omemo/tests/muc.js
Normal file
478
src/plugins/omemo/tests/muc.js
Normal file
@ -0,0 +1,478 @@
|
||||
/*global mock, converse */
|
||||
|
||||
const { $iq, $msg, $pres, Strophe, omemo } = converse.env;
|
||||
const u = converse.env.utils;
|
||||
|
||||
describe("The OMEMO module", function() {
|
||||
|
||||
it("enables encrypted groupchat messages to be sent and received",
|
||||
mock.initConverse(['chatBoxesFetched'], {}, async function (_converse) {
|
||||
|
||||
// MEMO encryption works only in members only conferences
|
||||
// that are non-anonymous.
|
||||
const features = [
|
||||
'http://jabber.org/protocol/muc',
|
||||
'jabber:iq:register',
|
||||
'muc_passwordprotected',
|
||||
'muc_hidden',
|
||||
'muc_temporary',
|
||||
'muc_membersonly',
|
||||
'muc_unmoderated',
|
||||
'muc_nonanonymous'
|
||||
];
|
||||
const muc_jid = 'lounge@montague.lit';
|
||||
await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo', features);
|
||||
const view = _converse.chatboxviews.get('lounge@montague.lit');
|
||||
await u.waitUntil(() => mock.initializedOMEMO(_converse));
|
||||
|
||||
const toolbar = await u.waitUntil(() => view.querySelector('.chat-toolbar'));
|
||||
const el = await u.waitUntil(() => toolbar.querySelector('.toggle-omemo'));
|
||||
el.click();
|
||||
expect(view.model.get('omemo_active')).toBe(true);
|
||||
|
||||
// newguy enters the room
|
||||
const contact_jid = 'newguy@montague.lit';
|
||||
let stanza = $pres({
|
||||
'to': 'romeo@montague.lit/orchard',
|
||||
'from': 'lounge@montague.lit/newguy'
|
||||
})
|
||||
.c('x', {xmlns: Strophe.NS.MUC_USER})
|
||||
.c('item', {
|
||||
'affiliation': 'none',
|
||||
'jid': 'newguy@montague.lit/_converse.js-290929789',
|
||||
'role': 'participant'
|
||||
}).tree();
|
||||
_converse.connection._dataRecv(mock.createRequest(stanza));
|
||||
|
||||
// Wait for Converse to fetch newguy's device list
|
||||
let iq_stanza = await u.waitUntil(() => mock.deviceListFetched(_converse, contact_jid));
|
||||
expect(Strophe.serialize(iq_stanza)).toBe(
|
||||
`<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>`);
|
||||
|
||||
// The server returns his device list
|
||||
stanza = $iq({
|
||||
'from': contact_jid,
|
||||
'id': iq_stanza.getAttribute('id'),
|
||||
'to': _converse.bare_jid,
|
||||
'type': 'result',
|
||||
}).c('pubsub', {'xmlns': "http://jabber.org/protocol/pubsub"})
|
||||
.c('items', {'node': "eu.siacs.conversations.axolotl.devicelist"})
|
||||
.c('item', {'xmlns': "http://jabber.org/protocol/pubsub"}) // TODO: must have an id attribute
|
||||
.c('list', {'xmlns': "eu.siacs.conversations.axolotl"})
|
||||
.c('device', {'id': '4e30f35051b7b8b42abe083742187228'}).up()
|
||||
_converse.connection._dataRecv(mock.createRequest(stanza));
|
||||
await u.waitUntil(() => _converse.omemo_store);
|
||||
expect(_converse.devicelists.length).toBe(2);
|
||||
|
||||
await u.waitUntil(() => mock.deviceListFetched(_converse, contact_jid));
|
||||
const devicelist = _converse.devicelists.get(contact_jid);
|
||||
expect(devicelist.devices.length).toBe(1);
|
||||
expect(devicelist.devices.at(0).get('id')).toBe('4e30f35051b7b8b42abe083742187228');
|
||||
expect(view.model.get('omemo_active')).toBe(true);
|
||||
|
||||
const icon = toolbar.querySelector('.toggle-omemo converse-icon');
|
||||
expect(u.hasClass('fa-unlock', icon)).toBe(false);
|
||||
expect(u.hasClass('fa-lock', icon)).toBe(true);
|
||||
|
||||
const textarea = view.querySelector('.chat-textarea');
|
||||
textarea.value = 'This message will be encrypted';
|
||||
const message_form = view.querySelector('converse-muc-message-form');
|
||||
message_form.onKeyDown({
|
||||
target: textarea,
|
||||
preventDefault: function preventDefault () {},
|
||||
keyCode: 13 // Enter
|
||||
});
|
||||
iq_stanza = await u.waitUntil(() => mock.bundleFetched(_converse, contact_jid, '4e30f35051b7b8b42abe083742187228'), 1000);
|
||||
console.log("Bundle fetched 4e30f35051b7b8b42abe083742187228");
|
||||
stanza = $iq({
|
||||
'from': contact_jid,
|
||||
'id': iq_stanza.getAttribute('id'),
|
||||
'to': _converse.bare_jid,
|
||||
'type': 'result',
|
||||
}).c('pubsub', {
|
||||
'xmlns': 'http://jabber.org/protocol/pubsub'
|
||||
}).c('items', {'node': "eu.siacs.conversations.axolotl.bundles:4e30f35051b7b8b42abe083742187228"})
|
||||
.c('item')
|
||||
.c('bundle', {'xmlns': 'eu.siacs.conversations.axolotl'})
|
||||
.c('signedPreKeyPublic', {'signedPreKeyId': '4223'}).t(btoa('1111')).up()
|
||||
.c('signedPreKeySignature').t(btoa('2222')).up()
|
||||
.c('identityKey').t(btoa('3333')).up()
|
||||
.c('prekeys')
|
||||
.c('preKeyPublic', {'preKeyId': '1'}).t(btoa('1001')).up()
|
||||
.c('preKeyPublic', {'preKeyId': '2'}).t(btoa('1002')).up()
|
||||
.c('preKeyPublic', {'preKeyId': '3'}).t(btoa('1003'));
|
||||
_converse.connection._dataRecv(mock.createRequest(stanza));
|
||||
|
||||
iq_stanza = await u.waitUntil(() => mock.bundleFetched(_converse, _converse.bare_jid, '482886413b977930064a5888b92134fe'), 1000);
|
||||
console.log("Bundle fetched 482886413b977930064a5888b92134fe");
|
||||
stanza = $iq({
|
||||
'from': _converse.bare_jid,
|
||||
'id': iq_stanza.getAttribute('id'),
|
||||
'to': _converse.bare_jid,
|
||||
'type': 'result',
|
||||
}).c('pubsub', {
|
||||
'xmlns': 'http://jabber.org/protocol/pubsub'
|
||||
}).c('items', {'node': "eu.siacs.conversations.axolotl.bundles:482886413b977930064a5888b92134fe"})
|
||||
.c('item')
|
||||
.c('bundle', {'xmlns': 'eu.siacs.conversations.axolotl'})
|
||||
.c('signedPreKeyPublic', {'signedPreKeyId': '4223'}).t(btoa('100000')).up()
|
||||
.c('signedPreKeySignature').t(btoa('200000')).up()
|
||||
.c('identityKey').t(btoa('300000')).up()
|
||||
.c('prekeys')
|
||||
.c('preKeyPublic', {'preKeyId': '1'}).t(btoa('1991')).up()
|
||||
.c('preKeyPublic', {'preKeyId': '2'}).t(btoa('1992')).up()
|
||||
.c('preKeyPublic', {'preKeyId': '3'}).t(btoa('1993'));
|
||||
|
||||
spyOn(_converse.connection, 'send');
|
||||
_converse.connection._dataRecv(mock.createRequest(stanza));
|
||||
await u.waitUntil(() => _converse.connection.send.calls.count(), 1000);
|
||||
const sent_stanza = _converse.connection.send.calls.all()[0].args[0];
|
||||
|
||||
expect(Strophe.serialize(sent_stanza)).toBe(
|
||||
`<message from="romeo@montague.lit/orchard" `+
|
||||
`id="${sent_stanza.getAttribute("id")}" `+
|
||||
`to="lounge@montague.lit" `+
|
||||
`type="groupchat" `+
|
||||
`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>`+
|
||||
`<encrypted xmlns="eu.siacs.conversations.axolotl">`+
|
||||
`<header sid="123456789">`+
|
||||
`<key rid="482886413b977930064a5888b92134fe">YzFwaDNSNzNYNw==</key>`+
|
||||
`<key rid="4e30f35051b7b8b42abe083742187228">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
|
||||
const obj = await omemo.encryptMessage('This is an encrypted message from the contact')
|
||||
// XXX: Normally the key will be encrypted via libsignal.
|
||||
// However, we're mocking libsignal in the tests, so we include it as plaintext in the message.
|
||||
stanza = $msg({
|
||||
'from': `${muc_jid}/newguy`,
|
||||
'to': _converse.connection.jid,
|
||||
'type': 'groupchat',
|
||||
'id': _converse.connection.getUniqueId()
|
||||
}).c('body').t('This is a fallback message').up()
|
||||
.c('encrypted', {'xmlns': Strophe.NS.OMEMO})
|
||||
.c('header', {'sid': '555'})
|
||||
.c('key', {'rid': _converse.omemo_store.get('device_id')}).t(u.arrayBufferToBase64(obj.key_and_tag)).up()
|
||||
.c('iv').t(obj.iv)
|
||||
.up().up()
|
||||
.c('payload').t(obj.payload);
|
||||
_converse.connection._dataRecv(mock.createRequest(stanza));
|
||||
await new Promise(resolve => view.model.messages.once('rendered', resolve));
|
||||
expect(view.model.messages.length).toBe(2);
|
||||
expect(view.querySelectorAll('.chat-msg__body')[1].textContent.trim())
|
||||
.toBe('This is an encrypted message from the contact');
|
||||
|
||||
expect(_converse.devicelists.length).toBe(2);
|
||||
expect(_converse.devicelists.at(0).get('jid')).toBe(_converse.bare_jid);
|
||||
expect(_converse.devicelists.at(1).get('jid')).toBe(contact_jid);
|
||||
}));
|
||||
|
||||
it("gracefully handles auth errors when trying to send encrypted groupchat messages",
|
||||
mock.initConverse(['chatBoxesFetched'], {}, async function (_converse) {
|
||||
|
||||
// MEMO encryption works only in members only conferences
|
||||
// that are non-anonymous.
|
||||
const features = [
|
||||
'http://jabber.org/protocol/muc',
|
||||
'jabber:iq:register',
|
||||
'muc_passwordprotected',
|
||||
'muc_hidden',
|
||||
'muc_temporary',
|
||||
'muc_membersonly',
|
||||
'muc_unmoderated',
|
||||
'muc_nonanonymous'
|
||||
];
|
||||
await mock.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'romeo', features);
|
||||
const view = _converse.chatboxviews.get('lounge@montague.lit');
|
||||
await u.waitUntil(() => mock.initializedOMEMO(_converse));
|
||||
|
||||
const contact_jid = 'newguy@montague.lit';
|
||||
let stanza = $pres({
|
||||
'to': 'romeo@montague.lit/orchard',
|
||||
'from': 'lounge@montague.lit/newguy'
|
||||
})
|
||||
.c('x', {xmlns: Strophe.NS.MUC_USER})
|
||||
.c('item', {
|
||||
'affiliation': 'none',
|
||||
'jid': 'newguy@montague.lit/_converse.js-290929789',
|
||||
'role': 'participant'
|
||||
}).tree();
|
||||
_converse.connection._dataRecv(mock.createRequest(stanza));
|
||||
|
||||
const toolbar = await u.waitUntil(() => view.querySelector('.chat-toolbar'));
|
||||
const toggle = await u.waitUntil(() => toolbar.querySelector('.toggle-omemo'));
|
||||
toggle.click();
|
||||
expect(view.model.get('omemo_active')).toBe(true);
|
||||
expect(view.model.get('omemo_supported')).toBe(true);
|
||||
|
||||
const textarea = await u.waitUntil(() => view.querySelector('.chat-textarea'));
|
||||
textarea.value = 'This message will be encrypted';
|
||||
const message_form = view.querySelector('converse-muc-message-form');
|
||||
message_form.onKeyDown({
|
||||
target: textarea,
|
||||
preventDefault: function preventDefault () {},
|
||||
keyCode: 13 // Enter
|
||||
});
|
||||
let iq_stanza = await u.waitUntil(() => mock.deviceListFetched(_converse, contact_jid));
|
||||
expect(Strophe.serialize(iq_stanza)).toBe(
|
||||
`<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>`);
|
||||
|
||||
stanza = $iq({
|
||||
'from': contact_jid,
|
||||
'id': iq_stanza.getAttribute('id'),
|
||||
'to': _converse.bare_jid,
|
||||
'type': 'result',
|
||||
}).c('pubsub', {'xmlns': "http://jabber.org/protocol/pubsub"})
|
||||
.c('items', {'node': "eu.siacs.conversations.axolotl.devicelist"})
|
||||
.c('item', {'xmlns': "http://jabber.org/protocol/pubsub"}) // TODO: must have an id attribute
|
||||
.c('list', {'xmlns': "eu.siacs.conversations.axolotl"})
|
||||
.c('device', {'id': '4e30f35051b7b8b42abe083742187228'}).up()
|
||||
|
||||
_converse.connection._dataRecv(mock.createRequest(stanza));
|
||||
await u.waitUntil(() => _converse.omemo_store);
|
||||
expect(_converse.devicelists.length).toBe(2);
|
||||
|
||||
const devicelist = _converse.devicelists.get(contact_jid);
|
||||
await u.waitUntil(() => mock.deviceListFetched(_converse, contact_jid));
|
||||
expect(devicelist.devices.length).toBe(1);
|
||||
expect(devicelist.devices.at(0).get('id')).toBe('4e30f35051b7b8b42abe083742187228');
|
||||
|
||||
iq_stanza = await u.waitUntil(() => mock.bundleFetched(_converse, _converse.bare_jid, '482886413b977930064a5888b92134fe'));
|
||||
stanza = $iq({
|
||||
'from': _converse.bare_jid,
|
||||
'id': iq_stanza.getAttribute('id'),
|
||||
'to': _converse.bare_jid,
|
||||
'type': 'result',
|
||||
}).c('pubsub', {
|
||||
'xmlns': 'http://jabber.org/protocol/pubsub'
|
||||
}).c('items', {'node': "eu.siacs.conversations.axolotl.bundles:482886413b977930064a5888b92134fe"})
|
||||
.c('item')
|
||||
.c('bundle', {'xmlns': 'eu.siacs.conversations.axolotl'})
|
||||
.c('signedPreKeyPublic', {'signedPreKeyId': '4223'}).t(btoa('100000')).up()
|
||||
.c('signedPreKeySignature').t(btoa('200000')).up()
|
||||
.c('identityKey').t(btoa('300000')).up()
|
||||
.c('prekeys')
|
||||
.c('preKeyPublic', {'preKeyId': '1'}).t(btoa('1991')).up()
|
||||
.c('preKeyPublic', {'preKeyId': '2'}).t(btoa('1992')).up()
|
||||
.c('preKeyPublic', {'preKeyId': '3'}).t(btoa('1993'));
|
||||
iq_stanza = await u.waitUntil(() => mock.bundleFetched(_converse, contact_jid, '4e30f35051b7b8b42abe083742187228'));
|
||||
|
||||
/* <iq xmlns="jabber:client" to="jc@opkode.com/converse.js-34183907" type="error" id="945c8ab3-b561-4d8a-92da-77c226bb1689:sendIQ" from="joris@konuro.net">
|
||||
* <pubsub xmlns="http://jabber.org/protocol/pubsub">
|
||||
* <items node="eu.siacs.conversations.axolotl.bundles:7580"/>
|
||||
* </pubsub>
|
||||
* <error code="401" type="auth">
|
||||
* <presence-subscription-required xmlns="http://jabber.org/protocol/pubsub#errors"/>
|
||||
* <not-authorized xmlns="urn:ietf:params:xml:ns:xmpp-stanzas"/>
|
||||
* </error>
|
||||
* </iq>
|
||||
*/
|
||||
stanza = $iq({
|
||||
'from': contact_jid,
|
||||
'id': iq_stanza.getAttribute('id'),
|
||||
'to': _converse.bare_jid,
|
||||
'type': 'result',
|
||||
}).c('pubsub', {'xmlns': 'http://jabber.org/protocol/pubsub'})
|
||||
.c('items', {'node': "eu.siacs.conversations.axolotl.bundles:4e30f35051b7b8b42abe083742187228"}).up().up()
|
||||
.c('error', {'code': '401', 'type': 'auth'})
|
||||
.c('presence-subscription-required', {'xmlns':"http://jabber.org/protocol/pubsub#errors" }).up()
|
||||
.c('not-authorized', {'xmlns': "urn:ietf:params:xml:ns:xmpp-stanzas"});
|
||||
_converse.connection._dataRecv(mock.createRequest(stanza));
|
||||
|
||||
await u.waitUntil(() => document.querySelectorAll('.alert-danger').length, 2000);
|
||||
const header = document.querySelector('.alert-danger .modal-title');
|
||||
expect(header.textContent).toBe("Error");
|
||||
expect(u.ancestor(header, '.modal-content').querySelector('.modal-body p').textContent.trim())
|
||||
.toBe("Sorry, we're unable to send an encrypted message because newguy@montague.lit requires you "+
|
||||
"to be subscribed to their presence in order to see their OMEMO information");
|
||||
|
||||
expect(view.model.get('omemo_supported')).toBe(false);
|
||||
expect(view.querySelector('.chat-textarea').value).toBe('This message will be encrypted');
|
||||
}));
|
||||
|
||||
|
||||
it("adds a toolbar button for starting an encrypted groupchat session",
|
||||
mock.initConverse(['chatBoxesFetched'], {}, async function (_converse) {
|
||||
|
||||
await mock.waitForRoster(_converse, 'current', 0);
|
||||
await mock.waitUntilDiscoConfirmed(
|
||||
_converse, _converse.bare_jid,
|
||||
[{'category': 'pubsub', 'type': 'pep'}],
|
||||
['http://jabber.org/protocol/pubsub#publish-options']
|
||||
);
|
||||
|
||||
// MEMO encryption works only in members-only conferences that are non-anonymous.
|
||||
const features = [
|
||||
'http://jabber.org/protocol/muc',
|
||||
'jabber:iq:register',
|
||||
'muc_passwordprotected',
|
||||
'muc_hidden',
|
||||
'muc_temporary',
|
||||
'muc_membersonly',
|
||||
'muc_unmoderated',
|
||||
'muc_nonanonymous'
|
||||
];
|
||||
await mock.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'romeo', features);
|
||||
const view = _converse.chatboxviews.get('lounge@montague.lit');
|
||||
await u.waitUntil(() => mock.initializedOMEMO(_converse));
|
||||
|
||||
const toolbar = await u.waitUntil(() => view.querySelector('.chat-toolbar'));
|
||||
let toggle = await u.waitUntil(() => toolbar.querySelector('.toggle-omemo'));
|
||||
expect(view.model.get('omemo_active')).toBe(undefined);
|
||||
expect(view.model.get('omemo_supported')).toBe(true);
|
||||
await u.waitUntil(() => toggle.dataset.disabled === "false");
|
||||
|
||||
let icon = toolbar.querySelector('.toggle-omemo converse-icon');
|
||||
expect(u.hasClass('fa-unlock', icon)).toBe(true);
|
||||
expect(u.hasClass('fa-lock', icon)).toBe(false);
|
||||
|
||||
toggle.click();
|
||||
toggle = toolbar.querySelector('.toggle-omemo');
|
||||
expect(toggle.dataset.disabled).toBe("false");
|
||||
expect(view.model.get('omemo_active')).toBe(true);
|
||||
expect(view.model.get('omemo_supported')).toBe(true);
|
||||
|
||||
await u.waitUntil(() => !u.hasClass('fa-unlock', toolbar.querySelector('.toggle-omemo converse-icon')));
|
||||
expect(u.hasClass('fa-lock', toolbar.querySelector('.toggle-omemo converse-icon'))).toBe(true);
|
||||
|
||||
let contact_jid = 'newguy@montague.lit';
|
||||
let stanza = $pres({
|
||||
to: 'romeo@montague.lit/orchard',
|
||||
from: 'lounge@montague.lit/newguy'
|
||||
})
|
||||
.c('x', {xmlns: Strophe.NS.MUC_USER})
|
||||
.c('item', {
|
||||
'affiliation': 'none',
|
||||
'jid': 'newguy@montague.lit/_converse.js-290929789',
|
||||
'role': 'participant'
|
||||
}).tree();
|
||||
_converse.connection._dataRecv(mock.createRequest(stanza));
|
||||
|
||||
let iq_stanza = await u.waitUntil(() => mock.deviceListFetched(_converse, contact_jid));
|
||||
expect(Strophe.serialize(iq_stanza)).toBe(
|
||||
`<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>`);
|
||||
|
||||
stanza = $iq({
|
||||
'from': contact_jid,
|
||||
'id': iq_stanza.getAttribute('id'),
|
||||
'to': _converse.bare_jid,
|
||||
'type': 'result',
|
||||
}).c('pubsub', {'xmlns': "http://jabber.org/protocol/pubsub"})
|
||||
.c('items', {'node': "eu.siacs.conversations.axolotl.devicelist"})
|
||||
.c('item', {'xmlns': "http://jabber.org/protocol/pubsub"}) // TODO: must have an id attribute
|
||||
.c('list', {'xmlns': "eu.siacs.conversations.axolotl"})
|
||||
.c('device', {'id': '4e30f35051b7b8b42abe083742187228'}).up()
|
||||
.c('device', {'id': 'ae890ac52d0df67ed7cfdf51b644e901'});
|
||||
_converse.connection._dataRecv(mock.createRequest(stanza));
|
||||
await u.waitUntil(() => _converse.omemo_store);
|
||||
expect(_converse.devicelists.length).toBe(2);
|
||||
|
||||
await u.waitUntil(() => mock.deviceListFetched(_converse, contact_jid));
|
||||
const devicelist = _converse.devicelists.get(contact_jid);
|
||||
expect(devicelist.devices.length).toBe(2);
|
||||
expect(devicelist.devices.at(0).get('id')).toBe('4e30f35051b7b8b42abe083742187228');
|
||||
expect(devicelist.devices.at(1).get('id')).toBe('ae890ac52d0df67ed7cfdf51b644e901');
|
||||
|
||||
expect(view.model.get('omemo_active')).toBe(true);
|
||||
toggle = toolbar.querySelector('.toggle-omemo');
|
||||
expect(toggle === null).toBe(false);
|
||||
expect(toggle.dataset.disabled).toBe("false");
|
||||
expect(view.model.get('omemo_supported')).toBe(true);
|
||||
|
||||
await u.waitUntil(() => !u.hasClass('fa-unlock', toolbar.querySelector('.toggle-omemo converse-icon')));
|
||||
expect(u.hasClass('fa-lock', toolbar.querySelector('.toggle-omemo converse-icon'))).toBe(true);
|
||||
|
||||
// Test that the button gets disabled when the room becomes
|
||||
// anonymous or semi-anonymous
|
||||
view.model.features.save({'nonanonymous': false, 'semianonymous': true});
|
||||
await u.waitUntil(() => !view.model.get('omemo_supported'));
|
||||
await u.waitUntil(() => view.querySelector('.toggle-omemo').dataset.disabled === "true");
|
||||
|
||||
view.model.features.save({'nonanonymous': true, 'semianonymous': false});
|
||||
await u.waitUntil(() => view.model.get('omemo_supported'));
|
||||
await u.waitUntil(() => view.querySelector('.toggle-omemo') !== null);
|
||||
expect(u.hasClass('fa-unlock', toolbar.querySelector('.toggle-omemo converse-icon'))).toBe(true);
|
||||
expect(u.hasClass('fa-lock', toolbar.querySelector('.toggle-omemo converse-icon'))).toBe(false);
|
||||
expect(view.querySelector('.toggle-omemo').dataset.disabled).toBe("false");
|
||||
|
||||
// Test that the button gets disabled when the room becomes open
|
||||
view.model.features.save({'membersonly': false, 'open': true});
|
||||
await u.waitUntil(() => !view.model.get('omemo_supported'));
|
||||
await u.waitUntil(() => view.querySelector('.toggle-omemo').dataset.disabled === "true");
|
||||
|
||||
view.model.features.save({'membersonly': true, 'open': false});
|
||||
await u.waitUntil(() => view.model.get('omemo_supported'));
|
||||
await u.waitUntil(() => view.querySelector('.toggle-omemo').dataset.disabled === "false");
|
||||
|
||||
expect(u.hasClass('fa-unlock', view.querySelector('.toggle-omemo converse-icon'))).toBe(true);
|
||||
expect(u.hasClass('fa-lock', view.querySelector('.toggle-omemo converse-icon'))).toBe(false);
|
||||
|
||||
expect(view.model.get('omemo_supported')).toBe(true);
|
||||
expect(view.model.get('omemo_active')).toBe(false);
|
||||
|
||||
view.querySelector('.toggle-omemo').click();
|
||||
expect(view.model.get('omemo_active')).toBe(true);
|
||||
|
||||
// Someone enters the room who doesn't have OMEMO support, while we
|
||||
// have OMEMO activated...
|
||||
contact_jid = 'oldguy@montague.lit';
|
||||
stanza = $pres({
|
||||
to: 'romeo@montague.lit/orchard',
|
||||
from: 'lounge@montague.lit/oldguy'
|
||||
})
|
||||
.c('x', {xmlns: Strophe.NS.MUC_USER})
|
||||
.c('item', {
|
||||
'affiliation': 'none',
|
||||
'jid': `${contact_jid}/_converse.js-290929788`,
|
||||
'role': 'participant'
|
||||
}).tree();
|
||||
_converse.connection._dataRecv(mock.createRequest(stanza));
|
||||
iq_stanza = await u.waitUntil(() => mock.deviceListFetched(_converse, contact_jid));
|
||||
expect(Strophe.serialize(iq_stanza)).toBe(
|
||||
`<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>`);
|
||||
|
||||
stanza = $iq({
|
||||
'from': contact_jid,
|
||||
'id': iq_stanza.getAttribute('id'),
|
||||
'to': _converse.bare_jid,
|
||||
'type': 'error'
|
||||
}).c('error', {'type': 'cancel'})
|
||||
.c('item-not-found', {'xmlns': "urn:ietf:params:xml:ns:xmpp-stanzas"});
|
||||
_converse.connection._dataRecv(mock.createRequest(stanza));
|
||||
|
||||
await u.waitUntil(() => !view.model.get('omemo_supported'));
|
||||
await u.waitUntil(() => view.querySelector('.chat-error .chat-info__message')?.textContent.trim() ===
|
||||
"oldguy doesn't appear to have a client that supports OMEMO. "+
|
||||
"Encrypted chat will no longer be possible in this grouchat."
|
||||
);
|
||||
|
||||
await u.waitUntil(() => toolbar.querySelector('.toggle-omemo').dataset.disabled === "true");
|
||||
icon = view.querySelector('.toggle-omemo converse-icon');
|
||||
expect(u.hasClass('fa-unlock', icon)).toBe(true);
|
||||
expect(u.hasClass('fa-lock', icon)).toBe(false);
|
||||
expect(toolbar.querySelector('.toggle-omemo').title).toBe('This groupchat needs to be members-only and non-anonymous in order to support OMEMO encrypted messages');
|
||||
}));
|
||||
});
|
@ -1,6 +1,6 @@
|
||||
/*global mock, converse */
|
||||
|
||||
const { $iq, $pres, $msg, omemo, Strophe } = converse.env;
|
||||
const { $iq, $msg, omemo, Strophe } = converse.env;
|
||||
const u = converse.env.utils;
|
||||
|
||||
describe("The OMEMO module", function() {
|
||||
@ -152,152 +152,6 @@ describe("The OMEMO module", function() {
|
||||
.toBe('Another received encrypted message without fallback');
|
||||
}));
|
||||
|
||||
it("enables encrypted groupchat messages to be sent and received",
|
||||
mock.initConverse(['chatBoxesFetched'], {}, async function (_converse) {
|
||||
|
||||
// MEMO encryption works only in members only conferences
|
||||
// that are non-anonymous.
|
||||
const features = [
|
||||
'http://jabber.org/protocol/muc',
|
||||
'jabber:iq:register',
|
||||
'muc_passwordprotected',
|
||||
'muc_hidden',
|
||||
'muc_temporary',
|
||||
'muc_membersonly',
|
||||
'muc_unmoderated',
|
||||
'muc_nonanonymous'
|
||||
];
|
||||
await mock.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'romeo', features);
|
||||
const view = _converse.chatboxviews.get('lounge@montague.lit');
|
||||
await u.waitUntil(() => mock.initializedOMEMO(_converse));
|
||||
|
||||
const toolbar = await u.waitUntil(() => view.querySelector('.chat-toolbar'));
|
||||
const el = await u.waitUntil(() => toolbar.querySelector('.toggle-omemo'));
|
||||
el.click();
|
||||
expect(view.model.get('omemo_active')).toBe(true);
|
||||
|
||||
// newguy enters the room
|
||||
const contact_jid = 'newguy@montague.lit';
|
||||
let stanza = $pres({
|
||||
'to': 'romeo@montague.lit/orchard',
|
||||
'from': 'lounge@montague.lit/newguy'
|
||||
})
|
||||
.c('x', {xmlns: Strophe.NS.MUC_USER})
|
||||
.c('item', {
|
||||
'affiliation': 'none',
|
||||
'jid': 'newguy@montague.lit/_converse.js-290929789',
|
||||
'role': 'participant'
|
||||
}).tree();
|
||||
_converse.connection._dataRecv(mock.createRequest(stanza));
|
||||
|
||||
// Wait for Converse to fetch newguy's device list
|
||||
let iq_stanza = await u.waitUntil(() => mock.deviceListFetched(_converse, contact_jid));
|
||||
expect(Strophe.serialize(iq_stanza)).toBe(
|
||||
`<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>`);
|
||||
|
||||
// The server returns his device list
|
||||
stanza = $iq({
|
||||
'from': contact_jid,
|
||||
'id': iq_stanza.getAttribute('id'),
|
||||
'to': _converse.bare_jid,
|
||||
'type': 'result',
|
||||
}).c('pubsub', {'xmlns': "http://jabber.org/protocol/pubsub"})
|
||||
.c('items', {'node': "eu.siacs.conversations.axolotl.devicelist"})
|
||||
.c('item', {'xmlns': "http://jabber.org/protocol/pubsub"}) // TODO: must have an id attribute
|
||||
.c('list', {'xmlns': "eu.siacs.conversations.axolotl"})
|
||||
.c('device', {'id': '4e30f35051b7b8b42abe083742187228'}).up()
|
||||
_converse.connection._dataRecv(mock.createRequest(stanza));
|
||||
await u.waitUntil(() => _converse.omemo_store);
|
||||
expect(_converse.devicelists.length).toBe(2);
|
||||
|
||||
await u.waitUntil(() => mock.deviceListFetched(_converse, contact_jid));
|
||||
const devicelist = _converse.devicelists.get(contact_jid);
|
||||
expect(devicelist.devices.length).toBe(1);
|
||||
expect(devicelist.devices.at(0).get('id')).toBe('4e30f35051b7b8b42abe083742187228');
|
||||
expect(view.model.get('omemo_active')).toBe(true);
|
||||
|
||||
const icon = toolbar.querySelector('.toggle-omemo converse-icon');
|
||||
expect(u.hasClass('fa-unlock', icon)).toBe(false);
|
||||
expect(u.hasClass('fa-lock', icon)).toBe(true);
|
||||
|
||||
const textarea = view.querySelector('.chat-textarea');
|
||||
textarea.value = 'This message will be encrypted';
|
||||
const message_form = view.querySelector('converse-muc-message-form');
|
||||
message_form.onKeyDown({
|
||||
target: textarea,
|
||||
preventDefault: function preventDefault () {},
|
||||
keyCode: 13 // Enter
|
||||
});
|
||||
iq_stanza = await u.waitUntil(() => mock.bundleFetched(_converse, contact_jid, '4e30f35051b7b8b42abe083742187228'), 1000);
|
||||
console.log("Bundle fetched 4e30f35051b7b8b42abe083742187228");
|
||||
stanza = $iq({
|
||||
'from': contact_jid,
|
||||
'id': iq_stanza.getAttribute('id'),
|
||||
'to': _converse.bare_jid,
|
||||
'type': 'result',
|
||||
}).c('pubsub', {
|
||||
'xmlns': 'http://jabber.org/protocol/pubsub'
|
||||
}).c('items', {'node': "eu.siacs.conversations.axolotl.bundles:4e30f35051b7b8b42abe083742187228"})
|
||||
.c('item')
|
||||
.c('bundle', {'xmlns': 'eu.siacs.conversations.axolotl'})
|
||||
.c('signedPreKeyPublic', {'signedPreKeyId': '4223'}).t(btoa('1111')).up()
|
||||
.c('signedPreKeySignature').t(btoa('2222')).up()
|
||||
.c('identityKey').t(btoa('3333')).up()
|
||||
.c('prekeys')
|
||||
.c('preKeyPublic', {'preKeyId': '1'}).t(btoa('1001')).up()
|
||||
.c('preKeyPublic', {'preKeyId': '2'}).t(btoa('1002')).up()
|
||||
.c('preKeyPublic', {'preKeyId': '3'}).t(btoa('1003'));
|
||||
_converse.connection._dataRecv(mock.createRequest(stanza));
|
||||
|
||||
iq_stanza = await u.waitUntil(() => mock.bundleFetched(_converse, _converse.bare_jid, '482886413b977930064a5888b92134fe'), 1000);
|
||||
console.log("Bundle fetched 482886413b977930064a5888b92134fe");
|
||||
stanza = $iq({
|
||||
'from': _converse.bare_jid,
|
||||
'id': iq_stanza.getAttribute('id'),
|
||||
'to': _converse.bare_jid,
|
||||
'type': 'result',
|
||||
}).c('pubsub', {
|
||||
'xmlns': 'http://jabber.org/protocol/pubsub'
|
||||
}).c('items', {'node': "eu.siacs.conversations.axolotl.bundles:482886413b977930064a5888b92134fe"})
|
||||
.c('item')
|
||||
.c('bundle', {'xmlns': 'eu.siacs.conversations.axolotl'})
|
||||
.c('signedPreKeyPublic', {'signedPreKeyId': '4223'}).t(btoa('100000')).up()
|
||||
.c('signedPreKeySignature').t(btoa('200000')).up()
|
||||
.c('identityKey').t(btoa('300000')).up()
|
||||
.c('prekeys')
|
||||
.c('preKeyPublic', {'preKeyId': '1'}).t(btoa('1991')).up()
|
||||
.c('preKeyPublic', {'preKeyId': '2'}).t(btoa('1992')).up()
|
||||
.c('preKeyPublic', {'preKeyId': '3'}).t(btoa('1993'));
|
||||
|
||||
spyOn(_converse.connection, 'send');
|
||||
_converse.connection._dataRecv(mock.createRequest(stanza));
|
||||
await u.waitUntil(() => _converse.connection.send.calls.count(), 1000);
|
||||
const sent_stanza = _converse.connection.send.calls.all()[0].args[0];
|
||||
|
||||
expect(Strophe.serialize(sent_stanza)).toBe(
|
||||
`<message from="romeo@montague.lit/orchard" `+
|
||||
`id="${sent_stanza.getAttribute("id")}" `+
|
||||
`to="lounge@montague.lit" `+
|
||||
`type="groupchat" `+
|
||||
`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>`+
|
||||
`<encrypted xmlns="eu.siacs.conversations.axolotl">`+
|
||||
`<header sid="123456789">`+
|
||||
`<key rid="482886413b977930064a5888b92134fe">YzFwaDNSNzNYNw==</key>`+
|
||||
`<key rid="4e30f35051b7b8b42abe083742187228">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>`);
|
||||
}));
|
||||
|
||||
it("will create a new device based on a received carbon message",
|
||||
mock.initConverse(['chatBoxesFetched'], {}, async function (_converse) {
|
||||
|
||||
@ -406,133 +260,6 @@ describe("The OMEMO module", function() {
|
||||
`</iq>`);
|
||||
}));
|
||||
|
||||
it("gracefully handles auth errors when trying to send encrypted groupchat messages",
|
||||
mock.initConverse(['chatBoxesFetched'], {}, async function (_converse) {
|
||||
|
||||
// MEMO encryption works only in members only conferences
|
||||
// that are non-anonymous.
|
||||
const features = [
|
||||
'http://jabber.org/protocol/muc',
|
||||
'jabber:iq:register',
|
||||
'muc_passwordprotected',
|
||||
'muc_hidden',
|
||||
'muc_temporary',
|
||||
'muc_membersonly',
|
||||
'muc_unmoderated',
|
||||
'muc_nonanonymous'
|
||||
];
|
||||
await mock.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'romeo', features);
|
||||
const view = _converse.chatboxviews.get('lounge@montague.lit');
|
||||
await u.waitUntil(() => mock.initializedOMEMO(_converse));
|
||||
|
||||
const contact_jid = 'newguy@montague.lit';
|
||||
let stanza = $pres({
|
||||
'to': 'romeo@montague.lit/orchard',
|
||||
'from': 'lounge@montague.lit/newguy'
|
||||
})
|
||||
.c('x', {xmlns: Strophe.NS.MUC_USER})
|
||||
.c('item', {
|
||||
'affiliation': 'none',
|
||||
'jid': 'newguy@montague.lit/_converse.js-290929789',
|
||||
'role': 'participant'
|
||||
}).tree();
|
||||
_converse.connection._dataRecv(mock.createRequest(stanza));
|
||||
|
||||
const toolbar = await u.waitUntil(() => view.querySelector('.chat-toolbar'));
|
||||
const toggle = await u.waitUntil(() => toolbar.querySelector('.toggle-omemo'));
|
||||
toggle.click();
|
||||
expect(view.model.get('omemo_active')).toBe(true);
|
||||
expect(view.model.get('omemo_supported')).toBe(true);
|
||||
|
||||
const textarea = await u.waitUntil(() => view.querySelector('.chat-textarea'));
|
||||
textarea.value = 'This message will be encrypted';
|
||||
const message_form = view.querySelector('converse-muc-message-form');
|
||||
message_form.onKeyDown({
|
||||
target: textarea,
|
||||
preventDefault: function preventDefault () {},
|
||||
keyCode: 13 // Enter
|
||||
});
|
||||
let iq_stanza = await u.waitUntil(() => mock.deviceListFetched(_converse, contact_jid));
|
||||
expect(Strophe.serialize(iq_stanza)).toBe(
|
||||
`<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>`);
|
||||
|
||||
stanza = $iq({
|
||||
'from': contact_jid,
|
||||
'id': iq_stanza.getAttribute('id'),
|
||||
'to': _converse.bare_jid,
|
||||
'type': 'result',
|
||||
}).c('pubsub', {'xmlns': "http://jabber.org/protocol/pubsub"})
|
||||
.c('items', {'node': "eu.siacs.conversations.axolotl.devicelist"})
|
||||
.c('item', {'xmlns': "http://jabber.org/protocol/pubsub"}) // TODO: must have an id attribute
|
||||
.c('list', {'xmlns': "eu.siacs.conversations.axolotl"})
|
||||
.c('device', {'id': '4e30f35051b7b8b42abe083742187228'}).up()
|
||||
|
||||
_converse.connection._dataRecv(mock.createRequest(stanza));
|
||||
await u.waitUntil(() => _converse.omemo_store);
|
||||
expect(_converse.devicelists.length).toBe(2);
|
||||
|
||||
const devicelist = _converse.devicelists.get(contact_jid);
|
||||
await u.waitUntil(() => mock.deviceListFetched(_converse, contact_jid));
|
||||
expect(devicelist.devices.length).toBe(1);
|
||||
expect(devicelist.devices.at(0).get('id')).toBe('4e30f35051b7b8b42abe083742187228');
|
||||
|
||||
iq_stanza = await u.waitUntil(() => mock.bundleFetched(_converse, _converse.bare_jid, '482886413b977930064a5888b92134fe'));
|
||||
stanza = $iq({
|
||||
'from': _converse.bare_jid,
|
||||
'id': iq_stanza.getAttribute('id'),
|
||||
'to': _converse.bare_jid,
|
||||
'type': 'result',
|
||||
}).c('pubsub', {
|
||||
'xmlns': 'http://jabber.org/protocol/pubsub'
|
||||
}).c('items', {'node': "eu.siacs.conversations.axolotl.bundles:482886413b977930064a5888b92134fe"})
|
||||
.c('item')
|
||||
.c('bundle', {'xmlns': 'eu.siacs.conversations.axolotl'})
|
||||
.c('signedPreKeyPublic', {'signedPreKeyId': '4223'}).t(btoa('100000')).up()
|
||||
.c('signedPreKeySignature').t(btoa('200000')).up()
|
||||
.c('identityKey').t(btoa('300000')).up()
|
||||
.c('prekeys')
|
||||
.c('preKeyPublic', {'preKeyId': '1'}).t(btoa('1991')).up()
|
||||
.c('preKeyPublic', {'preKeyId': '2'}).t(btoa('1992')).up()
|
||||
.c('preKeyPublic', {'preKeyId': '3'}).t(btoa('1993'));
|
||||
iq_stanza = await u.waitUntil(() => mock.bundleFetched(_converse, contact_jid, '4e30f35051b7b8b42abe083742187228'));
|
||||
|
||||
/* <iq xmlns="jabber:client" to="jc@opkode.com/converse.js-34183907" type="error" id="945c8ab3-b561-4d8a-92da-77c226bb1689:sendIQ" from="joris@konuro.net">
|
||||
* <pubsub xmlns="http://jabber.org/protocol/pubsub">
|
||||
* <items node="eu.siacs.conversations.axolotl.bundles:7580"/>
|
||||
* </pubsub>
|
||||
* <error code="401" type="auth">
|
||||
* <presence-subscription-required xmlns="http://jabber.org/protocol/pubsub#errors"/>
|
||||
* <not-authorized xmlns="urn:ietf:params:xml:ns:xmpp-stanzas"/>
|
||||
* </error>
|
||||
* </iq>
|
||||
*/
|
||||
stanza = $iq({
|
||||
'from': contact_jid,
|
||||
'id': iq_stanza.getAttribute('id'),
|
||||
'to': _converse.bare_jid,
|
||||
'type': 'result',
|
||||
}).c('pubsub', {'xmlns': 'http://jabber.org/protocol/pubsub'})
|
||||
.c('items', {'node': "eu.siacs.conversations.axolotl.bundles:4e30f35051b7b8b42abe083742187228"}).up().up()
|
||||
.c('error', {'code': '401', 'type': 'auth'})
|
||||
.c('presence-subscription-required', {'xmlns':"http://jabber.org/protocol/pubsub#errors" }).up()
|
||||
.c('not-authorized', {'xmlns': "urn:ietf:params:xml:ns:xmpp-stanzas"});
|
||||
_converse.connection._dataRecv(mock.createRequest(stanza));
|
||||
|
||||
await u.waitUntil(() => document.querySelectorAll('.alert-danger').length, 2000);
|
||||
const header = document.querySelector('.alert-danger .modal-title');
|
||||
expect(header.textContent).toBe("Error");
|
||||
expect(u.ancestor(header, '.modal-content').querySelector('.modal-body p').textContent.trim())
|
||||
.toBe("Sorry, we're unable to send an encrypted message because newguy@montague.lit requires you "+
|
||||
"to be subscribed to their presence in order to see their OMEMO information");
|
||||
|
||||
expect(view.model.get('omemo_supported')).toBe(false);
|
||||
expect(view.querySelector('.chat-textarea').value).toBe('This message will be encrypted');
|
||||
}));
|
||||
|
||||
it("can receive a PreKeySignalMessage",
|
||||
mock.initConverse(['chatBoxesFetched'], {}, async function (_converse) {
|
||||
|
||||
@ -1178,177 +905,6 @@ describe("The OMEMO module", function() {
|
||||
expect(u.hasClass('fa-unlock', icon)).toBe(true);
|
||||
}));
|
||||
|
||||
it("adds a toolbar button for starting an encrypted groupchat session",
|
||||
mock.initConverse(['chatBoxesFetched'], {}, async function (_converse) {
|
||||
|
||||
await mock.waitForRoster(_converse, 'current', 0);
|
||||
await mock.waitUntilDiscoConfirmed(
|
||||
_converse, _converse.bare_jid,
|
||||
[{'category': 'pubsub', 'type': 'pep'}],
|
||||
['http://jabber.org/protocol/pubsub#publish-options']
|
||||
);
|
||||
|
||||
// MEMO encryption works only in members-only conferences that are non-anonymous.
|
||||
const features = [
|
||||
'http://jabber.org/protocol/muc',
|
||||
'jabber:iq:register',
|
||||
'muc_passwordprotected',
|
||||
'muc_hidden',
|
||||
'muc_temporary',
|
||||
'muc_membersonly',
|
||||
'muc_unmoderated',
|
||||
'muc_nonanonymous'
|
||||
];
|
||||
await mock.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'romeo', features);
|
||||
const view = _converse.chatboxviews.get('lounge@montague.lit');
|
||||
await u.waitUntil(() => mock.initializedOMEMO(_converse));
|
||||
|
||||
const toolbar = await u.waitUntil(() => view.querySelector('.chat-toolbar'));
|
||||
let toggle = await u.waitUntil(() => toolbar.querySelector('.toggle-omemo'));
|
||||
expect(view.model.get('omemo_active')).toBe(undefined);
|
||||
expect(view.model.get('omemo_supported')).toBe(true);
|
||||
await u.waitUntil(() => toggle.dataset.disabled === "false");
|
||||
|
||||
let icon = toolbar.querySelector('.toggle-omemo converse-icon');
|
||||
expect(u.hasClass('fa-unlock', icon)).toBe(true);
|
||||
expect(u.hasClass('fa-lock', icon)).toBe(false);
|
||||
|
||||
toggle.click();
|
||||
toggle = toolbar.querySelector('.toggle-omemo');
|
||||
expect(toggle.dataset.disabled).toBe("false");
|
||||
expect(view.model.get('omemo_active')).toBe(true);
|
||||
expect(view.model.get('omemo_supported')).toBe(true);
|
||||
|
||||
await u.waitUntil(() => !u.hasClass('fa-unlock', toolbar.querySelector('.toggle-omemo converse-icon')));
|
||||
expect(u.hasClass('fa-lock', toolbar.querySelector('.toggle-omemo converse-icon'))).toBe(true);
|
||||
|
||||
let contact_jid = 'newguy@montague.lit';
|
||||
let stanza = $pres({
|
||||
to: 'romeo@montague.lit/orchard',
|
||||
from: 'lounge@montague.lit/newguy'
|
||||
})
|
||||
.c('x', {xmlns: Strophe.NS.MUC_USER})
|
||||
.c('item', {
|
||||
'affiliation': 'none',
|
||||
'jid': 'newguy@montague.lit/_converse.js-290929789',
|
||||
'role': 'participant'
|
||||
}).tree();
|
||||
_converse.connection._dataRecv(mock.createRequest(stanza));
|
||||
|
||||
let iq_stanza = await u.waitUntil(() => mock.deviceListFetched(_converse, contact_jid));
|
||||
expect(Strophe.serialize(iq_stanza)).toBe(
|
||||
`<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>`);
|
||||
|
||||
stanza = $iq({
|
||||
'from': contact_jid,
|
||||
'id': iq_stanza.getAttribute('id'),
|
||||
'to': _converse.bare_jid,
|
||||
'type': 'result',
|
||||
}).c('pubsub', {'xmlns': "http://jabber.org/protocol/pubsub"})
|
||||
.c('items', {'node': "eu.siacs.conversations.axolotl.devicelist"})
|
||||
.c('item', {'xmlns': "http://jabber.org/protocol/pubsub"}) // TODO: must have an id attribute
|
||||
.c('list', {'xmlns': "eu.siacs.conversations.axolotl"})
|
||||
.c('device', {'id': '4e30f35051b7b8b42abe083742187228'}).up()
|
||||
.c('device', {'id': 'ae890ac52d0df67ed7cfdf51b644e901'});
|
||||
_converse.connection._dataRecv(mock.createRequest(stanza));
|
||||
await u.waitUntil(() => _converse.omemo_store);
|
||||
expect(_converse.devicelists.length).toBe(2);
|
||||
|
||||
await u.waitUntil(() => mock.deviceListFetched(_converse, contact_jid));
|
||||
const devicelist = _converse.devicelists.get(contact_jid);
|
||||
expect(devicelist.devices.length).toBe(2);
|
||||
expect(devicelist.devices.at(0).get('id')).toBe('4e30f35051b7b8b42abe083742187228');
|
||||
expect(devicelist.devices.at(1).get('id')).toBe('ae890ac52d0df67ed7cfdf51b644e901');
|
||||
|
||||
expect(view.model.get('omemo_active')).toBe(true);
|
||||
toggle = toolbar.querySelector('.toggle-omemo');
|
||||
expect(toggle === null).toBe(false);
|
||||
expect(toggle.dataset.disabled).toBe("false");
|
||||
expect(view.model.get('omemo_supported')).toBe(true);
|
||||
|
||||
await u.waitUntil(() => !u.hasClass('fa-unlock', toolbar.querySelector('.toggle-omemo converse-icon')));
|
||||
expect(u.hasClass('fa-lock', toolbar.querySelector('.toggle-omemo converse-icon'))).toBe(true);
|
||||
|
||||
// Test that the button gets disabled when the room becomes
|
||||
// anonymous or semi-anonymous
|
||||
view.model.features.save({'nonanonymous': false, 'semianonymous': true});
|
||||
await u.waitUntil(() => !view.model.get('omemo_supported'));
|
||||
await u.waitUntil(() => view.querySelector('.toggle-omemo').dataset.disabled === "true");
|
||||
|
||||
view.model.features.save({'nonanonymous': true, 'semianonymous': false});
|
||||
await u.waitUntil(() => view.model.get('omemo_supported'));
|
||||
await u.waitUntil(() => view.querySelector('.toggle-omemo') !== null);
|
||||
expect(u.hasClass('fa-unlock', toolbar.querySelector('.toggle-omemo converse-icon'))).toBe(true);
|
||||
expect(u.hasClass('fa-lock', toolbar.querySelector('.toggle-omemo converse-icon'))).toBe(false);
|
||||
expect(view.querySelector('.toggle-omemo').dataset.disabled).toBe("false");
|
||||
|
||||
// Test that the button gets disabled when the room becomes open
|
||||
view.model.features.save({'membersonly': false, 'open': true});
|
||||
await u.waitUntil(() => !view.model.get('omemo_supported'));
|
||||
await u.waitUntil(() => view.querySelector('.toggle-omemo').dataset.disabled === "true");
|
||||
|
||||
view.model.features.save({'membersonly': true, 'open': false});
|
||||
await u.waitUntil(() => view.model.get('omemo_supported'));
|
||||
await u.waitUntil(() => view.querySelector('.toggle-omemo').dataset.disabled === "false");
|
||||
|
||||
expect(u.hasClass('fa-unlock', view.querySelector('.toggle-omemo converse-icon'))).toBe(true);
|
||||
expect(u.hasClass('fa-lock', view.querySelector('.toggle-omemo converse-icon'))).toBe(false);
|
||||
|
||||
expect(view.model.get('omemo_supported')).toBe(true);
|
||||
expect(view.model.get('omemo_active')).toBe(false);
|
||||
|
||||
view.querySelector('.toggle-omemo').click();
|
||||
expect(view.model.get('omemo_active')).toBe(true);
|
||||
|
||||
// Someone enters the room who doesn't have OMEMO support, while we
|
||||
// have OMEMO activated...
|
||||
contact_jid = 'oldguy@montague.lit';
|
||||
stanza = $pres({
|
||||
to: 'romeo@montague.lit/orchard',
|
||||
from: 'lounge@montague.lit/oldguy'
|
||||
})
|
||||
.c('x', {xmlns: Strophe.NS.MUC_USER})
|
||||
.c('item', {
|
||||
'affiliation': 'none',
|
||||
'jid': `${contact_jid}/_converse.js-290929788`,
|
||||
'role': 'participant'
|
||||
}).tree();
|
||||
_converse.connection._dataRecv(mock.createRequest(stanza));
|
||||
iq_stanza = await u.waitUntil(() => mock.deviceListFetched(_converse, contact_jid));
|
||||
expect(Strophe.serialize(iq_stanza)).toBe(
|
||||
`<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>`);
|
||||
|
||||
stanza = $iq({
|
||||
'from': contact_jid,
|
||||
'id': iq_stanza.getAttribute('id'),
|
||||
'to': _converse.bare_jid,
|
||||
'type': 'error'
|
||||
}).c('error', {'type': 'cancel'})
|
||||
.c('item-not-found', {'xmlns': "urn:ietf:params:xml:ns:xmpp-stanzas"});
|
||||
_converse.connection._dataRecv(mock.createRequest(stanza));
|
||||
|
||||
await u.waitUntil(() => !view.model.get('omemo_supported'));
|
||||
await u.waitUntil(() => view.querySelector('.chat-error .chat-info__message')?.textContent.trim() ===
|
||||
"oldguy doesn't appear to have a client that supports OMEMO. "+
|
||||
"Encrypted chat will no longer be possible in this grouchat."
|
||||
);
|
||||
|
||||
await u.waitUntil(() => toolbar.querySelector('.toggle-omemo').dataset.disabled === "true");
|
||||
icon = view.querySelector('.toggle-omemo converse-icon');
|
||||
expect(u.hasClass('fa-unlock', icon)).toBe(true);
|
||||
expect(u.hasClass('fa-lock', icon)).toBe(false);
|
||||
expect(toolbar.querySelector('.toggle-omemo').title).toBe('This groupchat needs to be members-only and non-anonymous in order to support OMEMO encrypted messages');
|
||||
}));
|
||||
|
||||
|
||||
it("shows OMEMO device fingerprints in the user details modal",
|
||||
mock.initConverse(['chatBoxesFetched'], {}, async function (_converse) {
|
||||
|
||||
|
@ -251,14 +251,30 @@ export function getSessionCipher (jid, id) {
|
||||
return new window.libsignal.SessionCipher(_converse.omemo_store, address);
|
||||
}
|
||||
|
||||
function getJIDForDecryption (attrs) {
|
||||
const from_jid = attrs.from_muc ? attrs.from_real_jid : attrs.from;
|
||||
if (!from_jid) {
|
||||
Object.assign(attrs, {
|
||||
'error_text': __("Sorry, could not decrypt a received OMEMO message because we don't have the XMPP address for that user."),
|
||||
'error_type': 'Decryption',
|
||||
'is_ephemeral': false,
|
||||
'is_error': true,
|
||||
'type': 'error'
|
||||
});
|
||||
throw new Error("Could not find JID to decrypt OMEMO message for");
|
||||
}
|
||||
return from_jid;
|
||||
}
|
||||
|
||||
async function handleDecryptedWhisperMessage (attrs, key_and_tag) {
|
||||
const encrypted = attrs.encrypted;
|
||||
const devicelist = _converse.devicelists.getDeviceList(attrs.from);
|
||||
const from_jid = getJIDForDecryption(attrs);
|
||||
const devicelist = _converse.devicelists.getDeviceList(from_jid);
|
||||
await devicelist._devices_promise;
|
||||
|
||||
const encrypted = attrs.encrypted;
|
||||
let device = devicelist.get(encrypted.device_id);
|
||||
if (!device) {
|
||||
device = await devicelist.devices.create({ 'id': encrypted.device_id, 'jid': attrs.from }, { 'promise': true });
|
||||
device = await devicelist.devices.create({ 'id': encrypted.device_id, 'jid': from_jid }, { 'promise': true });
|
||||
}
|
||||
if (encrypted.payload) {
|
||||
const key = key_and_tag.slice(0, 16);
|
||||
@ -285,7 +301,8 @@ function getDecryptionErrorAttributes (e) {
|
||||
}
|
||||
|
||||
async function decryptPrekeyWhisperMessage (attrs) {
|
||||
const session_cipher = getSessionCipher(attrs.from, parseInt(attrs.encrypted.device_id, 10));
|
||||
const from_jid = getJIDForDecryption(attrs);
|
||||
const session_cipher = getSessionCipher(from_jid, parseInt(attrs.encrypted.device_id, 10));
|
||||
const key = base64ToArrayBuffer(attrs.encrypted.key);
|
||||
let key_and_tag;
|
||||
try {
|
||||
@ -332,16 +349,7 @@ async function decryptPrekeyWhisperMessage (attrs) {
|
||||
}
|
||||
|
||||
async function decryptWhisperMessage (attrs) {
|
||||
const from_jid = attrs.from_muc ? attrs.from_real_jid : attrs.from;
|
||||
if (!from_jid) {
|
||||
Object.assign(attrs, {
|
||||
'error_text': __("Sorry, could not decrypt a received OMEMO message because we don't have the XMPP address for that user."),
|
||||
'error_type': 'Decryption',
|
||||
'is_ephemeral': false,
|
||||
'is_error': true,
|
||||
'type': 'error'
|
||||
});
|
||||
}
|
||||
const from_jid = getJIDForDecryption(attrs);
|
||||
const session_cipher = getSessionCipher(from_jid, parseInt(attrs.encrypted.device_id, 10));
|
||||
const key = base64ToArrayBuffer(attrs.encrypted.key);
|
||||
try {
|
||||
@ -432,9 +440,6 @@ export function generateDeviceID () {
|
||||
}
|
||||
|
||||
async function buildSession (device) {
|
||||
// TODO: check device-get('jid') versus the 'from' attribute which is used
|
||||
// to build a session when receiving an encrypted message in a MUC.
|
||||
// https://github.com/conversejs/converse.js/issues/1481#issuecomment-509183431
|
||||
const address = new libsignal.SignalProtocolAddress(device.get('jid'), device.get('id'));
|
||||
const sessionBuilder = new libsignal.SessionBuilder(_converse.omemo_store, address);
|
||||
const prekey = device.getRandomPreKey();
|
||||
|
Loading…
Reference in New Issue
Block a user