120 lines
4.9 KiB
JavaScript
120 lines
4.9 KiB
JavaScript
|
import concat from 'lodash-es/concat';
|
|||
|
import { UNTRUSTED } from '../consts.js';
|
|||
|
import { __ } from 'i18n';
|
|||
|
import { _converse, converse } from '@converse/headless/core';
|
|||
|
import {
|
|||
|
addKeysToMessageStanza,
|
|||
|
generateFingerprint,
|
|||
|
getDevicesForContact,
|
|||
|
getSession,
|
|||
|
omemo,
|
|||
|
} from '../utils.js';
|
|||
|
|
|||
|
const { Strophe, $msg } = converse.env;
|
|||
|
|
|||
|
const ConverseMixins = {
|
|||
|
|
|||
|
generateFingerprints: async function (jid) {
|
|||
|
const devices = await getDevicesForContact(jid);
|
|||
|
return Promise.all(devices.map(d => generateFingerprint(d)));
|
|||
|
},
|
|||
|
|
|||
|
getDeviceForContact: function (jid, device_id) {
|
|||
|
return getDevicesForContact(jid).then(devices => devices.get(device_id));
|
|||
|
},
|
|||
|
|
|||
|
contactHasOMEMOSupport: async function (jid) {
|
|||
|
/* Checks whether the contact advertises any OMEMO-compatible devices. */
|
|||
|
const devices = await getDevicesForContact(jid);
|
|||
|
return devices.length > 0;
|
|||
|
},
|
|||
|
|
|||
|
getBundlesAndBuildSessions: async function (chatbox) {
|
|||
|
const no_devices_err = __('Sorry, no devices found to which we can send an OMEMO encrypted message.');
|
|||
|
let devices;
|
|||
|
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'));
|
|||
|
if (their_devices.length === 0) {
|
|||
|
const err = new Error(no_devices_err);
|
|||
|
err.user_facing = true;
|
|||
|
throw err;
|
|||
|
}
|
|||
|
const own_devices = _converse.devicelists.get(_converse.bare_jid).devices;
|
|||
|
devices = [...own_devices.models, ...their_devices.models];
|
|||
|
}
|
|||
|
// Filter out our own device
|
|||
|
const id = _converse.omemo_store.get('device_id');
|
|||
|
devices = devices.filter(d => d.get('id') !== id);
|
|||
|
// Fetch bundles if necessary
|
|||
|
await Promise.all(devices.map(d => d.getBundle()));
|
|||
|
|
|||
|
const sessions = devices.filter(d => d).map(d => getSession(d));
|
|||
|
await Promise.all(sessions);
|
|||
|
if (sessions.includes(null)) {
|
|||
|
// We couldn't build a session for certain devices.
|
|||
|
devices = devices.filter(d => sessions[devices.indexOf(d)]);
|
|||
|
if (devices.length === 0) {
|
|||
|
const err = new Error(no_devices_err);
|
|||
|
err.user_facing = true;
|
|||
|
throw err;
|
|||
|
}
|
|||
|
}
|
|||
|
return devices;
|
|||
|
},
|
|||
|
|
|||
|
createOMEMOMessageStanza: function (chatbox, message, devices) {
|
|||
|
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 omemo.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 && device.get('active'))
|
|||
|
.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;
|
|||
|
});
|
|||
|
});
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
export default ConverseMixins;
|