openpgp.py 10.3 KB
Newer Older
Philipp Hörist's avatar
Philipp Hörist committed
1
# Copyright (C) 2019 Philipp Hörist <philipp AT hoerist.com>
Philipp Hörist's avatar
Philipp Hörist committed
2
#
Philipp Hörist's avatar
Philipp Hörist committed
3
# This file is part of the OpenPGP Gajim Plugin.
Philipp Hörist's avatar
Philipp Hörist committed
4
#
Philipp Hörist's avatar
Philipp Hörist committed
5
# OpenPGP Gajim Plugin is free software; you can redistribute it and/or modify
Philipp Hörist's avatar
Philipp Hörist 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
# OpenPGP Gajim Plugin is distributed in the hope that it will be useful,
Philipp Hörist's avatar
Philipp Hörist committed
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
Philipp Hörist's avatar
Philipp Hörist committed
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
Philipp Hörist's avatar
Philipp Hörist committed
12
13
14
# 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 OpenPGP Gajim Plugin. If not, see <http://www.gnu.org/licenses/>.
Philipp Hörist's avatar
Philipp Hörist committed
16

Philipp Hörist's avatar
Philipp Hörist committed
17
import sys
Philipp Hörist's avatar
Philipp Hörist committed
18
19
20
21
import time
import logging
from pathlib import Path

22
from nbxmpp.namespaces import Namespace
Philipp Hörist's avatar
Philipp Hörist committed
23
24
25
from nbxmpp import Node
from nbxmpp import StanzaMalformed
from nbxmpp.structs import StanzaHandler
26
27
from nbxmpp.errors import StanzaError
from nbxmpp.errors import MalformedStanzaError
Philipp Hörist's avatar
Philipp Hörist committed
28
29
30
31
from nbxmpp.modules.openpgp import PGPKeyMetadata
from nbxmpp.modules.openpgp import parse_signcrypt
from nbxmpp.modules.openpgp import create_signcrypt_node
from nbxmpp.modules.openpgp import create_message_stanza
Philipp Hörist's avatar
Philipp Hörist committed
32
33
34

from gajim.common import app
from gajim.common import configpaths
35
from gajim.common.nec import NetworkEvent
Philipp Hörist's avatar
Philipp Hörist committed
36
37
38
from gajim.common.const import EncryptionData
from gajim.common.modules.base import BaseModule
from gajim.common.modules.util import event_node
Philipp Hörist's avatar
Philipp Hörist committed
39

40
from openpgp.modules.util import ENCRYPTION_NAME
Philipp Hörist's avatar
Philipp Hörist committed
41
from openpgp.modules.util import NOT_ENCRYPTED_TAGS
Philipp Hörist's avatar
Philipp Hörist committed
42
from openpgp.modules.util import Key
Philipp Hörist's avatar
Philipp Hörist committed
43
from openpgp.modules.util import add_additional_data
Philipp Hörist's avatar
Philipp Hörist committed
44
from openpgp.modules.util import DecryptionFailed
Philipp Hörist's avatar
Philipp Hörist committed
45
46
from openpgp.modules.util import prepare_stanza
from openpgp.modules.key_store import PGPContacts
Philipp Hörist's avatar
Philipp Hörist committed
47
from openpgp.backend.sql import Storage
Philipp Hörist's avatar
Philipp Hörist committed
48
49
50
51
52

if sys.platform == 'win32':
    from openpgp.backend.pygpg import PythonGnuPG as PGPBackend
else:
    from openpgp.backend.gpgme import GPGME as PGPBackend
Philipp Hörist's avatar
Philipp Hörist committed
53
54


Philipp Hörist's avatar
Philipp Hörist committed
55
log = logging.getLogger('gajim.p.openpgp')
Philipp Hörist's avatar
Philipp Hörist committed
56
57
58


# Module name
59
name = ENCRYPTION_NAME
Philipp Hörist's avatar
Philipp Hörist committed
60
61
62
zeroconf = False


Philipp Hörist's avatar
Philipp Hörist committed
63
class OpenPGP(BaseModule):
64

Philipp Hörist's avatar
Philipp Hörist committed
65
66
67
68
69
70
71
72
73
    _nbxmpp_extends = 'OpenPGP'
    _nbxmpp_methods = [
        'set_keylist',
        'request_keylist',
        'set_public_key',
        'request_public_key',
        'set_secret_key',
        'request_secret_key',
    ]
