plugin.py 12.1 KB
Newer Older
Philipp Hörist's avatar
Philipp Hörist committed
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
# Copyright (C) 2019 Philipp Hörist <philipp AT hoerist.com>
# Copyright (C) 2015 Bahtiar `kalkin-` Gadimov <bahtiar@gadimov.de>
# Copyright (C) 2015 Daniel Gultsch <daniel@cgultsch.de>
#
# This file is part of OMEMO Gajim Plugin.
#
# OMEMO Gajim Plugin 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.
#
# OMEMO Gajim Plugin 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 OMEMO Gajim Plugin. If not, see <http://www.gnu.org/licenses/>.
18 19

import logging
20 21
import binascii
import threading
22
from enum import IntEnum, unique
Daniel Brötzmann's avatar
Daniel Brötzmann committed
23
from pathlib import Path
24

25
from gi.repository import GLib
Daniel Brötzmann's avatar
Daniel Brötzmann committed
26 27
from gi.repository import Gtk
from gi.repository import Gdk
28

29
from gajim import dialogs
30
from gajim.common import app, ged
31
from gajim.plugins import GajimPlugin
Philipp Hörist's avatar
Philipp Hörist committed
32
from gajim.plugins.plugins_i18n import _
33
from gajim.groupchat_control import GroupchatControl
34

Philipp Hörist's avatar
Philipp Hörist committed
35
from omemo import file_crypto
Daniel Brötzmann's avatar
Daniel Brötzmann committed
36
from omemo.gtk.key import KeyDialog
37
from omemo.gtk.config import OMEMOConfigDialog
Philipp Hörist's avatar
Philipp Hörist committed
38
from omemo.backend.aes import aes_encrypt_file
39

Alexander Krotov's avatar
Alexander Krotov committed
40

41
AXOLOTL_MISSING = 'You are missing Python3-Axolotl or use an outdated version'
Alexander Krotov's avatar
Alexander Krotov committed
42 43
PROTOBUF_MISSING = "OMEMO can't import Google Protobuf, you can find help in " \
                   "the GitHub Wiki"
44 45
ERROR_MSG = ''

46

47
log = logging.getLogger('gajim.p.omemo')
Philipp Hörist's avatar
Philipp Hörist committed
48 49 50 51 52
if log.getEffectiveLevel() == logging.DEBUG:
    log_axolotl = logging.getLogger('axolotl')
    log_axolotl.setLevel(logging.DEBUG)
    log_axolotl.addHandler(logging.StreamHandler())
    log_axolotl.propagate = False
53

54 55
try:
    import google.protobuf
56 57
except Exception as error:
    log.error(error)
58 59 60 61
    ERROR_MSG = PROTOBUF_MISSING

try:
    import axolotl
62 63
except Exception as error:
    log.error(error)
64 65 66 67
    ERROR_MSG = AXOLOTL_MISSING

if not ERROR_MSG:
    try:
68
        from omemo.modules import omemo
69 70 71
    except Exception as error:
        log.error(error)
        ERROR_MSG = 'Error: %s' % error
72 73


74 75 76 77
@unique
class UserMessages(IntEnum):
    QUERY_DEVICES = 0
    NO_FINGERPRINTS = 1
78
    UNDECIDED_FINGERPRINTS = 2
79 80


81 82
class OmemoPlugin(GajimPlugin):
    def init(self):
Philipp Hörist's avatar
Philipp Hörist committed
83
        # pylint: disable=attribute-defined-outside-init
84 85 86 87 88
        if ERROR_MSG:
            self.activatable = False
            self.available_text = ERROR_MSG
            self.config_dialog = None
            return
89 90
        self.encryption_name = 'OMEMO'
        self.allow_groupchat = True
91
        self.events_handlers = {
92
            'omemo-new-fingerprint': (ged.PRECORE, self._on_new_fingerprints),
93 94
            'signed-in': (ged.PRECORE, self._on_signed_in),
            'muc-config-changed': (ged.GUI2, self._on_muc_config_changed),
95
        }
Philipp Hörist's avatar
Philipp Hörist committed
96
        self.modules = [omemo]
97
        self.config_dialog = OMEMOConfigDialog(self)
98
        self.gui_extension_points = {
99
            'hyperlink_handler': (self._file_decryption, None),
100
            'encrypt' + self.encryption_name: (self._encrypt_message, None),
101
            'gc_encrypt' + self.encryption_name: (
102
                self._muc_encrypt_message, None),
103
            'send_message' + self.encryption_name: (
104
                self._before_sendmessage, None),
105
            'encryption_dialog' + self.encryption_name: (
Philipp Hörist's avatar
Philipp Hörist committed
106
                self._on_encryption_button_clicked, None),
107
            'encryption_state' + self.encryption_name: (
108
                self._encryption_state, None),
109
            'update_caps': (self._update_caps, None)}
110

111
        self.disabled_accounts = []
Philipp Hörist's avatar
Philipp Hörist committed
112
        self._windows = {}
