__init__.py 22.4 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 codecs
Maxime Buquet's avatar
Maxime Buquet committed
20
import asyncio
21
from slixmpp.plugins.xep_0060.stanza import Items, EventItems
22
from slixmpp.plugins.xep_0004 import Form
23
from slixmpp.plugins.base import BasePlugin, register_plugin
24
from slixmpp.exceptions import IqError, IqTimeout
25
from slixmpp.stanza import Message, Iq
26
from slixmpp.jid import JID
27

28 29 30 31 32
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

33 34
log = logging.getLogger(__name__)

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

    class DefaultOTPKPolicy:
        pass

    class SignalBackend:
        pass

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

60

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


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


69 70
def _load_device_id(data_dir: str) -> int:
    filepath = os.path.join(data_dir, 'device_id.json')
71 72 73 74 75 76 77 78 79 80 81 82
    # 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
83 84

def fp_from_ik(identity_key: bytes) -> str:
Maxime Buquet's avatar
Maxime Buquet committed
85
    """Convert identityKey to a string representation (fingerprint)"""
Maxime Buquet's avatar
Maxime Buquet committed
86
    return codecs.getencoder("hex")(identity_key)[0].decode("US-ASCII").upper()
Maxime Buquet's avatar
Maxime Buquet committed
87

88

89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106
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)


107 108 109 110 111 112 113
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'])

114 115 116 117 118 119 120 121
    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)
122 123 124 125

    return tag


126 127 128 129 130 131 132 133 134 135 136 137 138 139 140
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


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


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


class MissingOwnKey(XEP0384): pass


152 153 154
class NoAvailableSession(XEP0384): pass


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


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


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


174 175 176 177 178 179 180 181
class XEP_0384(BasePlugin):

    """
    XEP-0384: OMEMO
    """

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

190
    backend_loaded = HAS_OMEMO and HAS_OMEMO_BACKEND
191

192 193 194
    # OMEMO Bundles used for encryption
    bundles = {}  # type: Dict[str, Dict[int, ExtendedPublicBundle]]

195
    def plugin_init(self) -> None:
196
        if not self.backend_loaded:
197 198 199 200 201 202 203 204
            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)
205
            raise PluginCouldNotLoad
206

207
        if not self.data_dir:
208
            log.info("xep_0384 cannot be loaded as there is not data directory "
209 210 211
                     "specified")
            return None

212 213 214 215
        storage = self.storage_backend
        if self.storage_backend is None:
            storage = JSONFileStorage(self.data_dir)

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

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

232
        self.xmpp.add_event_handler('session_start', self.session_start)
233 234
        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)
235
        return None
236

237
    def plugin_end(self):
238 239 240
        if not self.backend_loaded:
            return

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

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

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

256 257 258 259 260 261
    def _set_node_config(
            self,
            node: str,
            persist_items: bool = True,
            access_model: Optional[str] = None,
        ) -> asyncio.Future:
262 263 264 265 266 267 268
        """
            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.

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

            To be complete, the code using this function should probably
            set the bundle node to the same access_model as the devicelist
            node.
        """
276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301
        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,
        )

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

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

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

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

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

331
        if publish_options and publish_options:
332 333 334 335 336 337
            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
338 339
        return iq

340
    async def _publish_bundle(self) -> None:
341
        if self._omemo.republish_bundle:
342
            iq = await self._generate_bundle_iq()
343 344 345 346 347 348 349 350 351 352 353 354
            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..')
355 356
                    # TODO: We should attempt setting this node to the same
                    # access_model as the devicelist node for completness.
357 358 359 360 361 362 363
                    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
364

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

373
        return _parse_bundle(self.omemo_backend, bundle)
374

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

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

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

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

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

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

        return None
Maxime Buquet's avatar
Maxime Buquet committed
409

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

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

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

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

436 437 438 439
        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()

440
        options = None
441 442 443 444 445 446 447 448 449
        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',
            })
450

451 452 453 454 455 456 457 458 459 460 461 462 463 464
        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..')
465 466 467 468 469 470 471 472
                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,
                )
473

474 475 476
    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', [])
