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:
parent
a06d180827
commit
9b1a7c70a3
|
@ -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:
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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();
|
||||
|
|
Loading…
Reference in New Issue