|
@@ -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) {
|
|
|
|
- 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) {
|
|
|
|
|
|
+ async 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);
|
|
|
|
- this.getBundlesAndBuildSessions()
|
|
|
|
- .then(devices => this.createOMEMOMessageStanza(message, devices))
|
|
|
|
- .then(stanza => 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);
|
|
|
|
|
|
+ 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);
|
|
|
|
+ }
|
|
} 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)
|
|
|
|
- .then(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(),
|
|
|
|
- bundle = parseBundle(bundle_el);
|
|
|
|
- this.save('bundle', bundle);
|
|
|
|
- return bundle;
|
|
|
|
- }).catch(iq => {
|
|
|
|
- _converse.log(iq.outerHTML, Strophe.LogLevel.ERROR);
|
|
|
|
- });
|
|
|
|
|
|
+ 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(),
|
|
|
|
+ bundle_el = sizzle(`bundle[xmlns="${Strophe.NS.OMEMO}"]`, publish_el).pop(),
|
|
|
|
+ bundle = parseBundle(bundle_el);
|
|
|
|
+ this.save('bundle', bundle);
|
|
|
|
+ return bundle;
|
|
},
|
|
},
|
|
|
|
|
|
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();
|