key.py 14.5 KB
Newer Older
Philipp Hörist's avatar
Philipp Hörist committed
1
# Copyright (C) 2019 Philipp Hörist <philipp AT hoerist.com>
Daniel Brötzmann's avatar
Daniel Brötzmann committed
2
#
Philipp Hörist's avatar
Philipp Hörist committed
3
# This file is part of OMEMO Gajim Plugin.
Daniel Brötzmann's avatar
Daniel Brötzmann committed
4
#
Philipp Hörist's avatar
Philipp Hörist committed
5
# OMEMO Gajim Plugin is free software; you can redistribute it and/or modify
Daniel Brötzmann's avatar
Daniel Brötzmann committed
6
7
8
# it under the terms of the GNU General Public License as published
# by the Free Software Foundation; version 3 only.
#
Philipp Hörist's avatar
Philipp Hörist committed
9
# OMEMO Gajim Plugin is distributed in the hope that it will be useful,
Daniel Brötzmann's avatar
Daniel Brötzmann committed
10
11
12
13
14
# 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
Philipp Hörist's avatar
Philipp Hörist committed
15
# along with OMEMO Gajim Plugin. If not, see <http://www.gnu.org/licenses/>.
Daniel Brötzmann's avatar
Daniel Brötzmann committed
16

17
import os
18
import time
19
import locale
Daniel Brötzmann's avatar
Daniel Brötzmann committed
20
import logging
21
import tempfile
Daniel Brötzmann's avatar
Daniel Brötzmann committed
22
23
24
25

from gi.repository import Gtk
from gi.repository import GdkPixbuf

26
27
28
from pkg_resources import get_distribution
from distutils.version import LooseVersion as V

Daniel Brötzmann's avatar
Daniel Brötzmann committed
29
30
from gajim.common import app
from gajim.plugins.plugins_i18n import _
31
from gajim.plugins.helpers import get_builder
Daniel Brötzmann's avatar
Daniel Brötzmann committed
32
33
34
35

from omemo.gtk.util import DialogButton, ButtonAction
from omemo.gtk.util import NewConfirmationDialog
from omemo.gtk.util import Trust
Philipp Hörist's avatar
Philipp Hörist committed
36
from omemo.backend.util import IdentityKeyExtended
Philipp Hörist's avatar
Philipp Hörist committed
37
from omemo.backend.util import get_fingerprint
Daniel Brötzmann's avatar
Daniel Brötzmann committed
38

39
log = logging.getLogger('gajim.p.omemo')
Daniel Brötzmann's avatar
Daniel Brötzmann committed
40

41

Daniel Brötzmann's avatar
Daniel Brötzmann committed
42
43
44
45
46
47
48
49
50
51
52
53
54
55
TRUST_DATA = {
    Trust.NOT_TRUSTED: ('dialog-error-symbolic',
                        _('Not Trusted'),
                        'error-color'),
    Trust.UNKNOWN: ('security-low-symbolic',
                    _('Not Decided'),
                    'warning-color'),
    Trust.VERIFIED: ('security-high-symbolic',
                     _('Trusted'),
                     'success-color')
}


class KeyDialog(Gtk.Dialog):
Philipp Hörist's avatar
Philipp Hörist committed
56
    def __init__(self, plugin, contact, transient, windows,
Daniel Brötzmann's avatar
Daniel Brötzmann committed
57
                 groupchat=False):
Philipp Hörist's avatar
Philipp Hörist committed
58
59
        super().__init__(title=_('OMEMO Fingerprints'),
                         destroy_with_parent=True)
Daniel Brötzmann's avatar
Daniel Brötzmann committed
60
61
62

        self.set_transient_for(transient)
        self.set_resizable(True)
Philipp Hörist's avatar
Philipp Hörist committed
63
        self.set_default_size(500, 450)
Daniel Brötzmann's avatar
Daniel Brötzmann committed
64
65
66
67
68

        self.get_style_context().add_class('omemo-key-dialog')

        self._groupchat = groupchat
        self._contact = contact
Philipp Hörist's avatar
Philipp Hörist committed
69
        self._windows = windows
