__init__.py 22.8 KB
Newer Older
1 2 3
#!/usr/bin/env python3
# -*- coding: utf-8 -*-

4
"""
5 6 7
    Slixmpp OMEMO plugin
    Copyright (C) 2019 Maxime “pep” Buquet <pep@bouah.net>
    This file is part of slixmpp-omemo.
8 9 10 11 12 13

    See the file LICENSE for copying permission.
"""

import logging

14
from typing import Any, Dict, List, Optional, Set, Tuple, Union
15

16 17
import os
import json
Maxime Buquet's avatar
Maxime Buquet committed
18
import base64
Maxime Buquet's avatar
Maxime Buquet committed
19
import asyncio
20
from slixmpp.plugins.xep_0060.stanza import Items, EventItems
21
from slixmpp.plugins.xep_0004 import Form
22
from slixmpp.plugins.base import BasePlugin, register_plugin
23
from slixmpp.exceptions import IqError, IqTimeout
24
from slixmpp.stanza import Message, Iq
25
from slixmpp.jid import JID
26

27 28 29 30 31
from .version import __version__, __version_info__
from .stanza import OMEMO_BASE_NS
from .stanza import OMEMO_DEVICES_NS, OMEMO_BUNDLES_NS
from .stanza import Bundle, Devices, Device, Encrypted, Key, PreKeyPublic

32 33
log = logging.getLogger(__name__)

34 35
HAS_OMEMO = False
HAS_OMEMO_BACKEND = False
36
try:
37
    import omemo.exceptions
38
    from omemo import SessionManager, ExtendedPublicBundle, DefaultOTPKPolicy
39
    from omemo.util import generateDeviceID
40
    from omemo.implementations import JSONFileStorage
41
    from omemo.backends import Backend
42
    HAS_OMEMO = True
43
    from omemo_backend_signal import BACKEND as SignalBackend
44
    HAS_OMEMO_BACKEND = True
45
except (ImportError,):
46 47 48 49 50 51 52 53 54
    class Backend:
        pass

    class DefaultOTPKPolicy:
        pass

    class SignalBackend:
        pass

55
TRUE_VALUES = {True, 'true', '1'}
56
PUBLISH_OPTIONS_NODE = 'http://jabber.org/protocol/pubsub#publish-options'
57
PUBSUB_ERRORS = 'http://jabber.org/protocol/pubsub#errors'
58

59

60
def b64enc(data: bytes) -> str:
Maxime Buquet's avatar
Maxime Buquet committed
61 62 63
    return base64.b64encode(bytes(bytearray(data))).decode('ASCII')


64
def b64dec(data: str) -> bytes:
65
    return base64.b64decode(data)
Maxime Buquet's avatar
Maxime Buquet committed
66 67


68 69
def _load_device_id(data_dir: str) -> int:
    filepath = os.path.join(data_dir, 'device_id.json')
70 71 72 73 74 75 76 77 78 79 80 81
    # Try reading file first, decoding, and if file was empty generate
    # new DeviceID
    try:
        with open(filepath, 'r') as f:
            did = json.load(f)
    except (FileNotFoundError, json.JSONDecodeError):
        did = generateDeviceID()
        with open(filepath, 'w') as f:
            json.dump(did, f)

    return did

Maxime Buquet's avatar
Maxime Buquet committed
82 83

def fp_from_ik(identity_key: bytes) -> str:
Maxime Buquet's avatar
Maxime Buquet committed
84
    """Convert identityKey to a string representation (fingerprint)"""
85
    return ":".join("{:02X}".format(octet) for octet in identity_key)
Maxime Buquet's avatar
Maxime Buquet committed
86

87

88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105
def _parse_bundle(backend: Backend, bundle: Bundle) -> ExtendedPublicBundle:
    identity_key = b64dec(bundle['identityKey']['value'].strip())
    spk = {
        'id': int(bundle['signedPreKeyPublic']['signedPreKeyId']),
        'key': b64dec(bundle['signedPreKeyPublic']['value'].strip()),
    }
    spk_signature = b64dec(bundle['signedPreKeySignature']['value'].strip())

    otpks = []
    for prekey in bundle['prekeys']:
        otpks.append({
            'id': int(prekey['preKeyId']),
            'key': b64dec(prekey['value'].strip()),
        })

    return ExtendedPublicBundle.parse(backend, identity_key, spk, spk_signature, otpks)


