From a3593dbc7d926510292b1c9c69dd56695c8e720f Mon Sep 17 00:00:00 2001 From: JC Brand Date: Wed, 25 Jul 2018 12:59:12 +0200 Subject: [PATCH] Implement and test sending of encrypted messages updates #497 --- dist/converse.js | 216 +++++++++++++++++++++++++++--------------- spec/omemo.js | 120 ++++++++++++++++++++++- src/converse-omemo.js | 179 ++++++++++++++++++++++------------ src/utils/core.js | 4 + src/utils/form.js | 2 +- tests/mock.js | 16 ++++ 6 files changed, 402 insertions(+), 135 deletions(-) diff --git a/dist/converse.js b/dist/converse.js index 5980f1464..7dc40591b 100644 --- a/dist/converse.js +++ b/dist/converse.js @@ -73263,85 +73263,143 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_ const _converse = this.__super__._converse; return new Promise((resolve, reject) => { _converse.getDevicesForContact(this.get('jid')).then(devices => { - const promises = devices.map(device => device.getBundle()); - Promise.all(promises).then(() => { - this.buildSessions(devices).then(() => resolve(devices)).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(devices) { - const _converse = this.__super__._converse, - device_id = _converse.omemo_store.get('device_id'); - - return Promise.all(_.map(devices, device => { - const recipient_id = device['id']; - const address = new libsignal.SignalProtocolAddress(parseInt(recipient_id, 10), device_id); - const sessionBuilder = new libsignal.SessionBuilder(_converse.omemo_store, address); - return sessionBuilder.processPreKey({ - 'registrationId': _converse.omemo_store.get('registration_id'), - 'identityKey': _converse.omemo_store.get('identity_keypair'), - 'signedPreKey': { - 'keyId': '', - // , - 'publicKey': '', - // , - 'signature': '' // - - }, - 'preKey': { - 'keyId': '', - // , - 'publicKey': '' // - - } - }); - })); - }, - - encryptMessage(message) {// TODO: - // const { _converse } = this.__super__; - // const plaintext = message.get('message'); - // const address = new libsignal.SignalProtocolAddress(recipientId, deviceId); - // return new Promise((resolve, reject) => { - // var sessionCipher = new window.libsignal.SessionCipher(_converse.omemo_store, address); - // sessionCipher.encrypt(plaintext).then((ciphertext) => {}); - // }); - }, - - createOMEMOMessageStanza(message, bundles) { - 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"); - - return new Promise((resolve, reject) => { - this.encryptMessage(message).then(payload => { - 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('encrypted').t(payload).c('header').t(payload).up(); - - _.forEach(bundles, bundle => { - const prekey = bundle.prekeys[Math.random(bundle.prekeys.length)].textContent; - stanza('key', { - 'rid': bundle.identity_key - }).t(prekey).up(); - }); // TODO: set storage hint urn:xmpp:hints - - - resolve(stanza); + Promise.all(devices.map(device => device.getBundle())).then(() => this.buildSessions(devices)).then(() => resolve(devices)).catch(_.partial(_converse.log, _, Strophe.LogLevel.ERROR)); }).catch(_.partial(_converse.log, _, Strophe.LogLevel.ERROR)); }); }, + buildSession(device) { + const _converse = this.__super__._converse; + const bundle = device.get('bundle'), + address = new libsignal.SignalProtocolAddress(device.get('jid'), device.get('id')), + sessionBuilder = new libsignal.SessionBuilder(_converse.omemo_store, address), + prekey = device.getRandomPreKey(); + return sessionBuilder.processPreKey({ + 'registrationId': _converse.omemo_store.get('registration_id'), + 'identityKey': _converse.omemo_store.get('identity_keypair'), + 'signedPreKey': { + 'keyId': bundle.signed_prekey.id, + // + 'publicKey': u.base64ToArrayBuffer(bundle.signed_prekey.public_key), + 'signature': u.base64ToArrayBuffer(bundle.signed_prekey.signature) + }, + 'preKey': { + 'keyId': prekey.id, + // + 'publicKey': u.base64ToArrayBuffer(prekey.key) + } + }); + }, + + buildSessions(devices) { + return Promise.all(devices.map(device => this.buildSession(device))); + }, + + encryptMessage(plaintext) { + // The client MUST use fresh, randomly generated key/IV pairs + // with AES-128 in Galois/Counter Mode (GCM). + const TAG_LENGTH = 128, + iv = window.crypto.getRandomValues(new window.Uint8Array(16)); + let key; + return window.crypto.subtle.generateKey({ + 'name': "AES-GCM", + 'length': 256 + }, true, // extractable + ["encrypt", "decrypt"] // key usages + ).then(result => { + key = result; + const algo = { + 'name': 'AES-GCM', + 'iv': iv, + 'tagLength': TAG_LENGTH + }; + return window.crypto.subtle.encrypt(algo, key, new TextEncoder().encode(plaintext)); + }).then(ciphertext => { + return window.crypto.subtle.exportKey("jwk", key).then(key_str => { + return Promise.resolve({ + 'key_str': key_str, + 'tag': ciphertext.slice(ciphertext.byteLength - (TAG_LENGTH + 7 >> 3)), + 'iv': iv + }); + }); + }); + }, + + encryptKey(plaintext, device) { + const _converse = this.__super__._converse, + address = new libsignal.SignalProtocolAddress(this.get('jid'), device.get('id')), + sessionCipher = new window.libsignal.SessionCipher(_converse.omemo_store, address); + return sessionCipher.encrypt(plaintext); + }, + + addKeysToMessageStanza(stanza, devices, payloads) { + for (var i in payloads) { + if (Object.prototype.hasOwnProperty.call(payloads, i)) { + const payload = btoa(JSON.stringify(payloads[i])); + const prekey = 3 == parseInt(payloads[i].type, 10); + + if (i == payloads.length - 1) { + stanza.c('key', { + 'rid': devices.get('id') + }).t(payload); + + if (prekey) { + stanza.attrs({ + 'prekey': prekey + }); + } + + stanza.up().c('iv').t(payloads[0].iv).up().up(); + } else { + stanza.c('key', { + prekey: prekey, + rid: devices.get('id') + }).t(payload).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"); // 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. + + + 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('encrypted', { + 'xmlns': Strophe.NS.OMEMO + }).c('header', { + 'sid': _converse.omemo_store.get('device_id') + }); + return this.encryptMessage(message).then(payload => { + // 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. + // TODO: need to include own devices here as well (and filter out distrusted devices) + const promises = devices.map(device => this.encryptKey(payload.key_str + payload.tag, device)); + return Promise.all(promises).then(payloads => this.addKeysToMessageStanza(stanza, devices, payloads)); + }); + }, + sendMessage(attrs) { + const _converse = this.__super__._converse; + if (this.get('omemo_active')) { const message = this.messages.create(attrs); - this.getBundlesAndBuildSessions().then(bundles => this.createOMEMOMessageStanza(message, bundles)).then(stanza => this.sendMessageStanza(stanza)); + this.getBundlesAndBuildSessions().then(devices => this.createOMEMOMessageStanza(message, devices)).then(stanza => this.sendMessageStanza(stanza)).catch(_.partial(_converse.log, _, Strophe.LogLevel.ERROR)); } else { return this.__super__.sendMessage.apply(this, arguments); } @@ -73643,6 +73701,12 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_ 'trusted': UNDECIDED }, + getRandomPreKey() { + // XXX: assumes that the bundle has already been fetched + const bundle = this.get('bundle'); + return bundle.prekeys[u.getRandomInt(bundle.prekeys.length)]; + }, + fetchBundleFromServer() { return new Promise((resolve, reject) => { const stanza = $iq({ @@ -73670,7 +73734,7 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_ * this device, if the information is not at hand already. */ if (this.get('bundle')) { - return Promise.resolve(this.get('bundle').toJSON()); + return Promise.resolve(this.get('bundle').toJSON(), this); } else { return this.fetchBundleFromServer(); } @@ -82446,6 +82510,10 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_ return bytes.buffer; }; + u.getRandomInt = function (max) { + return Math.floor(Math.random() * Math.floor(max)); + }; + u.getUniqueId = function () { return 'xxxxxxxx-xxxx'.replace(/[x]/g, function (c) { var r = Math.random() * 16 | 0, @@ -82474,7 +82542,7 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_ // // This is the utilities module. // -// Copyright (c) 2012-2017, Jan-Carel Brand +// Copyright (c) 2013-2018, Jan-Carel Brand // Licensed under the Mozilla Public License (MPLv2) // diff --git a/spec/omemo.js b/spec/omemo.js index 4c9561414..9fb8420ab 100644 --- a/spec/omemo.js +++ b/spec/omemo.js @@ -10,6 +10,122 @@ describe("The OMEMO module", function() { + it("enables encrypted messages to be sent", + mock.initConverseWithPromises( + null, ['rosterGroupsFetched'], {}, + function (done, _converse) { + + var sent_stanza; + let iq_stanza; + test_utils.createContacts(_converse, 'current', 1); + const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@localhost'; + + // First, fetch own device list + 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) { iq_stanza = iq.nodeTree;} + return node; + }).length; + }).then(() => { + const stanza = $iq({ + 'from': contact_jid, + 'id': iq_stanza.getAttribute('id'), + 'to': _converse.bare_jid, + 'type': 'result', + }).c('query', { + 'xmlns': 'http://jabber.org/protocol/disco#items', + 'node': 'eu.siacs.conversations.axolotl.devicelist' + }).c('device', {'id': '482886413b977930064a5888b92134fe'}).up() + _converse.connection._dataRecv(test_utils.createRequest(stanza)); + + _converse.emit('OMEMOInitialized'); + + // Check that device list for contact is fetched when chat is opened. + test_utils.openChatBoxFor(_converse, contact_jid); + return test_utils.waitUntil(() => { + return _.filter( + _converse.connection.IQ_stanzas, + (iq) => { + const node = iq.nodeTree.querySelector('iq[to="'+contact_jid+'"] query[node="eu.siacs.conversations.axolotl.devicelist"]'); + if (node) { iq_stanza = iq.nodeTree; } + return node; + }).length; + }); + }).then(() => { + const stanza = $iq({ + 'from': contact_jid, + 'id': iq_stanza.getAttribute('id'), + 'to': _converse.bare_jid, + 'type': 'result', + }).c('query', { + 'xmlns': 'http://jabber.org/protocol/disco#items', + 'node': 'eu.siacs.conversations.axolotl.devicelist' + }).c('device', {'id': '555'}).up() + _converse.connection._dataRecv(test_utils.createRequest(stanza)); + + const devicelist = _converse.devicelists.create({'jid': contact_jid}); + expect(devicelist.devices.length).toBe(1); + + const view = _converse.chatboxviews.get(contact_jid); + view.model.set('omemo_active', true); + + const textarea = view.el.querySelector('.chat-textarea'); + textarea.value = 'This message will be encrypted'; + view.keyPressed({ + target: textarea, + preventDefault: _.noop, + keyCode: 13 // Enter + }); + return test_utils.waitUntil(() => { + return _.filter( + _converse.connection.IQ_stanzas, + (iq) => { + const node = iq.nodeTree.querySelector('iq[to="'+contact_jid+'"] items[node="eu.siacs.conversations.axolotl.bundles:555"]'); + if (node) { iq_stanza = iq.nodeTree; } + return node; + }).length; + }); + }).then(() => { + const stanza = $iq({ + 'from': contact_jid, + 'id': iq_stanza.getAttribute('id'), + 'to': _converse.bare_jid, + 'type': 'result', + }).c('pubsub', { + 'xmlns': 'http://jabber.org/protocol/pubsub' + }).c('items', {'node': "eu.siacs.conversations.axolotl.bundles:555"}) + .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')); + + spyOn(_converse.connection, 'send').and.callFake(stanza => { sent_stanza = stanza }); + _converse.connection._dataRecv(test_utils.createRequest(stanza)); + return test_utils.waitUntil(() => sent_stanza); + }).then(function () { + expect(sent_stanza.toLocaleString()).toBe( + ``+ + `This is an OMEMO encrypted message which your client doesn’t seem to support. Find more information on https://conversations.im/omemo`+ + ``+ + `
`+ + `eyJpdiI6IjEyMzQ1In0=`+ + `12345`+ + `
`+ + `
`+ + `
`); + done(); + }); + })); + it("will add processing hints to sent out encrypted stanzas", mock.initConverseWithPromises( null, ['rosterGroupsFetched'], {}, @@ -24,8 +140,8 @@ function (done, _converse) { let iq_stanza; - test_utils.createContacts(_converse, 'current'); - const contact_jid = mock.cur_names[3].replace(/ /g,'.').toLowerCase() + '@localhost'; + test_utils.createContacts(_converse, 'current', 1); + const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@localhost'; test_utils.waitUntil(function () { return _.filter( diff --git a/src/converse-omemo.js b/src/converse-omemo.js index 670b87ab8..7c246bb30 100644 --- a/src/converse-omemo.js +++ b/src/converse-omemo.js @@ -93,86 +93,143 @@ return new Promise((resolve, reject) => { _converse.getDevicesForContact(this.get('jid')) .then((devices) => { - const promises = devices.map((device) => device.getBundle()); - Promise.all(promises).then(() => { - this.buildSessions(devices) - .then(() => resolve(devices)) - .catch(_.partial(_converse.log, _, Strophe.LogLevel.ERROR)); - }).catch(_.partial(_converse.log, _, Strophe.LogLevel.ERROR)); + Promise.all(devices.map((device) => device.getBundle())) + .then(() => this.buildSessions(devices)) + .then(() => resolve(devices)) + .catch(_.partial(_converse.log, _, Strophe.LogLevel.ERROR)); }).catch(_.partial(_converse.log, _, Strophe.LogLevel.ERROR)); - }).catch(_.partial(_converse.log, _, Strophe.LogLevel.ERROR)); + }); + }, + + buildSession (device) { + const { _converse } = this.__super__; + const bundle = device.get('bundle'), + address = new libsignal.SignalProtocolAddress(device.get('jid'), device.get('id')), + sessionBuilder = new libsignal.SessionBuilder(_converse.omemo_store, address), + prekey = device.getRandomPreKey(); + + return sessionBuilder.processPreKey({ + 'registrationId': _converse.omemo_store.get('registration_id'), + 'identityKey': _converse.omemo_store.get('identity_keypair'), + 'signedPreKey': { + 'keyId': bundle.signed_prekey.id, // + 'publicKey': u.base64ToArrayBuffer(bundle.signed_prekey.public_key), + 'signature': u.base64ToArrayBuffer(bundle.signed_prekey.signature) + }, + 'preKey': { + 'keyId': prekey.id, // + 'publicKey': u.base64ToArrayBuffer(prekey.key), + } + }) }, buildSessions (devices) { + return Promise.all(devices.map((device) => this.buildSession(device))); + }, + + encryptMessage (plaintext) { + // The client MUST use fresh, randomly generated key/IV pairs + // with AES-128 in Galois/Counter Mode (GCM). + const TAG_LENGTH = 128, + iv = window.crypto.getRandomValues(new window.Uint8Array(16)); + + let key; + return window.crypto.subtle.generateKey({ + 'name': "AES-GCM", + 'length': 256 + }, + true, // extractable + ["encrypt", "decrypt"] // key usages + ).then((result) => { + key = result; + const algo = { + 'name': 'AES-GCM', + 'iv': iv, + 'tagLength': TAG_LENGTH + } + return window.crypto.subtle.encrypt(algo, key, new TextEncoder().encode(plaintext)); + }).then((ciphertext) => { + return window.crypto.subtle.exportKey("jwk", key) + .then((key_str) => { + return Promise.resolve({ + 'key_str': key_str, + 'tag': ciphertext.slice(ciphertext.byteLength - ((TAG_LENGTH + 7) >> 3)), + 'iv': iv + }); + }); + }); + }, + + encryptKey (plaintext, device) { const { _converse } = this.__super__, - device_id = _converse.omemo_store.get('device_id'); + address = new libsignal.SignalProtocolAddress(this.get('jid'), device.get('id')), + sessionCipher = new window.libsignal.SessionCipher(_converse.omemo_store, address); - return Promise.all(_.map(devices, (device) => { - const recipient_id = device['id']; - const address = new libsignal.SignalProtocolAddress(parseInt(recipient_id, 10), device_id); - const sessionBuilder = new libsignal.SessionBuilder(_converse.omemo_store, address); - return sessionBuilder.processPreKey({ - 'registrationId': _converse.omemo_store.get('registration_id'), - 'identityKey': _converse.omemo_store.get('identity_keypair'), - 'signedPreKey': { - 'keyId': '', // , - 'publicKey': '', // , - 'signature': '', // - }, - 'preKey': { - 'keyId': '', // , - 'publicKey': '', // + return sessionCipher.encrypt(plaintext); + }, + + addKeysToMessageStanza (stanza, devices, payloads) { + for (var i in payloads) { + if (Object.prototype.hasOwnProperty.call(payloads, i)) { + const payload = btoa(JSON.stringify(payloads[i])) + const prekey = 3 == parseInt(payloads[i].type, 10) + if (i == payloads.length-1) { + stanza.c('key', {'rid': devices.get('id') }).t(payload) + if (prekey) { + stanza.attrs({'prekey': prekey}); + } + stanza.up().c('iv').t(payloads[0].iv).up().up() + } else { + stanza.c('key', {prekey: prekey, rid: devices.get('id') }).t(payload).up() } - }); - })); + } + } + return Promise.resolve(stanza); }, - encryptMessage (message) { - // TODO: - // const { _converse } = this.__super__; - // const plaintext = message.get('message'); - // const address = new libsignal.SignalProtocolAddress(recipientId, deviceId); - // return new Promise((resolve, reject) => { - // var sessionCipher = new window.libsignal.SessionCipher(_converse.omemo_store, address); - // sessionCipher.encrypt(plaintext).then((ciphertext) => {}); - // }); - }, - - createOMEMOMessageStanza (message, bundles) { + 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"); - return new Promise((resolve, reject) => { - this.encryptMessage(message).then((payload) => { - 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('encrypted').t(payload) - .c('header').t(payload).up() - _.forEach(bundles, (bundle) => { - const prekey = bundle.prekeys[Math.random(bundle.prekeys.length)].textContent; - stanza('key', {'rid': bundle.identity_key}).t(prekey).up() - }); - // TODO: set storage hint urn:xmpp:hints - resolve(stanza); - }).catch(_.partial(_converse.log, _, Strophe.LogLevel.ERROR)); + // 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. + 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('encrypted', {'xmlns': Strophe.NS.OMEMO}) + .c('header', {'sid': _converse.omemo_store.get('device_id')}); + + return this.encryptMessage(message).then((payload) => { + // 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. + + // TODO: need to include own devices here as well (and filter out distrusted devices) + const promises = devices.map(device => this.encryptKey(payload.key_str+payload.tag, device)); + return Promise.all(promises).then((payloads) => this.addKeysToMessageStanza(stanza, devices, payloads)); }); }, sendMessage (attrs) { + const { _converse } = this.__super__; if (this.get('omemo_active')) { const message = this.messages.create(attrs); this.getBundlesAndBuildSessions() - .then((bundles) => this.createOMEMOMessageStanza(message, bundles)) - .then((stanza) => this.sendMessageStanza(stanza)); + .then((devices) => this.createOMEMOMessageStanza(message, devices)) + .then((stanza) => this.sendMessageStanza(stanza)) + .catch(_.partial(_converse.log, _, Strophe.LogLevel.ERROR)); } else { return this.__super__.sendMessage.apply(this, arguments); } - }, + } }, ChatBoxView: { @@ -439,6 +496,12 @@ 'trusted': UNDECIDED }, + getRandomPreKey () { + // XXX: assumes that the bundle has already been fetched + const bundle = this.get('bundle'); + return bundle.prekeys[u.getRandomInt(bundle.prekeys.length)]; + }, + fetchBundleFromServer () { return new Promise((resolve, reject) => { const stanza = $iq({ @@ -467,7 +530,7 @@ * this device, if the information is not at hand already. */ if (this.get('bundle')) { - return Promise.resolve(this.get('bundle').toJSON()); + return Promise.resolve(this.get('bundle').toJSON(), this); } else { return this.fetchBundleFromServer(); } diff --git a/src/utils/core.js b/src/utils/core.js index 6b23157e0..c32302de1 100644 --- a/src/utils/core.js +++ b/src/utils/core.js @@ -883,6 +883,10 @@ return bytes.buffer }; + u.getRandomInt = function (max) { + return Math.floor(Math.random() * Math.floor(max)); + }; + u.getUniqueId = function () { return 'xxxxxxxx-xxxx'.replace(/[x]/g, function(c) { var r = Math.random() * 16 | 0, diff --git a/src/utils/form.js b/src/utils/form.js index 9b2f2fb52..56ba56a28 100644 --- a/src/utils/form.js +++ b/src/utils/form.js @@ -3,7 +3,7 @@ // // This is the utilities module. // -// Copyright (c) 2012-2017, Jan-Carel Brand +// Copyright (c) 2013-2018, Jan-Carel Brand // Licensed under the Mozilla Public License (MPLv2) // /*global define, escape, Jed */ diff --git a/tests/mock.js b/tests/mock.js index 22c884469..b58d91ad2 100644 --- a/tests/mock.js +++ b/tests/mock.js @@ -8,6 +8,22 @@ var $iq = converse.env.$iq; window.libsignal = { + 'SignalProtocolAddress': function (name, device_id) { + this.name = name; + this.deviceId = device_id; + }, + 'SessionCipher': function (storage, remote_address) { + this.remoteAddress = remote_address; + this.storage = storage; + this.encrypt = () => Promise.resolve({ + 'iv': '12345' + }); + }, + 'SessionBuilder': function (storage, remote_address) { + this.processPreKey = function () { + return Promise.resolve(); + } + }, 'KeyHelper': { 'generateIdentityKeyPair': function () { return Promise.resolve({