Add method to generate missing prekeys
When receiving a PreKeySignalMessage, then a prekey has been chosen and should now be removed from the list of available prekeys in the bundle, so that a different device doesn't choose it as well. AFAICT, libsignal removes the prekey, so it's then up to us to regenerate it and republish our bundle. updates #497
This commit is contained in:
parent
3d015c787f
commit
15a4bcd11e
174
spec/omemo.js
174
spec/omemo.js
@ -37,6 +37,40 @@
|
|||||||
).pop(), 'nodeTree');
|
).pop(), 'nodeTree');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function initializedOMEMO (_converse) {
|
||||||
|
return test_utils.waitUntil(() => deviceListFetched(_converse, _converse.bare_jid))
|
||||||
|
.then(iq_stanza => {
|
||||||
|
const stanza = $iq({
|
||||||
|
'from': _converse.bare_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.devicelist"})
|
||||||
|
.c('item', {'xmlns': "http://jabber.org/protocol/pubsub"}) // TODO: must have an id attribute
|
||||||
|
.c('list', {'xmlns': "eu.siacs.conversations.axolotl"})
|
||||||
|
.c('device', {'id': '482886413b977930064a5888b92134fe'});
|
||||||
|
_converse.connection._dataRecv(test_utils.createRequest(stanza));
|
||||||
|
return test_utils.waitUntil(() => ownDeviceHasBeenPublished(_converse))
|
||||||
|
}).then(iq_stanza => {
|
||||||
|
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(() => bundleHasBeenPublished(_converse))
|
||||||
|
}).then(iq_stanza => {
|
||||||
|
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 _converse.api.waitUntil('OMEMOInitialized');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
describe("The OMEMO module", function() {
|
describe("The OMEMO module", function() {
|
||||||
|
|
||||||
@ -73,43 +107,10 @@
|
|||||||
_converse.emit('rosterContactsFetched');
|
_converse.emit('rosterContactsFetched');
|
||||||
const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@localhost';
|
const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@localhost';
|
||||||
|
|
||||||
test_utils.waitUntil(() => deviceListFetched(_converse, _converse.bare_jid))
|
return test_utils.waitUntil(() => initializedOMEMO(_converse))
|
||||||
|
.then(() => test_utils.openChatBoxFor(_converse, contact_jid))
|
||||||
|
.then(() => test_utils.waitUntil(() => deviceListFetched(_converse, contact_jid)))
|
||||||
.then(iq_stanza => {
|
.then(iq_stanza => {
|
||||||
const stanza = $iq({
|
|
||||||
'from': _converse.bare_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.devicelist"})
|
|
||||||
.c('item', {'xmlns': "http://jabber.org/protocol/pubsub"}) // TODO: must have an id attribute
|
|
||||||
.c('list', {'xmlns': "eu.siacs.conversations.axolotl"})
|
|
||||||
.c('device', {'id': '482886413b977930064a5888b92134fe'});
|
|
||||||
_converse.connection._dataRecv(test_utils.createRequest(stanza));
|
|
||||||
|
|
||||||
return test_utils.waitUntil(() => ownDeviceHasBeenPublished(_converse))
|
|
||||||
}).then(iq_stanza => {
|
|
||||||
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(() => bundleHasBeenPublished(_converse))
|
|
||||||
}).then(iq_stanza => {
|
|
||||||
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 _converse.api.waitUntil('OMEMOInitialized');
|
|
||||||
}).then(() => test_utils.openChatBoxFor(_converse, contact_jid))
|
|
||||||
.then(() => test_utils.waitUntil(() => deviceListFetched(_converse, contact_jid)))
|
|
||||||
.then(iq_stanza => {
|
|
||||||
const stanza = $iq({
|
const stanza = $iq({
|
||||||
'from': contact_jid,
|
'from': contact_jid,
|
||||||
'id': iq_stanza.getAttribute('id'),
|
'id': iq_stanza.getAttribute('id'),
|
||||||
@ -158,7 +159,7 @@
|
|||||||
_converse.connection._dataRecv(test_utils.createRequest(stanza));
|
_converse.connection._dataRecv(test_utils.createRequest(stanza));
|
||||||
|
|
||||||
return test_utils.waitUntil(() => bundleFetched(_converse, _converse.bare_jid, '482886413b977930064a5888b92134fe'));
|
return test_utils.waitUntil(() => bundleFetched(_converse, _converse.bare_jid, '482886413b977930064a5888b92134fe'));
|
||||||
}).then((iq_stanza) => {
|
}).then(iq_stanza => {
|
||||||
const stanza = $iq({
|
const stanza = $iq({
|
||||||
'from': _converse.bare_jid,
|
'from': _converse.bare_jid,
|
||||||
'id': iq_stanza.getAttribute('id'),
|
'id': iq_stanza.getAttribute('id'),
|
||||||
@ -229,6 +230,103 @@
|
|||||||
}).catch(_.partial(_converse.log, _, Strophe.LogLevel.FATAL))
|
}).catch(_.partial(_converse.log, _, Strophe.LogLevel.FATAL))
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|
||||||
|
it("can receive a PreKeySignalMessage",
|
||||||
|
mock.initConverseWithPromises(
|
||||||
|
null, ['rosterGroupsFetched', 'chatBoxesFetched'], {},
|
||||||
|
function (done, _converse) {
|
||||||
|
|
||||||
|
_converse.NUM_PREKEYS = 5; // Restrict to 5, otherwise the resulting stanza is too large to easily test
|
||||||
|
let view, sent_stanza;
|
||||||
|
test_utils.createContacts(_converse, 'current', 1);
|
||||||
|
_converse.emit('rosterContactsFetched');
|
||||||
|
const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@localhost';
|
||||||
|
|
||||||
|
return test_utils.waitUntil(() => initializedOMEMO(_converse))
|
||||||
|
.then(() => _converse.ChatBox.prototype.encryptMessage('This is an encrypted message from the contact'))
|
||||||
|
.then(obj => {
|
||||||
|
// XXX: Normally the key will be encrypted via libsignal.
|
||||||
|
// However, we're mocking libsignal in the tests, so we include
|
||||||
|
// it as plaintext in the message.
|
||||||
|
const key = btoa(JSON.stringify({
|
||||||
|
'type': 1,
|
||||||
|
'body': obj.key_and_tag,
|
||||||
|
'registrationId': '1337'
|
||||||
|
}));
|
||||||
|
const stanza = $msg({
|
||||||
|
'from': contact_jid,
|
||||||
|
'to': _converse.connection.jid,
|
||||||
|
'type': 'chat',
|
||||||
|
'id': 'qwerty'
|
||||||
|
}).c('body').t('This is a fallback message').up()
|
||||||
|
.c('encrypted', {'xmlns': Strophe.NS.OMEMO})
|
||||||
|
.c('header', {'sid': '555'})
|
||||||
|
.c('key', {'prekey': 'true', 'rid': _converse.omemo_store.get('device_id')}).t(key).up()
|
||||||
|
.c('iv').t(obj.iv)
|
||||||
|
.up().up()
|
||||||
|
.c('payload').t(obj.payload);
|
||||||
|
|
||||||
|
const generateMissingPreKeys = _converse.omemo_store.generateMissingPreKeys;
|
||||||
|
spyOn(_converse.omemo_store, 'generateMissingPreKeys').and.callFake(() => {
|
||||||
|
// Since it's difficult to override
|
||||||
|
// decryptPreKeyWhisperMessage, where a prekey will be
|
||||||
|
// removed from the store, we do it here, before the
|
||||||
|
// missing prekeys are generated.
|
||||||
|
_converse.omemo_store.removePreKey(1);
|
||||||
|
return generateMissingPreKeys.apply(_converse.omemo_store, arguments);
|
||||||
|
});
|
||||||
|
_converse.connection._dataRecv(test_utils.createRequest(stanza));
|
||||||
|
return test_utils.waitUntil(() => _converse.chatboxviews.get(contact_jid))
|
||||||
|
}).then(iq_stanza => deviceListFetched(_converse, contact_jid))
|
||||||
|
.then(iq_stanza => {
|
||||||
|
const stanza = $iq({
|
||||||
|
'from': contact_jid,
|
||||||
|
'id': iq_stanza.getAttribute('id'),
|
||||||
|
'to': _converse.connection.jid,
|
||||||
|
'type': 'result',
|
||||||
|
}).c('pubsub', {'xmlns': "http://jabber.org/protocol/pubsub"})
|
||||||
|
.c('items', {'node': "eu.siacs.conversations.axolotl.devicelist"})
|
||||||
|
.c('item', {'xmlns': "http://jabber.org/protocol/pubsub"}) // TODO: must have an id attribute
|
||||||
|
.c('list', {'xmlns': "eu.siacs.conversations.axolotl"})
|
||||||
|
.c('device', {'id': '555'});
|
||||||
|
|
||||||
|
// XXX: the bundle gets published twice, we want to make sure
|
||||||
|
// that we wait for the 2nd, so we clear all the already sent
|
||||||
|
// stanzas.
|
||||||
|
_converse.connection.IQ_stanzas = [];
|
||||||
|
_converse.connection._dataRecv(test_utils.createRequest(stanza));
|
||||||
|
return test_utils.waitUntil(() => _converse.omemo_store);
|
||||||
|
}).then(() => test_utils.waitUntil(() => bundleHasBeenPublished(_converse)))
|
||||||
|
.then(iq_stanza => {
|
||||||
|
expect(iq_stanza.outerHTML).toBe(
|
||||||
|
`<iq from="dummy@localhost" type="set" xmlns="jabber:client" id="${iq_stanza.getAttribute('id')}">`+
|
||||||
|
`<pubsub xmlns="http://jabber.org/protocol/pubsub">`+
|
||||||
|
`<publish node="eu.siacs.conversations.axolotl.bundles:123456789">`+
|
||||||
|
`<item>`+
|
||||||
|
`<bundle xmlns="eu.siacs.conversations.axolotl">`+
|
||||||
|
`<signedPreKeyPublic signedPreKeyId="0">${btoa('1234')}</signedPreKeyPublic>`+
|
||||||
|
`<signedPreKeySignature>${btoa('11112222333344445555')}</signedPreKeySignature>`+
|
||||||
|
`<identityKey>${btoa('1234')}</identityKey>`+
|
||||||
|
`<prekeys>`+
|
||||||
|
`<preKeyPublic preKeyId="0">${btoa('1234')}</preKeyPublic>`+
|
||||||
|
`<preKeyPublic preKeyId="1">${btoa('1234')}</preKeyPublic>`+
|
||||||
|
`<preKeyPublic preKeyId="2">${btoa('1234')}</preKeyPublic>`+
|
||||||
|
`<preKeyPublic preKeyId="3">${btoa('1234')}</preKeyPublic>`+
|
||||||
|
`<preKeyPublic preKeyId="4">${btoa('1234')}</preKeyPublic>`+
|
||||||
|
`</prekeys>`+
|
||||||
|
`</bundle>`+
|
||||||
|
`</item>`+
|
||||||
|
`</publish>`+
|
||||||
|
`</pubsub>`+
|
||||||
|
`</iq>`)
|
||||||
|
const own_device = _converse.devicelists.get(_converse.bare_jid).devices.get(_converse.omemo_store.get('device_id'));
|
||||||
|
expect(own_device.get('bundle').prekeys.length).toBe(5);
|
||||||
|
expect(_converse.omemo_store.generateMissingPreKeys).toHaveBeenCalled();
|
||||||
|
done();
|
||||||
|
}).catch(_.partial(_converse.log, _, Strophe.LogLevel.FATAL))
|
||||||
|
}));
|
||||||
|
|
||||||
|
|
||||||
it("will add processing hints to sent out encrypted <message> stanzas",
|
it("will add processing hints to sent out encrypted <message> stanzas",
|
||||||
mock.initConverseWithPromises(
|
mock.initConverseWithPromises(
|
||||||
null, ['rosterGroupsFetched'], {},
|
null, ['rosterGroupsFetched'], {},
|
||||||
@ -237,6 +335,7 @@
|
|||||||
done();
|
done();
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|
||||||
it("updates device lists based on PEP messages",
|
it("updates device lists based on PEP messages",
|
||||||
mock.initConverseWithPromises(
|
mock.initConverseWithPromises(
|
||||||
null, ['rosterGroupsFetched'], {},
|
null, ['rosterGroupsFetched'], {},
|
||||||
@ -388,6 +487,7 @@
|
|||||||
}).catch(_.partial(_converse.log, _, Strophe.LogLevel.FATAL))
|
}).catch(_.partial(_converse.log, _, Strophe.LogLevel.FATAL))
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|
||||||
it("updates device bundles based on PEP messages",
|
it("updates device bundles based on PEP messages",
|
||||||
mock.initConverseWithPromises(
|
mock.initConverseWithPromises(
|
||||||
null, ['rosterGroupsFetched'], {},
|
null, ['rosterGroupsFetched'], {},
|
||||||
|
@ -157,8 +157,8 @@
|
|||||||
return _converse.getDevicesForContact(this.get('jid'))
|
return _converse.getDevicesForContact(this.get('jid'))
|
||||||
.then((their_devices) => {
|
.then((their_devices) => {
|
||||||
const device_id = _converse.omemo_store.get('device_id'),
|
const device_id = _converse.omemo_store.get('device_id'),
|
||||||
devicelist = _converse.devicelists.get(_converse.bare_jid),
|
devicelist = _converse.devicelists.get(_converse.bare_jid),
|
||||||
own_devices = devicelist.devices.filter(device => device.get('id') !== device_id);
|
own_devices = devicelist.devices.filter(device => device.get('id') !== device_id);
|
||||||
devices = _.concat(own_devices, their_devices.models);
|
devices = _.concat(own_devices, their_devices.models);
|
||||||
return Promise.all(devices.map(device => device.getBundle()));
|
return Promise.all(devices.map(device => device.getBundle()));
|
||||||
}).then(() => this.buildSessions(devices))
|
}).then(() => this.buildSessions(devices))
|
||||||
@ -240,32 +240,30 @@
|
|||||||
|
|
||||||
decrypt (attrs) {
|
decrypt (attrs) {
|
||||||
const { _converse } = this.__super__,
|
const { _converse } = this.__super__,
|
||||||
devicelist = _converse.devicelists.get(attrs.from),
|
address = new libsignal.SignalProtocolAddress(attrs.from, parseInt(attrs.encrypted.device_id, 10)),
|
||||||
device = devicelist.devices.get(attrs.encrypted.device_id),
|
|
||||||
address = new libsignal.SignalProtocolAddress(
|
|
||||||
attrs.from,
|
|
||||||
parseInt(attrs.encrypted.device_id, 10)
|
|
||||||
),
|
|
||||||
session_cipher = new window.libsignal.SessionCipher(_converse.omemo_store, address),
|
session_cipher = new window.libsignal.SessionCipher(_converse.omemo_store, address),
|
||||||
libsignal_payload = JSON.parse(atob(attrs.encrypted.key));
|
libsignal_payload = JSON.parse(atob(attrs.encrypted.key));
|
||||||
|
|
||||||
|
// https://xmpp.org/extensions/xep-0384.html#usecases-receiving
|
||||||
if (attrs.encrypted.prekey === 'true') {
|
if (attrs.encrypted.prekey === 'true') {
|
||||||
// If this is the case, a new session is built from this received element. The client
|
let plaintext;
|
||||||
// SHOULD then republish their bundle information, replacing the used PreKey, such
|
|
||||||
// that it won't be used again by a different client. If the client already has a session
|
|
||||||
// with the sender's device, it MUST replace this session with the newly built session.
|
|
||||||
// The client MUST delete the private key belonging to the PreKey after use.
|
|
||||||
return session_cipher.decryptPreKeyWhisperMessage(libsignal_payload.body, 'binary')
|
return session_cipher.decryptPreKeyWhisperMessage(libsignal_payload.body, 'binary')
|
||||||
.then(key_and_tag => {
|
.then(key_and_tag => {
|
||||||
const aes_data = this.getKeyAndTag(u.arrayBufferToString(key_and_tag));
|
if (attrs.encrypted.payload) {
|
||||||
return this.decryptMessage(_.extend(attrs.encrypted, {'key': aes_data.key, 'tag': aes_data.tag}));
|
const aes_data = this.getKeyAndTag(u.arrayBufferToString(key_and_tag));
|
||||||
}).then(plaintext => {
|
return this.decryptMessage(_.extend(attrs.encrypted, {'key': aes_data.key, 'tag': aes_data.tag}));
|
||||||
// TODO the prekey should now have been removed.
|
}
|
||||||
// Double-check that this is the case and then
|
return Promise.resolve();
|
||||||
// generate a new key to replace it, before
|
}).then(pt => {
|
||||||
// republishing.
|
plaintext = pt;
|
||||||
_converse.omemo_store.publishBundle()
|
return _converse.omemo_store.generateMissingPreKeys();
|
||||||
return _.extend(attrs, {'plaintext': plaintext});
|
}).then(() => _converse.omemo_store.publishBundle())
|
||||||
|
.then(() => {
|
||||||
|
if (plaintext) {
|
||||||
|
return _.extend(attrs, {'plaintext': plaintext});
|
||||||
|
} else {
|
||||||
|
return _.extend(attrs, {'is_only_key': true});
|
||||||
|
}
|
||||||
}).catch((e) => {
|
}).catch((e) => {
|
||||||
this.reportDecryptionError(e);
|
this.reportDecryptionError(e);
|
||||||
return attrs;
|
return attrs;
|
||||||
@ -446,6 +444,13 @@
|
|||||||
'click .toggle-omemo': 'toggleOMEMO'
|
'click .toggle-omemo': 'toggleOMEMO'
|
||||||
},
|
},
|
||||||
|
|
||||||
|
showMessage (message) {
|
||||||
|
// We don't show a message if it's only keying material
|
||||||
|
if (!message.get('is_only_key')) {
|
||||||
|
return this.__super__.showMessage.apply(this, arguments);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
renderOMEMOToolbarButton () {
|
renderOMEMOToolbarButton () {
|
||||||
const { _converse } = this.__super__,
|
const { _converse } = this.__super__,
|
||||||
{ __ } = _converse;
|
{ __ } = _converse;
|
||||||
@ -497,6 +502,10 @@
|
|||||||
.then(devices => Promise.all(devices.map(d => generateFingerprint(d))))
|
.then(devices => Promise.all(devices.map(d => generateFingerprint(d))))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_converse.getDeviceForContact = function (jid, device_id) {
|
||||||
|
return _converse.getDevicesForContact(jid).then(devices => devices.get(device_id));
|
||||||
|
}
|
||||||
|
|
||||||
_converse.getDevicesForContact = function (jid) {
|
_converse.getDevicesForContact = function (jid) {
|
||||||
let devicelist;
|
let devicelist;
|
||||||
return _converse.api.waitUntil('OMEMOInitialized')
|
return _converse.api.waitUntil('OMEMOInitialized')
|
||||||
@ -705,6 +714,26 @@
|
|||||||
return _converse.api.sendIQ(stanza);
|
return _converse.api.sendIQ(stanza);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
generateMissingPreKeys () {
|
||||||
|
const current_keys = this.getPreKeys(),
|
||||||
|
missing_keys = _.difference(_.invokeMap(_.range(0, _converse.NUM_PREKEYS), Number.prototype.toString), _.keys(current_keys));
|
||||||
|
|
||||||
|
if (missing_keys.length < 1) {
|
||||||
|
_converse.log("No missing prekeys to generate for our own device", Strophe.LogLevel.WARN);
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
return Promise.all(_.map(missing_keys, id => libsignal.KeyHelper.generatePreKey(parseInt(id, 10))))
|
||||||
|
.then(keys => {
|
||||||
|
_.forEach(keys, k => this.storePreKey(k.keyId, k.keyPair));
|
||||||
|
const marshalled_keys = _.map(this.getPreKeys(), k => ({'id': k.keyId, 'key': u.arrayBufferToBase64(k.pubKey)})),
|
||||||
|
devicelist = _converse.devicelists.get(_converse.bare_jid),
|
||||||
|
device = devicelist.devices.get(this.get('device_id'));
|
||||||
|
|
||||||
|
return device.getBundle()
|
||||||
|
.then(bundle => device.save('bundle', _.extend(bundle, {'prekeys': marshalled_keys})));
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
generateBundle () {
|
generateBundle () {
|
||||||
/* The first thing that needs to happen if a client wants to
|
/* The first thing that needs to happen if a client wants to
|
||||||
* start using OMEMO is they need to generate an IdentityKey
|
* start using OMEMO is they need to generate an IdentityKey
|
||||||
|
@ -21,6 +21,11 @@
|
|||||||
'body': 'c1ph3R73X7',
|
'body': 'c1ph3R73X7',
|
||||||
'registrationId': '1337'
|
'registrationId': '1337'
|
||||||
});
|
});
|
||||||
|
this.decryptPreKeyWhisperMessage = (key_and_tag) => {
|
||||||
|
// TODO: remove the prekey
|
||||||
|
return Promise.resolve(u.stringToArrayBuffer(key_and_tag));
|
||||||
|
};
|
||||||
|
|
||||||
this.decryptWhisperMessage = (key_and_tag) => {
|
this.decryptWhisperMessage = (key_and_tag) => {
|
||||||
return Promise.resolve(u.stringToArrayBuffer(key_and_tag));
|
return Promise.resolve(u.stringToArrayBuffer(key_and_tag));
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user