Daniel Brötzmann pushed to branch omemo_integration at gajim / gajim
Commits: b77dd4cc by wurstsalat at 2022-10-21T21:30:11+02:00 new: OMEMO: Add handling of MucAdded signal
- - - - - a1a7aa70 by wurstsalat at 2022-10-21T21:40:38+02:00 fix: OMEMO: Make QR code icon symbolic to allow color adjustments
- - - - - f0a2c915 by wurstsalat at 2022-10-21T23:57:06+02:00 new: OMEMO: Add EncryptionAnnouncementRow
- - - - -
9 changed files:
- gajim/common/client.py - gajim/common/const.py - gajim/common/events.py - gajim/common/modules/omemo.py - gajim/data/gui/omemo_trust_manager.ui - gajim/data/icons/hicolor/scalable/categories/qr-code-scan.svg → gajim/data/icons/hicolor/scalable/categories/qr-code-scan-symbolic.svg - gajim/gtk/control.py - + gajim/gtk/conversation/rows/encryption_announcement.py - gajim/gtk/conversation/view.py
Changes:
===================================== gajim/common/client.py ===================================== @@ -484,7 +484,10 @@ def send_message(self, message: OutgoingMessage) -> None:
if method == 'OMEMO': try: - self.get_module('OMEMO').encrypt_message(message) + can_send = self.get_module('OMEMO').encrypt_message(message) + if not can_send: + return + except Exception: app.ged.raise_event( MessageNotSent(client=self._client,
===================================== gajim/common/const.py ===================================== @@ -43,6 +43,18 @@ class EncryptionData(NamedTuple): additional_data: Any = None
+class EncryptionInfoMsg(Enum): + BAD_OMEMO_CONFIG = _('This chat’s configuration is unsuitable for ' + 'encryption with OMEMO. To use OMEMO in this chat, ' + 'it should be non-anonymous and members-only.') + NO_FINGERPRINTS = _('To send an encrypted message, you have to decide ' + 'whether to trust the device of your contact.') + QUERY_DEVICES = _('No devices found to encypt this message to. ' + 'Querying for devices now…') + UNDECIDED_FINGERPRINTS = _('There are devices for which you have not made ' + 'a trust decision yet.') + + class Entity(NamedTuple): jid: JID node: str
===================================== gajim/common/events.py ===================================== @@ -38,6 +38,7 @@ from nbxmpp.const import StatusCode
from gajim.common.file_props import FileProp +from gajim.common.const import EncryptionInfoMsg from gajim.common.const import JingleState from gajim.common.const import KindConstant from gajim.common.helpers import AdditionalDataDict @@ -779,6 +780,8 @@ class MUCUserStatusShowChanged(ApplicationEvent):
@dataclass -class OMEMONewFingerprint(ApplicationEvent): - name: str = field(init=False, default='omemo-new-fingerprint') - chat_control: Any +class EncryptionAnnouncement(ApplicationEvent): + name: str = field(init=False, default='encryption-check') + account: str + jid: JID + message: EncryptionInfoMsg
===================================== gajim/common/modules/omemo.py ===================================== @@ -27,6 +27,7 @@ from pathlib import Path
from nbxmpp.namespaces import Namespace +from nbxmpp.protocol import JID from nbxmpp.protocol import Message from nbxmpp.protocol import NodeProcessed from nbxmpp.protocol import Presence @@ -49,9 +50,12 @@ from gajim.common import ged from gajim.common import types from gajim.common.structs import OutgoingMessage +from gajim.common.const import EncryptionInfoMsg from gajim.common.const import EncryptionData from gajim.common.const import Trust as GajimTrust from gajim.common.i18n import _ +from gajim.common.events import EncryptionAnnouncement +from gajim.common.events import MucAdded from gajim.common.events import MucDiscoUpdate from gajim.common.events import SignedIn from gajim.common.modules.base import BaseModule @@ -67,7 +71,6 @@ from gajim.common.omemo.state import DecryptionFailed from gajim.common.omemo.state import DuplicateMessage from gajim.common.omemo.util import Trust -from gajim.common.events import OMEMONewFingerprint from gajim.common.modules.util import prepare_stanza
@@ -120,9 +123,11 @@ def __init__(self, client: types.Client) -> None: ]
self._register_pubsub_handler(self._devicelist_notification_received) - self.register_event('signed-in', ged.CORE, self._on_signed_in) - self.register_event( - 'muc-disco-update', ged.GUI1, self._on_muc_disco_update) + self.register_events([ + ('signed-in', ged.CORE, self._on_signed_in), + ('muc-disco-update', ged.GUI1, self._on_muc_disco_update), + ('muc-added', ged.GUI1, self._on_muc_added) + ])
self.encryption_name = 'OMEMO' self.allow_groupchat = True @@ -145,6 +150,27 @@ def _on_signed_in(self, _event: SignedIn) -> None: def _on_muc_disco_update(self, event: MucDiscoUpdate) -> None: self._check_if_omemo_capable(str(event.jid))
+ def _on_muc_added(self, event: MucAdded) -> None: + client = app.get_client(event.account) + contact = client.get_module('Contacts').get_contact(event.jid) + if not isinstance(contact, GroupchatContact): + self._log.warning('%s is not a groupchat contact', contact) + return + + # Event is triggert on every join, avoid multiple connects + contact.disconnect_all_from_obj(self) + contact.connect('room-joined', self._on_room_joined) + + def _on_room_joined(self, + contact: GroupchatContact, + _signal_name: str + ) -> None: + + jid = str(contact.jid) + self._check_if_omemo_capable(jid) + if self.is_omemo_groupchat(jid): + self.get_affiliation_list(jid) + @property def backend(self) -> OmemoState: return self._backend @@ -154,12 +180,91 @@ def _get_backend(self) -> OmemoState: db_path = data_dir / f'omemo_{self._own_jid}.db' return OmemoState(self._own_jid, db_path, self._account, self)
+ def _can_send_message(self, event: OutgoingMessage) -> bool: + contact = event.contact + jid = str(contact.jid) + if contact.is_groupchat: + if not self.is_omemo_groupchat(jid): + app.ged.raise_event(EncryptionAnnouncement( + account=contact.account, + jid=contact.jid, + message=EncryptionInfoMsg.BAD_OMEMO_CONFIG)) + return False + + missing = True + for member_jid in self.backend.get_muc_members(jid): + if not self.are_keys_missing(member_jid): + missing = False + if missing: + self._log.info('%s => No Trusted Fingerprints for %s', + contact.account, jid) + app.ged.raise_event(EncryptionAnnouncement( + account=contact.account, + jid=contact.jid, + message=EncryptionInfoMsg.NO_FINGERPRINTS)) + return False + else: + # check if we have devices for the contact + if not self.backend.get_devices(jid, without_self=True): + self.request_devicelist(jid) + app.ged.raise_event(EncryptionAnnouncement( + account=contact.account, + jid=contact.jid, + message=EncryptionInfoMsg.QUERY_DEVICES)) + return False + + # check if bundles are missing for some devices + if self.backend.storage.hasUndecidedFingerprints(jid): + self._log.info('%s => Undecided Fingerprints for %s', + contact.account, jid) + app.ged.raise_event(EncryptionAnnouncement( + account=contact.account, + jid=contact.jid, + message=EncryptionInfoMsg.UNDECIDED_FINGERPRINTS)) + return False + + self._log.debug('%s => Sending Message to %s', + contact.account, jid) + + return True + + def _new_fingerprints_available(self, event: OutgoingMessage) -> bool: + contact = event.contact + if contact.is_groupchat: + for member_jid in self.backend.get_muc_members(str(contact.jid), + without_self=False): + fingerprints = self.backend.storage.getNewFingerprints( + member_jid) + if fingerprints: + app.ged.raise_event(EncryptionAnnouncement( + account=contact.account, + jid=contact.jid, + message=EncryptionInfoMsg.UNDECIDED_FINGERPRINTS)) + return True + else: + fingerprints = self.backend.storage.getNewFingerprints( + str(contact.jid)) + if fingerprints: + app.ged.raise_event(EncryptionAnnouncement( + account=contact.account, + jid=contact.jid, + message=EncryptionInfoMsg.UNDECIDED_FINGERPRINTS)) + return True + + return False + def is_omemo_groupchat(self, room_jid: str) -> bool: return room_jid in self._omemo_groupchats
- def encrypt_message(self, event: OutgoingMessage) -> None: + def encrypt_message(self, event: OutgoingMessage) -> bool: if not event.message: - return + return False + + if self._new_fingerprints_available(event): + return False + + if not self._can_send_message(event): + return False
omemo_message = self.backend.encrypt(str(event.jid), event.message) if omemo_message is None: @@ -178,6 +283,7 @@ def encrypt_message(self, event: OutgoingMessage) -> None: 'trust': GajimTrust[Trust.VERIFIED.name]}
self._debug_print_stanza(event.stanza) + return True
def encrypt_file(self, transfer: HTTPFileTransfer, @@ -392,12 +498,6 @@ def is_contact_in_roster(self, jid: str) -> bool: contact = self._client.get_module('Contacts').get_contact(jid) return contact.subscription == 'both'
- def on_room_joined(self, contact: GroupchatContact) -> None: - jid = str(contact.jid) - self._check_if_omemo_capable(jid) - if self.is_omemo_groupchat(jid): - self.get_affiliation_list(jid) - def _check_if_omemo_capable(self, jid: str) -> None: disco_info = app.storage.cache.get_last_disco_info(jid) if disco_info.muc_is_members_only and disco_info.muc_is_nonanonymous: @@ -433,8 +533,8 @@ def are_keys_missing(self, contact_jid: str) -> bool: # Fetch Bundles of own other Devices if self._own_jid not in self._query_for_bundles:
- devices_without_session = self.backend \ - .devices_without_sessions(self._own_jid) + devices_without_session = self.backend.devices_without_sessions( + self._own_jid)
self._query_for_bundles.append(self._own_jid)
@@ -445,8 +545,8 @@ def are_keys_missing(self, contact_jid: str) -> bool: # Fetch Bundles of contacts devices if contact_jid not in self._query_for_bundles:
- devices_without_session = self.backend \ - .devices_without_sessions(contact_jid) + devices_without_session = self.backend.devices_without_sessions( + contact_jid)
self._query_for_bundles.append(contact_jid)
@@ -468,9 +568,7 @@ def request_bundle(self, jid: str, device_id: int):
self._log.info('Fetch device bundle %s %s', device_id, jid)
- bundle = yield self._nbxmpp('OMEMO').request_bundle( - jid, - device_id) + bundle = yield self._nbxmpp('OMEMO').request_bundle(jid, device_id)
if is_error(bundle) or bundle is None: self._log.info('Bundle request failed: %s %s: %s', @@ -484,9 +582,10 @@ def request_bundle(self, jid: str, device_id: int):
# Trigger dialog to trust new Fingerprints if # the Chat Window is Open - ctrl = app.window.get_control(self._account, jid) - if ctrl: - app.ged.raise_event(OMEMONewFingerprint(chat_control=ctrl)) + app.ged.raise_event(EncryptionAnnouncement( + account=self._account, + jid=JID.from_string(jid), + message=EncryptionInfoMsg.UNDECIDED_FINGERPRINTS))
def set_devicelist(self, devicelist: Optional[list[int]] = None) -> None: devicelist_: set[int] = set([self.backend.own_device])
===================================== gajim/data/gui/omemo_trust_manager.ui ===================================== @@ -219,7 +219,7 @@ <object class="GtkImage"> <property name="visible">True</property> <property name="can-focus">False</property> - <property name="icon-name">qr-code-scan</property> + <property name="icon-name">qr-code-scan-symbolic</property> </object> </child> <style>
===================================== gajim/data/icons/hicolor/scalable/categories/qr-code-scan.svg → gajim/data/icons/hicolor/scalable/categories/qr-code-scan-symbolic.svg =====================================
===================================== gajim/gtk/control.py ===================================== @@ -219,6 +219,7 @@ def _register_events(self) -> None: ged.GUI2, self._on_jingle_request_received), ('file-request-received', ged.GUI2, self._on_file_request_event), ('file-request-sent', ged.GUI2, self._on_file_request_event), + ('encryption-check', ged.GUI2, self._on_encryption_announcement), ])
def _is_event_processable(self, event: Any) -> bool: @@ -424,6 +425,16 @@ def _on_file_request_event(self,
self.add_jingle_file_transfer(event=event)
+ def _on_encryption_announcement(self, + event: events.EncryptionAnnouncement + ) -> None: + + if not self._is_event_processable(event): + return + + if self._allow_add_message(): + self._scrolled_view.add_encryption_announcement(event) + @property def is_chat(self) -> bool: return isinstance(self.contact, BareContact)
===================================== gajim/gtk/conversation/rows/encryption_announcement.py ===================================== @@ -0,0 +1,86 @@ +# This file is part of Gajim. +# +# Gajim is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published +# by the Free Software Foundation; version 3 only. +# +# Gajim is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Gajim. If not, see http://www.gnu.org/licenses/. + +import time +from datetime import datetime + +from gi.repository import Gtk + +from gajim.common.const import AvatarSize +from gajim.common.const import EncryptionInfoMsg +from gajim.common.events import EncryptionAnnouncement +from gajim.common.i18n import _ +from gajim.common.modules.contacts import BareContact + +from .base import BaseRow +from .widgets import DateTimeLabel +from .widgets import SimpleLabel + +from ...util import open_window + + +class EncryptionAnnouncementRow(BaseRow): + def __init__(self, event: EncryptionAnnouncement) -> None: + BaseRow.__init__(self, event.account) + + self.type = 'encryption_announcement' + timestamp = time.time() + self.timestamp = datetime.fromtimestamp(timestamp) + self._event = event + + avatar_placeholder = Gtk.Box() + avatar_placeholder.set_size_request(AvatarSize.ROSTER, -1) + + icon = Gtk.Image.new_from_icon_name('channel-secure-symbolic', + Gtk.IconSize.LARGE_TOOLBAR) + icon.get_style_context().add_class('dim-label') + avatar_placeholder.add(icon) + self.grid.attach(avatar_placeholder, 0, 0, 1, 1) + + timestamp_widget = DateTimeLabel(self.timestamp) + timestamp_widget.set_valign(Gtk.Align.START) + timestamp_widget.set_margin_start(0) + self.grid.attach(timestamp_widget, 1, 0, 1, 1) + + self._label = SimpleLabel() + self._label.set_text(event.message.value) + self.grid.attach(self._label, 1, 1, 1, 1) + + if event.message in (EncryptionInfoMsg.NO_FINGERPRINTS, + EncryptionInfoMsg.UNDECIDED_FINGERPRINTS): + button = Gtk.Button(label=_('Manage Trust')) + button.set_halign(Gtk.Align.START) + button.connect('clicked', self._on_manage_trust_clicked) + self.grid.attach(button, 1, 2, 1, 1) + + self.show_all() + + def _on_manage_trust_clicked(self, _button: Gtk.Button) -> None: + contact = self._client.get_module('Contacts').get_contact( + self._event.jid) + if contact.is_groupchat: + open_window('GroupchatDetails', + contact=contact, + page='encryption-omemo') + return + + if isinstance(contact, BareContact) and contact.is_self: + window = open_window('AccountsWindow') + window.select_account(contact.account, page='encryption-omemo') + return + + open_window('ContactInfo', + account=contact.account, + contact=contact, + page='encryption-omemo')
===================================== gajim/gtk/conversation/view.py ===================================== @@ -57,6 +57,7 @@ from .rows.call import CallRow from .rows.command_output import CommandOutputRow from .rows.date import DateRow +from .rows.encryption_announcement import EncryptionAnnouncementRow from .rows.file_transfer import FileTransferRow from .rows.file_transfer_jingle import FileTransferJingleRow from .rows.muc_subject import MUCSubject @@ -424,6 +425,14 @@ def add_jingle_file_transfer(self, db_message=db_message) self._insert_message(jingle_transfer_row)
+ def add_encryption_announcement(self, + event: events.EncryptionAnnouncement + ) -> None: + + assert self._contact is not None + row = EncryptionAnnouncementRow(event) + self._insert_message(row) + def add_call_message(self, event: Optional[events.JingleRequestReceived] = None, db_message: Optional[ConversationRow] = None
View it on GitLab: https://dev.gajim.org/gajim/gajim/-/compare/cc8766812ab41113651005a348767fea...