Daniel Brötzmann's avatar
Daniel Brötzmann committed
70
71
        self._account = self._contact.account.name
        self._plugin = plugin
Philipp Hörist's avatar
Philipp Hörist committed
72
        self._omemo = self._plugin.get_omemo(self._account)
Daniel Brötzmann's avatar
Daniel Brötzmann committed
73
        self._own_jid = app.get_jid_from_account(self._account)
74
        self._show_inactive = False
Daniel Brötzmann's avatar
Daniel Brötzmann committed
75

76
77
        path = self._plugin.local_file_path('gtk/key.ui')
        self._ui = get_builder(path)
Daniel Brötzmann's avatar
Daniel Brötzmann committed
78

79
        self._ui.header.set_text(_('Fingerprints for %s') % self._contact.jid)
Daniel Brötzmann's avatar
Daniel Brötzmann committed
80

81
82
        omemo_img_path = self._plugin.local_file_path('omemo.png')
        self._ui.omemo_image.set_from_file(omemo_img_path)
Daniel Brötzmann's avatar
Daniel Brötzmann committed
83

84
85
86
        self._ui.list.set_filter_func(self._filter_func, None)
        self._ui.list.set_sort_func(self._sort_func, None)

87
88
89
        self._identity_key = self._omemo.backend.storage.getIdentityKeyPair()
        ownfpr_format = get_fingerprint(self._identity_key, formatted=True)
        self._ui.own_fingerprint.set_text(ownfpr_format)
Daniel Brötzmann's avatar
Daniel Brötzmann committed
90

91
        self.get_content_area().add(self._ui.grid)
Daniel Brötzmann's avatar
Daniel Brötzmann committed
92
93

        self.update()
94
        self._load_qrcode()
95
        self._ui.connect_signals(self)
Daniel Brötzmann's avatar
Daniel Brötzmann committed
96
97
98
        self.connect('destroy', self._on_destroy)
        self.show_all()

99
    def _filter_func(self, row, _user_data):
100
101
102
        search_text = self._ui.search.get_text()
        if search_text and search_text.lower() not in str(row.jid):
            return False
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
        if self._show_inactive:
            return True
        return row.active

    @staticmethod
    def _sort_func(row1, row2, _user_data):
        result = locale.strcoll(str(row1.jid), str(row2.jid))
        if result != 0:
            return result

        if row1.active != row2.active:
            return -1 if row1.active else 1

        if row1.trust != row2.trust:
            return -1 if row1.trust > row2.trust else 1
        return 0

120
121
122
    def _on_search_changed(self, _entry):
        self._ui.list.invalidate_filter()

Daniel Brötzmann's avatar
Daniel Brötzmann committed
123
    def update(self):
124
        self._ui.list.foreach(self._ui.list.remove)
Daniel Brötzmann's avatar
Daniel Brötzmann committed
125
126
127
128
129
        self._load_fingerprints(self._own_jid)
        self._load_fingerprints(self._contact.jid, self._groupchat is True)

    def _load_fingerprints(self, contact_jid, groupchat=False):
        if groupchat:
Philipp Hörist's avatar
Philipp Hörist committed
130
131
            members = list(self._omemo.backend.get_muc_members(contact_jid))
            sessions = self._omemo.backend.storage.getSessionsFromJids(members)
Daniel Brötzmann's avatar
Daniel Brötzmann committed
132
        else:
Philipp Hörist's avatar
Philipp Hörist committed
133
134
            sessions = self._omemo.backend.storage.getSessionsFromJid(contact_jid)

Philipp Hörist's avatar
Philipp Hörist committed
135
        rows = {}
136
137
138
139
        if groupchat:
            results = self._omemo.backend.storage.getMucFingerprints(members)
        else:
            results = self._omemo.backend.storage.getFingerprints(contact_jid)
Philipp Hörist's avatar
Philipp Hörist committed
140
141
142
        for result in results:
            rows[result.public_key] = KeyRow(result.recipient_id,
                                             result.public_key,
143
144
                                             result.trust,
                                             result.timestamp)
Philipp Hörist's avatar
Philipp Hörist committed
145

