diff --git a/CHANGES.md b/CHANGES.md index 6da0b2402..b02c2618c 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -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: diff --git a/src/headless/shared/parsers.js b/src/headless/shared/parsers.js index f30e4fe51..26990f311 100644 --- a/src/headless/shared/parsers.js +++ b/src/headless/shared/parsers.js @@ -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; } diff --git a/src/plugins/omemo/api.js b/src/plugins/omemo/api.js index 694f5d3cb..0fc426417 100644 --- a/src/plugins/omemo/api.js +++ b/src/plugins/omemo/api.js @@ -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'); diff --git a/src/plugins/omemo/store.js b/src/plugins/omemo/store.js index 48944ce32..05c18a80a 100644 --- a/src/plugins/omemo/store.js +++ b/src/plugins/omemo/store.js @@ -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(); diff --git a/src/plugins/omemo/utils.js b/src/plugins/omemo/utils.js index 3a4c8fde2..592e8c807 100644 --- a/src/plugins/omemo/utils.js +++ b/src/plugins/omemo/utils.js @@ -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();