Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] Split OMEMO plugin into view and headless components #3117

Draft
wants to merge 6 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/source/plugin_development.rst
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ modular and self-contained way, without having to change other files.

This ensures that plugins are fully optional (one of the design goals of
Converse) and can be removed from the main build without breaking the app.
For example, the ``converse-omemo``,
For example, the ``converse-omemo-views``,
``converse-rosterview``, ``converse-dragresize``, ``converse-minimize``,
``converse-muc`` and ``converse-muc-views`` plugins can all be removed from the
build without breaking the app.
Expand Down
2 changes: 1 addition & 1 deletion src/converse.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import "./plugins/muc-views/index.js"; // Views related to MUC
import "./plugins/minimize/index.js"; // Allows chat boxes to be minimized
import "./plugins/notifications/index.js";
import "./plugins/profile/index.js";
import "./plugins/omemo/index.js";
import "./plugins/omemo-views/index.js";
import "./plugins/push/index.js"; // XEP-0357 Push Notifications
import "./plugins/register/index.js"; // XEP-0077 In-band registration
import "./plugins/roomslist/index.js"; // Show currently open chat rooms
Expand Down
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
49 changes: 16 additions & 33 deletions src/plugins/omemo/index.js → src/headless/plugins/omemo/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,33 +2,29 @@
* @copyright The Converse.js contributors
* @license Mozilla Public License (MPLv2)
*/
import './fingerprints.js';
import './profile.js';
import 'shared/modals/user-details.js';
import ConverseMixins from './mixins/converse.js';
import omemo_api from './api.js';
import Device from './device.js';
import DeviceList from './devicelist.js';
import DeviceLists from './devicelists.js';
import Devices from './devices.js';
import OMEMOStore from './store.js';
import log from '@converse/headless/log';
import omemo_api from './api.js';
import { _converse, api, converse } from '@converse/headless/core';

import {
createOMEMOMessageStanza,
encryptFile,
getOMEMOToolbarButton,
getOutgoingMessageAttributes,
handleEncryptedFiles,
handleMessageSendError,
initOMEMO,
omemo,
onChatBoxesInitialized,
onChatInitialized,
onChatBoxInitialized,
parseEncryptedMessage,
registerPEPPushHandler,
registerPEPPushHandler
setEncryptedFileURL,
} from './utils.js';
} from './utils.js'

const { Strophe } = converse.env;

Expand All @@ -42,18 +38,18 @@ Strophe.addNamespace('OMEMO_BUNDLES', Strophe.NS.OMEMO + '.bundles');

