Wait on `OMEMOInitialized` promise...

before parsing message stanza for encryption parameters.

Otherwise we might not know what our own device-id/sid is, and therefore
can't decrypt the incoming message.

Fixes #2733
This commit is contained in:
JC Brand 2021-11-23 22:56:27 +01:00
parent a06d180827
commit 9b1a7c70a3
5 changed files with 74 additions and 61 deletions

View File

@ -16,6 +16,7 @@
- #2704: Send button doesn't work in a multi-user chat
- #2725: Send new presence status to all connected MUCs
- #2728: Not sending headers with upload request
- #2733: OMEMO Messages received while client closed not decrypted
- Emit a `change` event when a configuration setting changes
- 3 New configuration settings:

View File

@ -56,40 +56,16 @@ export function getStanzaIDs (stanza, original_stanza) {
return attrs;
}
export function getEncryptionAttributes (stanza, _converse) {
export function getEncryptionAttributes (stanza) {
const eme_tag = sizzle(`encryption[xmlns="${Strophe.NS.EME}"]`, stanza).pop();
const namespace = eme_tag?.getAttribute('namespace');
const attrs = {};
if (namespace) {
attrs.is_encrypted = true;
attrs.encryption_namespace = namespace;
if (namespace !== Strophe.NS.OMEMO) {
// Found an encrypted message, but it's not OMEMO
return attrs;
}
}
const encrypted = sizzle(`encrypted[xmlns="${Strophe.NS.OMEMO}"]`, stanza).pop();
if (!eme_tag) {
attrs.is_encrypted = !!encrypted;
}
if (!encrypted || api.settings.get('clear_cache_on_logout')) {
return attrs;
}
const header = encrypted.querySelector('header');
attrs.encrypted = { 'device_id': header.getAttribute('sid') };
const device_id = _converse.omemo_store?.get('device_id');
const key = device_id && sizzle(`key[rid="${device_id}"]`, encrypted).pop();
if (key) {
Object.assign(attrs.encrypted, {
'iv': header.querySelector('iv').textContent,
'key': key.textContent,
'payload': encrypted.querySelector('payload')?.textContent || null,
'prekey': ['true', '1'].includes(key.getAttribute('prekey'))
});
} else if (sizzle(`encrypted[xmlns="${Strophe.NS.OMEMO}"]`, stanza).pop()) {
attrs.is_encrypted = true;
attrs.encryption_namespace = Strophe.NS.OMEMO;
}
return attrs;
}

View File

@ -1,4 +1,4 @@
import { _converse } from '@converse/headless/core';
import { _converse, api } from '@converse/headless/core';
import { generateFingerprint } from './utils.js';
export default {
@ -10,6 +10,14 @@ export default {
* @memberOf _converse.api
*/
'omemo': {
/**
* Returns the device ID of the current device.
*/
async getDeviceID () {
await api.waitUntil('OMEMOInitialized');
return _converse.omemo_store.get('device_id');
},
/**
* The "bundle" namespace groups methods relevant to the user's
* OMEMO bundle.
@ -25,6 +33,7 @@ export default {
* @returns {promise} Promise which resolves once we have a result from the server.
*/
'generate': async () => {
await api.waitUntil('OMEMOInitialized');
// Remove current device
const devicelist = _converse.devicelists.get(_converse.bare_jid);
const device_id = _converse.omemo_store.get('device_id');

View File

@ -154,9 +154,7 @@ const OMEMOStore = Model.extend({
key.startsWith('session' + identifier) ? key : false
);
const attrs = {};
keys.forEach(key => {
attrs[key] = undefined;
});
keys.forEach(key => { attrs[key] = undefined; });
this.save(attrs);
return Promise.resolve();
},
@ -208,7 +206,7 @@ const OMEMOStore = Model.extend({
},
/**
* Generate a the data used by the X3DH key agreement protocol
* Generate the data used by the X3DH key agreement protocol
* that can be used to build a session with a device.
*/
async generateBundle () {
@ -234,7 +232,7 @@ const OMEMOStore = Model.extend({
});
const signed_prekey = await libsignal.KeyHelper.generateSignedPreKey(identity_keypair, 0);
_converse.omemo_store.storeSignedPreKey(signed_prekey);
this.storeSignedPreKey(signed_prekey);
bundle['signed_prekey'] = {
'id': signed_prekey.keyId,
'public_key': u.arrayBufferToBase64(signed_prekey.keyPair.pubKey),
@ -243,7 +241,7 @@ const OMEMOStore = Model.extend({
const keys = await Promise.all(
range(0, _converse.NUM_PREKEYS).map(id => libsignal.KeyHelper.generatePreKey(id))
);
keys.forEach(k => _converse.omemo_store.storePreKey(k.keyId, k.keyPair));
keys.forEach(k => this.storePreKey(k.keyId, k.keyPair));
const devicelist = _converse.devicelists.get(_converse.bare_jid);
const device = await devicelist.devices.create(
{ 'id': bundle.device_id, 'jid': _converse.bare_jid },
@ -262,7 +260,7 @@ const OMEMOStore = Model.extend({
this._setup_promise = new Promise((resolve, reject) => {
this.fetch({
'success': () => {
if (!_converse.omemo_store.get('device_id')) {
if (!this.get('device_id')) {
this.generateBundle().then(resolve).catch(reject);
} else {
resolve();

View File

@ -203,27 +203,48 @@ export function handleEncryptedFiles (richtext) {
richtext.addAnnotations((text, offset) => addEncryptedFiles(text, offset, richtext));
}
export function parseEncryptedMessage (stanza, attrs) {
if (attrs.is_encrypted) {
if (!attrs.encrypted.key) {
return Object.assign(attrs, {
'error_condition': 'not-encrypted-for-this-device',
'error_type': 'Decryption',
'is_ephemeral': true,
'is_error': true,
'type': 'error'
});
} else {
// https://xmpp.org/extensions/xep-0384.html#usecases-receiving
if (attrs.encrypted.prekey === true) {
return decryptPrekeyWhisperMessage(attrs);
} else {
return decryptWhisperMessage(attrs);
}
}
} else {
/**
* Hook handler for { @link parseMessage } and { @link parseMUCMessage }, which
* parses the passed in `message` stanza for OMEMO attributes and then sets
* them on the attrs object.
* @param { XMLElement } stanza - The message stanza
* @param { (MUCMessageAttributes|MessageAttributes) } attrs
* @returns (MUCMessageAttributes|MessageAttributes)
*/
export async function parseEncryptedMessage (stanza, attrs) {
if (api.settings.get('clear_cache_on_logout') ||
!attrs.is_encrypted ||
attrs.encryption_namespace !== Strophe.NS.OMEMO) {
return attrs;
}
const encrypted_el = sizzle(`encrypted[xmlns="${Strophe.NS.OMEMO}"]`, stanza).pop();
const header = encrypted_el.querySelector('header');
attrs.encrypted = { 'device_id': header.getAttribute('sid') };
const device_id = await api.omemo?.getDeviceID();
const key = device_id && sizzle(`key[rid="${device_id}"]`, encrypted_el).pop();
if (key) {
Object.assign(attrs.encrypted, {
'iv': header.querySelector('iv').textContent,
'key': key.textContent,
'payload': encrypted_el.querySelector('payload')?.textContent || null,
'prekey': ['true', '1'].includes(key.getAttribute('prekey'))
});
} else {
return Object.assign(attrs, {
'error_condition': 'not-encrypted-for-this-device',
'error_type': 'Decryption',
'is_ephemeral': true,
'is_error': true,
'type': 'error'
});
}
// https://xmpp.org/extensions/xep-0384.html#usecases-receiving
if (attrs.encrypted.prekey === true) {
return decryptPrekeyWhisperMessage(attrs);
} else {
return decryptWhisperMessage(attrs);
}
}
export function onChatBoxesInitialized () {
@ -499,7 +520,7 @@ function updateBundleFromStanza (stanza) {
const jid = stanza.getAttribute('from');
const bundle_el = sizzle(`item > bundle`, items_el).pop();
const devicelist = _converse.devicelists.getDeviceList(jid);
const device = devicelist.devices.get(device_id) || devicelist.devices.create({ 'id': device_id, 'jid': jid });
const device = devicelist.devices.get(device_id) || devicelist.devices.create({ 'id': device_id, jid });
device.save({ 'bundle': parseBundle(bundle_el) });
}
@ -566,11 +587,21 @@ export function restoreOMEMOSession () {
}
function fetchDeviceLists () {
return new Promise((success, error) => _converse.devicelists.fetch({ success, 'error': (m, e) => error(e) }));
_converse.devicelists = new _converse.DeviceLists();
const id = `converse.devicelists-${_converse.bare_jid}`;
initStorage(_converse.devicelists, id);
return new Promise(resolve => {
_converse.devicelists.fetch({
'success': resolve,
'error': (m, e) => {
log.error(e);
resolve();
}
})
});
}
async function fetchOwnDevices () {
await fetchDeviceLists();
let own_devicelist = _converse.devicelists.get(_converse.bare_jid);
if (own_devicelist) {
own_devicelist.fetchDevices();
@ -585,10 +616,8 @@ export async function initOMEMO () {
log.warn('Not initializing OMEMO, since this browser is not trusted or clear_cache_on_logout is set to true');
return;
}
_converse.devicelists = new _converse.DeviceLists();
const id = `converse.devicelists-${_converse.bare_jid}`;
initStorage(_converse.devicelists, id);
try {
await fetchDeviceLists();
await fetchOwnDevices();
await restoreOMEMOSession();
await _converse.omemo_store.publishBundle();