Initial code for handling a bundle update via PEP

udpates #497
This commit is contained in:
JC Brand 2018-05-20 10:16:18 +02:00
parent 839210f87c
commit 41db49ffca
2 changed files with 104 additions and 48 deletions

View File

@ -4,7 +4,7 @@
// Copyright (c) 2013-2018, the Converse.js developers // Copyright (c) 2013-2018, the Converse.js developers
// Licensed under the Mozilla Public License (MPLv2) // Licensed under the Mozilla Public License (MPLv2)
/* global libsignal, ArrayBuffer */ /* global libsignal, ArrayBuffer, parseInt */
(function (root, factory) { (function (root, factory) {
define([ define([
@ -49,6 +49,33 @@
}); });
} }
function parseBundle (bundle_el) {
/* Given an XML element representing a user's OMEMO bundle, parse it
* and return a map.
*/
const signed_prekey_public_el = bundle_el.querySelector('signedPreKeyPublic'),
signed_prekey_signature_el = bundle_el.querySelector('signedPreKeySignature'),
identity_key_el = bundle_el.querySelector('identityKey');
const prekeys = _.map(
sizzle(`> prekeys > preKeyPublic`, bundle_el),
(el) => {
return {
'id': parseInt(el.getAttribute('keyId'), 10),
'key': u.base64ToArrayBuffer(el.textContent)
}
});
return {
'identity_key': bundle_el.querySelector('> identityKey').textContent,
'signed_prekey': {
'id': parseInt(signed_prekey_public_el.getAttribute('signedPreKeyId'), 10),
'public_key': u.base64ToArrayBuffer(signed_prekey_public_el.textContent),
'signature': u.base64ToArrayBuffer(signed_prekey_signature_el.textContent)
},
'prekeys': prekeys
}
}
converse.plugins.add('converse-omemo', { converse.plugins.add('converse-omemo', {
@ -61,8 +88,20 @@
overrides: { overrides: {
ChatBox: { ChatBox: {
parseBundleFromIQ (device_id, stanza) {
const publish_el = sizzle(`items[node="${Strophe.NS.OMEMO_BUNDLES}:${device_id}"]`, stanza).pop();
const bundle_el = sizzle(`bundle[xmlns="${Strophe.NS.OMEMO}"]`, publish_el).pop();
return parseBundle(bundle_el);
},
fetchBundle (device_id) { fetchBundle (device_id) {
const { _converse } = this.__super__; const { _converse } = this.__super__,
device = _converse.devicelists.get(this.get('jid')).devices.get(device_id);
if (device.get('bundle')) {
return Promise.resolve(device.get('bundle').toJSON());
} else {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const stanza = $iq({ const stanza = $iq({
'type': 'get', 'type': 'get',
@ -72,46 +111,40 @@
.c('items', {'xmlns': `${Strophe.NS.OMEMO_BUNDLES}:${device_id}`}); .c('items', {'xmlns': `${Strophe.NS.OMEMO_BUNDLES}:${device_id}`});
_converse.connection.sendIQ( _converse.connection.sendIQ(
stanza, stanza,
(iq) => resolve((device_id, this.parseBundle(iq))), (iq) => {
const bundle = this.parseBundleFromIQ(iq);
bundle.device_id = device_id;
resolve(bundle);
},
reject, reject,
_converse.IQ_TIMEOUT _converse.IQ_TIMEOUT
); );
}); });
},
parseBundle (device_id, stanza) {
const publish_el = sizzle(`publish[node="${Strophe.NS.OMEMO_BUNDLES}:${device_id}"]`, stanza).pop();
const bundle_el = sizzle(`bundle[xmlns="${Strophe.NS.OMEMO}"]`, publish_el).pop();
const prekeys = _.map(
sizzle(`> prekeys > preKeyPublic`, bundle_el),
(key) => { return (key.getAttribute('id'), key.textContent) });
return {
'device_id': device_id,
'identity_key': bundle_el.querySelector('> identityKey').textContent,
'prekeys': prekeys
} }
}, },
fetchBundles () { fetchBundlesAndBuildSessions () {
const { _converse } = this.__super__; const { _converse } = this.__super__;
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
getDevicesForContact(this.get('jid')) getDevicesForContact(this.get('jid'))
.then((devices) => Promise.all(_.map(devices, (device_id) => this.fetchBundle(device_id)))) .then((devices) => {
.then((bundles) => { const bundle_promises = _.map(devices, (device_id) => this.fetchBundle(device_id));
this.buildSessions() Promise.all(bundle_promises).then(() => {
this.buildSessions(devices)
.then(() => resolve(bundles)) .then(() => resolve(bundles))
.catch(_.partial(_converse.log, _, Strophe.LogLevel.ERROR)); .catch(_.partial(_converse.log, _, Strophe.LogLevel.ERROR));
});
}).catch(_.partial(_converse.log, _, Strophe.LogLevel.ERROR));
}).catch(_.partial(_converse.log, _, Strophe.LogLevel.ERROR));
}).catch(_.partial(_converse.log, _, Strophe.LogLevel.ERROR)); }).catch(_.partial(_converse.log, _, Strophe.LogLevel.ERROR));
}, },
buildSessions (bundles) { buildSessions (devices) {
const { _converse } = this.__super__, const { _converse } = this.__super__,
device_id = _converse.omemo_store.get('device_id'); device_id = _converse.omemo_store.get('device_id');
return Promise.all(_.map(bundles, (bundle) => { return Promise.all(_.map(devices, (device) => {
const recipient_id = bundles['device_id']; const recipient_id = device['id'];
const address = new libsignal.SignalProtocolAddress(recipient_id, device_id); const address = new libsignal.SignalProtocolAddress(recipient_id, device_id);
const sessionBuilder = new libsignal.SessionBuilder(_converse.omemo_store, address); const sessionBuilder = new libsignal.SessionBuilder(_converse.omemo_store, address);
return sessionBuilder.processPreKey({ return sessionBuilder.processPreKey({
@ -168,7 +201,8 @@
createMessageStanza (message) { createMessageStanza (message) {
if (this.get('omemo_active')) { if (this.get('omemo_active')) {
return this.fetchBundles().then((bundles) => this.createOMEMOMessageStanza(message, bundles)); return this.fetchBundlesAndBuildSessions()
.then((bundles) => this.createOMEMOMessageStanza(message, bundles));
} else { } else {
return Promise.resolve(this.__super__.createMessageStanza.apply(this, arguments)); return Promise.resolve(this.__super__.createMessageStanza.apply(this, arguments));
} }
@ -308,7 +342,7 @@
if (trusted === undefined) { if (trusted === undefined) {
return Promise.resolve(true); return Promise.resolve(true);
} }
return Promise.resolve(u.arrayBuffer2String(identity_key) === u.arrayBuffer2String(trusted)); return Promise.resolve(u.arrayBufferToString(identity_key) === u.arrayBufferToString(trusted));
}, },
loadIdentityKey (identifier) { loadIdentityKey (identifier) {
@ -325,7 +359,7 @@
const address = new libsignal.SignalProtocolAddress.fromString(identifier), const address = new libsignal.SignalProtocolAddress.fromString(identifier),
existing = this.get('identity_key'+address.getName()); existing = this.get('identity_key'+address.getName());
this.save('identity_key'+address.getName(), identity_key) this.save('identity_key'+address.getName(), identity_key)
if (existing && u.arrayBuffer2String(identity_key) !== u.arrayBuffer2String(existing)) { if (existing && u.arrayBufferToString(identity_key) !== u.arrayBufferToString(existing)) {
return Promise.resolve(true); return Promise.resolve(true);
} else { } else {
return Promise.resolve(false); return Promise.resolve(false);
@ -396,6 +430,7 @@
if (!_converse.omemo_store.get('device_id')) { if (!_converse.omemo_store.get('device_id')) {
generateBundle() generateBundle()
.then((data) => { .then((data) => {
// TODO: should storeSession be used here?
_converse.omemo_store.save(data); _converse.omemo_store.save(data);
resolve(); resolve();
}).catch(_.partial(_converse.log, _, Strophe.LogLevel.ERROR)); }).catch(_.partial(_converse.log, _, Strophe.LogLevel.ERROR));
@ -504,7 +539,7 @@
function publishBundle () { function publishBundle () {
const store = _converse.omemo_store, const store = _converse.omemo_store,
signed_prekey = store.get('signed_prekey'), signed_prekey = store.get('signed_prekey'),
identity_key = u.arrayBuffer2Base64(store.get('identity_keypair').pubKey); identity_key = u.arrayBufferToBase64(store.get('identity_keypair').pubKey);
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const stanza = $iq({ const stanza = $iq({
@ -515,7 +550,7 @@
.c('item') .c('item')
.c('bundle', {'xmlns': Strophe.NS.OMEMO}) .c('bundle', {'xmlns': Strophe.NS.OMEMO})
.c('signedPreKeyPublic', {'signedPreKeyId': signed_prekey.keyId}) .c('signedPreKeyPublic', {'signedPreKeyId': signed_prekey.keyId})
.t(u.arrayBuffer2Base64(signed_prekey.keyPair.pubKey)).up() .t(u.arrayBufferToBase64(signed_prekey.keyPair.pubKey)).up()
.c('signedPreKeySignature').up() // TODO .c('signedPreKeySignature').up() // TODO
.c('identityKey').t(identity_key).up() .c('identityKey').t(identity_key).up()
.c('prekeys'); .c('prekeys');
@ -523,7 +558,7 @@
store.get('prekeys'), store.get('prekeys'),
(prekey) => { (prekey) => {
stanza.c('preKeyPublic', {'preKeyId': prekey.keyId}) stanza.c('preKeyPublic', {'preKeyId': prekey.keyId})
.t(u.arrayBuffer2Base64(prekey.keyPair.pubKey)).up(); .t(u.arrayBufferToBase64(prekey.keyPair.pubKey)).up();
}); });
_converse.connection.sendIQ(stanza, resolve, reject, _converse.IQ_TIMEOUT); _converse.connection.sendIQ(stanza, resolve, reject, _converse.IQ_TIMEOUT);
}); });
@ -561,11 +596,23 @@
}); });
} }
function updateBundleFromStanza (stanza) {
const items_el = sizzle(`items[node="${Strophe.NS.OMEMO_BUNDLES}"]`, stanza),
device_id = items_el.getAttribute('node').split(':')[1],
from = stanza.getAttribute('from'),
bundle_el = sizzle(`item list[xmlns="${Strophe.NS.OMEMO}"] bundle`, items_el).pop(),
bundle = parseBundle(bundle_el);
const device = _converse.devicelists.get(from).devices.get(device_id);
device.save({'bundle': bundle});
}
function updateDevicesFromStanza (stanza) { function updateDevicesFromStanza (stanza) {
// TODO: check whether our own device_id is still on the list, // TODO: check whether our own device_id is still on the list,
// otherwise we need to update it. // otherwise we need to update it.
const device_ids = _.map( const device_ids = _.map(
sizzle(`items[node="${Strophe.NS.OMEMO_DEVICELIST}"] item[xmlns="${Strophe.NS.OMEMO}"] device`, stanza), sizzle(`items[node="${Strophe.NS.OMEMO_DEVICELIST}"] item list[xmlns="${Strophe.NS.OMEMO}"] device`, stanza),
(device) => device.getAttribute('id')); (device) => device.getAttribute('id'));
const removed_ids = _.difference(_converse.devices.pluck('id'), device_ids); const removed_ids = _.difference(_converse.devices.pluck('id'), device_ids);
@ -586,6 +633,7 @@
_converse.connection.addHandler((message) => { _converse.connection.addHandler((message) => {
if (message.querySelector('event[xmlns="'+Strophe.NS.PUBSUB+'#event"]')) { if (message.querySelector('event[xmlns="'+Strophe.NS.PUBSUB+'#event"]')) {
updateDevicesFromStanza(message); updateDevicesFromStanza(message);
updateBundleFromStanza(message);
updateOwnDeviceList(); updateOwnDeviceList();
} }
}, null, 'message', 'headline', null, _converse.bare_jid); }, null, 'message', 'headline', null, _converse.bare_jid);
@ -594,7 +642,7 @@
function restoreOMEMOSession () { function restoreOMEMOSession () {
if (_.isUndefined(_converse.omemo_store)) { if (_.isUndefined(_converse.omemo_store)) {
_converse.omemo_store = new _converse.OMEMOStore(); _converse.omemo_store = new _converse.OMEMOStore();
_converse.omemo_store.browserStorage = new Backbone.BrowserStorage.session( _converse.omemo_store.browserStorage = new Backbone.BrowserStorage[_converse.storage](
b64_sha1(`converse.omemosession-${_converse.bare_jid}`) b64_sha1(`converse.omemosession-${_converse.bare_jid}`)
); );
} }
@ -603,7 +651,7 @@
function initOMEMO() { function initOMEMO() {
_converse.devicelists = new _converse.DeviceLists(); _converse.devicelists = new _converse.DeviceLists();
_converse.devicelists.browserStorage = new Backbone.BrowserStorage.session( _converse.devicelists.browserStorage = new Backbone.BrowserStorage[_converse.storage](
b64_sha1(`converse.devicelists-${_converse.bare_jid}`) b64_sha1(`converse.devicelists-${_converse.bare_jid}`)
); );
fetchOwnDevices() fetchOwnDevices()

View File

@ -835,15 +835,23 @@
return result; return result;
}; };
u.arrayBuffer2String = function (ab) { u.arrayBufferToString = function (ab) {
var enc = new TextDecoder("utf-8"); var enc = new TextDecoder("utf-8");
return enc.decode(new Uint8Array(ab)); return enc.decode(new Uint8Array(ab));
}; };
u.arrayBuffer2Base64 = function (ab) { u.arrayBufferToBase64 = function (ab) {
return btoa(new Uint8Array(ab) return btoa(new Uint8Array(ab)
.reduce((data, byte) => data + String.fromCharCode(byte), '')); .reduce((data, byte) => data + String.fromCharCode(byte), ''));
}; };
u.base64ToArrayBuffer = function (b64) {
const binary_string = window.atob(b64),
len = binary_string.length,
bytes = new Uint8Array(len);
_.forEach(_.range(0, len), (i) => bytes.push(binary_string.charCodeAt(i))); // eslint-disable-line lodash/prefer-map
return bytes.buffer;
};
return u; return u;
})); }));