Skip to content
Closed
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.
File renamed without changes.
50 changes: 16 additions & 34 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,28 @@
* @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 +37,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 +65,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 +84,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';
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
154 changes: 24 additions & 130 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 Down Expand Up @@ -197,59 +190,37 @@ 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
});
return url;
},
parse_options
);
} catch (error) {
console.log('WHOOPSIES', error);
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 +277,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 +424,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 +642,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 +677,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 +758,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
}
1 change: 1 addition & 0 deletions src/headless/shared/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export const CORE_PLUGINS = [
'converse-headlines',
'converse-mam',
'converse-muc',
'converse-omemo',
'converse-ping',
'converse-pubsub',
'converse-roster',
Expand Down
Loading