113 114 115

        self.config_default_values = {'DISABLED_ACCOUNTS': ([], ''), }

116
        for account in self.config['DISABLED_ACCOUNTS']:
117 118
            self.disabled_accounts.append(account)

119
        # add aesgcm:// uri scheme to config
120
        schemes = app.config.get('uri_schemes')
121 122
        if 'aesgcm://' not in schemes.split():
            schemes += ' aesgcm://'
123
            app.config.set('uri_schemes', schemes)
124

Daniel Brötzmann's avatar
Daniel Brötzmann committed
125 126
        self._load_css()

127 128 129 130 131 132 133 134 135 136 137
    def _is_enabled_account(self, account):
        if account in self.disabled_accounts:
            return False
        if account == 'Local':
            return False
        return True

    @staticmethod
    def get_omemo(account):
        return app.connections[account].get_module('OMEMO')

Philipp Hörist's avatar
Philipp Hörist committed
138 139
    @staticmethod
    def _load_css():
Daniel Brötzmann's avatar
Daniel Brötzmann committed
140 141
        path = Path(__file__).parent / 'gtk' / 'style.css'
        try:
142
            with path.open("r") as file:
Philipp Hörist's avatar
Philipp Hörist committed
143
                css = file.read()
Daniel Brötzmann's avatar
Daniel Brötzmann committed
144 145 146 147 148 149 150 151 152 153 154 155
        except Exception as exc:
            log.error('Error loading css: %s', exc)
            return

        try:
            provider = Gtk.CssProvider()
            provider.load_from_data(bytes(css.encode('utf-8')))
            Gtk.StyleContext.add_provider_for_screen(Gdk.Screen.get_default(),
                                                     provider, 610)
        except Exception:
            log.exception('Error loading application css')

156
    def activate(self):
157 158
        """
        Method called when the Plugin is activated in the PluginManager
159
        """
160
        for account in app.connections:
161
            if not self._is_enabled_account(account):
162
                continue
163
            self.get_omemo(account).activate()
164

165 166 167
    def deactivate(self):
        """
        Method called when the Plugin is deactivated in the PluginManager
168
        """
169
        for account in app.connections:
170
            if not self._is_enabled_account(account):
171
                continue
172
            self.get_omemo(account).deactivate()
173

174 175 176 177 178 179 180 181 182 183 184 185 186
    def _on_signed_in(self, event):
        account = event.conn.name
        if not self._is_enabled_account(account):
            return
        self.get_omemo(account).on_signed_in()

    def _on_muc_config_changed(self, event):
        if not self._is_enabled_account(event.account):
            return
        self.get_omemo(event.account).on_muc_config_changed(event)

    def _update_caps(self, account):
        if not self._is_enabled_account(account):
187
            return
188
        self.get_omemo(account).update_caps(account)
189

Philipp Hörist's avatar
Philipp Hörist committed
190 191
    @staticmethod
    def activate_encryption(chat_control):
192
        if isinstance(chat_control, GroupchatControl):
193
            omemo_con = app.connections[chat_control.account].get_module('OMEMO')
Philipp Hörist's avatar
Philipp Hörist committed
194
            if not omemo_con.is_omemo_groupchat(chat_control.room_jid):
195
                dialogs.ErrorDialog(
196 197 198
                    _('Bad Configuration'),
                    _('To use OMEMO in a Groupchat, the Groupchat should be'
                      ' non-anonymous and members-only.'))
199 200 201
                return False
        return True

202 203 204
    def _muc_encrypt_message(self, conn, obj, callback):
        account = conn.name
        if not self._is_enabled_account(account):
205
            return
206
        self.get_omemo(account).encrypt_message(conn, obj, callback, True)
207

208 209 210
    def _encrypt_message(self, conn, obj, callback):
        account = conn.name
        if not self._is_enabled_account(account):
211
            return
212
        self.get_omemo(account).encrypt_message(conn, obj, callback, False)
213

214
    def _file_decryption(self, uri, instance, window):
Philipp Hörist's avatar
Philipp Hörist committed
215
        file_crypto.FileDecryption(self).hyperlink_handler(
216
            uri, instance, window)
217

Philipp Hörist's avatar
Philipp Hörist committed
218
    def encrypt_file(self, file, _account, callback):
219
        thread = threading.Thread(target=self._encrypt_file_thread,
220
                                  args=(file, callback))
221 222 223
        thread.daemon = True
        thread.start()

224
    @staticmethod
Philipp Hörist's avatar
Philipp Hörist committed
225
    def _encrypt_file_thread(file, callback, *args, **kwargs):
Philipp Hörist's avatar
Philipp Hörist committed
226
        result = aes_encrypt_file(file.get_data(full=True))
227
        file.encrypted = True
Philipp Hörist's avatar
Philipp Hörist committed
228 229 230
        file.size = len(result.payload)
        file.user_data = binascii.hexlify(result.iv + result.key).decode()
        file.data = result.payload
231 232 233 234
        if file.event.isSet():
            return
        GLib.idle_add(callback, file)