Philipp Hörist's avatar
Philipp Hörist committed
146
        for item in sessions:
147
148
149
            if item.record.isFresh():
                return
            identity_key = item.record.getSessionState().getRemoteIdentityKey()
Philipp Hörist's avatar
Philipp Hörist committed
150
151
152
153
154
155
            identity_key = IdentityKeyExtended(identity_key.getPublicKey())
            try:
                key_row = rows[identity_key]
            except KeyError:
                log.warning('Could not find session identitykey %s',
                            item.device_id)
156
157
                self._omemo.backend.storage.deleteSession(item.recipient_id,
                                                          item.device_id)
Philipp Hörist's avatar
Philipp Hörist committed
158
159
160
161
162
163
                continue

            key_row.active = item.active
            key_row.device_id = item.device_id

        for row in rows.values():
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
            self._ui.list.add(row)

    @staticmethod
    def _get_qrcode(jid, sid, identity_key):
        fingerprint = get_fingerprint(identity_key)
        path = os.path.join(tempfile.gettempdir(),
                            'omemo_{}.png'.format(jid))

        ver_string = 'xmpp:{}?omemo-sid-{}={}'.format(jid, sid, fingerprint)
        log.debug('Verification String: %s', ver_string)

        import qrcode
        qr = qrcode.QRCode(version=None, error_correction=2,
                           box_size=4, border=1)
        qr.add_data(ver_string)
        qr.make(fit=True)
        qr.make()

182
        fill_color = 'black'
183
184
185
        back_color = 'transparent'
        if app.css_config.prefer_dark:
            back_color = 'white'
186
187
188
189
190
191
192
193
        if V(get_distribution('qrcode').version) < V('6.0'):
            # meaning of fill_color and back_color were switched
            # before this commit in qrcode between versions 5.3
            # and 6.0: https://github.com/lincolnloop/python-qrcode/
            # commit/01f440d64b7d1f61bb75161ce118b86eca85b15c
            back_color, fill_color = fill_color, back_color

        img = qr.make_image(fill_color=fill_color, back_color=back_color)
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
        img.save(path)
        return path

    def _load_qrcode(self):
        try:
            path = self._get_qrcode(self._own_jid,
                                    self._omemo.backend.own_device,
                                    self._identity_key)
        except ImportError:
            log.exception('Failed to generate QR code')
            self._ui.qrcode.hide()
            self._ui.qrinfo.show()
        else:
            pixbuf = GdkPixbuf.Pixbuf.new_from_file(path)
            self._ui.qrcode.set_from_pixbuf(pixbuf)
            self._ui.qrcode.show()
            self._ui.qrinfo.hide()
Daniel Brötzmann's avatar
Daniel Brötzmann committed
211

212
213
214
215
    def _on_show_inactive(self, switch, param):
        self._show_inactive = switch.get_active()
        self._ui.list.invalidate_filter()

Daniel Brötzmann's avatar
Daniel Brötzmann committed
216
    def _on_destroy(self, *args):
Philipp Hörist's avatar
Philipp Hörist committed
217
        del self._windows['dialog']
Daniel Brötzmann's avatar
Daniel Brötzmann committed
218
219
220


class KeyRow(Gtk.ListBoxRow):
221
    def __init__(self, jid, identity_key, trust, last_seen):
Daniel Brötzmann's avatar
Daniel Brötzmann committed
222
223
224
        Gtk.ListBoxRow.__init__(self)
        self.set_activatable(False)

Philipp Hörist's avatar
Philipp Hörist committed
225
226
227
        self._active = False
        self._device_id = None
        self._identity_key = identity_key
Daniel Brötzmann's avatar
Daniel Brötzmann committed
228
229
        self.trust = trust
        self.jid = jid
Philipp Hörist's avatar
Philipp Hörist committed
230

231
232
        grid = Gtk.Grid()
        grid.set_column_spacing(12)
Daniel Brötzmann's avatar
Daniel Brötzmann committed
233
234

        self._trust_button = TrustButton(self)
235
        grid.attach(self._trust_button, 1, 1, 1, 3)
Daniel Brötzmann's avatar
Daniel Brötzmann committed
236

