diff --git a/spec/omemo.js b/spec/omemo.js index 3371bab77..4582b2c56 100644 --- a/spec/omemo.js +++ b/spec/omemo.js @@ -15,7 +15,25 @@ }); }, 'generateRegistrationId': function () { - return 1234; + return '31415'; + }, + 'generatePreKey': function (keyid) { + return Promise.resolve({ + 'keyId': keyid, + 'keyPair': { + 'pubKey': 1234, + 'privKey': 4321 + } + }); + }, + 'generateSignedPreKey': function (identity_keypair, keyid) { + return Promise.resolve({ + 'keyId': keyid, + 'keyPair': { + 'pubKey': 1234, + 'privKey': 4321 + } + }); } } }; @@ -35,15 +53,14 @@ null, ['rosterGroupsFetched'], {}, function (done, _converse) { - let devicelist_iq, - disco_info_iq; + let iq_stanza; test_utils.createContacts(_converse, 'current'); const contact_jid = mock.cur_names[2].replace(/ /g,'.').toLowerCase() + '@localhost'; test_utils.waitUntil(function () { return _.filter(_converse.connection.IQ_stanzas, function (iq) { const node = iq.nodeTree.querySelector('iq[to="dummy@localhost"] query[xmlns="http://jabber.org/protocol/disco#info"]'); - if (node) { disco_info_iq = iq; } + if (node) { iq_stanza = iq.nodeTree; } return node; }).length > 0; }, 1000).then(function () { @@ -52,32 +69,57 @@ 'type': 'result', 'from': 'dummy@localhost', 'to': 'dummy@localhost/resource', - 'id': disco_info_iq.nodeTree.getAttribute('id'), + 'id': iq_stanza.getAttribute('id'), }).c('query', {'xmlns': 'http://jabber.org/protocol/disco#info'}) .c('identity', { 'category': 'pubsub', 'type': 'pep'}); _converse.connection._dataRecv(test_utils.createRequest(stanza)); + + return test_utils.waitUntil(() => { + return _.filter(_converse.connection.IQ_stanzas, function (iq) { + const node = iq.nodeTree.querySelector('publish[node="eu.siacs.conversations.axolotl.bundles:31415"]'); + if (node) { iq_stanza = iq.nodeTree; } + return node; + }).length; + }); + }).then(function () { + expect(iq_stanza.getAttributeNames().sort().join()).toBe(["from", "type", "xmlns", "id"].sort().join()); + expect(iq_stanza.querySelector('prekeys').childNodes.length).toBe(100); + + const signed_prekeys = iq_stanza.querySelectorAll('signedPreKeyPublic'); + expect(signed_prekeys.length).toBe(1); + const signed_prekey = signed_prekeys[0]; + expect(signed_prekey.getAttribute('signedPreKeyId')).toBe('0') + expect(iq_stanza.querySelectorAll('signedPreKeySignature').length).toBe(1); + expect(iq_stanza.querySelectorAll('identityKey').length).toBe(1); + + const stanza = $iq({ + 'from': _converse.bare_jid, + 'id': iq_stanza.getAttribute('id'), + 'to': _converse.bare_jid, + 'type': 'result'}); + _converse.connection._dataRecv(test_utils.createRequest(stanza)); + return test_utils.waitUntil(() => { return _.filter( _converse.connection.IQ_stanzas, (iq) => { const node = iq.nodeTree.querySelector('iq[to="'+_converse.bare_jid+'"] query[node="eu.siacs.conversations.axolotl.devicelist"]'); - if (node) { - devicelist_iq = iq; - } + if (node) { iq_stanza = iq.nodeTree;} return node; }).length; }); }).then(function () { - expect(devicelist_iq.toLocaleString()).toBe( - ""+ - ""+ - ""); + expect(iq_stanza.outerHTML).toBe( + ''+ + ''+ + ''); + const stanza = $iq({ 'from': contact_jid, - 'id': devicelist_iq.nodeTree.getAttribute('id'), + 'id': iq_stanza.getAttribute('id'), 'to': _converse.bare_jid, 'type': 'result', }).c('query', { @@ -97,20 +139,18 @@ _converse.connection.IQ_stanzas, (iq) => { const node = iq.nodeTree.querySelector('iq[to="'+contact_jid+'"] query[node="eu.siacs.conversations.axolotl.devicelist"]'); - if (node) { - devicelist_iq = iq; - } + if (node) { iq_stanza = iq.nodeTree; } return node; }).length;}); }).then(function () { - expect(devicelist_iq.toLocaleString()).toBe( - ""+ - ""+ - ""); + expect(iq_stanza.outerHTML).toBe( + ''+ + ''+ + ''); const stanza = $iq({ 'from': contact_jid, - 'id': devicelist_iq.nodeTree.getAttribute('id'), + 'id': iq_stanza.getAttribute('id'), 'to': _converse.bare_jid, 'type': 'result', }).c('query', { @@ -139,7 +179,7 @@ toolbar.querySelector('.toggle-omemo').click(); expect(view.toggleOMEMO).toHaveBeenCalled(); done(); - }); + }).catch(_.partial(console.error, _)); })); }); diff --git a/src/converse-chatview.js b/src/converse-chatview.js index bf8a9e42e..3e4456dbb 100644 --- a/src/converse-chatview.js +++ b/src/converse-chatview.js @@ -387,6 +387,7 @@ this.addSpoilerButton(options); this.addFileUploadButton(); this.insertEmojiPicker(); + _converse.emit('renderToolbar', this); return this; }, diff --git a/src/converse-omemo.js b/src/converse-omemo.js index 3c84e8f47..b50d7ec5c 100644 --- a/src/converse-omemo.js +++ b/src/converse-omemo.js @@ -4,6 +4,8 @@ // Copyright (c) 2013-2018, the Converse.js developers // Licensed under the Mozilla Public License (MPLv2) +/* global libsignal */ + (function (root, factory) { define([ "converse-core", @@ -12,11 +14,13 @@ }(this, function (converse, tpl_toolbar_omemo) { const { Backbone, Promise, Strophe, sizzle, $iq, _, b64_sha1 } = converse.env; + const u = converse.env.utils; Strophe.addNamespace('OMEMO', "eu.siacs.conversations.axolotl"); Strophe.addNamespace('OMEMO_DEVICELIST', Strophe.NS.OMEMO+".devicelist"); Strophe.addNamespace('OMEMO_VERIFICATION', Strophe.NS.OMEMO+".verification"); Strophe.addNamespace('OMEMO_WHITELISTED', Strophe.NS.OMEMO+".whitelisted"); + Strophe.addNamespace('OMEMO_BUNDLES', Strophe.NS.OMEMO+".bundles"); const UNDECIDED = 0; const TRUSTED = 1; @@ -37,6 +41,7 @@ } function contactHasOMEMOSupport (_converse, jid) { + /* Checks whether the contact advertises any OMEMO-compatible devices. */ return new Promise((resolve, reject) => { getDevicesForContact(_converse, jid).then((devices) => { resolve(devices.length > 0) @@ -57,10 +62,13 @@ return !_.isNil(window.libsignal); }, + dependencies: ["converse-chatview"], + overrides: { + ChatBoxView: { events: { - 'click .toggle-omemo': 'toggleOMEMO', + 'click .toggle-omemo': 'toggleOMEMO' }, toggleOMEMO (ev) { @@ -68,7 +76,7 @@ ev.preventDefault(); }, - addOMEMOToolbarButton (options) { + addOMEMOToolbarButton () { const { _converse } = this.__super__, { __ } = _converse; Promise.all([ @@ -83,12 +91,6 @@ tpl_toolbar_omemo({'__': __})); } }).catch(_.partial(_converse.log, _, Strophe.LogLevel.ERROR)); - }, - - renderToolbar (toolbar, options) { - const result = this.__super__.renderToolbar.apply(this, arguments); - this.addOMEMOToolbarButton(options); - return result; } } }, @@ -101,31 +103,51 @@ _converse.api.promises.add(['OMEMOInitialized']); + function generateBundle () { + return new Promise((resolve, reject) => { + libsignal.KeyHelper.generateIdentityKeyPair().then((identity_keypair) => { + const data = { + 'device_id': libsignal.KeyHelper.generateRegistrationId(), + 'pubkey': identity_keypair.pubKey, + 'privkey': identity_keypair.privKey, + 'prekeys': {} + }; + const signed_prekey_id = '0'; + libsignal.KeyHelper.generateSignedPreKey(identity_keypair, signed_prekey_id) + .then((signed_prekey) => { + data['signed_prekey'] = signed_prekey; + const key_promises = _.map(_.range(0, 100), (id) => libsignal.KeyHelper.generatePreKey(id)); + Promise.all(key_promises).then((keys) => { + data['prekeys'] = keys; + resolve(data) + }); + }).catch(_.partial(_converse.log, _, Strophe.LogLevel.ERROR)); + }); + }).catch(_.partial(_converse.log, _, Strophe.LogLevel.ERROR)); + } - _converse.OMEMOSession = Backbone.Model.extend({ - initialize () { - this.keyhelper = window.libsignal.KeyHelper; - }, + + _converse.OMEMOStore = Backbone.Model.extend({ fetchSession () { - return new Promise((resolve, reject) => { - this.fetch({ - 'success': () => { - if (!_converse.omemo_session.get('registration_id')) { - this.keyhelper.generateIdentityKeyPair().then((keypair) => { - _converse.omemo_session.set({ - 'registration_id': this.keyhelper.generateRegistrationId(), - 'pub_key': keypair.pubKey, - 'priv_key': keypair.privKey - }); + if (_.isUndefined(this._setup_promise)) { + this._setup_promise = new Promise((resolve, reject) => { + this.fetch({ + 'success': () => { + if (!_converse.omemo_store.get('device_id')) { + generateBundle() + .then((data) => { + _converse.omemo_store.save(data); + resolve(); + }).catch(_.partial(_converse.log, _, Strophe.LogLevel.ERROR)); + } else { resolve(); - }); - } else { - resolve(); + } } - } + }); }); - }); + } + return this._setup_promise; } }); @@ -199,11 +221,29 @@ function publishBundle () { - // TODO: publish bundle information (public key and pre keys) - // Keep the used device id consistant. You have to republish - // this because you don't know if the server was restarted or might have - // otherwise lost the information. - return Promise.resolve(); + const store = _converse.omemo_store, + signed_prekey = store.get('signed_prekey'); + return new Promise((resolve, reject) => { + const stanza = $iq({ + 'from': _converse.bare_jid, + 'type': 'set' + }).c('pubsub', {'xmlns': Strophe.NS.PUBSUB}) + .c('publish', {'node': `${Strophe.NS.OMEMO_BUNDLES}:${store.get('device_id')}`}) + .c('item') + .c('bundle', {'xmlns': Strophe.NS.OMEMO}) + .c('signedPreKeyPublic', {'signedPreKeyId': signed_prekey.keyId}) + .t(u.arrayBuffer2Base64(signed_prekey.keyPair.pubKey)).up() + .c('signedPreKeySignature').up() + .c('identityKey').up() + .c('prekeys'); + _.forEach( + store.get('prekeys'), + (prekey) => { + stanza.c('preKeyPublic', {'preKeyId': prekey.keyId}) + .t(u.arrayBuffer2Base64(prekey.keyPair.pubKey)).up(); + }); + _converse.connection.sendIQ(stanza, resolve, reject, _converse.IQ_TIMEOUT); + }); } function fetchDeviceLists () { @@ -215,13 +255,15 @@ * Also, deduplicate devices if necessary. */ return new Promise((resolve, reject) => { - let own_devicelist = _converse.devicelists.get(_converse.bare_jid); - if (_.isNil(own_devicelist)) { - own_devicelist = _converse.devicelists.create({'jid': _converse.bare_jid}); - } - own_devicelist.fetchDevices().then(resolve).catch(reject); - // TODO: if our own device is not onthe list, add it. - // TODO: deduplicate + fetchDeviceLists().then(() => { + let own_devicelist = _converse.devicelists.get(_converse.bare_jid); + if (_.isNil(own_devicelist)) { + own_devicelist = _converse.devicelists.create({'jid': _converse.bare_jid}); + } + own_devicelist.fetchDevices().then(resolve).catch(reject); + // TODO: if our own device is not onthe list, add it. + // TODO: deduplicate + }); }); } @@ -252,13 +294,21 @@ }, null, 'message', 'headline', null, _converse.bare_jid); } + function restoreOMEMOSession () { + _converse.omemo_store = new _converse.OMEMOStore(); + _converse.omemo_store.browserStorage = new Backbone.BrowserStorage.session( + b64_sha1(`converse.omemosession-${_converse.bare_jid}`) + ); + return _converse.omemo_store.fetchSession() + } + function initOMEMO () { /* Publish our bundle and then fetch our own device list. * If our device list does not contain this device's id, publish the * device list with the id added. Also deduplicate device ids in the list. */ - publishBundle() - .then(() => fetchDeviceLists()) + restoreOMEMOSession() + .then(() => publishBundle()) .then(() => updateOwnDeviceList()) .then(() => _converse.emit('OMEMOInitialized')) .catch(_.partial(_converse.log, _, Strophe.LogLevel.ERROR)); @@ -270,15 +320,10 @@ b64_sha1(`converse.devicelists-${_converse.bare_jid}`) ); - _converse.omemo_session = new _converse.OMEMOSession(); - _converse.omemo_session.browserStorage = new Backbone.BrowserStorage.session( - b64_sha1(`converse.omemosession-${_converse.bare_jid}`) - ); - _converse.omemo_session.fetchSession() - .then(initOMEMO) - .catch(_.partial(_converse.log, _, Strophe.LogLevel.ERROR)); + initOMEMO(); } + _converse.api.listen.on('renderToolbar', (view) => view.addOMEMOToolbarButton()); _converse.api.listen.on('statusInitialized', onStatusInitialized); _converse.api.listen.on('connected', registerPEPPushHandler); _converse.api.listen.on('afterTearDown', () => _converse.devices.reset()); diff --git a/src/utils/core.js b/src/utils/core.js index 1fbf9eba7..8a153c1ee 100644 --- a/src/utils/core.js +++ b/src/utils/core.js @@ -823,14 +823,11 @@ return text.replace(_converse.geouri_regex, replacement); }; - u.getSelectValues = function(select) { - var result = []; - var options = select && select.options; - var opt; - + u.getSelectValues = function (select) { + const result = []; + const options = select && select.options; for (var i=0, iLen=options.length; i data + String.fromCharCode(byte), '') + }; + return u; })); diff --git a/tests/runner.js b/tests/runner.js index 05a5abf23..ac504ad36 100644 --- a/tests/runner.js +++ b/tests/runner.js @@ -47,6 +47,7 @@ var specs = [ "spec/ping", "spec/xmppstatus", "spec/mam", + "spec/omemo", "spec/controlbox", "spec/roster", "spec/chatbox",