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