1# Copyright (C) 2019 Philipp Hörist <philipp AT hoerist.com>
2#
3# This file is part of nbxmpp.
4#
5# This program is free software; you can redistribute it and/or
6# modify it under the terms of the GNU General Public License
7# as published by the Free Software Foundation; either version 3
8# of the License, or (at your option) any later version.
9#
10# This program is distributed in the hope that it will be useful,
11# but WITHOUT ANY WARRANTY; without even the implied warranty of
12# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13# GNU General Public License for more details.
14#
15# You should have received a copy of the GNU General Public License
16# along with this program; If not, see <http://www.gnu.org/licenses/>.
17
18import time
19import random
20import string
21
22from nbxmpp.namespaces import Namespace
23from nbxmpp.protocol import NodeProcessed
24from nbxmpp.protocol import Node
25from nbxmpp.protocol import StanzaMalformed
26from nbxmpp.protocol import JID
27from nbxmpp.util import b64decode
28from nbxmpp.util import b64encode
29from nbxmpp.structs import StanzaHandler
30from nbxmpp.structs import PGPKeyMetadata
31from nbxmpp.structs import PGPPublicKey
32from nbxmpp.errors import MalformedStanzaError
33from nbxmpp.task import iq_request_task
34from nbxmpp.modules.date_and_time import parse_datetime
35from nbxmpp.modules.base import BaseModule
36from nbxmpp.modules.util import finalize
37from nbxmpp.modules.util import raise_if_error
38
39
40class OpenPGP(BaseModule):
41
42    _depends = {
43        'publish': 'PubSub',
44        'request_items': 'PubSub',
45    }
46
47    def __init__(self, client):
48        BaseModule.__init__(self, client)
49
50        self._client = client
51        self.handlers = [
52            StanzaHandler(name='message',
53                          callback=self._process_pubsub_openpgp,
54                          ns=Namespace.PUBSUB_EVENT,
55                          priority=16),
56            StanzaHandler(name='message',
57                          callback=self._process_openpgp_message,
58                          ns=Namespace.OPENPGP,
59                          priority=7),
60        ]
61
62    def _process_openpgp_message(self, _client, stanza, properties):
63        openpgp = stanza.getTag('openpgp', namespace=Namespace.OPENPGP)
64        if openpgp is None:
65            self._log.warning('No openpgp node found')
66            self._log.warning(stanza)
67            return
68
69        data = openpgp.getData()
70        if not data:
71            self._log.warning('No data in openpgp node found')
72            self._log.warning(stanza)
73            return
74
75        self._log.info('Encrypted message received')
76        try:
77            properties.openpgp = b64decode(data, return_type=bytes)
78        except Exception:
79            self._log.warning('b64decode failed')
80            self._log.warning(stanza)
81            return
82
83    def _process_pubsub_openpgp(self, _client, stanza, properties):
84        """
85        <item>
86            <public-keys-list xmlns='urn:xmpp:openpgp:0'>
87              <pubkey-metadata
88                v4-fingerprint='1357B01865B2503C18453D208CAC2A9678548E35'
89                date='2018-03-01T15:26:12Z'
90                />
91              <pubkey-metadata
92                v4-fingerprint='67819B343B2AB70DED9320872C6464AF2A8E4C02'
93                date='1953-05-16T12:00:00Z'
94                />
95            </public-keys-list>
96        </item>
97        """
98
99        if not properties.is_pubsub_event:
100            return
101
102        if properties.pubsub_event.node != Namespace.OPENPGP_PK:
103            return
104
105        item = properties.pubsub_event.item
106        if item is None:
107            # Retract, Deleted or Purged
108            return
109
110        try:
111            data = _parse_keylist(properties.jid, item)
112        except ValueError as error:
113            self._log.warning(error)
114            self._log.warning(stanza)
115            raise NodeProcessed
116
117        if data is None:
118            self._log.info('Received PGP keylist: %s - no keys set',
119                           properties.jid)
120            return
121
122        pubsub_event = properties.pubsub_event._replace(data=data)
123        self._log.info('Received PGP keylist: %s - %s', properties.jid, data)
124
125        properties.pubsub_event = pubsub_event
126
127    @iq_request_task
128    def set_keylist(self, keylist, public=True):
129        task = yield
130
131        access_model = 'open' if public else 'presence'
132
133        options = {
134            'pubsub#persist_items': 'true',
135            'pubsub#access_model': access_model,
136        }
137
138        self._log.info('Set keylist: %s', keylist)
139
140        result = yield self.publish(Namespace.OPENPGP_PK,
141                                    _make_keylist(keylist),
142                                    id_='current',
143                                    options=options,
144                                    force_node_options=True)
145
146        yield finalize(task, result)
147
148    @iq_request_task
149    def set_public_key(self, key, fingerprint, date, public=True):
150        task = yield
151
152        self._log.info('Set public key')
153
154        access_model = 'open' if public else 'presence'
155
156        options = {
157            'pubsub#persist_items': 'true',
158            'pubsub#access_model': access_model,
159        }
160
161        result = yield self.publish(f'{Namespace.OPENPGP_PK}:{fingerprint}',
162                                    _make_public_key(key, date),
163                                    id_='current',
164                                    options=options,
165                                    force_node_options=True)
166
167        yield finalize(task, result)
168
169    @iq_request_task
170    def request_public_key(self, jid, fingerprint):
171        task = yield
172
173        self._log.info('Request public key from: %s %s', jid, fingerprint)
174
175        items = yield self.request_items(
176            f'{Namespace.OPENPGP_PK}:{fingerprint}',
177            max_items=1,
178            jid=jid)
179
180        raise_if_error(items)
181
182        if not items:
183            yield task.set_result(None)
184
185        try:
186            key = _parse_public_key(jid, items[0])
187        except ValueError as error:
188            raise MalformedStanzaError(str(error), items)
189
190        yield key
191
192    @iq_request_task
193    def request_keylist(self, jid=None):
194        task = yield
195
196        self._log.info('Request keylist from: %s', jid)
197
198        items = yield self.request_items(
199            Namespace.OPENPGP_PK,
200            max_items=1,
201            jid=jid)
202
203        raise_if_error(items)
204
205        if not items:
206            yield task.set_result(None)
207
208        try:
209            keylist = _parse_keylist(jid, items[0])
210        except ValueError as error:
211            raise MalformedStanzaError(str(error), items)
212
213        self._log.info('Received keylist: %s', keylist)
214        yield keylist
215
216    @iq_request_task
217    def request_secret_key(self):
218        task = yield
219
220        self._log.info('Request secret key')
221
222        items = yield self.request_items(
223            Namespace.OPENPGP_SK,
224            max_items=1)
225
226        raise_if_error(items)
227
228        if not items:
229            yield task.set_result(None)
230
231        try:
232            secret_key = _parse_secret_key(items[0])
233        except ValueError as error:
234            raise MalformedStanzaError(str(error), items)
235
236        yield secret_key
237
238    @iq_request_task
239    def set_secret_key(self, secret_key):
240        task = yield
241
242        self._log.info('Set public key')
243
244        options = {
245            'pubsub#persist_items': 'true',
246            'pubsub#access_model': 'whitelist',
247        }
248
249        self._log.info('Set secret key')
250
251        result = yield self.publish(Namespace.OPENPGP_SK,
252                                    _make_secret_key(secret_key),
253                                    id_='current',
254                                    options=options,
255                                    force_node_options=True)
256
257        yield finalize(task, result)
258
259
260def parse_signcrypt(stanza):
261    '''
262    <signcrypt xmlns='urn:xmpp:openpgp:0'>
263      <to jid='juliet@example.org'/>
264      <time stamp='2014-07-10T17:06:00+02:00'/>
265      <rpad>
266        f0rm1l4n4-mT8y33j!Y%fRSrcd^ZE4Q7VDt1L%WEgR!kv
267      </rpad>
268      <payload>
269        <body xmlns='jabber:client'>
270          This is a secret message.
271        </body>
272      </payload>
273    </signcrypt>
274    '''
275    if (stanza.getName() != 'signcrypt' or
276            stanza.getNamespace() != Namespace.OPENPGP):
277        raise StanzaMalformed('Invalid signcrypt node')
278
279    to_nodes = stanza.getTags('to')
280    if not to_nodes:
281        raise StanzaMalformed('missing to nodes')
282
283    recipients = []
284    for to_node in to_nodes:
285        jid = to_node.getAttr('jid')
286        try:
287            recipients.append(JID.from_string(jid))
288        except Exception as error:
289            raise StanzaMalformed('Invalid jid: %s %s' % (jid, error))
290
291    timestamp = stanza.getTagAttr('time', 'stamp')
292    if timestamp is None:
293        raise StanzaMalformed('Invalid timestamp')
294
295    payload = stanza.getTag('payload')
296    if payload is None or payload.getChildren() is None:
297        raise StanzaMalformed('Invalid payload node')
298    return payload.getChildren(), recipients, timestamp
299
300
301def create_signcrypt_node(stanza, recipients, not_encrypted_nodes):
302    '''
303    <signcrypt xmlns='urn:xmpp:openpgp:0'>
304      <to jid='juliet@example.org'/>
305      <time stamp='2014-07-10T17:06:00+02:00'/>
306      <rpad>
307        f0rm1l4n4-mT8y33j!Y%fRSrcd^ZE4Q7VDt1L%WEgR!kv
308      </rpad>
309      <payload>
310        <body xmlns='jabber:client'>
311          This is a secret message.
312        </body>
313      </payload>
314    </signcrypt>
315    '''
316    encrypted_nodes = []
317    child_nodes = list(stanza.getChildren())
318    for node in child_nodes:
319        if (node.getName(), node.getNamespace()) not in not_encrypted_nodes:
320            if not node.getNamespace():
321                node.setNamespace(Namespace.CLIENT)
322            encrypted_nodes.append(node)
323            stanza.delChild(node)
324
325    signcrypt = Node('signcrypt', attrs={'xmlns': Namespace.OPENPGP})
326    for recipient in recipients:
327        signcrypt.addChild('to', attrs={'jid': str(recipient)})
328
329    timestamp = time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime())
330    signcrypt.addChild('time', attrs={'stamp': timestamp})
331
332    signcrypt.addChild('rpad').addData(get_rpad())
333
334    payload = signcrypt.addChild('payload')
335
336    for node in encrypted_nodes:
337        payload.addChild(node=node)
338
339    return signcrypt
340
341
342def get_rpad():
343    rpad_range = random.randint(30, 50)
344    return ''.join(
345        random.choice(string.ascii_letters) for _ in range(rpad_range))
346
347
348def create_message_stanza(stanza, encrypted_payload, with_fallback_text):
349    b64encoded_payload = b64encode(encrypted_payload)
350
351    openpgp_node = Node('openpgp', attrs={'xmlns': Namespace.OPENPGP})
352    openpgp_node.addData(b64encoded_payload)
353    stanza.addChild(node=openpgp_node)
354
355    eme_node = Node('encryption', attrs={'xmlns': Namespace.EME,
356                                         'namespace': Namespace.OPENPGP})
357    stanza.addChild(node=eme_node)
358
359    if with_fallback_text:
360        stanza.setBody(
361            '[This message is *encrypted* with OpenPGP (See :XEP:`0373`]')
362
363
364def _make_keylist(keylist):
365    item = Node('public-keys-list', {'xmlns': Namespace.OPENPGP})
366    if keylist is not None:
367        for key in keylist:
368            date = time.strftime('%Y-%m-%dT%H:%M:%SZ',
369                                 time.gmtime(key.date))
370            attrs = {'v4-fingerprint': key.fingerprint,
371                     'date': date}
372            item.addChild('pubkey-metadata', attrs=attrs)
373    return item
374
375
376def _make_public_key(key, date):
377    date = time.strftime(
378        '%Y-%m-%dT%H:%M:%SZ', time.gmtime(date))
379    item = Node('pubkey', attrs={'xmlns': Namespace.OPENPGP,
380                                 'date': date})
381    data = item.addChild('data')
382    data.addData(b64encode(key))
383    return item
384
385
386def _make_secret_key(secret_key):
387    item = Node('secretkey', {'xmlns': Namespace.OPENPGP})
388    if secret_key is not None:
389        item.setData(b64encode(secret_key))
390    return item
391
392
393def _parse_public_key(jid, item):
394    pub_key = item.getTag('pubkey', namespace=Namespace.OPENPGP)
395    if pub_key is None:
396        raise ValueError('pubkey node missing')
397
398    date = parse_datetime(pub_key.getAttr('date'), epoch=True)
399
400    data = pub_key.getTag('data')
401    if data is None:
402        raise ValueError('data node missing')
403
404    try:
405        key = b64decode(data.getData(), return_type=bytes)
406    except Exception as error:
407        raise ValueError(f'decoding error: {error}')
408
409    return PGPPublicKey(jid, key, date)
410
411
412def _parse_keylist(jid, item):
413    keylist_node = item.getTag('public-keys-list',
414                               namespace=Namespace.OPENPGP)
415    if keylist_node is None:
416        raise ValueError('public-keys-list node missing')
417
418    metadata = keylist_node.getTags('pubkey-metadata')
419    if not metadata:
420        return None
421
422    data = []
423    for key in metadata:
424        fingerprint = key.getAttr('v4-fingerprint')
425        date = key.getAttr('date')
426        if fingerprint is None or date is None:
427            raise ValueError('Invalid metadata node')
428
429        timestamp = parse_datetime(date, epoch=True)
430        if timestamp is None:
431            raise ValueError('Invalid date timestamp: %s' % date)
432
433        data.append(PGPKeyMetadata(jid, fingerprint, timestamp))
434    return data
435
436
437def _parse_secret_key(item):
438    sec_key = item.getTag('secretkey', namespace=Namespace.OPENPGP)
439    if sec_key is None:
440        raise ValueError('secretkey node missing')
441
442    data = sec_key.getData()
443    if not data:
444        raise ValueError('secretkey data missing')
445
446    try:
447        key = b64decode(data, return_type=bytes)
448    except Exception as error:
449        raise ValueError(f'decoding error: {error}')
450
451    return key
452