Philipp Hörist's avatar
Philipp Hörist committed
74
75

    def __init__(self, con):
Philipp Hörist's avatar
Philipp Hörist committed
76
        BaseModule.__init__(self, con)
Philipp Hörist's avatar
Philipp Hörist committed
77

Philipp Hörist's avatar
Philipp Hörist committed
78
79
80
        self.handlers = [
            StanzaHandler(name='message',
                          callback=self.decrypt_message,
81
                          ns=Namespace.OPENPGP,
Philipp Hörist's avatar
Philipp Hörist committed
82
83
                          priority=9),
        ]
Philipp Hörist's avatar
Philipp Hörist committed
84

Philipp Hörist's avatar
Philipp Hörist committed
85
        self._register_pubsub_handler(self._keylist_notification_received)
Philipp Hörist's avatar
Philipp Hörist committed
86

Philipp Hörist's avatar
Philipp Hörist committed
87
88
89
90
        self.own_jid = self._con.get_own_jid()

        own_bare_jid = self.own_jid.getBare()
        path = Path(configpaths.get('MY_DATA')) / 'openpgp' / own_bare_jid
Philipp Hörist's avatar
Philipp Hörist committed
91
        if not path.exists():
Philipp Hörist's avatar
Philipp Hörist committed
92
            path.mkdir(mode=0o700, parents=True)
Philipp Hörist's avatar
Philipp Hörist committed
93

Philipp Hörist's avatar
Philipp Hörist committed
94
        self._pgp = PGPBackend(self.own_jid, path)
Philipp Hörist's avatar
Philipp Hörist committed
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
        self._storage = Storage(path)
        self._contacts = PGPContacts(self._pgp, self._storage)
        self._fingerprint, self._date = self.get_own_key_details()
        log.info('Own Fingerprint at start: %s', self._fingerprint)

    @property
    def secret_key_available(self):
        return self._fingerprint is not None

    def get_own_key_details(self):
        self._fingerprint, self._date = self._pgp.get_own_key_details()
        return self._fingerprint, self._date

    def generate_key(self):
        self._pgp.generate_key()

Philipp Hörist's avatar
Philipp Hörist committed
111
112
    def set_public_key(self):
        log.info('%s => Publish public key', self._account)
Philipp Hörist's avatar
Philipp Hörist committed
113
        key = self._pgp.export_key(self._fingerprint)
Philipp Hörist's avatar
Philipp Hörist committed
114
115
        self._nbxmpp('OpenPGP').set_public_key(
            key, self._fingerprint, self._date)
Philipp Hörist's avatar
Philipp Hörist committed
116

Philipp Hörist's avatar
Philipp Hörist committed
117
118
119
120
121
122
123
124
125
    def request_public_key(self, jid, fingerprint):
        log.info('%s => Request public key %s - %s',
                 self._account, fingerprint, jid)
        self._nbxmpp('OpenPGP').request_public_key(
            jid,
            fingerprint,
            callback=self._public_key_received,
            user_data=fingerprint)

126
127
128
129
130
    def _public_key_received(self, task):
        fingerprint = task.get_user_data()
        try:
            result = task.finish()
        except (StanzaError, MalformedStanzaError) as error:
Philipp Hörist's avatar
Philipp Hörist committed
131
            log.error('%s => Public Key not found: %s',
132
                      self._account, error)
Philipp Hörist's avatar
Philipp Hörist committed
133
            return
Philipp Hörist's avatar
Philipp Hörist committed
134

Philipp Hörist's avatar
Philipp Hörist committed
135
136
137
        imported_key = self._pgp.import_key(result.key, result.jid)
        if imported_key is not None:
            self._contacts.set_public_key(result.jid, fingerprint)
Philipp Hörist's avatar
Philipp Hörist committed
138

Philipp Hörist's avatar
Philipp Hörist committed
139
    def set_keylist(self, keylist=None):
Philipp Hörist's avatar
Philipp Hörist committed
140
        if keylist is None:
Philipp Hörist's avatar
Philipp Hörist committed
141
142
143
            keylist = [PGPKeyMetadata(None, self._fingerprint, self._date)]
        log.info('%s => Publish keylist', self._account)
        self._nbxmpp('OpenPGP').set_keylist(keylist)