106 107 108 109 110 111 112
def _generate_encrypted_payload(encrypted) -> Encrypted:
    tag = Encrypted()

    tag['header']['sid'] = str(encrypted['sid'])
    tag['header']['iv']['value'] = b64enc(encrypted['iv'])
    tag['payload']['value'] = b64enc(encrypted['payload'])

113 114 115 116 117 118 119 120
    for bare_jid, devices in encrypted['keys'].items():
        for rid, device in devices.items():
            key = Key()
            key['value'] = b64enc(device['data'])
            key['rid'] = str(rid)
            if device['pre_key']:
                key['prekey'] = '1'
            tag['header'].append(key)
121 122 123 124

    return tag


125 126 127 128 129 130 131 132 133 134 135 136 137 138 139
def _make_publish_options_form(fields: Dict[str, Any]) -> Form:
    options = Form()
    options['type'] = 'submit'
    options.add_field(
        var='FORM_TYPE',
        ftype='hidden',
        value='http://jabber.org/protocol/pubsub#publish-options',
    )

    for var, value in fields.items():
        options.add_field(var=var, value=value)

    return options


140 141 142 143
# XXX: This should probably be moved in plugins/base.py?
class PluginCouldNotLoad(Exception): pass


144 145 146 147 148 149 150
# Generic exception
class XEP0384(Exception): pass


class MissingOwnKey(XEP0384): pass


151 152 153
class NoAvailableSession(XEP0384): pass


154 155 156
class EncryptionPrepareException(XEP0384):
    def __init__(self, errors):
        self.errors = errors
157 158


159 160 161 162 163
class UntrustedException(XEP0384):
    def __init__(self, bare_jid, device, ik):
        self.bare_jid = JID(bare_jid)
        self.device = device
        self.ik = ik
164 165


166 167 168 169 170
class UndecidedException(XEP0384):
    def __init__(self, bare_jid, device, ik):
        self.bare_jid = JID(bare_jid)
        self.device = device
        self.ik = ik
171 172


173 174 175 176
class ErroneousPayload(XEP0384):
    """To be raised when the payload is not of the form we expect"""


177 178 179 180 181 182 183 184
class XEP_0384(BasePlugin):

    """
    XEP-0384: OMEMO
    """

    name = 'xep_0384'
    description = 'XEP-0384 OMEMO'
185
    dependencies = {'xep_0004', 'xep_0060', 'xep_0163'}
Maxime Buquet's avatar
Maxime Buquet committed
186
    default_config = {
187
        'data_dir': None,
188
        'storage_backend': None,
189
        'otpk_policy': DefaultOTPKPolicy,
190
        'omemo_backend': SignalBackend,
Maxime Buquet's avatar
Maxime Buquet committed
191 192
    }

193
    backend_loaded = HAS_OMEMO and HAS_OMEMO_BACKEND
194

195 196 197
    # OMEMO Bundles used for encryption
    bundles = {}  # type: Dict[str, Dict[int, ExtendedPublicBundle]]

198
    def plugin_init(self) -> None:
199
        if not self.backend_loaded:
200 201 202 203 204 205 206 207
            log_str = ("xep_0384 cannot be loaded as the backend omemo library "
                       "is not available. ")
            if not HAS_OMEMO_BACKEND:
                log_str += ("Make sure you have a python OMEMO backend "
                            "(python-omemo-backend-signal) installed")
            else:
                log_str += "Make sure you have the python OMEMO library installed."
            log.error(log_str)
208
            raise PluginCouldNotLoad
209

210
        if not self.data_dir:
211 212
            raise PluginCouldNotLoad("xep_0384 cannot be loaded as there is "
                                     "no data directory specified.")
213

214 215 216 217
        storage = self.storage_backend
        if self.storage_backend is None:
            storage = JSONFileStorage(self.data_dir)

218
        otpkpolicy = self.otpk_policy
219
        bare_jid = self.xmpp.boundjid.bare
220
        self._device_id = _load_device_id(self.data_dir)
221 222

        try:
223 224 225
            self._omemo = SessionManager.create(
                storage,
                otpkpolicy,
226
                self.omemo_backend,
227 228 229
                bare_jid,
                self._device_id,
            )
230
        except:
231
            log.error("Couldn't load the OMEMO object; ¯\\_(ツ)_/¯")
232
            raise PluginCouldNotLoad
233

234
        self.xmpp.add_event_handler('session_start', self.session_start)