477

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

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

484 485 486 487
    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
488 489
            as keys, and a dict with 'key', 'trust' as values that can also be
            None.
490 491 492 493 494 495 496 497 498 499 500 501 502 503 504

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

505
        return self._omemo.getTrustForJID(jid.bare)
Maxime Buquet's avatar
Maxime Buquet committed
506

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

510 511 512 513 514 515 516 517 518
    def decrypt_message(
        self,
        encrypted: Encrypted,
        sender: JID,
        allow_untrusted: bool = False,
    ) -> Optional[str]:
        header = encrypted['header']
        payload = b64dec(encrypted['payload']['value'])

519
        jid = sender.bare
520
        sid = int(header['sid'])
521 522 523 524

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

        key = Key(key)
528
        isPrekeyMessage = key['prekey'] in TRUE_VALUES
529 530
        message = b64dec(key['value'])
        iv = b64dec(header['iv']['value'])
531

532 533
        # XXX: 'cipher' is part of KeyTransportMessages and is used when no payload
        # is passed. We do not implement this yet.
534
        try:
535
            body = self._omemo.decryptMessage(
536 537 538 539 540 541
                jid,
                sid,
                iv,
                message,
                isPrekeyMessage,
                payload,
542
                allow_untrusted=allow_untrusted,
543 544
            )
            return body
545
        except (omemo.exceptions.NoSessionException,):
546 547 548 549
            # 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.
550
            raise NoAvailableSession(jid, sid)
551 552 553
        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
554
            if exn.problem == 'untrusted':
555 556
                raise UntrustedException(exn.bare_jid, exn.device, exn.ik)
            raise
557 558
        finally:
            asyncio.ensure_future(self._publish_bundle())
559

560 561 562 563 564 565
    async def encrypt_message(
        self,
        plaintext: str,
        recipients: List[JID],
        expect_problems: Optional[Dict[JID, List[int]]] = None,
    ) -> Encrypted:
566 567 568 569 570 571 572 573 574 575
        """
        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]

576
        old_errors = None  # type: Optional[List[Tuple[Exception, Any, Any]]]
577
        while True:
578 579
            # Try to encrypt and resolve errors until there is no error at all
            # or if we hit the same set of errors.
580 581
            errors = []  # type: List[omemo.exceptions.OMEMOException]

582 583 584
            if expect_problems is not None:
                expect_problems = {jid.bare: did for (jid, did) in expect_problems.items()}

585 586 587 588
            try:
                encrypted = self._omemo.encryptMessage(
                    recipients,
                    plaintext.encode('utf-8'),
589
                    self.bundles,
590
                    expect_problems=expect_problems,
591 592
                )
                return _generate_encrypted_payload(encrypted)
593 594
            except omemo.exceptions.EncryptionProblemsException as exn:
                errors = exn.problems
595

596
            if errors == old_errors:
597
                raise EncryptionPrepareException(errors)
598 599 600

            old_errors = errors

601 602
            for exn in errors:
                if isinstance(exn, omemo.exceptions.NoDevicesException):
603
                    await self._fetch_device_list(JID(exn.bare_jid))
604 605
                elif isinstance(exn, omemo.exceptions.MissingBundleException):
                    bundle = await self._fetch_bundle(exn.bare_jid, exn.device)
606
                    if bundle is not None:
607
                        devices = self.bundles.setdefault(exn.bare_jid, {})
608
                        devices[exn.device] = bundle
609 610
                elif isinstance(exn, omemo.exceptions.TrustException):
                    # On TrustException, there are two possibilities.
611 612 613 614 615
                    # 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
616
                    if exn.problem == 'undecided':
617
                        raise UndecidedException(exn.bare_jid, exn.device, exn.ik)
618 619
                    distrusted_jid = JID(exn.bare_jid)
                    expect_problems.setdefault(distrusted_jid, []).append(exn.device)
620 621 622 623 624 625 626 627
                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
628
                    # decide what to do with it, as there isn't much we can do if
629 630
                    # other issues can't be resolved.
                    continue
631 632 633


register_plugin(XEP_0384)