Philipp Hörist's avatar
Philipp Hörist committed
144

145
    @event_node(Namespace.OPENPGP_PK)
Philipp Hörist's avatar
Philipp Hörist committed
146
    def _keylist_notification_received(self, _con, _stanza, properties):
147
148
149
150
        if properties.pubsub_event.retracted:
            return

        keylist = properties.pubsub_event.data or []
Philipp Hörist's avatar
Philipp Hörist committed
151

Philipp Hörist's avatar
Philipp Hörist committed
152
        self._process_keylist(keylist, properties.jid)
Philipp Hörist's avatar
Philipp Hörist committed
153

Philipp Hörist's avatar
Philipp Hörist committed
154
    def request_keylist(self, jid=None):
Philipp Hörist's avatar
Philipp Hörist committed
155
156
        if jid is None:
            jid = self.own_jid
Philipp Hörist's avatar
Philipp Hörist committed
157
158
159
160
161
162
163
        log.info('%s => Fetch keylist %s', self._account, jid)

        self._nbxmpp('OpenPGP').request_keylist(
            jid,
            callback=self._keylist_received,
            user_data=jid)

164
165
166
167
168
    def _keylist_received(self, task):
        jid = task.get_user_data()
        try:
            keylist = task.finish()
        except (StanzaError, MalformedStanzaError) as error:
Philipp Hörist's avatar
Philipp Hörist committed
169
            log.error('%s => Keylist query failed: %s',
170
                      self._account, error)
Philipp Hörist's avatar
Philipp Hörist committed
171
172
            if self.own_jid.bareMatch(jid) and self._fingerprint is not None:
                self.set_keylist()
Philipp Hörist's avatar
Philipp Hörist committed
173
174
            return

Philipp Hörist's avatar
Philipp Hörist committed
175
        log.info('Keylist received from %s', jid)
176
        self._process_keylist(keylist, jid)
Philipp Hörist's avatar
Philipp Hörist committed
177

Philipp Hörist's avatar
Philipp Hörist committed
178
    def _process_keylist(self, keylist, from_jid):
Philipp Hörist's avatar
Philipp Hörist committed
179
        if not keylist:
Philipp Hörist's avatar
Philipp Hörist committed
180
            log.warning('%s => Empty keylist received from %s',
Philipp Hörist's avatar
Philipp Hörist committed
181
182
                        self._account, from_jid)
            self._contacts.process_keylist(self.own_jid, keylist)
Philipp Hörist's avatar
Philipp Hörist committed
183
184
            if self.own_jid.bareMatch(from_jid) and self._fingerprint is not None:
                self.set_keylist()
Philipp Hörist's avatar
Philipp Hörist committed
185
186
            return

Philipp Hörist's avatar
Philipp Hörist committed
187
188
        if self.own_jid.bareMatch(from_jid):
            log.info('Received own keylist')
Philipp Hörist's avatar
Philipp Hörist committed
189
190
191
192
193
194
195
196
197
            for key in keylist:
                log.info(key.fingerprint)
            for key in keylist:
                # Check if own fingerprint is published
                if key.fingerprint == self._fingerprint:
                    log.info('Own key found in keys list')
                    return
            log.info('Own key not published')
            if self._fingerprint is not None:
Philipp Hörist's avatar
Philipp Hörist committed
198
                keylist.append(Key(self._fingerprint, self._date))
Philipp Hörist's avatar
Philipp Hörist committed
199
                self.set_keylist(keylist)
Philipp Hörist's avatar
Philipp Hörist committed
200
201
202
203
204
205
206
207
            return

        missing_pub_keys = self._contacts.process_keylist(from_jid, keylist)

        for key in keylist:
            log.info(key.fingerprint)

        for fingerprint in missing_pub_keys:
Philipp Hörist's avatar
Philipp Hörist committed
208
            self.request_public_key(from_jid, fingerprint)
Philipp Hörist's avatar
Philipp Hörist committed
209

Philipp Hörist's avatar
Philipp Hörist committed
210
211
    def decrypt_message(self, _con, stanza, properties):
        if not properties.is_openpgp:
Philipp Hörist's avatar
Philipp Hörist committed
212
213
214
            return

        try:
Philipp Hörist's avatar
Philipp Hörist committed
215
            payload, fingerprint = self._pgp.decrypt(properties.openpgp)
