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 - #2704: Send button doesn't work in a multi-user chat
- #2725: Send new presence status to all connected MUCs - #2725: Send new presence status to all connected MUCs
- #2728: Not sending headers with upload request - #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 - Emit a `change` event when a configuration setting changes
- 3 New configuration settings: - 3 New configuration settings:

View File

@ -56,40 +56,16 @@ export function getStanzaIDs (stanza, original_stanza) {
return attrs; return attrs;
} }
export function getEncryptionAttributes (stanza, _converse) { export function getEncryptionAttributes (stanza) {
const eme_tag = sizzle(`encryption[xmlns="${Strophe.NS.EME}"]`, stanza).pop(); const eme_tag = sizzle(`encryption[xmlns="${Strophe.NS.EME}"]`, stanza).pop();
const namespace = eme_tag?.getAttribute('namespace'); const namespace = eme_tag?.getAttribute('namespace');
const attrs = {}; const attrs = {};
if (namespace) { if (namespace) {
attrs.is_encrypted = true; attrs.is_encrypted = true;
attrs.encryption_namespace = namespace; attrs.encryption_namespace = namespace;
if (namespace !== Strophe.NS.OMEMO) { } else if (sizzle(`encrypted[xmlns="${Strophe.NS.OMEMO}"]`, stanza).pop()) {
// Found an encrypted message, but it's not OMEMO attrs.is_encrypted = true;
return attrs; attrs.encryption_namespace = Strophe.NS.OMEMO;
}
}
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'))
});
} }
return attrs; 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'; import { generateFingerprint } from './utils.js';
export default { export default {
@ -10,6 +10,14 @@ export default {
* @memberOf _converse.api * @memberOf _converse.api
*/ */
'omemo': { '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 * The "bundle" namespace groups methods relevant to the user's
* OMEMO bundle. * OMEMO bundle.
@ -25,6 +33,7 @@ export default {
* @returns {promise} Promise which resolves once we have a result from the server. * @returns {promise} Promise which resolves once we have a result from the server.
*/ */
'generate': async () => { 'generate': async () => {
await api.waitUntil('OMEMOInitialized');
// Remove current device // Remove current device
const devicelist = _converse.devicelists.get(_converse.bare_jid); const devicelist = _converse.devicelists.get(_converse.bare_jid);
const device_id = _converse.omemo_store.get('device_id'); 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 key.startsWith('session' + identifier) ? key : false
); );
const attrs = {}; const attrs = {};
keys.forEach(key => { keys.forEach(key => { attrs[key] = undefined; });
attrs[key] = undefined;
});
this.save(attrs); this.save(attrs);
return Promise.resolve(); 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. * that can be used to build a session with a device.
*/ */
async generateBundle () { async generateBundle () {
@ -234,7 +232,7 @@ const OMEMOStore = Model.extend({
}); });
const signed_prekey = await libsignal.KeyHelper.generateSignedPreKey(identity_keypair, 0); const signed_prekey = await libsignal.KeyHelper.generateSignedPreKey(identity_keypair, 0);
_converse.omemo_store.storeSignedPreKey(signed_prekey); this.storeSignedPreKey(signed_prekey);
bundle['signed_prekey'] = { bundle['signed_prekey'] = {
'id': signed_prekey.keyId, 'id': signed_prekey.keyId,
'public_key': u.arrayBufferToBase64(signed_prekey.keyPair.pubKey), 'public_key': u.arrayBufferToBase64(signed_prekey.keyPair.pubKey),
@ -243,7 +241,7 @@ const OMEMOStore = Model.extend({
const keys = await Promise.all( const keys = await Promise.all(
range(0, _converse.NUM_PREKEYS).map(id => libsignal.KeyHelper.generatePreKey(id)) 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 devicelist = _converse.devicelists.get(_converse.bare_jid);
const device = await devicelist.devices.create( const device = await devicelist.devices.create(
{ 'id': bundle.device_id, 'jid': _converse.bare_jid }, { 'id': bundle.device_id, 'jid': _converse.bare_jid },
@ -262,7 +260,7 @@ const OMEMOStore = Model.extend({
this._setup_promise = new Promise((resolve, reject) => { this._setup_promise = new Promise((resolve, reject) => {
this.fetch({ this.fetch({
'success': () => { 'success': () => {
if (!_converse.omemo_store.get('device_id')) { if (!this.get('device_id')) {
this.generateBundle().then(resolve).catch(reject); this.generateBundle().then(resolve).catch(reject);
} else { } else {
resolve(); resolve();

View File

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