235 236
        self.xmpp['xep_0060'].map_node_event(OMEMO_DEVICES_NS, 'omemo_device_list')
        self.xmpp.add_event_handler('omemo_device_list_publish', self._receive_device_list)
237
        return None
238

239
    def plugin_end(self):
240 241 242
        if not self.backend_loaded:
            return

243
        self.xmpp.remove_event_handler('session_start', self.session_start)
244
        self.xmpp.remove_event_handler('omemo_device_list_publish', self._receive_device_list)
245 246
        self.xmpp['xep_0163'].remove_interest(OMEMO_DEVICES_NS)

247
    async def session_start(self, _jid):
248 249
        if self.backend_loaded:
            self.xmpp['xep_0163'].add_interest(OMEMO_DEVICES_NS)
250 251 252 253
            await asyncio.wait([
                self._set_device_list(),
                self._publish_bundle(),
            ])
254

255
    def my_device_id(self) -> int:
lumi's avatar
lumi committed
256 257
        return self._device_id

258 259 260 261 262 263
    def _set_node_config(
            self,
            node: str,
            persist_items: bool = True,
            access_model: Optional[str] = None,
        ) -> asyncio.Future:
264 265 266 267 268 269 270
        """
            Sets OMEMO devicelist or bundle node configuration.

            This function is meant to be used once we've tried publish options
            and they came back with precondition-not-met. This means an
            existing node is not using our defaults.

271 272
            By default this function will be overwriting pubsub#persist_items
            only, leaving pubsub#access_model as it was.
273 274 275 276 277

            To be complete, the code using this function should probably
            set the bundle node to the same access_model as the devicelist
            node.
        """
278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303
        form = Form()
        form['type'] = 'submit'
        form.add_field(
            var='FORM_TYPE',
            ftype='hidden',
            value='http://jabber.org/protocol/pubsub#node_config',
        )
        if persist_items:
            form.add_field(
                var='pubsub#persist_items',
                ftype='boolean',
                value=True,
            )
        if access_model is not None:
            form.add_field(
                var='FORM_TYPE',
                ftype='text-single',
                value=access_model,
            )

        return self.xmpp['xep_0060'].set_node_config(
            self.xmpp.boundjid.bare,
            node,
            form,
        )

304
    async def _generate_bundle_iq(self, publish_options: bool = True) -> Iq:
305
        bundle = self._omemo.public_bundle.serialize(self.omemo_backend)
Maxime Buquet's avatar
Maxime Buquet committed
306

307
        jid = self.xmpp.boundjid
308 309
        disco = await self.xmpp['xep_0030'].get_info(jid.bare)
        publish_options = PUBLISH_OPTIONS_NODE in disco['disco_info'].get_features()
310

Maxime Buquet's avatar
Maxime Buquet committed
311
        iq = self.xmpp.Iq(stype='set')
312

Maxime Buquet's avatar
Maxime Buquet committed
313 314 315
        publish = iq['pubsub']['publish']
        publish['node'] = '%s:%d' % (OMEMO_BUNDLES_NS, self._device_id)
        payload = publish['item']['bundle']
316
        signedPreKeyPublic = b64enc(bundle['spk']['key'])
Maxime Buquet's avatar
Maxime Buquet committed
317
        payload['signedPreKeyPublic']['value'] = signedPreKeyPublic
318
        payload['signedPreKeyPublic']['signedPreKeyId'] = str(bundle['spk']['id'])
Maxime Buquet's avatar
Maxime Buquet committed
319
        payload['signedPreKeySignature']['value'] = b64enc(
320
            bundle['spk_signature']
Maxime Buquet's avatar
Maxime Buquet committed
321
        )
322
        identityKey = b64enc(bundle['ik'])
Maxime Buquet's avatar
Maxime Buquet committed
323 324 325
        payload['identityKey']['value'] = identityKey

        prekeys = []
326
        for otpk in bundle['otpks']:
Maxime Buquet's avatar
Maxime Buquet committed
327 328
            prekey = PreKeyPublic()
            prekey['preKeyId'] = str(otpk['id'])
329
            prekey['value'] = b64enc(otpk['key'])
Maxime Buquet's avatar
Maxime Buquet committed
330 331 332
            prekeys.append(prekey)
        payload['prekeys'] = prekeys

333
        if publish_options and publish_options:
334 335 336 337 338 339
            options = _make_publish_options_form({
                'pubsub#persist_items': True,
                'pubsub#access_model': 'open',
            })
            iq['pubsub']['publish_options'] = options

