parent
d051085626
commit
9aca32ad97
65
dist/converse.js
vendored
65
dist/converse.js
vendored
@ -55980,6 +55980,15 @@ const KEY_ALGO = {
|
||||
'length': 128
|
||||
};
|
||||
|
||||
class IQError extends Error {
|
||||
constructor(message, iq) {
|
||||
super(message, iq);
|
||||
this.name = 'IQError';
|
||||
this.iq = iq;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function parseBundle(bundle_el) {
|
||||
/* Given an XML element representing a user's OMEMO bundle, parse it
|
||||
* and return a map.
|
||||
@ -56011,7 +56020,7 @@ _converse_headless_converse_core__WEBPACK_IMPORTED_MODULE_0__["default"].plugins
|
||||
return !_.isNil(window.libsignal) && !f.includes('converse-omemo', _converse.blacklisted_plugins);
|
||||
},
|
||||
|
||||
dependencies: ["converse-chatview"],
|
||||
dependencies: ["converse-chatview", "converse-pubsub"],
|
||||
overrides: {
|
||||
ProfileModal: {
|
||||
events: {
|
||||
@ -56267,6 +56276,31 @@ _converse_headless_converse_core__WEBPACK_IMPORTED_MODULE_0__["default"].plugins
|
||||
}));
|
||||
},
|
||||
|
||||
handleMessageSendError(e) {
|
||||
const _converse = this.__super__._converse,
|
||||
__ = _converse.__;
|
||||
|
||||
if (e.name === 'IQError') {
|
||||
this.save('omemo_supported', false);
|
||||
const err_msgs = [];
|
||||
|
||||
if (sizzle(`presence-subscription-required[xmlns="${Strophe.NS.PUBSUB_ERROR}"]`, e.iq).length) {
|
||||
err_msgs.push(__("Sorry, we're unable to send an encrypted message because %1$s " + "requires you to be subscribed to their presence in order to see their OMEMO information", e.iq.getAttribute('from')));
|
||||
} else if (sizzle(`remote-server-not-found[xmlns="urn:ietf:params:xml:ns:xmpp-stanzas"]`, e.iq).length) {
|
||||
err_msgs.push(__("Sorry, we're unable to send an encrypted message because the remote server for %1$s could not be found", e.iq.getAttribute('from')));
|
||||
} else {
|
||||
err_msgs.push(__("Unable to send an encrypted message due to an unexpected error."));
|
||||
err_msgs.push(e.iq.outerHTML);
|
||||
}
|
||||
|
||||
_converse.api.alert.show(Strophe.LogLevel.ERROR, __('Error'), err_msgs);
|
||||
|
||||
_converse.log(e, Strophe.LogLevel.ERROR);
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
},
|
||||
|
||||
async sendMessage(attrs) {
|
||||
const _converse = this.__super__._converse,
|
||||
__ = _converse.__;
|
||||
@ -56274,19 +56308,13 @@ _converse_headless_converse_core__WEBPACK_IMPORTED_MODULE_0__["default"].plugins
|
||||
if (this.get('omemo_active') && attrs.message) {
|
||||
attrs['is_encrypted'] = true;
|
||||
attrs['plaintext'] = attrs.message;
|
||||
const devices = await _converse.getBundlesAndBuildSessions(this);
|
||||
const stanza = await _converse.createOMEMOMessageStanza(this, this.messages.create(attrs), devices);
|
||||
|
||||
try {
|
||||
const devices = await _converse.getBundlesAndBuildSessions(this);
|
||||
const stanza = await _converse.createOMEMOMessageStanza(this, this.messages.create(attrs), devices);
|
||||
this.sendMessageStanza(stanza);
|
||||
} catch (e) {
|
||||
this.messages.create({
|
||||
'message': __("Sorry, could not send the message due to an error.") + ` ${e.message}`,
|
||||
'type': 'error'
|
||||
});
|
||||
|
||||
_converse.log(e, Strophe.LogLevel.ERROR);
|
||||
|
||||
this.handleMessageSendError(e);
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -56770,7 +56798,7 @@ _converse_headless_converse_core__WEBPACK_IMPORTED_MODULE_0__["default"].plugins
|
||||
return _converse.api.pubsub.publish(null, node, item, options);
|
||||
},
|
||||
|
||||
generateMissingPreKeys() {
|
||||
async generateMissingPreKeys() {
|
||||
const current_keys = this.getPreKeys(),
|
||||
missing_keys = _.difference(_.invokeMap(_.range(0, _converse.NUM_PREKEYS), Number.prototype.toString), _.keys(current_keys));
|
||||
|
||||
@ -56780,7 +56808,8 @@ _converse_headless_converse_core__WEBPACK_IMPORTED_MODULE_0__["default"].plugins
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
return Promise.all(_.map(missing_keys, id => libsignal.KeyHelper.generatePreKey(parseInt(id, 10)))).then(keys => {
|
||||
const keys = await Promise.all(_.map(missing_keys, id => libsignal.KeyHelper.generatePreKey(parseInt(id, 10))));
|
||||
|
||||
_.forEach(keys, k => this.storePreKey(k.keyId, k.keyPair));
|
||||
|
||||
const marshalled_keys = _.map(this.getPreKeys(), k => ({
|
||||
@ -56790,10 +56819,10 @@ _converse_headless_converse_core__WEBPACK_IMPORTED_MODULE_0__["default"].plugins
|
||||
devicelist = _converse.devicelists.get(_converse.bare_jid),
|
||||
device = devicelist.devices.get(this.get('device_id'));
|
||||
|
||||
return device.getBundle().then(bundle => device.save('bundle', _.extend(bundle, {
|
||||
const bundle = await device.getBundle();
|
||||
device.save('bundle', _.extend(bundle, {
|
||||
'prekeys': marshalled_keys
|
||||
})));
|
||||
});
|
||||
}));
|
||||
},
|
||||
|
||||
async generateBundle() {
|
||||
@ -56892,7 +56921,11 @@ _converse_headless_converse_core__WEBPACK_IMPORTED_MODULE_0__["default"].plugins
|
||||
try {
|
||||
iq = await _converse.api.sendIQ(stanza);
|
||||
} catch (iq) {
|
||||
return _converse.log(iq.outerHTML, Strophe.LogLevel.ERROR);
|
||||
throw new IQError("Could not fetch bundle", iq);
|
||||
}
|
||||
|
||||
if (iq.querySelector('error')) {
|
||||
throw new IQError("Could not fetch bundle", iq);
|
||||
}
|
||||
|
||||
const publish_el = sizzle(`items[node="${Strophe.NS.OMEMO_BUNDLES}:${this.get('id')}"]`, iq).pop(),
|
||||
|
128
spec/omemo.js
128
spec/omemo.js
@ -371,6 +371,134 @@
|
||||
done();
|
||||
}));
|
||||
|
||||
it("gracefully handles auth errors when trying to send encrypted groupchat messages",
|
||||
mock.initConverseWithPromises(
|
||||
null, ['rosterGroupsFetched', 'chatBoxesFetched'], {},
|
||||
async function (done, _converse) {
|
||||
|
||||
// MEMO encryption works only in members only conferences
|
||||
// that are non-anonymous.
|
||||
const features = [
|
||||
'http://jabber.org/protocol/muc',
|
||||
'jabber:iq:register',
|
||||
'muc_passwordprotected',
|
||||
'muc_hidden',
|
||||
'muc_temporary',
|
||||
'muc_membersonly',
|
||||
'muc_unmoderated',
|
||||
'muc_nonanonymous'
|
||||
];
|
||||
await test_utils.openAndEnterChatRoom(_converse, 'lounge', 'localhost', 'dummy', features);
|
||||
const view = _converse.chatboxviews.get('lounge@localhost');
|
||||
await test_utils.waitUntil(() => initializedOMEMO(_converse));
|
||||
|
||||
const contact_jid = 'newguy@localhost';
|
||||
let stanza = $pres({
|
||||
'to': 'dummy@localhost/resource',
|
||||
'from': 'lounge@localhost/newguy'
|
||||
})
|
||||
.c('x', {xmlns: Strophe.NS.MUC_USER})
|
||||
.c('item', {
|
||||
'affiliation': 'none',
|
||||
'jid': 'newguy@localhost/_converse.js-290929789',
|
||||
'role': 'participant'
|
||||
}).tree();
|
||||
_converse.connection._dataRecv(test_utils.createRequest(stanza));
|
||||
|
||||
const toolbar = view.el.querySelector('.chat-toolbar');
|
||||
const toggle = toolbar.querySelector('.toggle-omemo');
|
||||
toggle.click();
|
||||
expect(view.model.get('omemo_active')).toBe(true);
|
||||
expect(view.model.get('omemo_supported')).toBe(true);
|
||||
|
||||
const textarea = view.el.querySelector('.chat-textarea');
|
||||
textarea.value = 'This message will be encrypted';
|
||||
view.keyPressed({
|
||||
target: textarea,
|
||||
preventDefault: _.noop,
|
||||
keyCode: 13 // Enter
|
||||
});
|
||||
let iq_stanza = await test_utils.waitUntil(() => deviceListFetched(_converse, contact_jid));
|
||||
expect(iq_stanza.toLocaleString()).toBe(
|
||||
`<iq from="dummy@localhost" id="${iq_stanza.nodeTree.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.nodeTree.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(test_utils.createRequest(stanza));
|
||||
await test_utils.waitUntil(() => _converse.omemo_store);
|
||||
expect(_converse.devicelists.length).toBe(2);
|
||||
|
||||
const devicelist = _converse.devicelists.get(contact_jid);
|
||||
expect(devicelist.devices.length).toBe(1);
|
||||
expect(devicelist.devices.at(0).get('id')).toBe('4e30f35051b7b8b42abe083742187228');
|
||||
|
||||
iq_stanza = await test_utils.waitUntil(() => bundleFetched(_converse, _converse.bare_jid, '482886413b977930064a5888b92134fe'));
|
||||
stanza = $iq({
|
||||
'from': _converse.bare_jid,
|
||||
'id': iq_stanza.nodeTree.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 test_utils.waitUntil(() => 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.nodeTree.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(test_utils.createRequest(stanza));
|
||||
|
||||
await test_utils.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@localhost requires you "+
|
||||
"to be subscribed to their presence in order to see their OMEMO information");
|
||||
|
||||
expect(view.model.get('omemo_supported')).toBe(false);
|
||||
expect(view.el.querySelector('.chat-textarea').value).toBe('This message will be encrypted');
|
||||
done();
|
||||
}));
|
||||
|
||||
it("can receive a PreKeySignalMessage",
|
||||
mock.initConverseWithPromises(
|
||||
null, ['rosterGroupsFetched', 'chatBoxesFetched'], {},
|
||||
|
@ -27,6 +27,15 @@ const KEY_ALGO = {
|
||||
};
|
||||
|
||||
|
||||
class IQError extends Error {
|
||||
constructor (message, iq) {
|
||||
super(message, iq);
|
||||
this.name = 'IQError';
|
||||
this.iq = iq;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function parseBundle (bundle_el) {
|
||||
/* Given an XML element representing a user's OMEMO bundle, parse it
|
||||
* and return a map.
|
||||
@ -61,7 +70,7 @@ converse.plugins.add('converse-omemo', {
|
||||
return !_.isNil(window.libsignal) && !f.includes('converse-omemo', _converse.blacklisted_plugins);
|
||||
},
|
||||
|
||||
dependencies: ["converse-chatview"],
|
||||
dependencies: ["converse-chatview", "converse-pubsub"],
|
||||
|
||||
overrides: {
|
||||
|
||||
@ -309,6 +318,31 @@ converse.plugins.add('converse-omemo', {
|
||||
.then(payload => ({'payload': payload, 'device': device}));
|
||||
},
|
||||
|
||||
handleMessageSendError (e) {
|
||||
const { _converse } = this.__super__,
|
||||
{ __ } = _converse;
|
||||
if (e.name === 'IQError') {
|
||||
this.save('omemo_supported', false);
|
||||
|
||||
const err_msgs = [];
|
||||
if (sizzle(`presence-subscription-required[xmlns="${Strophe.NS.PUBSUB_ERROR}"]`, e.iq).length) {
|
||||
err_msgs.push(__("Sorry, we're unable to send an encrypted message because %1$s "+
|
||||
"requires you to be subscribed to their presence in order to see their OMEMO information",
|
||||
e.iq.getAttribute('from')));
|
||||
} else if (sizzle(`remote-server-not-found[xmlns="urn:ietf:params:xml:ns:xmpp-stanzas"]`, e.iq).length) {
|
||||
err_msgs.push(__("Sorry, we're unable to send an encrypted message because the remote server for %1$s could not be found",
|
||||
e.iq.getAttribute('from')));
|
||||
} else {
|
||||
err_msgs.push(__("Unable to send an encrypted message due to an unexpected error."));
|
||||
err_msgs.push(e.iq.outerHTML);
|
||||
}
|
||||
_converse.api.alert.show(Strophe.LogLevel.ERROR, __('Error'), err_msgs);
|
||||
_converse.log(e, Strophe.LogLevel.ERROR);
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
},
|
||||
|
||||
async sendMessage (attrs) {
|
||||
const { _converse } = this.__super__,
|
||||
{ __ } = _converse;
|
||||
@ -316,16 +350,12 @@ converse.plugins.add('converse-omemo', {
|
||||
if (this.get('omemo_active') && attrs.message) {
|
||||
attrs['is_encrypted'] = true;
|
||||
attrs['plaintext'] = attrs.message;
|
||||
try {
|
||||
const devices = await _converse.getBundlesAndBuildSessions(this);
|
||||
const stanza = await _converse.createOMEMOMessageStanza(this, this.messages.create(attrs), devices);
|
||||
try {
|
||||
this.sendMessageStanza(stanza);
|
||||
} catch (e) {
|
||||
this.messages.create({
|
||||
'message': __("Sorry, could not send the message due to an error.") + ` ${e.message}`,
|
||||
'type': 'error',
|
||||
});
|
||||
_converse.log(e, Strophe.LogLevel.ERROR);
|
||||
this.handleMessageSendError(e);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
@ -346,7 +376,6 @@ converse.plugins.add('converse-omemo', {
|
||||
this.model.on('change:omemo_supported', this.onOMEMOSupportedDetermined, this);
|
||||
},
|
||||
|
||||
|
||||
showMessage (message) {
|
||||
// We don't show a message if it's only keying material
|
||||
if (!message.get('is_only_key')) {
|
||||
@ -768,7 +797,7 @@ converse.plugins.add('converse-omemo', {
|
||||
return _converse.api.pubsub.publish(null, node, item, options);
|
||||
},
|
||||
|
||||
generateMissingPreKeys () {
|
||||
async generateMissingPreKeys () {
|
||||
const current_keys = this.getPreKeys(),
|
||||
missing_keys = _.difference(_.invokeMap(_.range(0, _converse.NUM_PREKEYS), Number.prototype.toString), _.keys(current_keys));
|
||||
|
||||
@ -776,16 +805,14 @@ converse.plugins.add('converse-omemo', {
|
||||
_converse.log("No missing prekeys to generate for our own device", Strophe.LogLevel.WARN);
|
||||
return Promise.resolve();
|
||||
}
|
||||
return Promise.all(_.map(missing_keys, id => libsignal.KeyHelper.generatePreKey(parseInt(id, 10))))
|
||||
.then(keys => {
|
||||
const keys = await Promise.all(_.map(missing_keys, id => libsignal.KeyHelper.generatePreKey(parseInt(id, 10))));
|
||||
_.forEach(keys, k => this.storePreKey(k.keyId, k.keyPair));
|
||||
const marshalled_keys = _.map(this.getPreKeys(), k => ({'id': k.keyId, 'key': u.arrayBufferToBase64(k.pubKey)})),
|
||||
devicelist = _converse.devicelists.get(_converse.bare_jid),
|
||||
device = devicelist.devices.get(this.get('device_id'));
|
||||
|
||||
return device.getBundle()
|
||||
.then(bundle => device.save('bundle', _.extend(bundle, {'prekeys': marshalled_keys})));
|
||||
});
|
||||
const bundle = await device.getBundle();
|
||||
device.save('bundle', _.extend(bundle, {'prekeys': marshalled_keys}));
|
||||
},
|
||||
|
||||
async generateBundle () {
|
||||
@ -871,8 +898,11 @@ converse.plugins.add('converse-omemo', {
|
||||
let iq;
|
||||
try {
|
||||
iq = await _converse.api.sendIQ(stanza)
|
||||
} catch(iq) {
|
||||
return _converse.log(iq.outerHTML, Strophe.LogLevel.ERROR);
|
||||
} catch (iq) {
|
||||
throw new IQError("Could not fetch bundle", iq);
|
||||
}
|
||||
if (iq.querySelector('error')) {
|
||||
throw new IQError("Could not fetch bundle", iq);
|
||||
}
|
||||
const publish_el = sizzle(`items[node="${Strophe.NS.OMEMO_BUNDLES}:${this.get('id')}"]`, iq).pop(),
|
||||
bundle_el = sizzle(`bundle[xmlns="${Strophe.NS.OMEMO}"]`, publish_el).pop(),
|
||||
|
Loading…
Reference in New Issue
Block a user