237
        jid_label = Gtk.Label(label=jid)
Daniel Brötzmann's avatar
Daniel Brötzmann committed
238
239
240
241
242
        jid_label.get_style_context().add_class('dim-label')
        jid_label.set_selectable(False)
        jid_label.set_halign(Gtk.Align.START)
        jid_label.set_valign(Gtk.Align.START)
        jid_label.set_hexpand(True)
243
        grid.attach(jid_label, 2, 1, 1, 1)
Daniel Brötzmann's avatar
Daniel Brötzmann committed
244

Philipp Hörist's avatar
Philipp Hörist committed
245
246
        self.fingerprint = Gtk.Label(
            label=self._identity_key.get_fingerprint(formatted=True))
247
        self.fingerprint.get_style_context().add_class('omemo-mono')
Philipp Hörist's avatar
Philipp Hörist committed
248
249
250
251
252
        self.fingerprint.get_style_context().add_class('omemo-inactive-color')
        self.fingerprint.set_selectable(True)
        self.fingerprint.set_halign(Gtk.Align.START)
        self.fingerprint.set_valign(Gtk.Align.START)
        self.fingerprint.set_hexpand(True)
253
        grid.attach(self.fingerprint, 2, 2, 1, 1)
Daniel Brötzmann's avatar
Daniel Brötzmann committed
254

255
256
257
258
259
260
261
262
263
264
265
266
267
268
        if last_seen is not None:
            last_seen = time.strftime('%d-%m-%Y %H:%M:%S',
                                      time.localtime(last_seen))
        else:
            last_seen = _('Never')
        last_seen_label = Gtk.Label(label=_('Last seen: %s') % last_seen)
        last_seen_label.set_halign(Gtk.Align.START)
        last_seen_label.set_valign(Gtk.Align.START)
        last_seen_label.set_hexpand(True)
        last_seen_label.get_style_context().add_class('omemo-last-seen')
        last_seen_label.get_style_context().add_class('dim-label')
        grid.attach(last_seen_label, 2, 3, 1, 1)

        self.add(grid)
Daniel Brötzmann's avatar
Daniel Brötzmann committed
269
270
271
272
        self.show_all()

    def delete_fingerprint(self, *args):
        def _remove():
Philipp Hörist's avatar
Philipp Hörist committed
273
            backend = self.get_toplevel()._omemo.backend
Daniel Brötzmann's avatar
Daniel Brötzmann committed
274

Philipp Hörist's avatar
Philipp Hörist committed
275
            backend.remove_device(self.jid, self.device_id)
Philipp Hörist's avatar
Philipp Hörist committed
276
            backend.storage.deleteSession(self.jid, self.device_id)
Philipp Hörist's avatar
Philipp Hörist committed
277
278
            backend.storage.deleteIdentity(self.jid, self._identity_key)

Daniel Brötzmann's avatar
Daniel Brötzmann committed
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
            self.get_parent().remove(self)
            self.destroy()

        buttons = {
            Gtk.ResponseType.CANCEL: DialogButton(_('Cancel')),
            Gtk.ResponseType.OK: DialogButton(_('Delete'),
                                              _remove,
                                              ButtonAction.DESTRUCTIVE),
        }

        NewConfirmationDialog(
            _('Delete Fingerprint'),
            _('Doing so will permanently delete this Fingerprint'),
            buttons,
            transient_for=self.get_toplevel())

    def set_trust(self):
        icon_name, tooltip, css_class = TRUST_DATA[self.trust]
        image = self._trust_button.get_child()
        image.set_from_icon_name(icon_name, Gtk.IconSize.MENU)
        image.get_style_context().add_class(css_class)
        image.set_tooltip_text(tooltip)

Philipp Hörist's avatar
Philipp Hörist committed
302
        backend = self.get_toplevel()._omemo.backend
Philipp Hörist's avatar
Philipp Hörist committed
303
304
305
306
307
308
309
310
        backend.storage.setTrust(self._identity_key, self.trust)

    @property
    def active(self):
        return self._active

    @active.setter
    def active(self, active):
311
        context = self.fingerprint.get_style_context()