Maxime Buquet's avatar
Maxime Buquet committed
340 341
        return iq

342
    async def _publish_bundle(self) -> None:
343
        if self._omemo.republish_bundle:
344
            iq = await self._generate_bundle_iq()
345 346 347 348 349 350 351 352 353 354 355 356
            try:
                await iq.send()
            except IqError as e:
                # TODO: Slixmpp should handle pubsub#errors so we don't have to
                # fish the element ourselves
                precondition = e.iq['error'].xml.find(
                    '{%s}%s' % (PUBSUB_ERRORS, 'precondition-not-met'),
                )
                if precondition is not None:
                    log.debug('The node we tried to publish was already '
                              'existing with a different configuration. '
                              'Trying to configure manually..')
357 358
                    # TODO: We should attempt setting this node to the same
                    # access_model as the devicelist node for completness.
359 360 361 362 363 364 365
                    try:
                        await self._set_node_config(OMEMO_BUNDLES_NS)
                    except IqError:
                        log.debug('Failed to set node to persistent after precondition-not-met')
                        raise
                    iq = await self._generate_bundle_iq(publish_options=False)
                    await iq.send()
Maxime Buquet's avatar
Maxime Buquet committed
366

367
    async def _fetch_bundle(self, jid: str, device_id: int) -> Optional[ExtendedPublicBundle]:
368
        node = '%s:%d' % (OMEMO_BUNDLES_NS, device_id)
369 370 371 372
        try:
            iq = await self.xmpp['xep_0060'].get_items(jid, node)
        except (IqError, IqTimeout):
            return None
373 374
        bundle = iq['pubsub']['items']['item']['bundle']

375
        return _parse_bundle(self.omemo_backend, bundle)
376

377
    async def _fetch_device_list(self, jid: JID) -> None:
378
        """Manually query PEP OMEMO_DEVICES_NS nodes"""
379
        iq = await self.xmpp['xep_0060'].get_items(jid.full, OMEMO_DEVICES_NS)
380 381
        return await self._read_device_list(jid, iq['pubsub']['items'])

382 383
    def _store_device_ids(self, jid: str, items: Union[Items, EventItems]) -> None:
        """Store Device list"""
384
        device_ids = []  # type: List[int]
385
        items = list(items)
386 387
        if items:
            device_ids = [int(d['id']) for d in items[0]['devices']]
388
        return self._omemo.newDeviceList(str(jid), device_ids)
Maxime Buquet's avatar
Maxime Buquet committed
389

390
    def _receive_device_list(self, msg: Message) -> None:
391
        """Handler for received PEP OMEMO_DEVICES_NS payloads"""
392
        asyncio.ensure_future(
Maxime Buquet's avatar
Maxime Buquet committed
393
            self._read_device_list(msg['from'], msg['pubsub_event']['items']),
394
        )
395

396
    async def _read_device_list(self, jid: JID, items: Union[Items, EventItems]) -> None:
397
        """Read items and devices if we need to set the device list again or not"""
398 399
        bare_jid = jid.bare
        self._store_device_ids(bare_jid, items)
400

401
        items = list(items)
402 403 404
        device_ids = []
        if items:
            device_ids = [int(d['id']) for d in items[0]['devices']]
405

406
        if bare_jid == self.xmpp.boundjid.bare and \
407
           self._device_id not in device_ids:
408 409 410
            await self._set_device_list()

        return None
Maxime Buquet's avatar
Maxime Buquet committed
411

412 413
    async def _set_device_list(self, device_ids: Optional[Set[int]] = None) -> None:
        own_jid = self.xmpp.boundjid
lumi's avatar
lumi committed
414 415 416

        try:
            iq = await self.xmpp['xep_0060'].get_items(
417
                own_jid.bare, OMEMO_DEVICES_NS,
lumi's avatar
lumi committed
418 419
            )
            items = iq['pubsub']['items']
420
            self._store_device_ids(own_jid.bare, items)
lumi's avatar
lumi committed
421 422
        except IqError as iq_err:
            if iq_err.condition == "item-not-found":
423
                self._store_device_ids(own_jid.bare, [])
lumi's avatar
lumi committed
424 425
            else:
                return  # XXX: Handle this!
Maxime Buquet's avatar
Maxime Buquet committed
426

427 428
        if device_ids is None:
            device_ids = self.get_device_list(own_jid)
Maxime Buquet's avatar
Maxime Buquet committed
429 430

        devices = []
431
        for i in device_ids:
