omemoplugin.py 11.6 KB
Newer Older
1
# -*- coding: utf-8 -*-
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

'''
Copyright 2015 Bahtiar `kalkin-` Gadimov <bahtiar@gadimov.de>
Copyright 2015 Daniel Gultsch <daniel@cgultsch.de>
Copyright 2016 Philipp Hörist <philipp@hoerist.com>

This file is part of Gajim-OMEMO plugin.

The Gajim-OMEMO 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, either version 3 of the License, or (at your option) any
later version.

Gajim-OMEMO 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
the Gajim-OMEMO plugin.  If not, see <http://www.gnu.org/licenses/>.
'''
22
23

import logging
24
25
import binascii
import threading
26
from enum import IntEnum, unique
27

28
from gi.repository import GLib
29

30
from gajim import dialogs
31
from gajim.common import app, ged
32
33
34
from gajim.common.pep import SUPPORTED_PERSONAL_USER_EVENTS
from gajim.plugins import GajimPlugin
from gajim.groupchat_control import GroupchatControl
35

36
from omemo.xmpp import DevicelistPEP
37

38
CRYPTOGRAPHY_MISSING = 'You are missing Python-Cryptography'
39
40
41
42
43
AXOLOTL_MISSING = 'You are missing Python-Axolotl or use an outdated version'
PROTOBUF_MISSING = 'OMEMO cant import Google Protobuf, you can find help in ' \
                   'the GitHub Wiki'
ERROR_MSG = ''

44

45
46
log = logging.getLogger('gajim.plugin_system.omemo')

47
try:
48
    from omemo import file_crypto
49
50
except Exception as error:
    log.exception(error)
51
52
    ERROR_MSG = CRYPTOGRAPHY_MISSING

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

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

if not ERROR_MSG:
    try:
67
68
69
70
71
        from omemo.omemo_connection import OMEMOConnection
        from omemo.ui import OMEMOConfigDialog, FingerprintWindow
    except Exception as error:
        log.error(error)
        ERROR_MSG = 'Error: %s' % error
72
73
74
75
76

# pylint: disable=no-init
# pylint: disable=attribute-defined-outside-init


77
78
79
80
81
82
@unique
class UserMessages(IntEnum):
    QUERY_DEVICES = 0
    NO_FINGERPRINTS = 1


83
84
85
86
87
88
89
90
class OmemoPlugin(GajimPlugin):
    def init(self):
        """ Init """
        if ERROR_MSG:
            self.activatable = False
            self.available_text = ERROR_MSG
            self.config_dialog = None
            return
91
92
        self.encryption_name = 'OMEMO'
        self.allow_groupchat = True
93
94
95
96
97
        self.events_handlers = {
            'signed-in': (ged.PRECORE, self.signed_in),
            }

        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
102
103
            'gc_encrypt' + self.encryption_name: (
                self._gc_encrypt_message, None),
            'decrypt': (self._message_received, None),
104
105
106
107
108
            'send_message' + self.encryption_name: (
                self.before_sendmessage, None),
            'encryption_dialog' + self.encryption_name: (
                self.on_encryption_button_clicked, None),
            'encryption_state' + self.encryption_name: (
109
110
                self.encryption_state, None),
            'update_caps': (self._update_caps, None)}
111

112
113
        SUPPORTED_PERSONAL_USER_EVENTS.append(DevicelistPEP)
        self.disabled_accounts = []
114
        self.windowinstances = {}
115
        self.connections = {}
116
117
118

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

119
        for account in self.config['DISABLED_ACCOUNTS']:
120
121
            self.disabled_accounts.append(account)

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

128
129
130
131
132
133
134
135
    def signed_in(self, event):
        """ Method called on SignIn

            Parameters
            ----------
            event : SignedInEvent
        """
        account = event.conn.name
136
137
        if account == 'Local':
            return
138
139
        if account in self.disabled_accounts:
            return
140
141
142
        if account not in self.connections:
            self.connections[account] = OMEMOConnection(account, self)
            self.connections[account].signed_in(event)
143
144
145
146

    def activate(self):
        """ Method called when the Plugin is activated in the PluginManager
        """
147
        for account in app.connections:
148
149
            if account == 'Local':
                continue
150
151
            if account in self.disabled_accounts:
                continue
152
153
            self.connections[account] = OMEMOConnection(account, self)
            self.connections[account].activate()
154
155
156
157

    def deactivate(self):
        """ Method called when the Plugin is deactivated in the PluginManager
        """
158
        for account in self.connections:
159
160
            if account == 'Local':
                continue
161
            self.connections[account].deactivate()
162

163
164
165
    def _update_caps(self, account):
        if account == 'Local':
            return
166
167
        if account not in self.connections:
            self.connections[account] = OMEMOConnection(account, self)
168
169
        self.connections[account].update_caps(account)

170
171
    def activate_encryption(self, chat_control):
        if isinstance(chat_control, GroupchatControl):
172
173
            omemo_con = self.connections[chat_control.account]
            if chat_control.room_jid not in omemo_con.groupchat:
174
                dialogs.ErrorDialog(
175
176
177
                    _('Bad Configuration'),
                    _('To use OMEMO in a Groupchat, the Groupchat should be'
                      ' non-anonymous and members-only.'))
178
179
180
                return False
        return True

