Add test for sending/receiving MUC OMEMO messages
While adding support for MUCs, I refactored converse-omemo somewhat to move functions out of `overrides` and to use async/await Updates #1180
This commit is contained in:
parent
f64fdb8088
commit
9c05ca9a09
295
dist/converse.js
vendored
295
dist/converse.js
vendored
|
@ -56122,54 +56122,6 @@ _converse_headless_converse_core__WEBPACK_IMPORTED_MODULE_0__["default"].plugins
|
||||||
|
|
||||||
},
|
},
|
||||||
ChatBox: {
|
ChatBox: {
|
||||||
getBundlesAndBuildSessions() {
|
|
||||||
const _converse = this.__super__._converse;
|
|
||||||
let devices;
|
|
||||||
return _converse.getDevicesForContact(this.get('jid')).then(their_devices => {
|
|
||||||
const device_id = _converse.omemo_store.get('device_id'),
|
|
||||||
devicelist = _converse.devicelists.get(_converse.bare_jid),
|
|
||||||
own_devices = devicelist.devices.filter(device => device.get('id') !== device_id);
|
|
||||||
|
|
||||||
devices = _.concat(own_devices, their_devices.models);
|
|
||||||
return Promise.all(devices.map(device => device.getBundle()));
|
|
||||||
}).then(() => this.buildSessions(devices));
|
|
||||||
},
|
|
||||||
|
|
||||||
async buildSession(device) {
|
|
||||||
const _converse = this.__super__._converse,
|
|
||||||
address = new libsignal.SignalProtocolAddress(device.get('jid'), device.get('id')),
|
|
||||||
sessionBuilder = new libsignal.SessionBuilder(_converse.omemo_store, address),
|
|
||||||
prekey = device.getRandomPreKey(),
|
|
||||||
bundle = await device.getBundle();
|
|
||||||
return sessionBuilder.processPreKey({
|
|
||||||
'registrationId': parseInt(device.get('id'), 10),
|
|
||||||
'identityKey': u.base64ToArrayBuffer(bundle.identity_key),
|
|
||||||
'signedPreKey': {
|
|
||||||
'keyId': bundle.signed_prekey.id,
|
|
||||||
// <Number>
|
|
||||||
'publicKey': u.base64ToArrayBuffer(bundle.signed_prekey.public_key),
|
|
||||||
'signature': u.base64ToArrayBuffer(bundle.signed_prekey.signature)
|
|
||||||
},
|
|
||||||
'preKey': {
|
|
||||||
'keyId': prekey.id,
|
|
||||||
// <Number>
|
|
||||||
'publicKey': u.base64ToArrayBuffer(prekey.key)
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
getSession(device) {
|
|
||||||
const _converse = this.__super__._converse,
|
|
||||||
address = new libsignal.SignalProtocolAddress(device.get('jid'), device.get('id'));
|
|
||||||
return _converse.omemo_store.loadSession(address.toString()).then(session => {
|
|
||||||
if (session) {
|
|
||||||
return Promise.resolve();
|
|
||||||
} else {
|
|
||||||
return this.buildSession(device);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
async encryptMessage(plaintext) {
|
async encryptMessage(plaintext) {
|
||||||
// The client MUST use fresh, randomly generated key/IV pairs
|
// The client MUST use fresh, randomly generated key/IV pairs
|
||||||
// with AES-128 in Galois/Counter Mode (GCM).
|
// with AES-128 in Galois/Counter Mode (GCM).
|
||||||
|
@ -56310,10 +56262,6 @@ _converse_headless_converse_core__WEBPACK_IMPORTED_MODULE_0__["default"].plugins
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
buildSessions(devices) {
|
|
||||||
return Promise.all(devices.map(device => this.getSession(device))).then(() => devices);
|
|
||||||
},
|
|
||||||
|
|
||||||
getSessionCipher(jid, id) {
|
getSessionCipher(jid, id) {
|
||||||
const _converse = this.__super__._converse,
|
const _converse = this.__super__._converse,
|
||||||
address = new libsignal.SignalProtocolAddress(jid, id);
|
address = new libsignal.SignalProtocolAddress(jid, id);
|
||||||
|
@ -56328,95 +56276,26 @@ _converse_headless_converse_core__WEBPACK_IMPORTED_MODULE_0__["default"].plugins
|
||||||
}));
|
}));
|
||||||
},
|
},
|
||||||
|
|
||||||
addKeysToMessageStanza(stanza, dicts, iv) {
|
async sendMessage(attrs) {
|
||||||
for (var i in dicts) {
|
|
||||||
if (Object.prototype.hasOwnProperty.call(dicts, i)) {
|
|
||||||
const payload = dicts[i].payload,
|
|
||||||
device = dicts[i].device,
|
|
||||||
prekey = 3 == parseInt(payload.type, 10);
|
|
||||||
stanza.c('key', {
|
|
||||||
'rid': device.get('id')
|
|
||||||
}).t(btoa(payload.body));
|
|
||||||
|
|
||||||
if (prekey) {
|
|
||||||
stanza.attrs({
|
|
||||||
'prekey': prekey
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
stanza.up();
|
|
||||||
|
|
||||||
if (i == dicts.length - 1) {
|
|
||||||
stanza.c('iv').t(iv).up().up();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return Promise.resolve(stanza);
|
|
||||||
},
|
|
||||||
|
|
||||||
createOMEMOMessageStanza(message, devices) {
|
|
||||||
const _converse = this.__super__._converse,
|
|
||||||
__ = _converse.__;
|
|
||||||
|
|
||||||
const body = __("This is an OMEMO encrypted message which your client doesn’t seem to support. " + "Find more information on https://conversations.im/omemo");
|
|
||||||
|
|
||||||
if (!message.get('message')) {
|
|
||||||
throw new Error("No message body to encrypt!");
|
|
||||||
}
|
|
||||||
|
|
||||||
const stanza = $msg({
|
|
||||||
'from': _converse.connection.jid,
|
|
||||||
'to': this.get('jid'),
|
|
||||||
'type': this.get('message_type'),
|
|
||||||
'id': message.get('msgid')
|
|
||||||
}).c('body').t(body).up().c('request', {
|
|
||||||
'xmlns': Strophe.NS.RECEIPTS
|
|
||||||
}).up() // An encrypted header is added to the message for
|
|
||||||
// each device that is supposed to receive it.
|
|
||||||
// These headers simply contain the key that the
|
|
||||||
// payload message is encrypted with,
|
|
||||||
// and they are separately encrypted using the
|
|
||||||
// session corresponding to the counterpart device.
|
|
||||||
.c('encrypted', {
|
|
||||||
'xmlns': Strophe.NS.OMEMO
|
|
||||||
}).c('header', {
|
|
||||||
'sid': _converse.omemo_store.get('device_id')
|
|
||||||
});
|
|
||||||
return this.encryptMessage(message.get('message')).then(obj => {
|
|
||||||
// The 16 bytes key and the GCM authentication tag (The tag
|
|
||||||
// SHOULD have at least 128 bit) are concatenated and for each
|
|
||||||
// intended recipient device, i.e. both own devices as well as
|
|
||||||
// devices associated with the contact, the result of this
|
|
||||||
// concatenation is encrypted using the corresponding
|
|
||||||
// long-standing SignalProtocol session.
|
|
||||||
const promises = devices.filter(device => device.get('trusted') != UNTRUSTED).map(device => this.encryptKey(obj.key_and_tag, device));
|
|
||||||
return Promise.all(promises).then(dicts => this.addKeysToMessageStanza(stanza, dicts, obj.iv)).then(stanza => {
|
|
||||||
stanza.c('payload').t(obj.payload).up().up();
|
|
||||||
stanza.c('store', {
|
|
||||||
'xmlns': Strophe.NS.HINTS
|
|
||||||
});
|
|
||||||
return stanza;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
sendMessage(attrs) {
|
|
||||||
const _converse = this.__super__._converse,
|
const _converse = this.__super__._converse,
|
||||||
__ = _converse.__;
|
__ = _converse.__;
|
||||||
|
|
||||||
if (this.get('omemo_active') && attrs.message) {
|
if (this.get('omemo_active') && attrs.message) {
|
||||||
attrs['is_encrypted'] = true;
|
attrs['is_encrypted'] = true;
|
||||||
attrs['plaintext'] = attrs.message;
|
attrs['plaintext'] = attrs.message;
|
||||||
const message = this.messages.create(attrs);
|
const devices = await _converse.getBundlesAndBuildSessions(this);
|
||||||
this.getBundlesAndBuildSessions().then(devices => this.createOMEMOMessageStanza(message, devices)).then(stanza => this.sendMessageStanza(stanza)).catch(e => {
|
const stanza = await _converse.createOMEMOMessageStanza(this, this.messages.create(attrs), devices);
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.sendMessageStanza(stanza);
|
||||||
|
} catch (e) {
|
||||||
this.messages.create({
|
this.messages.create({
|
||||||
'message': __("Sorry, could not send the message due to an error.") + ` ${e.message}`,
|
'message': __("Sorry, could not send the message due to an error.") + ` ${e.message}`,
|
||||||
'type': 'error'
|
'type': 'error'
|
||||||
});
|
});
|
||||||
|
|
||||||
_converse.log(e, Strophe.LogLevel.ERROR);
|
_converse.log(e, Strophe.LogLevel.ERROR);
|
||||||
});
|
}
|
||||||
} else {
|
} else {
|
||||||
return this.__super__.sendMessage.apply(this, arguments);
|
return this.__super__.sendMessage.apply(this, arguments);
|
||||||
}
|
}
|
||||||
|
@ -56532,15 +56411,15 @@ _converse_headless_converse_core__WEBPACK_IMPORTED_MODULE_0__["default"].plugins
|
||||||
}
|
}
|
||||||
|
|
||||||
_converse.generateFingerprints = async function (jid) {
|
_converse.generateFingerprints = async function (jid) {
|
||||||
const devices = await _converse.getDevicesForContact(jid);
|
const devices = await getDevicesForContact(jid);
|
||||||
return Promise.all(devices.map(d => generateFingerprint(d)));
|
return Promise.all(devices.map(d => generateFingerprint(d)));
|
||||||
};
|
};
|
||||||
|
|
||||||
_converse.getDeviceForContact = function (jid, device_id) {
|
_converse.getDeviceForContact = function (jid, device_id) {
|
||||||
return _converse.getDevicesForContact(jid).then(devices => devices.get(device_id));
|
return getDevicesForContact(jid).then(devices => devices.get(device_id));
|
||||||
};
|
};
|
||||||
|
|
||||||
_converse.getDevicesForContact = async function (jid) {
|
async function getDevicesForContact(jid) {
|
||||||
await _converse.api.waitUntil('OMEMOInitialized');
|
await _converse.api.waitUntil('OMEMOInitialized');
|
||||||
|
|
||||||
const devicelist = _converse.devicelists.get(jid) || _converse.devicelists.create({
|
const devicelist = _converse.devicelists.get(jid) || _converse.devicelists.create({
|
||||||
|
@ -56549,11 +56428,11 @@ _converse_headless_converse_core__WEBPACK_IMPORTED_MODULE_0__["default"].plugins
|
||||||
|
|
||||||
await devicelist.fetchDevices();
|
await devicelist.fetchDevices();
|
||||||
return devicelist.devices;
|
return devicelist.devices;
|
||||||
};
|
}
|
||||||
|
|
||||||
_converse.contactHasOMEMOSupport = async function (jid) {
|
_converse.contactHasOMEMOSupport = async function (jid) {
|
||||||
/* Checks whether the contact advertises any OMEMO-compatible devices. */
|
/* Checks whether the contact advertises any OMEMO-compatible devices. */
|
||||||
const devices = await _converse.getDevicesForContact(jid);
|
const devices = await getDevicesForContact(jid);
|
||||||
return devices.length > 0;
|
return devices.length > 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -56576,6 +56455,140 @@ _converse_headless_converse_core__WEBPACK_IMPORTED_MODULE_0__["default"].plugins
|
||||||
return device_id.toString();
|
return device_id.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function buildSession(device) {
|
||||||
|
const address = new libsignal.SignalProtocolAddress(device.get('jid'), device.get('id')),
|
||||||
|
sessionBuilder = new libsignal.SessionBuilder(_converse.omemo_store, address),
|
||||||
|
prekey = device.getRandomPreKey(),
|
||||||
|
bundle = await device.getBundle();
|
||||||
|
return sessionBuilder.processPreKey({
|
||||||
|
'registrationId': parseInt(device.get('id'), 10),
|
||||||
|
'identityKey': u.base64ToArrayBuffer(bundle.identity_key),
|
||||||
|
'signedPreKey': {
|
||||||
|
'keyId': bundle.signed_prekey.id,
|
||||||
|
// <Number>
|
||||||
|
'publicKey': u.base64ToArrayBuffer(bundle.signed_prekey.public_key),
|
||||||
|
'signature': u.base64ToArrayBuffer(bundle.signed_prekey.signature)
|
||||||
|
},
|
||||||
|
'preKey': {
|
||||||
|
'keyId': prekey.id,
|
||||||
|
// <Number>
|
||||||
|
'publicKey': u.base64ToArrayBuffer(prekey.key)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSession(device) {
|
||||||
|
const address = new libsignal.SignalProtocolAddress(device.get('jid'), device.get('id'));
|
||||||
|
return _converse.omemo_store.loadSession(address.toString()).then(session => {
|
||||||
|
if (session) {
|
||||||
|
return Promise.resolve();
|
||||||
|
} else {
|
||||||
|
return buildSession(device);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
_converse.getBundlesAndBuildSessions = async function (chatbox) {
|
||||||
|
let devices;
|
||||||
|
|
||||||
|
const id = _converse.omemo_store.get('device_id');
|
||||||
|
|
||||||
|
if (chatbox.get('type') === _converse.CHATROOMS_TYPE) {
|
||||||
|
const collections = await Promise.all(chatbox.occupants.map(o => getDevicesForContact(o.get('jid'))));
|
||||||
|
devices = collections.reduce((a, b) => _.concat(a, b.models), []);
|
||||||
|
} else if (chatbox.get('type') === _converse.PRIVATE_CHAT_TYPE) {
|
||||||
|
const their_devices = await getDevicesForContact(chatbox.get('jid')),
|
||||||
|
devicelist = _converse.devicelists.get(_converse.bare_jid),
|
||||||
|
own_devices = devicelist.devices.filter(d => d.get('id') !== id);
|
||||||
|
|
||||||
|
devices = _.concat(own_devices, their_devices.models);
|
||||||
|
} // Filter out our own device
|
||||||
|
|
||||||
|
|
||||||
|
devices = devices.filter(d => d.get('id') !== id);
|
||||||
|
await Promise.all(devices.map(d => d.getBundle()));
|
||||||
|
await Promise.all(devices.map(d => getSession(d)));
|
||||||
|
return devices;
|
||||||
|
};
|
||||||
|
|
||||||
|
function addKeysToMessageStanza(stanza, dicts, iv) {
|
||||||
|
for (var i in dicts) {
|
||||||
|
if (Object.prototype.hasOwnProperty.call(dicts, i)) {
|
||||||
|
const payload = dicts[i].payload,
|
||||||
|
device = dicts[i].device,
|
||||||
|
prekey = 3 == parseInt(payload.type, 10);
|
||||||
|
stanza.c('key', {
|
||||||
|
'rid': device.get('id')
|
||||||
|
}).t(btoa(payload.body));
|
||||||
|
|
||||||
|
if (prekey) {
|
||||||
|
stanza.attrs({
|
||||||
|
'prekey': prekey
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
stanza.up();
|
||||||
|
|
||||||
|
if (i == dicts.length - 1) {
|
||||||
|
stanza.c('iv').t(iv).up().up();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.resolve(stanza);
|
||||||
|
}
|
||||||
|
|
||||||
|
_converse.createOMEMOMessageStanza = function (chatbox, message, devices) {
|
||||||
|
const __ = _converse.__;
|
||||||
|
|
||||||
|
const body = __("This is an OMEMO encrypted message which your client doesn’t seem to support. " + "Find more information on https://conversations.im/omemo");
|
||||||
|
|
||||||
|
if (!message.get('message')) {
|
||||||
|
throw new Error("No message body to encrypt!");
|
||||||
|
}
|
||||||
|
|
||||||
|
const stanza = $msg({
|
||||||
|
'from': _converse.connection.jid,
|
||||||
|
'to': chatbox.get('jid'),
|
||||||
|
'type': chatbox.get('message_type'),
|
||||||
|
'id': message.get('msgid')
|
||||||
|
}).c('body').t(body).up();
|
||||||
|
|
||||||
|
if (message.get('type') === 'chat') {
|
||||||
|
stanza.c('request', {
|
||||||
|
'xmlns': Strophe.NS.RECEIPTS
|
||||||
|
}).up();
|
||||||
|
} // An encrypted header is added to the message for
|
||||||
|
// each device that is supposed to receive it.
|
||||||
|
// These headers simply contain the key that the
|
||||||
|
// payload message is encrypted with,
|
||||||
|
// and they are separately encrypted using the
|
||||||
|
// session corresponding to the counterpart device.
|
||||||
|
|
||||||
|
|
||||||
|
stanza.c('encrypted', {
|
||||||
|
'xmlns': Strophe.NS.OMEMO
|
||||||
|
}).c('header', {
|
||||||
|
'sid': _converse.omemo_store.get('device_id')
|
||||||
|
});
|
||||||
|
return chatbox.encryptMessage(message.get('message')).then(obj => {
|
||||||
|
// The 16 bytes key and the GCM authentication tag (The tag
|
||||||
|
// SHOULD have at least 128 bit) are concatenated and for each
|
||||||
|
// intended recipient device, i.e. both own devices as well as
|
||||||
|
// devices associated with the contact, the result of this
|
||||||
|
// concatenation is encrypted using the corresponding
|
||||||
|
// long-standing SignalProtocol session.
|
||||||
|
const promises = devices.filter(device => device.get('trusted') != UNTRUSTED).map(device => chatbox.encryptKey(obj.key_and_tag, device));
|
||||||
|
return Promise.all(promises).then(dicts => addKeysToMessageStanza(stanza, dicts, obj.iv)).then(stanza => {
|
||||||
|
stanza.c('payload').t(obj.payload).up().up();
|
||||||
|
stanza.c('store', {
|
||||||
|
'xmlns': Strophe.NS.HINTS
|
||||||
|
});
|
||||||
|
return stanza;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
_converse.OMEMOStore = Backbone.Model.extend({
|
_converse.OMEMOStore = Backbone.Model.extend({
|
||||||
Direction: {
|
Direction: {
|
||||||
SENDING: 1,
|
SENDING: 1,
|
||||||
|
@ -56872,7 +56885,7 @@ _converse_headless_converse_core__WEBPACK_IMPORTED_MODULE_0__["default"].plugins
|
||||||
return bundle.prekeys[u.getRandomInt(bundle.prekeys.length)];
|
return bundle.prekeys[u.getRandomInt(bundle.prekeys.length)];
|
||||||
},
|
},
|
||||||
|
|
||||||
fetchBundleFromServer() {
|
async fetchBundleFromServer() {
|
||||||
const stanza = $iq({
|
const stanza = $iq({
|
||||||
'type': 'get',
|
'type': 'get',
|
||||||
'from': _converse.bare_jid,
|
'from': _converse.bare_jid,
|
||||||
|
@ -56882,15 +56895,19 @@ _converse_headless_converse_core__WEBPACK_IMPORTED_MODULE_0__["default"].plugins
|
||||||
}).c('items', {
|
}).c('items', {
|
||||||
'node': `${Strophe.NS.OMEMO_BUNDLES}:${this.get('id')}`
|
'node': `${Strophe.NS.OMEMO_BUNDLES}:${this.get('id')}`
|
||||||
});
|
});
|
||||||
return _converse.api.sendIQ(stanza).then(iq => {
|
let iq;
|
||||||
|
|
||||||
|
try {
|
||||||
|
iq = await _converse.api.sendIQ(stanza);
|
||||||
|
} catch (iq) {
|
||||||
|
return _converse.log(iq.outerHTML, Strophe.LogLevel.ERROR);
|
||||||
|
}
|
||||||
|
|
||||||
const publish_el = sizzle(`items[node="${Strophe.NS.OMEMO_BUNDLES}:${this.get('id')}"]`, iq).pop(),
|
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(),
|
bundle_el = sizzle(`bundle[xmlns="${Strophe.NS.OMEMO}"]`, publish_el).pop(),
|
||||||
bundle = parseBundle(bundle_el);
|
bundle = parseBundle(bundle_el);
|
||||||
this.save('bundle', bundle);
|
this.save('bundle', bundle);
|
||||||
return bundle;
|
return bundle;
|
||||||
}).catch(iq => {
|
|
||||||
_converse.log(iq.outerHTML, Strophe.LogLevel.ERROR);
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
|
|
||||||
getBundle() {
|
getBundle() {
|
||||||
|
|
141
spec/omemo.js
141
spec/omemo.js
|
@ -225,6 +225,147 @@
|
||||||
done();
|
done();
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
it("enables encrypted groupchat messages to be sent and received",
|
||||||
|
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 toolbar = view.el.querySelector('.chat-toolbar');
|
||||||
|
let toggle = toolbar.querySelector('.toggle-omemo');
|
||||||
|
toggle.click();
|
||||||
|
expect(view.model.get('omemo_active')).toBe(true);
|
||||||
|
|
||||||
|
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));
|
||||||
|
|
||||||
|
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');
|
||||||
|
|
||||||
|
toggle = toolbar.querySelector('.toggle-omemo');
|
||||||
|
expect(view.model.get('omemo_active')).toBe(true);
|
||||||
|
expect(u.hasClass('fa-unlock', toggle)).toBe(false);
|
||||||
|
expect(u.hasClass('fa-lock', toggle)).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
|
||||||
|
});
|
||||||
|
iq_stanza = await test_utils.waitUntil(() => bundleFetched(_converse, contact_jid, '4e30f35051b7b8b42abe083742187228'));
|
||||||
|
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"})
|
||||||
|
.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(test_utils.createRequest(stanza));
|
||||||
|
|
||||||
|
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'));
|
||||||
|
|
||||||
|
spyOn(_converse.connection, 'send');
|
||||||
|
_converse.connection._dataRecv(test_utils.createRequest(stanza));
|
||||||
|
await test_utils.waitUntil(() => _converse.connection.send.calls.count());
|
||||||
|
const sent_stanza = _converse.connection.send.calls.all()[0].args[0];
|
||||||
|
|
||||||
|
expect(Strophe.serialize(sent_stanza)).toBe(
|
||||||
|
`<message from="dummy@localhost/resource" `+
|
||||||
|
`id="${sent_stanza.nodeTree.getAttribute("id")}" `+
|
||||||
|
`to="lounge@localhost" `+
|
||||||
|
`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.nodeTree.querySelector("iv").textContent}</iv>`+
|
||||||
|
`</header>`+
|
||||||
|
`<payload>${sent_stanza.nodeTree.querySelector("payload").textContent}</payload>`+
|
||||||
|
`</encrypted>`+
|
||||||
|
`<store xmlns="urn:xmpp:hints"/>`+
|
||||||
|
`</message>`);
|
||||||
|
done();
|
||||||
|
}));
|
||||||
|
|
||||||
it("can receive a PreKeySignalMessage",
|
it("can receive a PreKeySignalMessage",
|
||||||
mock.initConverseWithPromises(
|
mock.initConverseWithPromises(
|
||||||
null, ['rosterGroupsFetched', 'chatBoxesFetched'], {},
|
null, ['rosterGroupsFetched', 'chatBoxesFetched'], {},
|
||||||
|
|
|
@ -163,54 +163,6 @@ converse.plugins.add('converse-omemo', {
|
||||||
|
|
||||||
ChatBox: {
|
ChatBox: {
|
||||||
|
|
||||||
getBundlesAndBuildSessions () {
|
|
||||||
const { _converse } = this.__super__;
|
|
||||||
let devices;
|
|
||||||
return _converse.getDevicesForContact(this.get('jid'))
|
|
||||||
.then((their_devices) => {
|
|
||||||
const device_id = _converse.omemo_store.get('device_id'),
|
|
||||||
devicelist = _converse.devicelists.get(_converse.bare_jid),
|
|
||||||
own_devices = devicelist.devices.filter(device => device.get('id') !== device_id);
|
|
||||||
devices = _.concat(own_devices, their_devices.models);
|
|
||||||
return Promise.all(devices.map(device => device.getBundle()));
|
|
||||||
}).then(() => this.buildSessions(devices))
|
|
||||||
},
|
|
||||||
|
|
||||||
async buildSession (device) {
|
|
||||||
const { _converse } = this.__super__,
|
|
||||||
address = new libsignal.SignalProtocolAddress(device.get('jid'), device.get('id')),
|
|
||||||
sessionBuilder = new libsignal.SessionBuilder(_converse.omemo_store, address),
|
|
||||||
prekey = device.getRandomPreKey(),
|
|
||||||
bundle = await device.getBundle();
|
|
||||||
|
|
||||||
return sessionBuilder.processPreKey({
|
|
||||||
'registrationId': parseInt(device.get('id'), 10),
|
|
||||||
'identityKey': u.base64ToArrayBuffer(bundle.identity_key),
|
|
||||||
'signedPreKey': {
|
|
||||||
'keyId': bundle.signed_prekey.id, // <Number>
|
|
||||||
'publicKey': u.base64ToArrayBuffer(bundle.signed_prekey.public_key),
|
|
||||||
'signature': u.base64ToArrayBuffer(bundle.signed_prekey.signature)
|
|
||||||
},
|
|
||||||
'preKey': {
|
|
||||||
'keyId': prekey.id, // <Number>
|
|
||||||
'publicKey': u.base64ToArrayBuffer(prekey.key),
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
getSession (device) {
|
|
||||||
const { _converse } = this.__super__,
|
|
||||||
address = new libsignal.SignalProtocolAddress(device.get('jid'), device.get('id'));
|
|
||||||
|
|
||||||
return _converse.omemo_store.loadSession(address.toString()).then(session => {
|
|
||||||
if (session) {
|
|
||||||
return Promise.resolve();
|
|
||||||
} else {
|
|
||||||
return this.buildSession(device);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
async encryptMessage (plaintext) {
|
async encryptMessage (plaintext) {
|
||||||
// The client MUST use fresh, randomly generated key/IV pairs
|
// The client MUST use fresh, randomly generated key/IV pairs
|
||||||
// with AES-128 in Galois/Counter Mode (GCM).
|
// with AES-128 in Galois/Counter Mode (GCM).
|
||||||
|
@ -343,9 +295,6 @@ converse.plugins.add('converse-omemo', {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
buildSessions (devices) {
|
|
||||||
return Promise.all(devices.map(device => this.getSession(device))).then(() => devices);
|
|
||||||
},
|
|
||||||
|
|
||||||
getSessionCipher (jid, id) {
|
getSessionCipher (jid, id) {
|
||||||
const { _converse } = this.__super__,
|
const { _converse } = this.__super__,
|
||||||
|
@ -360,89 +309,24 @@ converse.plugins.add('converse-omemo', {
|
||||||
.then(payload => ({'payload': payload, 'device': device}));
|
.then(payload => ({'payload': payload, 'device': device}));
|
||||||
},
|
},
|
||||||
|
|
||||||
addKeysToMessageStanza (stanza, dicts, iv) {
|
async sendMessage (attrs) {
|
||||||
for (var i in dicts) {
|
|
||||||
if (Object.prototype.hasOwnProperty.call(dicts, i)) {
|
|
||||||
const payload = dicts[i].payload,
|
|
||||||
device = dicts[i].device,
|
|
||||||
prekey = 3 == parseInt(payload.type, 10);
|
|
||||||
|
|
||||||
stanza.c('key', {'rid': device.get('id') }).t(btoa(payload.body));
|
|
||||||
if (prekey) {
|
|
||||||
stanza.attrs({'prekey': prekey});
|
|
||||||
}
|
|
||||||
stanza.up();
|
|
||||||
if (i == dicts.length-1) {
|
|
||||||
stanza.c('iv').t(iv).up().up()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return Promise.resolve(stanza);
|
|
||||||
},
|
|
||||||
|
|
||||||
createOMEMOMessageStanza (message, devices) {
|
|
||||||
const { _converse } = this.__super__, { __ } = _converse;
|
|
||||||
const body = __("This is an OMEMO encrypted message which your client doesn’t seem to support. "+
|
|
||||||
"Find more information on https://conversations.im/omemo");
|
|
||||||
|
|
||||||
if (!message.get('message')) {
|
|
||||||
throw new Error("No message body to encrypt!");
|
|
||||||
}
|
|
||||||
const stanza = $msg({
|
|
||||||
'from': _converse.connection.jid,
|
|
||||||
'to': this.get('jid'),
|
|
||||||
'type': this.get('message_type'),
|
|
||||||
'id': message.get('msgid')
|
|
||||||
}).c('body').t(body).up()
|
|
||||||
.c('request', {'xmlns': Strophe.NS.RECEIPTS}).up()
|
|
||||||
// An encrypted header is added to the message for
|
|
||||||
// each device that is supposed to receive it.
|
|
||||||
// These headers simply contain the key that the
|
|
||||||
// payload message is encrypted with,
|
|
||||||
// and they are separately encrypted using the
|
|
||||||
// session corresponding to the counterpart device.
|
|
||||||
.c('encrypted', {'xmlns': Strophe.NS.OMEMO})
|
|
||||||
.c('header', {'sid': _converse.omemo_store.get('device_id')});
|
|
||||||
|
|
||||||
return this.encryptMessage(message.get('message')).then(obj => {
|
|
||||||
// The 16 bytes key and the GCM authentication tag (The tag
|
|
||||||
// SHOULD have at least 128 bit) are concatenated and for each
|
|
||||||
// intended recipient device, i.e. both own devices as well as
|
|
||||||
// devices associated with the contact, the result of this
|
|
||||||
// concatenation is encrypted using the corresponding
|
|
||||||
// long-standing SignalProtocol session.
|
|
||||||
const promises = devices
|
|
||||||
.filter(device => device.get('trusted') != UNTRUSTED)
|
|
||||||
.map(device => this.encryptKey(obj.key_and_tag, device));
|
|
||||||
|
|
||||||
return Promise.all(promises)
|
|
||||||
.then(dicts => this.addKeysToMessageStanza(stanza, dicts, obj.iv))
|
|
||||||
.then(stanza => {
|
|
||||||
stanza.c('payload').t(obj.payload).up().up();
|
|
||||||
stanza.c('store', {'xmlns': Strophe.NS.HINTS});
|
|
||||||
return stanza;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
sendMessage (attrs) {
|
|
||||||
const { _converse } = this.__super__,
|
const { _converse } = this.__super__,
|
||||||
{ __ } = _converse;
|
{ __ } = _converse;
|
||||||
|
|
||||||
if (this.get('omemo_active') && attrs.message) {
|
if (this.get('omemo_active') && attrs.message) {
|
||||||
attrs['is_encrypted'] = true;
|
attrs['is_encrypted'] = true;
|
||||||
attrs['plaintext'] = attrs.message;
|
attrs['plaintext'] = attrs.message;
|
||||||
const message = this.messages.create(attrs);
|
const devices = await _converse.getBundlesAndBuildSessions(this);
|
||||||
this.getBundlesAndBuildSessions()
|
const stanza = await _converse.createOMEMOMessageStanza(this, this.messages.create(attrs), devices);
|
||||||
.then(devices => this.createOMEMOMessageStanza(message, devices))
|
try {
|
||||||
.then(stanza => this.sendMessageStanza(stanza))
|
this.sendMessageStanza(stanza);
|
||||||
.catch(e => {
|
} catch (e) {
|
||||||
this.messages.create({
|
this.messages.create({
|
||||||
'message': __("Sorry, could not send the message due to an error.") + ` ${e.message}`,
|
'message': __("Sorry, could not send the message due to an error.") + ` ${e.message}`,
|
||||||
'type': 'error',
|
'type': 'error',
|
||||||
});
|
});
|
||||||
_converse.log(e, Strophe.LogLevel.ERROR);
|
_converse.log(e, Strophe.LogLevel.ERROR);
|
||||||
});
|
}
|
||||||
} else {
|
} else {
|
||||||
return this.__super__.sendMessage.apply(this, arguments);
|
return this.__super__.sendMessage.apply(this, arguments);
|
||||||
}
|
}
|
||||||
|
@ -554,15 +438,15 @@ converse.plugins.add('converse-omemo', {
|
||||||
}
|
}
|
||||||
|
|
||||||
_converse.generateFingerprints = async function (jid) {
|
_converse.generateFingerprints = async function (jid) {
|
||||||
const devices = await _converse.getDevicesForContact(jid)
|
const devices = await getDevicesForContact(jid)
|
||||||
return Promise.all(devices.map(d => generateFingerprint(d)));
|
return Promise.all(devices.map(d => generateFingerprint(d)));
|
||||||
}
|
}
|
||||||
|
|
||||||
_converse.getDeviceForContact = function (jid, device_id) {
|
_converse.getDeviceForContact = function (jid, device_id) {
|
||||||
return _converse.getDevicesForContact(jid).then(devices => devices.get(device_id));
|
return getDevicesForContact(jid).then(devices => devices.get(device_id));
|
||||||
}
|
}
|
||||||
|
|
||||||
_converse.getDevicesForContact = async function (jid) {
|
async function getDevicesForContact (jid) {
|
||||||
await _converse.api.waitUntil('OMEMOInitialized');
|
await _converse.api.waitUntil('OMEMOInitialized');
|
||||||
const devicelist = _converse.devicelists.get(jid) || _converse.devicelists.create({'jid': jid});
|
const devicelist = _converse.devicelists.get(jid) || _converse.devicelists.create({'jid': jid});
|
||||||
await devicelist.fetchDevices();
|
await devicelist.fetchDevices();
|
||||||
|
@ -571,11 +455,10 @@ converse.plugins.add('converse-omemo', {
|
||||||
|
|
||||||
_converse.contactHasOMEMOSupport = async function (jid) {
|
_converse.contactHasOMEMOSupport = async function (jid) {
|
||||||
/* Checks whether the contact advertises any OMEMO-compatible devices. */
|
/* Checks whether the contact advertises any OMEMO-compatible devices. */
|
||||||
const devices = await _converse.getDevicesForContact(jid);
|
const devices = await getDevicesForContact(jid);
|
||||||
return devices.length > 0;
|
return devices.length > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function generateDeviceID () {
|
function generateDeviceID () {
|
||||||
/* Generates a device ID, making sure that it's unique */
|
/* Generates a device ID, making sure that it's unique */
|
||||||
const existing_ids = _converse.devicelists.get(_converse.bare_jid).devices.pluck('id');
|
const existing_ids = _converse.devicelists.get(_converse.bare_jid).devices.pluck('id');
|
||||||
|
@ -591,6 +474,127 @@ converse.plugins.add('converse-omemo', {
|
||||||
return device_id.toString();
|
return device_id.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function buildSession (device) {
|
||||||
|
const address = new libsignal.SignalProtocolAddress(device.get('jid'), device.get('id')),
|
||||||
|
sessionBuilder = new libsignal.SessionBuilder(_converse.omemo_store, address),
|
||||||
|
prekey = device.getRandomPreKey(),
|
||||||
|
bundle = await device.getBundle();
|
||||||
|
|
||||||
|
return sessionBuilder.processPreKey({
|
||||||
|
'registrationId': parseInt(device.get('id'), 10),
|
||||||
|
'identityKey': u.base64ToArrayBuffer(bundle.identity_key),
|
||||||
|
'signedPreKey': {
|
||||||
|
'keyId': bundle.signed_prekey.id, // <Number>
|
||||||
|
'publicKey': u.base64ToArrayBuffer(bundle.signed_prekey.public_key),
|
||||||
|
'signature': u.base64ToArrayBuffer(bundle.signed_prekey.signature)
|
||||||
|
},
|
||||||
|
'preKey': {
|
||||||
|
'keyId': prekey.id, // <Number>
|
||||||
|
'publicKey': u.base64ToArrayBuffer(prekey.key),
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSession (device) {
|
||||||
|
const address = new libsignal.SignalProtocolAddress(device.get('jid'), device.get('id'));
|
||||||
|
return _converse.omemo_store.loadSession(address.toString()).then(session => {
|
||||||
|
if (session) {
|
||||||
|
return Promise.resolve();
|
||||||
|
} else {
|
||||||
|
return buildSession(device);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
_converse.getBundlesAndBuildSessions = async function (chatbox) {
|
||||||
|
let devices;
|
||||||
|
const id = _converse.omemo_store.get('device_id');
|
||||||
|
if (chatbox.get('type') === _converse.CHATROOMS_TYPE) {
|
||||||
|
const collections = await Promise.all(chatbox.occupants.map(o => getDevicesForContact(o.get('jid'))));
|
||||||
|
devices = collections.reduce((a, b) => _.concat(a, b.models), []);
|
||||||
|
|
||||||
|
} else if (chatbox.get('type') === _converse.PRIVATE_CHAT_TYPE) {
|
||||||
|
const their_devices = await getDevicesForContact(chatbox.get('jid')),
|
||||||
|
devicelist = _converse.devicelists.get(_converse.bare_jid),
|
||||||
|
own_devices = devicelist.devices.filter(d => d.get('id') !== id);
|
||||||
|
devices = _.concat(own_devices, their_devices.models);
|
||||||
|
}
|
||||||
|
// Filter out our own device
|
||||||
|
devices = devices.filter(d => d.get('id') !== id);
|
||||||
|
|
||||||
|
await Promise.all(devices.map(d => d.getBundle()));
|
||||||
|
await Promise.all(devices.map(d => getSession(d)));
|
||||||
|
return devices;
|
||||||
|
}
|
||||||
|
|
||||||
|
function addKeysToMessageStanza (stanza, dicts, iv) {
|
||||||
|
for (var i in dicts) {
|
||||||
|
if (Object.prototype.hasOwnProperty.call(dicts, i)) {
|
||||||
|
const payload = dicts[i].payload,
|
||||||
|
device = dicts[i].device,
|
||||||
|
prekey = 3 == parseInt(payload.type, 10);
|
||||||
|
|
||||||
|
stanza.c('key', {'rid': device.get('id') }).t(btoa(payload.body));
|
||||||
|
if (prekey) {
|
||||||
|
stanza.attrs({'prekey': prekey});
|
||||||
|
}
|
||||||
|
stanza.up();
|
||||||
|
if (i == dicts.length-1) {
|
||||||
|
stanza.c('iv').t(iv).up().up()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Promise.resolve(stanza);
|
||||||
|
}
|
||||||
|
|
||||||
|
_converse.createOMEMOMessageStanza = function (chatbox, message, devices) {
|
||||||
|
const { __ } = _converse;
|
||||||
|
const body = __("This is an OMEMO encrypted message which your client doesn’t seem to support. "+
|
||||||
|
"Find more information on https://conversations.im/omemo");
|
||||||
|
|
||||||
|
if (!message.get('message')) {
|
||||||
|
throw new Error("No message body to encrypt!");
|
||||||
|
}
|
||||||
|
const stanza = $msg({
|
||||||
|
'from': _converse.connection.jid,
|
||||||
|
'to': chatbox.get('jid'),
|
||||||
|
'type': chatbox.get('message_type'),
|
||||||
|
'id': message.get('msgid')
|
||||||
|
}).c('body').t(body).up()
|
||||||
|
|
||||||
|
if (message.get('type') === 'chat') {
|
||||||
|
stanza.c('request', {'xmlns': Strophe.NS.RECEIPTS}).up();
|
||||||
|
}
|
||||||
|
// An encrypted header is added to the message for
|
||||||
|
// each device that is supposed to receive it.
|
||||||
|
// These headers simply contain the key that the
|
||||||
|
// payload message is encrypted with,
|
||||||
|
// and they are separately encrypted using the
|
||||||
|
// session corresponding to the counterpart device.
|
||||||
|
stanza.c('encrypted', {'xmlns': Strophe.NS.OMEMO})
|
||||||
|
.c('header', {'sid': _converse.omemo_store.get('device_id')});
|
||||||
|
|
||||||
|
return chatbox.encryptMessage(message.get('message')).then(obj => {
|
||||||
|
// The 16 bytes key and the GCM authentication tag (The tag
|
||||||
|
// SHOULD have at least 128 bit) are concatenated and for each
|
||||||
|
// intended recipient device, i.e. both own devices as well as
|
||||||
|
// devices associated with the contact, the result of this
|
||||||
|
// concatenation is encrypted using the corresponding
|
||||||
|
// long-standing SignalProtocol session.
|
||||||
|
const promises = devices
|
||||||
|
.filter(device => device.get('trusted') != UNTRUSTED)
|
||||||
|
.map(device => chatbox.encryptKey(obj.key_and_tag, device));
|
||||||
|
|
||||||
|
return Promise.all(promises)
|
||||||
|
.then(dicts => addKeysToMessageStanza(stanza, dicts, obj.iv))
|
||||||
|
.then(stanza => {
|
||||||
|
stanza.c('payload').t(obj.payload).up().up();
|
||||||
|
stanza.c('store', {'xmlns': Strophe.NS.HINTS});
|
||||||
|
return stanza;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
_converse.OMEMOStore = Backbone.Model.extend({
|
_converse.OMEMOStore = Backbone.Model.extend({
|
||||||
|
|
||||||
|
@ -857,7 +861,7 @@ converse.plugins.add('converse-omemo', {
|
||||||
return bundle.prekeys[u.getRandomInt(bundle.prekeys.length)];
|
return bundle.prekeys[u.getRandomInt(bundle.prekeys.length)];
|
||||||
},
|
},
|
||||||
|
|
||||||
fetchBundleFromServer () {
|
async fetchBundleFromServer () {
|
||||||
const stanza = $iq({
|
const stanza = $iq({
|
||||||
'type': 'get',
|
'type': 'get',
|
||||||
'from': _converse.bare_jid,
|
'from': _converse.bare_jid,
|
||||||
|
@ -865,16 +869,17 @@ converse.plugins.add('converse-omemo', {
|
||||||
}).c('pubsub', {'xmlns': Strophe.NS.PUBSUB})
|
}).c('pubsub', {'xmlns': Strophe.NS.PUBSUB})
|
||||||
.c('items', {'node': `${Strophe.NS.OMEMO_BUNDLES}:${this.get('id')}`});
|
.c('items', {'node': `${Strophe.NS.OMEMO_BUNDLES}:${this.get('id')}`});
|
||||||
|
|
||||||
return _converse.api.sendIQ(stanza)
|
let iq;
|
||||||
.then(iq => {
|
try {
|
||||||
|
iq = await _converse.api.sendIQ(stanza)
|
||||||
|
} catch(iq) {
|
||||||
|
return _converse.log(iq.outerHTML, Strophe.LogLevel.ERROR);
|
||||||
|
}
|
||||||
const publish_el = sizzle(`items[node="${Strophe.NS.OMEMO_BUNDLES}:${this.get('id')}"]`, iq).pop(),
|
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(),
|
bundle_el = sizzle(`bundle[xmlns="${Strophe.NS.OMEMO}"]`, publish_el).pop(),
|
||||||
bundle = parseBundle(bundle_el);
|
bundle = parseBundle(bundle_el);
|
||||||
this.save('bundle', bundle);
|
this.save('bundle', bundle);
|
||||||
return bundle;
|
return bundle;
|
||||||
}).catch(iq => {
|
|
||||||
_converse.log(iq.outerHTML, Strophe.LogLevel.ERROR);
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
|
|
||||||
getBundle () {
|
getBundle () {
|
||||||
|
@ -1129,7 +1134,6 @@ converse.plugins.add('converse-omemo', {
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
_converse.api.listen.on('afterTearDown', () => {
|
_converse.api.listen.on('afterTearDown', () => {
|
||||||
if (_converse.devicelists) {
|
if (_converse.devicelists) {
|
||||||
_converse.devicelists.reset();
|
_converse.devicelists.reset();
|
||||||
|
|
Loading…
Reference in New Issue
Block a user