Maxime Buquet's avatar
Maxime Buquet committed
432 433 434 435 436 437
            d = Device()
            d['id'] = str(i)
            devices.append(d)
        payload = Devices()
        payload['devices'] = devices

438 439 440 441
        jid = self.xmpp.boundjid
        disco = await self.xmpp['xep_0030'].get_info(jid.bare)
        publish_options = PUBLISH_OPTIONS_NODE in disco['disco_info'].get_features()

442
        options = None
443 444 445 446 447 448 449 450 451
        if publish_options:
            options = _make_publish_options_form({
                'pubsub#persist_items': True,
                # Everybody will be able to encrypt for us, without having to add
                # us into their roster. This obviously leaks the number of devices
                # and the associated metadata of us pushing new device lists every
                # so often.
                'pubsub#access_model': 'open',
            })
452

453 454 455 456 457 458 459 460 461 462 463 464 465 466
        try:
            await self.xmpp['xep_0060'].publish(
                own_jid.bare, OMEMO_DEVICES_NS, payload=payload, options=options,
            )
        except IqError as e:
            # TODO: Slixmpp should handle pubsub#errors so we don't have to
            # fish the element ourselves
            precondition = e.iq['error'].xml.find(
                '{%s}%s' % (PUBSUB_ERRORS, 'precondition-not-met'),
            )
            if precondition is not None:
                log.debug('The node we tried to publish was already '
                          'existing with a different configuration. '
                          'Trying to configure manually..')
467 468 469 470 471 472 473 474
                try:
                    await self._set_node_config(OMEMO_DEVICES_NS)
                except IqError:
                    log.debug('Failed to set node to persistent after precondition-not-met')
                    raise
                await self.xmpp['xep_0060'].publish(
                    own_jid.bare, OMEMO_DEVICES_NS, payload=payload,
                )
475

476 477 478
    def get_device_list(self, jid: JID) -> List[str]:
        """Return active device ids. Always contains our own device id."""
        return self._omemo.getDevices(jid.bare).get('active', [])
479

480
    def trust(self, jid: JID, device_id: int, ik: bytes) -> None:
481
        self._omemo.setTrust(jid.bare, device_id, ik, True)
482 483

    def distrust(self, jid: JID, device_id: int, ik: bytes) -> None:
484
        self._omemo.setTrust(jid.bare, device_id, ik, False)
485

486 487 488 489
    def get_trust_for_jid(self, jid: JID) -> Dict[str, List[Optional[Dict[str, Any]]]]:
        """
            Fetches trust for JID. The returned dictionary will contain active
            and inactive devices. Each of these dict will contain device ids
490 491
            as keys, and a dict with 'key', 'trust' as values that can also be
            None.
492 493 494 495 496 497 498 499 500 501 502 503 504 505 506

            Example:
            {
                'active': {
                    123456: {
                        'key': bytes,
                        'trust': bool,
                    }
                }
                'inactive': {
                    234567: None,
                }
            }
        """

507
        return self._omemo.getTrustForJID(jid.bare)
Maxime Buquet's avatar
Maxime Buquet committed
508

509
    def is_encrypted(self, msg: Message) -> bool:
510 511
        return msg.xml.find('{%s}encrypted' % OMEMO_BASE_NS) is not None

512 513 514 515 516 517 518
    def decrypt_message(
        self,
        encrypted: Encrypted,
        sender: JID,
        allow_untrusted: bool = False,
    ) -> Optional[str]:
        header = encrypted['header']
519 520
        if encrypted['payload']['value'] is None:
            raise ErroneousPayload('The payload element was empty')
521 522
        payload = b64dec(encrypted['payload']['value'])

523
        jid = sender.bare
524
        sid = int(header['sid'])
525 526 527 528

        key = header.xml.find("{%s}key[@rid='%s']" % (
            OMEMO_BASE_NS, self._device_id))
        if key is None:
529
            raise MissingOwnKey("Encrypted message is not for us")
530 531

        key = Key(key)
532
        isPrekeyMessage = key['prekey'] in TRUE_VALUES
533 534
        if key['value'] is None:
            raise ErroneousPayload('The key element was empty')
535
        message = b64dec(key['value'])
536 537
        if header['iv']['value'] is None:
            raise ErroneousPayload('The iv element was empty')
538
        iv = b64dec(header['iv']['value'])
539

540 541
        # XXX: 'cipher' is part of KeyTransportMessages and is used when no payload
        # is passed. We do not implement this yet.