181
182
183
184
185
186
187
188
189
190
    def _message_received(self, conn, obj, callback):
        self.connections[conn.name].message_received(conn, obj, callback)

    def _gc_encrypt_message(self, conn, obj, callback):
        self.connections[conn.name].gc_encrypt_message(conn, obj, callback)

    def _encrypt_message(self, conn, obj, callback):
        self.connections[conn.name].encrypt_message(conn, obj, callback)

    def _file_decryption(self, url, kind, instance, window):
Philipp Hörist's avatar
Philipp Hörist committed
191
        file_crypto.FileDecryption(self).hyperlink_handler(
192
            url, kind, instance, window)
193

194
    def encrypt_file(self, file, callback):
195
        thread = threading.Thread(target=self._encrypt_file_thread,
196
                                  args=(file, callback))
197
198
199
        thread.daemon = True
        thread.start()

200
    @staticmethod
Philipp Hörist's avatar
Philipp Hörist committed
201
202
    def _encrypt_file_thread(file, callback, *args, **kwargs):
        encrypted_data, key, iv = file_crypto.encrypt_file(
203
            file.get_data(full=True))
204
205
206
207
208
209
210
211
        file.encrypted = True
        file.size = len(encrypted_data)
        file.user_data = binascii.hexlify(iv + key).decode('utf-8')
        file.data = encrypted_data
        if file.event.isSet():
            return
        GLib.idle_add(callback, file)

212
213
214
215
216
217
218
219
    @staticmethod
    def encryption_state(chat_control, state):
        state['visible'] = True
        state['authenticated'] = True

    def on_encryption_button_clicked(self, chat_control):
        self.show_fingerprint_window(chat_control)

220
221
222
    def get_omemo(self, account):
        return self.connections[account].omemo

223
224
225
    def before_sendmessage(self, chat_control):
        account = chat_control.account
        contact = chat_control.contact
226
        con = self.connections[account]
227
228
        self.new_fingerprints_available(chat_control)
        if isinstance(chat_control, GroupchatControl):
229
            room = chat_control.room_jid
230
            missing = True
231
            own_jid = app.get_jid_from_account(account)
232
233
            for nick in con.groupchat[room]:
                real_jid = con.groupchat[room][nick]
234
235
                if real_jid == own_jid:
                    continue
236
                if not con.are_keys_missing(real_jid):
237
238
                    missing = False
            if missing:
239
240
241
                log.info('%s => No Trusted Fingerprints for %s',
                         account, room)
                self.print_message(chat_control, UserMessages.NO_FINGERPRINTS)
242
        else:
243
244
245
246
247
248
249
250
251
252
253
            # check if we have devices for the contact
            if not self.get_omemo(account).device_list_for(contact.jid):
                con.query_devicelist(contact.jid, True)
                self.print_message(chat_control, UserMessages.QUERY_DEVICES)
                chat_control.sendmessage = False
                return
            # check if bundles are missing for some devices
            if con.are_keys_missing(contact.jid):
                log.info('%s => No Trusted Fingerprints for %s',
                         account, contact.jid)
                self.print_message(chat_control, UserMessages.NO_FINGERPRINTS)
254
255
                chat_control.sendmessage = False
            else:
256
257
                log.debug('%s => Sending Message to %s',
                          account, contact.jid)
258
259
260
261

    def new_fingerprints_available(self, chat_control):
        jid = chat_control.contact.jid
        account = chat_control.account
262
        con = self.connections[account]
263
        omemo = self.get_omemo(account)
264
265
        if isinstance(chat_control, GroupchatControl):
            room_jid = chat_control.room_jid
266
267
268
            if room_jid in con.groupchat:
                for nick in con.groupchat[room_jid]:
                    real_jid = con.groupchat[room_jid][nick]
269
                    fingerprints = omemo.store. \
270
271
272
273
274
                        getNewFingerprints(real_jid)
                    if fingerprints:
                        self.show_fingerprint_window(
                            chat_control, fingerprints)
        elif not isinstance(chat_control, GroupchatControl):
275
            fingerprints = omemo.store.getNewFingerprints(jid)
276
277
278
279
280
281
282
            if fingerprints:
                self.show_fingerprint_window(
                    chat_control, fingerprints)

    def show_fingerprint_window(self, chat_control, fingerprints=None):
        contact = chat_control.contact
        account = chat_control.account
283
        omemo = self.get_omemo(account)
284
285
286
287
288
289
290
291
292
293
294
295
        transient = chat_control.parent_win.window
        if 'dialog' not in self.windowinstances:
            if isinstance(chat_control, GroupchatControl):
                self.windowinstances['dialog'] = \
                    FingerprintWindow(self, contact, transient,
                                      self.windowinstances, groupchat=True)
            else:
                self.windowinstances['dialog'] = \
                    FingerprintWindow(self, contact, transient,
                                      self.windowinstances)
            self.windowinstances['dialog'].show_all()
            if fingerprints:
296
297
298
                log.debug('%s => Showing Fingerprint Prompt for %s',
                          account, contact.jid)
                omemo.store.setShownFingerprints(fingerprints)
299
300
301
        else:
            self.windowinstances['dialog'].update_context_list()
            if fingerprints:
302
                omemo.store.setShownFingerprints(fingerprints)
303

304
    @staticmethod
305
306
307
308
309
310
311
312
313
    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!')
        if msg is None:
            return
314
        chat_control.print_conversation_line(msg, 'status', '', None)