converse.plugins.add('converse-omemo', {
enabled (_converse) {
return (
window.libsignal &&
_converse.config.get('trusted') &&
!api.settings.get('clear_cache_on_logout') &&
!_converse.api.settings.get('blacklisted_plugins').includes('converse-omemo')
);
return (
window.libsignal &&
_converse.config.get('trusted') &&
!api.settings.get('clear_cache_on_logout') &&
!_converse.api.settings.get('blacklisted-plugins').includes('converse-omemo-views')
);
},

dependencies: ['converse-chatview', 'converse-pubsub', 'converse-profile'],
dependencies: ['converse-chat', 'converse-pubsub'],

initialize () {
api.settings.extend({ 'omemo_default': false });
initialize() {
api.settings.extend({ 'omemo-default': false });
api.promises.add(['OMEMOInitialized']);

_converse.NUM_PREKEYS = 100; // Set here so that tests can override
Expand All @@ -70,6 +66,8 @@ converse.plugins.add('converse-omemo', {
/******************** Event Handlers ********************/
api.waitUntil('chatBoxesInitialized').then(onChatBoxesInitialized);

api.listen.on('chatBoxInitialized', onChatBoxInitialized);

api.listen.on('getOutgoingMessageAttributes', getOutgoingMessageAttributes);

api.listen.on('createMessageStanza', async (chat, data) => {
Expand All @@ -87,26 +85,11 @@ converse.plugins.add('converse-omemo', {
api.listen.on('parseMessage', parseEncryptedMessage);
api.listen.on('parseMUCMessage', parseEncryptedMessage);

api.listen.on('chatBoxViewInitialized', onChatInitialized);
api.listen.on('chatRoomViewInitialized', onChatInitialized);

api.listen.on('connected', registerPEPPushHandler);
api.listen.on('getToolbarButtons', getOMEMOToolbarButton);

api.listen.on('statusInitialized', initOMEMO);
api.listen.on('addClientFeatures', () => api.disco.own.features.add(`${Strophe.NS.OMEMO_DEVICELIST}+notify`));

api.listen.on('afterMessageBodyTransformed', handleEncryptedFiles);

api.listen.on('userDetailsModalInitialized', contact => {
const jid = contact.get('jid');
_converse.generateFingerprints(jid).catch(e => log.error(e));
});

api.listen.on('profileModalInitialized', () => {
_converse.generateFingerprints(_converse.bare_jid).catch(e => log.error(e));
});

api.listen.on('clearSession', () => {
delete _converse.omemo_store
if (_converse.shouldClearCache() && _converse.devicelists) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import log from '@converse/headless/log';
import range from 'lodash-es/range';
import omit from 'lodash-es/omit';
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like this file can also be moved to the headless plugin. Generally speaking, all models should go into headless, and OMEMOStore is a model.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, okay! All the tests passed but when I tried running in the browser it's not actually running. So let me debug that and then I'll get to this 🙂

import { Model } from '@converse/skeletor/src/model.js';
import { generateDeviceID } from './utils.js';
import { generateDeviceID } from '@converse/headless/plugins/omemo/utils.js';
import { _converse, api, converse } from '@converse/headless/core';

const { Strophe, $build, u } = converse.env;
Expand Down
155 changes: 24 additions & 131 deletions src/plugins/omemo/utils.js → src/headless/plugins/omemo/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,12 @@
import concat from 'lodash-es/concat';
import difference from 'lodash-es/difference';
import log from '@converse/headless/log';
import tpl_audio from 'templates/audio.js';
import tpl_file from 'templates/file.js';
import tpl_image from 'templates/image.js';
import tpl_video from 'templates/video.js';
import { KEY_ALGO, UNTRUSTED, TAG_LENGTH } from './consts.js';
import { MIMETYPES_MAP } from 'utils/file.js';
import { __ } from 'i18n';
import { _converse, converse, api } from '@converse/headless/core';
import { html } from 'lit';
import { getURI } from '@converse/headless/utils/url.js';
import { initStorage } from '@converse/headless/utils/storage.js';
import { isError } from '@converse/headless/utils/core.js';
import { isAudioURL, isImageURL, isVideoURL, getURI } from '@converse/headless/utils/url.js';
import { until } from 'lit/directives/until.js';
import {
appendArrayBuffer,
arrayBufferToBase64,
Expand All @@ -25,7 +18,7 @@ import {
stringToArrayBuffer
} from '@converse/headless/utils/arraybuffer.js';

const { Strophe, URI, sizzle, u } = converse.env;
const { Strophe, sizzle, u } = converse.env;

export function formatFingerprint (fp) {
fp = fp.replace(/^05/, '');
Expand Down Expand Up @@ -197,36 +190,26 @@ async function getAndDecryptFile (uri) {
}
}

function getTemplateForObjectURL (uri, obj_url, richtext) {
if (isError(obj_url)) {
return html`<p class="error">${obj_url.message}</p>`;
}

const file_url = uri.toString();
if (isImageURL(file_url)) {
return tpl_image({
'src': obj_url,
'onClick': richtext.onImgClick,
'onLoad': richtext.onImgLoad
});
} else if (isAudioURL(file_url)) {
return tpl_audio(obj_url);
} else if (isVideoURL(file_url)) {
return tpl_video(obj_url);
} else {
return tpl_file(obj_url, uri.filename());
}

export const omemo = {
decryptMessage,
encryptMessage,
formatFingerprint
}

function addEncryptedFiles(text, offset, richtext) {
export function processEncryptedFiles (text) {
const objs = [];
try {
const parse_options = { 'start': /\b(aesgcm:\/\/)/gi };
URI.withinString(
text,
(url, start, end) => {
objs.push({ url, start, end });
const uri = getURI(text.slice(o.start, o.end));
objs.push({
uri,
start,
end,
obj_url: getAndDecryptFile(uri); // this is a promise

Check notice

Code scanning / CodeQL

Syntax error

Error: Unexpected token
});
return url;
},
parse_options
Expand All @@ -235,21 +218,8 @@ function addEncryptedFiles(text, offset, richtext) {
log.debug(error);
return;
}
objs.forEach(o => {
const uri = getURI(text.slice(o.start, o.end));
const promise = getAndDecryptFile(uri)
.then(obj_url => getTemplateForObjectURL(uri, obj_url, richtext));

const template = html`${until(promise, '')}`;
richtext.addTemplateResult(o.start + offset, o.end + offset, template);
});
}

export function handleEncryptedFiles (richtext) {
if (!_converse.config.get('trusted')) {
return;
}
richtext.addAnnotations((text, offset) => addEncryptedFiles(text, offset, richtext));
return objs;
}

/**
Expand Down Expand Up @@ -306,31 +276,25 @@ export function onChatBoxesInitialized () {
});
}

export function onChatInitialized (el) {
el.listenTo(el.model.messages, 'add', message => {
export function onChatBoxInitialized(model) {
model.listenTo(model.messages, 'add', message => {
if (message.get('is_encrypted') && !message.get('is_error')) {
el.model.save('omemo_supported', true);
model.save('omemo_supported', true);
}
});
el.listenTo(el.model, 'change:omemo_supported', () => {
if (!el.model.get('omemo_supported') && el.model.get('omemo_active')) {
el.model.set('omemo_active', false);
} else {
// Manually trigger an update, setting omemo_active to
// false above will automatically trigger one.
el.querySelector('converse-chat-toolbar')?.requestUpdate();
model.listenTo(model, 'change:omemo_supported', () => {
if (!model.get('omemo_supported') && model.get('omemo_active')) {
model.set('omemo_active', false);
}
});
el.listenTo(el.model, 'change:omemo_active', () => {
el.querySelector('converse-chat-toolbar').requestUpdate();
});
}

export function getSessionCipher (jid, id) {
const address = new libsignal.SignalProtocolAddress(jid, id);
return new window.libsignal.SessionCipher(_converse.omemo_store, address);
}


function getJIDForDecryption (attrs) {
const from_jid = attrs.from_muc ? attrs.from_real_jid : attrs.from;
if (!from_jid) {
Expand Down Expand Up @@ -459,6 +423,7 @@ export function addKeysToMessageStanza (stanza, dicts, iv) {
return Promise.resolve(stanza);
}


/**
* Given an XML element representing a user's OMEMO bundle, parse it
* and return a map.
Expand Down Expand Up @@ -676,6 +641,7 @@ export async function initOMEMO (reconnecting) {
api.trigger('OMEMOInitialized');
}


async function onOccupantAdded (chatroom, occupant) {
if (occupant.isSelf() || !chatroom.features.get('nonanonymous') || !chatroom.features.get('membersonly')) {
return;
Expand Down Expand Up @@ -710,73 +676,6 @@ async function checkOMEMOSupported (chatbox) {
}
}

function toggleOMEMO (ev) {
ev.stopPropagation();
ev.preventDefault();
const toolbar_el = u.ancestor(ev.target, 'converse-chat-toolbar');
if (!toolbar_el.model.get('omemo_supported')) {
let messages;
if (toolbar_el.model.get('type') === _converse.CHATROOMS_TYPE) {
messages = [
__(
'Cannot use end-to-end encryption in this groupchat, ' +
'either the groupchat has some anonymity or not all participants support OMEMO.'
)
];
} else {
messages = [
__(
"Cannot use end-to-end encryption because %1$s uses a client that doesn't support OMEMO.",
toolbar_el.model.contact.getDisplayName()
)
];
}
return api.alert('error', __('Error'), messages);
}
toolbar_el.model.save({ 'omemo_active': !toolbar_el.model.get('omemo_active') });
}

export function getOMEMOToolbarButton (toolbar_el, buttons) {
const model = toolbar_el.model;
const is_muc = model.get('type') === _converse.CHATROOMS_TYPE;
let title;
if (model.get('omemo_supported')) {
const i18n_plaintext = __('Messages are being sent in plaintext');
const i18n_encrypted = __('Messages are sent encrypted');
title = model.get('omemo_active') ? i18n_encrypted : i18n_plaintext;
} else if (is_muc) {
title = __(
'This groupchat needs to be members-only and non-anonymous in ' +
'order to support OMEMO encrypted messages'
);
} else {
title = __('OMEMO encryption is not supported');
}

let color;
if (model.get('omemo_supported')) {
if (model.get('omemo_active')) {
color = is_muc ? `var(--muc-color)` : `var(--chat-toolbar-btn-color)`;
} else {
color = `var(--error-color)`;
}
} else {
color = `var(--muc-toolbar-btn-disabled-color)`;
}
buttons.push(html`
<button class="toggle-omemo" title="${title}" data-disabled=${!model.get('omemo_supported')} @click=${toggleOMEMO}>
<converse-icon
class="fa ${model.get('omemo_active') ? `fa-lock` : `fa-unlock`}"
path-prefix="${api.settings.get('assets_path')}"
size="1em"
color="${color}"
></converse-icon>
</button>
`);
return buttons;
}


async function getBundlesAndBuildSessions (chatbox) {
const no_devices_err = __('Sorry, no devices found to which we can send an OMEMO encrypted message.');
let devices;
Expand Down Expand Up @@ -858,9 +757,3 @@ export async function createOMEMOMessageStanza (chat, data) {
stanza.c('encryption', { 'xmlns': Strophe.NS.EME, namespace: Strophe.NS.OMEMO });
return { message, stanza };
}

export const omemo = {
decryptMessage,
encryptMessage,
formatFingerprint
}
File renamed without changes.
Loading