converse.js
Version:
Browser based XMPP chat client
437 lines (388 loc) • 22.2 kB
JavaScript
/*global mock, converse */
const { Strophe, sizzle, stx, u, omemo } = converse.env;
describe("An OMEMO encrypted message", function() {
beforeAll(() => jasmine.addMatchers({ toEqualStanza: jasmine.toEqualStanza }));
it("can be edited", 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 = 'But soft, what light through yonder airlock breaks?';
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"],
})
);
await u.waitUntil(() => view.querySelectorAll('.chat-msg__text').length);
expect(view.querySelectorAll('.chat-msg').length).toBe(1);
expect(view.querySelector('.chat-msg__text').textContent)
.toBe('But soft, what light through yonder airlock breaks?');
await u.waitUntil(() => textarea.value === '');
message_form.onKeyDown({
target: textarea,
key: "ArrowUp",
});
await u.waitUntil(() => textarea.value === 'But soft, what light through yonder airlock breaks?');
expect(view.model.messages.at(0).get('correcting')).toBe(true);
const first_msg = view.model.messages.findWhere({'message': 'But soft, what light through yonder airlock breaks?'});
const newer_text = 'But soft, what light through yonder door breaks?';
textarea.value = newer_text;
message_form.onKeyDown({
target: textarea,
preventDefault: function preventDefault () {},
key: "Enter",
});
await u.waitUntil(() => view.querySelector('.chat-msg__text').textContent.replace(/<!-.*?->/g, '') === newer_text);
await u.waitUntil(() => _converse.api.connection.get().sent_stanzas.filter(s => s.nodeName === 'message').length === 3);
const msg = _converse.api.connection.get().sent_stanzas.pop();
const fallback_text = 'This is an OMEMO encrypted message which your client doesn’t seem to support. Find more information on https://conversations.im/omemo';
expect(msg).toEqualStanza(stx`
<message from="romeo@montague.lit/orchard" id="${msg.getAttribute('id')}"
to="mercutio@montague.lit" type="chat"
xmlns="jabber:client">
<body>${fallback_text}</body>
<active xmlns="http://jabber.org/protocol/chatstates"/>
<request xmlns="urn:xmpp:receipts"/>
<replace id="${first_msg.get("msgid")}" xmlns="urn:xmpp:message-correct:0"/>
<origin-id id="${msg.querySelector('origin-id').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>${msg.querySelector('header iv').textContent}</iv>
</header>
<payload>${msg.querySelector('payload').textContent}</payload>
</encrypted>
<store xmlns="urn:xmpp:hints"/>
<encryption namespace="eu.siacs.conversations.axolotl" xmlns="urn:xmpp:eme:0"/>
</message>`);
let older_versions = first_msg.get('older_versions');
let keys = Object.keys(older_versions);
expect(keys.length).toBe(1);
expect(older_versions[keys[0]]).toBe('But soft, what light through yonder airlock breaks?');
expect(first_msg.get('plaintext')).toBe(newer_text);
expect(first_msg.get('is_encrypted')).toBe(true);
expect(first_msg.get('body')).toBe(fallback_text);
expect(first_msg.get('message')).toBe(fallback_text);
message_form.onKeyDown({
target: textarea,
key: "ArrowUp",
});
await u.waitUntil(() => textarea.value === 'But soft, what light through yonder door breaks?');
const newest_text = 'But soft, what light through yonder window breaks?';
textarea.value = newest_text;
message_form.onKeyDown({
target: textarea,
preventDefault: function preventDefault () {},
key: "Enter",
});
await u.waitUntil(() => view.querySelector('.chat-msg__text').textContent.replace(/<!-.*?->/g, '') === newest_text);
keys = Object.keys(older_versions);
expect(keys.length).toBe(2);
expect(older_versions[keys[0]]).toBe('But soft, what light through yonder airlock breaks?');
expect(older_versions[keys[1]]).toBe('But soft, what light through yonder door breaks?');
const first_rcvd_msg_id = u.getUniqueId();
let obj = await omemo.encryptMessage('This is an encrypted message from the contact')
_converse.api.connection.get()._dataRecv(mock.createRequest(stx`
<message from="${contact_jid}"
to="${_converse.api.connection.get().jid}"
type="chat"
id="${first_rcvd_msg_id}"
xmlns="jabber:client">
<body>${fallback_text}</body>
<origin-id id="${first_rcvd_msg_id}" xmlns="urn:xmpp:sid:0"/>
<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>`));
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');
const msg_id = u.getUniqueId();
obj = await omemo.encryptMessage('This is an edited encrypted message from the contact')
_converse.api.connection.get()._dataRecv(mock.createRequest(stx`
<message from="${contact_jid}"
to="${_converse.api.connection.get().jid}"
type="chat"
id="${msg_id}"
xmlns="jabber:client">
<body>${fallback_text}</body>
<replace id="${first_rcvd_msg_id}" xmlns="urn:xmpp:message-correct:0"/>
<origin-id id="${msg_id}" xmlns="urn:xmpp:sid:0"/>
<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>`));
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 edited encrypted message from the contact');
const message = view.model.messages.at(1);
older_versions = message.get('older_versions');
keys = Object.keys(older_versions);
expect(keys.length).toBe(1);
expect(older_versions[keys[0]]).toBe('This is an encrypted message from the contact');
expect(message.get('plaintext')).toBe('This is an edited encrypted message from the contact');
expect(message.get('is_encrypted')).toBe(true);
expect(message.get('body')).toBe(fallback_text);
expect(message.get('message')).toBe(fallback_text);
expect(message.get('msgid')).toBe(first_rcvd_msg_id);
}));
});
describe("An OMEMO encrypted MUC message", function() {
beforeAll(() => jasmine.addMatchers({ toEqualStanza: jasmine.toEqualStanza }));
it("can be edited", 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 { api } = _converse;
const { jid: own_jid } = api.connection.get();
const nick = 'romeo';
const muc_jid = 'lounge@montague.lit';
await mock.openAndEnterMUC(_converse, muc_jid, nick, features);
await u.waitUntil(() => mock.initializedOMEMO(_converse));
const view = _converse.chatboxviews.get(muc_jid);
const toolbar = await u.waitUntil(() => view.querySelector('.chat-toolbar'));
const omemo_toggle = await u.waitUntil(() => toolbar.querySelector('.toggle-omemo'));
omemo_toggle.click();
expect(view.model.get('omemo_active')).toBe(true);
// newguy enters the room
const contact_jid = 'newguy@montague.lit';
let stanza = stx`
<presence to='romeo@montague.lit/orchard' from='lounge@montague.lit/newguy' xmlns="jabber:client">
<x xmlns='${Strophe.NS.MUC_USER}'>
<item affiliation='none' jid='newguy@montague.lit/_converse.js-290929789' role='participant'/>
</x>
</presence>`;
_converse.api.connection.get()._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(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>`);
// The server returns his device list
stanza = stx`
<iq from='${contact_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='4e30f35051b7b8b42abe083742187228'/>
</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(2);
await u.waitUntil(() => mock.deviceListFetched(_converse, contact_jid));
const devicelist = _converse.state.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 original_text = 'This message will be encrypted';
const textarea = view.querySelector('.chat-textarea');
textarea.value = original_text;
const message_form = view.querySelector('converse-muc-message-form');
message_form.onKeyDown({
target: textarea,
preventDefault: function preventDefault () {},
key: "Enter",
});
await u.waitUntil(() => mock.bundleFetched(_converse, {
jid: contact_jid,
device_id: '4e30f35051b7b8b42abe083742187228',
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="${own_jid}"
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>
<active xmlns="http://jabber.org/protocol/chatstates"/>
<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="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>`);
await u.waitUntil(() => textarea.value === '');
const first_msg = view.model.messages.findWhere({'message': original_text});
message_form.onKeyDown({
target: textarea,
key: "ArrowUp",
});
await u.waitUntil(() => textarea.value === original_text);
expect(view.model.messages.at(0).get('correcting')).toBe(true);
const new_text = 'This is an edit of the encrypted message';
textarea.value = new_text;
message_form.onKeyDown({
target: textarea,
preventDefault: function preventDefault () {},
key: "Enter",
});
await u.waitUntil(() => view.querySelector('.chat-msg__text').textContent.replace(/<!-.*?->/g, '') === new_text);
const fallback_text = 'This is an OMEMO encrypted message which your client doesn’t seem to support. Find more information on https://conversations.im/omemo';
let older_versions = first_msg.get('older_versions');
let keys = Object.keys(older_versions);
expect(keys.length).toBe(1);
expect(older_versions[keys[0]]).toBe(original_text);
expect(first_msg.get('plaintext')).toBe(new_text);
expect(first_msg.get('is_encrypted')).toBe(true);
expect(first_msg.get('body')).toBe(fallback_text);
expect(first_msg.get('message')).toBe(fallback_text);
await u.waitUntil(() => _converse.api.connection.get().sent_stanzas.filter(s => s.nodeName === 'message').length === 2);
const msg = _converse.api.connection.get().sent_stanzas.pop();
expect(msg).toEqualStanza(stx`
<message from="${own_jid}" id="${msg.getAttribute("id")}" to="${muc_jid}" type="groupchat" xmlns="jabber:client">
<body>${fallback_text}</body>
<active xmlns="http://jabber.org/protocol/chatstates"/>
<replace id="${first_msg.get("msgid")}" xmlns="urn:xmpp:message-correct:0"/>
<origin-id id="${msg.querySelector('origin-id').getAttribute("id")}" xmlns="urn:xmpp:sid:0"/>
<encrypted xmlns="eu.siacs.conversations.axolotl">
<header sid="123456789">
<key rid="482886413b977930064a5888b92134fe">YzFwaDNSNzNYNw==</key>
<key rid="4e30f35051b7b8b42abe083742187228">YzFwaDNSNzNYNw==</key>
<iv>${msg.querySelector("iv").textContent}</iv>
</header>
<payload>${msg.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 first_received_id = _converse.api.connection.get().getUniqueId()
const first_received_message = 'This is an encrypted message from the contact';
const first_obj = await omemo.encryptMessage(first_received_message)
_converse.api.connection.get()._dataRecv(mock.createRequest(stx`
<message from="${muc_jid}/newguy"
to="${_converse.api.connection.get().jid}"
type="groupchat"
id="${first_received_id}"
xmlns="jabber:client">
<body>${fallback_text}</body>
<encrypted xmlns="${Strophe.NS.OMEMO}">
<header sid="555">
<key rid="${_converse.state.omemo_store.get('device_id')}">${u.arrayBufferToBase64(first_obj.key_and_tag)}</key>
<iv>${first_obj.iv}</iv>
</header>
<payload>${first_obj.payload}</payload>
</encrypted>
</message>`));
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(first_received_message);
expect(_converse.state.devicelists.length).toBe(2);
expect(_converse.state.devicelists.at(0).get('jid')).toBe(_converse.bare_jid);
expect(_converse.state.devicelists.at(1).get('jid')).toBe(contact_jid);
const second_received_message = 'This is an edited encrypted message from the contact';
const second_obj = await omemo.encryptMessage(second_received_message)
_converse.api.connection.get()._dataRecv(mock.createRequest(stx`
<message from="${muc_jid}/newguy"
to="${_converse.api.connection.get().jid}"
type="groupchat"
id="${_converse.api.connection.get().getUniqueId()}"
xmlns="jabber:client">
<body>${fallback_text}</body>
<replace id="${first_received_id}" xmlns="urn:xmpp:message-correct:0"/>
<encrypted xmlns="${Strophe.NS.OMEMO}">
<header sid="555">
<key rid="${_converse.state.omemo_store.get('device_id')}">${u.arrayBufferToBase64(second_obj.key_and_tag)}</key>
<iv>${second_obj.iv}</iv>
</header>
<payload>${second_obj.payload}</payload>
</encrypted>
</message>`));
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(second_received_message);
const message = view.model.messages.at(1);
older_versions = message.get('older_versions');
keys = Object.keys(older_versions);
expect(keys.length).toBe(1);
expect(older_versions[keys[0]]).toBe('This is an encrypted message from the contact');
expect(message.get('plaintext')).toBe('This is an edited encrypted message from the contact');
expect(message.get('is_encrypted')).toBe(true);
expect(message.get('body')).toBe(fallback_text);
expect(message.get('msgid')).toBe(first_received_id);
}));
});