235
    @staticmethod
236
    def _encryption_state(_chat_control, state):
237 238 239
        state['visible'] = True
        state['authenticated'] = True

Philipp Hörist's avatar
Philipp Hörist committed
240
    def _on_encryption_button_clicked(self, chat_control):
241
        self._show_fingerprint_window(chat_control)
242

243
    def _before_sendmessage(self, chat_control):
244
        account = chat_control.account
245
        if not self._is_enabled_account(account):
246
            return
247
        contact = chat_control.contact
Philipp Hörist's avatar
Philipp Hörist committed
248
        omemo = self.get_omemo(account)
249 250
        self.new_fingerprints_available(chat_control)
        if isinstance(chat_control, GroupchatControl):
251
            room = chat_control.room_jid
252
            missing = True
Philipp Hörist's avatar
Philipp Hörist committed
253 254
            for jid in omemo.backend.get_muc_members(room):
                if not omemo.are_keys_missing(jid):
255 256
                    missing = False
            if missing:
257 258 259
                log.info('%s => No Trusted Fingerprints for %s',
                         account, room)
                self.print_message(chat_control, UserMessages.NO_FINGERPRINTS)
260
        else:
261
            # check if we have devices for the contact
262
            if not omemo.backend.get_devices(contact.jid, without_self=True):
263
                omemo.request_devicelist(contact.jid)
264 265 266 267
                self.print_message(chat_control, UserMessages.QUERY_DEVICES)
                chat_control.sendmessage = False
                return
            # check if bundles are missing for some devices
268 269
            if omemo.backend.storage.hasUndecidedFingerprints(contact.jid):
                log.info('%s => Undecided Fingerprints for %s',
270
                         account, contact.jid)
271
                self.print_message(chat_control, UserMessages.UNDECIDED_FINGERPRINTS)
272 273
                chat_control.sendmessage = False
            else:
274 275
                log.debug('%s => Sending Message to %s',
                          account, contact.jid)
276

277 278 279
    def _on_new_fingerprints(self, event):
        self.new_fingerprints_available(event.chat_control)

280 281 282
    def new_fingerprints_available(self, chat_control):
        jid = chat_control.contact.jid
        account = chat_control.account
283
        omemo = self.get_omemo(account)
284
        if isinstance(chat_control, GroupchatControl):
Philipp Hörist's avatar
Philipp Hörist committed
285 286 287 288
            for jid_ in omemo.backend.get_muc_members(chat_control.room_jid,
                                                      without_self=False):
                fingerprints = omemo.backend.storage.getNewFingerprints(jid_)
                if fingerprints:
289
                    self._show_fingerprint_window(
Philipp Hörist's avatar
Philipp Hörist committed
290 291
                        chat_control, fingerprints)
                    break
292
        elif not isinstance(chat_control, GroupchatControl):
Philipp Hörist's avatar
Philipp Hörist committed
293
            fingerprints = omemo.backend.storage.getNewFingerprints(jid)
294
            if fingerprints:
295
                self._show_fingerprint_window(
296 297
                    chat_control, fingerprints)

298
    def _show_fingerprint_window(self, chat_control, fingerprints=None):
299 300
        contact = chat_control.contact
        account = chat_control.account
301
        omemo = self.get_omemo(account)
302
        transient = chat_control.parent_win.window
Philipp Hörist's avatar
Philipp Hörist committed
303 304

        if 'dialog' not in self._windows:
305
            is_groupchat = isinstance(chat_control, GroupchatControl)
Philipp Hörist's avatar
Philipp Hörist committed
306
            self._windows['dialog'] = \
Daniel Brötzmann's avatar
Daniel Brötzmann committed
307
                KeyDialog(self, contact, transient,
Philipp Hörist's avatar
Philipp Hörist committed
308
                          self._windows, groupchat=is_groupchat)
309
            if fingerprints:
310 311
                log.debug('%s => Showing Fingerprint Prompt for %s',
                          account, contact.jid)
Philipp Hörist's avatar
Philipp Hörist committed
312
                omemo.backend.storage.setShownFingerprints(fingerprints)
313
        else:
Philipp Hörist's avatar
Philipp Hörist committed
314 315
            self._windows['dialog'].present()
            self._windows['dialog'].update()
316
            if fingerprints:
Philipp Hörist's avatar
Philipp Hörist committed
317
                omemo.backend.storage.setShownFingerprints(fingerprints)
318

319
    @staticmethod
320 321 322 323 324 325 326
    def print_message(chat_control, kind):
        msg = None
        if kind == UserMessages.QUERY_DEVICES:
            msg = _('No devices found. Query in progress...')
        elif kind == UserMessages.NO_FINGERPRINTS:
            msg = _('To send an encrypted message, you have to '
                    'first trust the fingerprint of your contact!')
327 328
        elif kind == UserMessages.UNDECIDED_FINGERPRINTS:
            msg = _('You have undecided fingerprints')
329 330
        if msg is None:
            return
331
        chat_control.add_status_message(msg)