542
        try:
543
            body = self._omemo.decryptMessage(
544 545 546 547 548 549
                jid,
                sid,
                iv,
                message,
                isPrekeyMessage,
                payload,
550
                allow_untrusted=allow_untrusted,
551 552
            )
            return body
553
        except (omemo.exceptions.NoSessionException,):
554 555 556 557
            # This might happen when the sender is sending using a session
            # that we don't know about (deleted session storage, etc.). In
            # this case we can't decrypt the message and it's going to be lost
            # in any case, but we want to tell the user, always.
558
            raise NoAvailableSession(jid, sid)
559 560 561
        except (omemo.exceptions.TrustException,) as exn:
            if exn.problem == 'undecided':
                raise UndecidedException(exn.bare_jid, exn.device, exn.ik)
Maxime Buquet's avatar
Maxime Buquet committed
562
            if exn.problem == 'untrusted':
563 564
                raise UntrustedException(exn.bare_jid, exn.device, exn.ik)
            raise
565 566
        finally:
            asyncio.ensure_future(self._publish_bundle())
567

568 569 570 571 572 573
    async def encrypt_message(
        self,
        plaintext: str,
        recipients: List[JID],
        expect_problems: Optional[Dict[JID, List[int]]] = None,
    ) -> Encrypted:
574 575 576 577 578 579 580 581 582 583
        """
        Returns an encrypted payload to be placed into a message.

        The API for getting an encrypted payload consists of trying first
        and fixing errors progressively. The actual sending happens once the
        application (us) thinks we're good to go.
        """

        recipients = [jid.bare for jid in recipients]

584
        old_errors = None  # type: Optional[List[Tuple[Exception, Any, Any]]]
585
        while True:
586 587
            # Try to encrypt and resolve errors until there is no error at all
            # or if we hit the same set of errors.
588 589
            errors = []  # type: List[omemo.exceptions.OMEMOException]

590 591 592 593
            if expect_problems is None:
                expect_problems = {}

            expect_problems = {jid.bare: did for (jid, did) in expect_problems.items()}
594

595 596 597 598
            try:
                encrypted = self._omemo.encryptMessage(
                    recipients,
                    plaintext.encode('utf-8'),
599
                    self.bundles,
600
                    expect_problems=expect_problems,
601 602
                )
                return _generate_encrypted_payload(encrypted)
603 604
            except omemo.exceptions.EncryptionProblemsException as exception:
                errors = exception.problems
605

606
            if errors == old_errors:
607
                raise EncryptionPrepareException(errors)
608 609 610

            old_errors = errors

611 612
            for exn in errors:
                if isinstance(exn, omemo.exceptions.NoDevicesException):
613
                    await self._fetch_device_list(JID(exn.bare_jid))
614 615
                elif isinstance(exn, omemo.exceptions.MissingBundleException):
                    bundle = await self._fetch_bundle(exn.bare_jid, exn.device)
616
                    if bundle is not None:
617
                        devices = self.bundles.setdefault(exn.bare_jid, {})
618
                        devices[exn.device] = bundle
619 620
                elif isinstance(exn, omemo.exceptions.TrustException):
                    # On TrustException, there are two possibilities.
621 622 623 624 625
                    # Either trust has not been explicitely set yet, and is
                    # 'undecided', or the device is explicitely not
                    # trusted. When undecided, we need to ask our user to make
                    # a choice. If untrusted, then we can safely tell the
                    # OMEMO lib to not encrypt to this device
626
                    if exn.problem == 'undecided':
627
                        raise UndecidedException(exn.bare_jid, exn.device, exn.ik)
628 629
                    distrusted_jid = JID(exn.bare_jid)
                    expect_problems.setdefault(distrusted_jid, []).append(exn.device)
630 631 632 633 634 635 636 637
                elif isinstance(exn, omemo.exceptions.NoEligibleDevicesException):
                    # This error is returned by the library to specify that
                    # encryption is not possible to any device of a user.
                    # This always comes with a more specific exception, (empty
                    # device list, missing bundles, trust issues, etc.).
                    # This does the heavy lifting of state management, and
                    # seeing if it's possible to encrypt at all, or not.
                    # This exception is only passed to the user, that should
Maxime Buquet's avatar
Maxime Buquet committed
638
                    # decide what to do with it, as there isn't much we can do if
639 640
                    # other issues can't be resolved.
                    continue
641 642 643


register_plugin(XEP_0384)