Philipp Hörist's avatar
Philipp Hörist committed
312
        self._active = bool(active)
313
314
315
316
        if self._active:
            context.remove_class('omemo-inactive-color')
        else:
            context.add_class('omemo-inactive-color')
Philipp Hörist's avatar
Philipp Hörist committed
317
318
319
320
321
322
323
324
325
        self._trust_button.update()

    @property
    def device_id(self):
        return self._device_id

    @device_id.setter
    def device_id(self, device_id):
        self._device_id = device_id
Daniel Brötzmann's avatar
Daniel Brötzmann committed
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368


class TrustButton(Gtk.MenuButton):
    def __init__(self, row):
        Gtk.MenuButton.__init__(self)
        self._row = row
        self._css_class = ''
        self.set_popover(TrustPopver(row))
        self.set_valign(Gtk.Align.CENTER)
        self.update()

    def update(self):
        icon_name, tooltip, css_class = TRUST_DATA[self._row.trust]
        image = self.get_child()
        image.set_from_icon_name(icon_name, Gtk.IconSize.MENU)
        # Remove old color from icon
        image.get_style_context().remove_class(self._css_class)

        if not self._row.active:
            css_class = 'omemo-inactive-color'
            tooltip = '%s - %s' % (_('Inactive'), tooltip)

        image.get_style_context().add_class(css_class)
        self._css_class = css_class
        self.set_tooltip_text(tooltip)


class TrustPopver(Gtk.Popover):
    def __init__(self, row):
        Gtk.Popover.__init__(self)
        self._row = row
        self._listbox = Gtk.ListBox()
        self._listbox.set_selection_mode(Gtk.SelectionMode.NONE)
        if row.trust != Trust.VERIFIED:
            self._listbox.add(VerifiedOption())
        if row.trust != Trust.NOT_TRUSTED:
            self._listbox.add(NotTrustedOption())
        self._listbox.add(DeleteOption())
        self.add(self._listbox)
        self._listbox.show_all()
        self._listbox.connect('row-activated', self._activated)
        self.get_style_context().add_class('omemo-trust-popover')

Philipp Hörist's avatar
Philipp Hörist committed
369
    def _activated(self, _listbox, row):
Daniel Brötzmann's avatar
Daniel Brötzmann committed
370
371
372
373
374
375
376
377
378
379
        self.popdown()
        if row.type_ is None:
            self._row.delete_fingerprint()
        else:
            self._row.trust = row.type_
            self._row.set_trust()
            self.get_relative_to().update()
            self.update()

    def update(self):
Philipp Hörist's avatar
Philipp Hörist committed
380
        self._listbox.foreach(self._listbox.remove)
Daniel Brötzmann's avatar
Daniel Brötzmann committed
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
        if self._row.trust != Trust.VERIFIED:
            self._listbox.add(VerifiedOption())
        if self._row.trust != Trust.NOT_TRUSTED:
            self._listbox.add(NotTrustedOption())
        self._listbox.add(DeleteOption())


class MenuOption(Gtk.ListBoxRow):
    def __init__(self):
        Gtk.ListBoxRow.__init__(self)
        box = Gtk.Box()
        box.set_spacing(6)

        image = Gtk.Image.new_from_icon_name(self.icon,
                                             Gtk.IconSize.MENU)
        label = Gtk.Label(label=self.label)
        image.get_style_context().add_class(self.color)

        box.add(image)
        box.add(label)
        self.add(box)
        self.show_all()


class VerifiedOption(MenuOption):

    type_ = Trust.VERIFIED
    icon = 'security-high-symbolic'
    label = _('Trusted')
    color = 'success-color'

    def __init__(self):
        MenuOption.__init__(self)


class NotTrustedOption(MenuOption):

    type_ = Trust.NOT_TRUSTED
    icon = 'dialog-error-symbolic'
    label = _('Not Trusted')
    color = 'error-color'

    def __init__(self):
        MenuOption.__init__(self)


class DeleteOption(MenuOption):

    type_ = None
    icon = 'user-trash-symbolic'
    label = _('Delete')
    color = ''

    def __init__(self):
        MenuOption.__init__(self)