Philipp Hörist's avatar
Philipp Hörist committed
216
217
218
219
        except DecryptionFailed as error:
            log.warning(error)
            return

Philipp Hörist's avatar
Philipp Hörist committed
220
        signcrypt = Node(node=payload)
Philipp Hörist's avatar
Philipp Hörist committed
221

Philipp Hörist's avatar
Philipp Hörist committed
222
        try:
223
            payload, recipients, _timestamp = parse_signcrypt(signcrypt)
Philipp Hörist's avatar
Philipp Hörist committed
224
225
226
        except StanzaMalformed as error:
            log.warning('Decryption failed: %s', error)
            log.warning(payload)
Philipp Hörist's avatar
Philipp Hörist committed
227
228
            return

229
        if not any(map(self.own_jid.bareMatch, recipients)):
Philipp Hörist's avatar
Philipp Hörist committed
230
            log.warning('to attr not valid')
231
            log.warning(signcrypt)
Philipp Hörist's avatar
Philipp Hörist committed
232
233
234
235
236
237
238
            return

        keys = self._contacts.get_keys(properties.jid.bare)
        fingerprints = [key.fingerprint for key in keys]
        if fingerprint not in fingerprints:
            log.warning('Invalid fingerprint on message: %s', fingerprint)
            log.warning('Expected: %s', fingerprints)
Philipp Hörist's avatar
Philipp Hörist committed
239
            return
Philipp Hörist's avatar
Philipp Hörist committed
240

Philipp Hörist's avatar
Philipp Hörist committed
241
242
        log.info('Received OpenPGP message from: %s', properties.jid)
        prepare_stanza(stanza, payload)
243

Philipp Hörist's avatar
Philipp Hörist committed
244
245
        properties.encrypted = EncryptionData({'name': ENCRYPTION_NAME,
                                               'fingerprint': fingerprint})
Philipp Hörist's avatar
Philipp Hörist committed
246
247
248
249
250
251
252
253
254
255

    def encrypt_message(self, obj, callback):
        keys = self._contacts.get_keys(obj.jid)
        if not keys:
            log.error('Droping stanza to %s, because we have no key', obj.jid)
            return

        keys += self._contacts.get_keys(self.own_jid)
        keys += [Key(self._fingerprint, None)]

256
257
258
        payload = create_signcrypt_node(obj.stanza,
                                        [obj.jid],
                                        NOT_ENCRYPTED_TAGS)
Philipp Hörist's avatar
Philipp Hörist committed
259
260
261
262
263

        encrypted_payload, error = self._pgp.encrypt(payload, keys)
        if error:
            log.error('Error: %s', error)
            app.nec.push_incoming_event(
264
265
266
267
268
                NetworkEvent('message-not-sent',
                             conn=self._con,
                             jid=obj.jid,
                             message=obj.message,
                             error=error,
Philipp Hörist's avatar
Philipp Hörist committed
269
270
                             time_=time.time(),
                             session=None))
Philipp Hörist's avatar
Philipp Hörist committed
271
272
            return

273
        create_message_stanza(obj.stanza, encrypted_payload, bool(obj.message))
274
275
276
        add_additional_data(obj.additional_data,
                            self._fingerprint)

Philipp Hörist's avatar
Philipp Hörist committed
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
        obj.encrypted = ENCRYPTION_NAME
        callback(obj)

    @staticmethod
    def print_msg_to_log(stanza):
        """ Prints a stanza in a fancy way to the log """
        log.debug('-'*15)
        stanzastr = '\n' + stanza.__str__(fancy=True)
        stanzastr = stanzastr[0:-1]
        log.debug(stanzastr)
        log.debug('-'*15)

    def get_keys(self, jid=None, only_trusted=True):
        if jid is None:
            jid = self.own_jid
        return self._contacts.get_keys(jid, only_trusted=only_trusted)

    def clear_fingerprints(self):
Philipp Hörist's avatar
Philipp Hörist committed
295
        self.set_keylist()
Philipp Hörist's avatar
Philipp Hörist committed
296
297
298
299
300
301
302
303
304

    def cleanup(self):
        self._storage.cleanup()
        self._pgp = None
        self._contacts = None


def get_instance(*args, **kwargs):
    return OpenPGP(*args, **kwargs), 'OpenPGP'