comparison sat/plugins/plugin_xep_0384.py @ 3911:8289ac1b34f4

plugin XEP-0384: Fully reworked to adjust to the reworked python-omemo: - support for both (modern) OMEMO under the `urn:xmpp:omemo:2` namespace and (legacy) OMEMO under the `eu.siacs.conversations.axolotl` namespace - maintains one identity across both versions of OMEMO - migrates data from the old plugin - includes more features for protocol stability - uses SCE for modern OMEMO - fully type-checked, linted and format-checked - added type hints to various pieces of backend code used by the plugin - added stubs for some Twisted APIs used by the plugin under stubs/ (use `export MYPYPATH=stubs/` before running mypy) - core (xmpp): enabled `send` trigger and made it an asyncPoint fix 375
author Syndace <me@syndace.dev>
date Tue, 23 Aug 2022 21:06:24 +0200
parents cc653b2685f0
children 4cb38c8312a1
comparison
equal deleted inserted replaced
3910:199598223f82 3911:8289ac1b34f4
1 #!/usr/bin/env python3 1 #!/usr/bin/env python3
2 2
3 # SAT plugin for OMEMO encryption 3 # Libervia plugin for OMEMO encryption
4 # Copyright (C) 2009-2021 Jérôme Poisson (goffi@goffi.org) 4 # Copyright (C) 2022-2022 Tim Henkes (me@syndace.dev)
5 5
6 # This program is free software: you can redistribute it and/or modify 6 # This program is free software: you can redistribute it and/or modify
7 # it under the terms of the GNU Affero General Public License as published by 7 # it under the terms of the GNU Affero General Public License as published by
8 # the Free Software Foundation, either version 3 of the License, or 8 # the Free Software Foundation, either version 3 of the License, or
9 # (at your option) any later version. 9 # (at your option) any later version.
14 # GNU Affero General Public License for more details. 14 # GNU Affero General Public License for more details.
15 15
16 # You should have received a copy of the GNU Affero General Public License 16 # You should have received a copy of the GNU Affero General Public License
17 # along with this program. If not, see <http://www.gnu.org/licenses/>. 17 # along with this program. If not, see <http://www.gnu.org/licenses/>.
18 18
19 import enum
19 import logging 20 import logging
20 import random 21 import time
21 import base64 22 from typing import (
22 from functools import partial 23 Any, Callable, Dict, FrozenSet, List, Literal, NamedTuple, Optional, Set, Type, cast
24 )
25 import uuid
26 import xml.etree.ElementTree as ET
23 from xml.sax.saxutils import quoteattr 27 from xml.sax.saxutils import quoteattr
28
29 from typing_extensions import Never, assert_never
30 from wokkel import muc, pubsub # type: ignore[import]
31
32 from sat.core import exceptions
33 from sat.core.constants import Const as C
34 from sat.core.core_types import MessageData, SatXMPPEntity
24 from sat.core.i18n import _, D_ 35 from sat.core.i18n import _, D_
25 from sat.core.constants import Const as C 36 from sat.core.log import getLogger, Logger
26 from sat.core.log import getLogger 37 from sat.core.sat_main import SAT
27 from sat.core import exceptions 38 from sat.core.xmpp import SatXMPPClient
28 from twisted.internet import defer, reactor 39 from sat.memory import persistent
40 from sat.plugins.plugin_misc_text_commands import TextCommands
41 from sat.plugins.plugin_xep_0045 import XEP_0045
42 from sat.plugins.plugin_xep_0060 import XEP_0060
43 from sat.plugins.plugin_xep_0163 import XEP_0163
44 from sat.plugins.plugin_xep_0334 import XEP_0334
45 from sat.plugins.plugin_xep_0359 import XEP_0359
46 from sat.plugins.plugin_xep_0420 import XEP_0420, SCEAffixPolicy, SCEProfile
47 from sat.tools import xml_tools
48 from twisted.internet import defer
49 from twisted.words.protocols.jabber import error, jid
29 from twisted.words.xish import domish 50 from twisted.words.xish import domish
30 from twisted.words.protocols.jabber import jid 51
31 from twisted.words.protocols.jabber import error as jabber_error
32 from sat.memory import persistent
33 from sat.tools import xml_tools
34 try: 52 try:
35 import omemo 53 import omemo
36 from omemo import exceptions as omemo_excpt 54 import omemo.identity_key_pair
37 from omemo.extendedpublicbundle import ExtendedPublicBundle 55 import twomemo
38 except ImportError: 56 import twomemo.etree
57 import oldmemo
58 import oldmemo.etree
59 import oldmemo.migrations
60 from xmlschema import XMLSchemaValidationError
61
62 # An explicit version check of the OMEMO libraries should not be required here, since
63 # the stored data is fully versioned and the library will complain if a downgrade is
64 # attempted.
65 except ImportError as import_error:
39 raise exceptions.MissingModule( 66 raise exceptions.MissingModule(
40 'Missing module omemo, please download/install it. You can use ' 67 "You are missing one or more package required by the OMEMO plugin. Please"
41 '"pip install omemo"' 68 " download/install the pip packages 'omemo', 'twomemo', 'oldmemo' and"
42 ) 69 " 'xmlschema'."
43 try: 70 ) from import_error
44 from omemo_backend_signal import BACKEND as omemo_backend 71
45 except ImportError: 72
46 raise exceptions.MissingModule( 73 __all__ = [ # pylint: disable=unused-variable
47 'Missing module omemo-backend-signal, please download/install it. You can use ' 74 "PLUGIN_INFO",
48 '"pip install omemo-backend-signal"' 75 "OMEMO"
49 ) 76 ]
50 77
51 log = getLogger(__name__) 78
79 log = cast(Logger, getLogger(__name__)) # type: ignore[no-untyped-call]
80
81
82 string_to_domish = cast(Callable[[str], domish.Element], xml_tools.ElementParser())
83
52 84
53 PLUGIN_INFO = { 85 PLUGIN_INFO = {
54 C.PI_NAME: "OMEMO", 86 C.PI_NAME: "OMEMO",
55 C.PI_IMPORT_NAME: "XEP-0384", 87 C.PI_IMPORT_NAME: "XEP-0384",
56 C.PI_TYPE: "SEC", 88 C.PI_TYPE: "SEC",
57 C.PI_PROTOCOLS: ["XEP-0384"], 89 C.PI_PROTOCOLS: [ "XEP-0384" ],
58 C.PI_DEPENDENCIES: ["XEP-0163", "XEP-0280", "XEP-0334", "XEP-0060"], 90 C.PI_DEPENDENCIES: [ "XEP-0163", "XEP-0280", "XEP-0334", "XEP-0060", "XEP-0420" ],
59 C.PI_RECOMMENDATIONS: ["XEP-0045", "XEP-0359", C.TEXT_CMDS], 91 C.PI_RECOMMENDATIONS: [ "XEP-0045", "XEP-0359", C.TEXT_CMDS ],
60 C.PI_MAIN: "OMEMO", 92 C.PI_MAIN: "OMEMO",
61 C.PI_HANDLER: "no", 93 C.PI_HANDLER: "no",
62 C.PI_DESCRIPTION: _("""Implementation of OMEMO"""), 94 C.PI_DESCRIPTION: _("""Implementation of OMEMO"""),
63 } 95 }
64 96
65 OMEMO_MIN_VER = (0, 11, 0)
66 NS_OMEMO = "eu.siacs.conversations.axolotl"
67 NS_OMEMO_DEVICES = NS_OMEMO + ".devicelist"
68 NS_OMEMO_BUNDLE = NS_OMEMO + ".bundles:{device_id}"
69 KEY_STATE = "STATE"
70 KEY_DEVICE_ID = "DEVICE_ID"
71 KEY_SESSION = "SESSION"
72 KEY_TRUST = "TRUST"
73 # devices which have been automatically trusted by policy like BTBV
74 KEY_AUTO_TRUST = "AUTO_TRUST"
75 # list of peer bare jids where trust UI has been used at least once
76 # this is useful to activate manual trust with BTBV policy
77 KEY_MANUAL_TRUST = "MANUAL_TRUST"
78 KEY_ACTIVE_DEVICES = "DEVICES"
79 KEY_INACTIVE_DEVICES = "INACTIVE_DEVICES"
80 KEY_ALL_JIDS = "ALL_JIDS"
81 # time before plaintext cache for MUC is expired
82 # expressed in seconds, reset on each new MUC message
83 MUC_CACHE_TTL = 60 * 5
84 97
85 PARAM_CATEGORY = "Security" 98 PARAM_CATEGORY = "Security"
86 PARAM_NAME = "omemo_policy" 99 PARAM_NAME = "omemo_policy"
87 100
88 101
89 # we want to manage log emitted by omemo module ourselves 102 class LogHandler(logging.Handler):
90 103 """
91 class SatHandler(logging.Handler): 104 Redirect python-omemo's log output to Libervia's log system.
92 105 """
93 def emit(self, record): 106
107 def emit(self, record: logging.LogRecord) -> None:
94 log.log(record.levelname, record.getMessage()) 108 log.log(record.levelname, record.getMessage())
95 109
96 @staticmethod 110
97 def install(): 111 sm_logger = logging.getLogger(omemo.SessionManager.LOG_TAG)
98 omemo_sm_logger = logging.getLogger("omemo.SessionManager") 112 sm_logger.setLevel(logging.DEBUG)
99 omemo_sm_logger.propagate = False 113 sm_logger.propagate = False
100 omemo_sm_logger.addHandler(SatHandler()) 114 sm_logger.addHandler(LogHandler())
101 115
102 116
103 SatHandler.install() 117 ikp_logger = logging.getLogger(omemo.identity_key_pair.IdentityKeyPair.LOG_TAG)
104 118 ikp_logger.setLevel(logging.DEBUG)
105 119 ikp_logger.propagate = False
106 def b64enc(data): 120 ikp_logger.addHandler(LogHandler())
107 return base64.b64encode(bytes(bytearray(data))).decode("US-ASCII") 121
108 122
109 123 # TODO: Add handling for device labels, i.e. show device labels in the trust UI and give
110 def promise2Deferred(promise_): 124 # the user a way to change their own device label.
111 """Create a Deferred and fire it when promise is resolved 125
112 126
113 @param promise_(promise.Promise): promise to convert 127 class MUCPlaintextCacheKey(NamedTuple):
114 @return (defer.Deferred): deferred instance linked to the promise 128 # pylint: disable=invalid-name
115 """ 129 """
116 d = defer.Deferred() 130 Structure identifying an encrypted message sent to a MUC.
117 promise_.then( 131 """
118 lambda result: reactor.callFromThread(d.callback, result), 132
119 lambda exc: reactor.callFromThread(d.errback, exc) 133 client: SatXMPPClient
134 room_jid: jid.JID
135 message_uid: str
136
137
138 # TODO: Convert without serialization/parsing
139 # On a medium-to-large-sized oldmemo message stanza, 10000 runs of this function took
140 # around 0.6 seconds on my setup.
141 def etree_to_domish(element: ET.Element) -> domish.Element:
142 """
143 @param element: An ElementTree element.
144 @return: The ElementTree element converted to a domish element.
145 """
146
147 return string_to_domish(ET.tostring(element, encoding="unicode"))
148
149
150 # TODO: Convert without serialization/parsing
151 # On a medium-to-large-sized oldmemo message stanza, 10000 runs of this function took less
152 # than one second on my setup.
153 def domish_to_etree(element: domish.Element) -> ET.Element:
154 """
155 @param element: A domish element.
156 @return: The domish element converted to an ElementTree element.
157 """
158
159 return ET.fromstring(element.toXml())
160
161
162 def domish_to_etree2(element: domish.Element) -> ET.Element:
163 """
164 WIP
165 """
166
167 element_name = element.name
168 if element.uri is not None:
169 element_name = "{" + element.uri + "}" + element_name
170
171 attrib: Dict[str, str] = {}
172 for qname, value in element.attributes.items():
173 attribute_name = qname[1] if isinstance(qname, tuple) else qname
174 attribute_namespace = qname[0] if isinstance(qname, tuple) else None
175 if attribute_namespace is not None:
176 attribute_name = "{" + attribute_namespace + "}" + attribute_name
177
178 attrib[attribute_name] = value
179
180 result = ET.Element(element_name, attrib)
181
182 last_child: Optional[ET.Element] = None
183 for child in element.children:
184 if isinstance(child, str):
185 if last_child is None:
186 result.text = child
187 else:
188 last_child.tail = child
189 else:
190 last_child = domish_to_etree2(child)
191 result.append(last_child)
192
193 return result
194
195
196 @enum.unique
197 class TrustLevel(enum.Enum):
198 """
199 The trust levels required for BTBV and manual trust.
200 """
201
202 TRUSTED: str = "TRUSTED"
203 BLINDLY_TRUSTED: str = "BLINDLY_TRUSTED"
204 UNDECIDED: str = "UNDECIDED"
205 DISTRUSTED: str = "DISTRUSTED"
206
207 def to_omemo_trust_level(self) -> omemo.TrustLevel:
208 """
209 @return: This custom trust level evaluated to one of the OMEMO trust levels.
210 """
211
212 if self is TrustLevel.TRUSTED or self is TrustLevel.BLINDLY_TRUSTED:
213 return omemo.TrustLevel.TRUSTED
214 if self is TrustLevel.UNDECIDED:
215 return omemo.TrustLevel.UNDECIDED
216 if self is TrustLevel.DISTRUSTED:
217 return omemo.TrustLevel.DISTRUSTED
218
219 return assert_never(self)
220
221
222 TWOMEMO_DEVICE_LIST_NODE = "urn:xmpp:omemo:2:devices"
223 OLDMEMO_DEVICE_LIST_NODE = "eu.siacs.conversations.axolotl.devicelist"
224
225
226 class StorageImpl(omemo.Storage):
227 """
228 Storage implementation for OMEMO based on :class:`persistent.LazyPersistentBinaryDict`
229 """
230
231 def __init__(self, profile: str) -> None:
232 """
233 @param profile: The profile this OMEMO data belongs to.
234 """
235
236 # persistent.LazyPersistentBinaryDict does not cache at all, so keep the caching
237 # option of omemo.Storage enabled.
238 super().__init__()
239
240 self.__storage = persistent.LazyPersistentBinaryDict("XEP-0384", profile)
241
242 async def _load(self, key: str) -> omemo.Maybe[omemo.JSONType]:
243 try:
244 return omemo.Just(await self.__storage[key])
245 except KeyError:
246 return omemo.Nothing()
247 except Exception as e:
248 raise omemo.StorageException(f"Error while loading key {key}") from e
249
250 async def _store(self, key: str, value: omemo.JSONType) -> None:
251 try:
252 await self.__storage.force(key, value)
253 except Exception as e:
254 raise omemo.StorageException(f"Error while storing key {key}: {value}") from e
255
256 async def _delete(self, key: str) -> None:
257 try:
258 await self.__storage.remove(key)
259 except KeyError:
260 pass
261 except Exception as e:
262 raise omemo.StorageException(f"Error while deleting key {key}") from e
263
264
265 class LegacyStorageImpl(oldmemo.migrations.LegacyStorage):
266 """
267 Legacy storage implementation to migrate data from the old XEP-0384 plugin.
268 """
269
270 KEY_DEVICE_ID = "DEVICE_ID"
271 KEY_STATE = "STATE"
272 KEY_SESSION = "SESSION"
273 KEY_ACTIVE_DEVICES = "DEVICES"
274 KEY_INACTIVE_DEVICES = "INACTIVE_DEVICES"
275 KEY_TRUST = "TRUST"
276 KEY_ALL_JIDS = "ALL_JIDS"
277
278 def __init__(self, profile: str, own_bare_jid: str) -> None:
279 """
280 @param profile: The profile this OMEMO data belongs to.
281 @param own_bare_jid: The own bare JID, to return by the :meth:`loadOwnData` call.
282 """
283
284 self.__storage = persistent.LazyPersistentBinaryDict("XEP-0384", profile)
285 self.__own_bare_jid = own_bare_jid
286
287 async def loadOwnData(self) -> Optional[oldmemo.migrations.OwnData]:
288 own_device_id = await self.__storage.get(LegacyStorageImpl.KEY_DEVICE_ID, None)
289 if own_device_id is None:
290 return None
291
292 return oldmemo.migrations.OwnData(
293 own_bare_jid=self.__own_bare_jid,
294 own_device_id=own_device_id
295 )
296
297 async def deleteOwnData(self) -> None:
298 try:
299 await self.__storage.remove(LegacyStorageImpl.KEY_DEVICE_ID)
300 except KeyError:
301 pass
302
303 async def loadState(self) -> Optional[oldmemo.migrations.State]:
304 return cast(
305 Optional[oldmemo.migrations.State],
306 await self.__storage.get(LegacyStorageImpl.KEY_STATE, None)
307 )
308
309 async def deleteState(self) -> None:
310 try:
311 await self.__storage.remove(LegacyStorageImpl.KEY_STATE)
312 except KeyError:
313 pass
314
315 async def loadSession(
316 self,
317 bare_jid: str,
318 device_id: int
319 ) -> Optional[oldmemo.migrations.Session]:
320 key = "\n".join([ LegacyStorageImpl.KEY_SESSION, bare_jid, str(device_id) ])
321
322 return cast(
323 Optional[oldmemo.migrations.Session],
324 await self.__storage.get(key, None)
325 )
326
327 async def deleteSession(self, bare_jid: str, device_id: int) -> None:
328 key = "\n".join([ LegacyStorageImpl.KEY_SESSION, bare_jid, str(device_id) ])
329
330 try:
331 await self.__storage.remove(key)
332 except KeyError:
333 pass
334
335 async def loadActiveDevices(self, bare_jid: str) -> Optional[List[int]]:
336 key = "\n".join([ LegacyStorageImpl.KEY_ACTIVE_DEVICES, bare_jid ])
337
338 return cast(
339 Optional[List[int]],
340 await self.__storage.get(key, None)
341 )
342
343 async def loadInactiveDevices(self, bare_jid: str) -> Optional[Dict[int, int]]:
344 key = "\n".join([ LegacyStorageImpl.KEY_INACTIVE_DEVICES, bare_jid ])
345
346 return cast(
347 Optional[Dict[int, int]],
348 await self.__storage.get(key, None)
349 )
350
351 async def deleteActiveDevices(self, bare_jid: str) -> None:
352 key = "\n".join([ LegacyStorageImpl.KEY_ACTIVE_DEVICES, bare_jid ])
353
354 try:
355 await self.__storage.remove(key)
356 except KeyError:
357 pass
358
359 async def deleteInactiveDevices(self, bare_jid: str) -> None:
360 key = "\n".join([ LegacyStorageImpl.KEY_INACTIVE_DEVICES, bare_jid ])
361
362 try:
363 await self.__storage.remove(key)
364 except KeyError:
365 pass
366
367 async def loadTrust(
368 self,
369 bare_jid: str,
370 device_id: int
371 ) -> Optional[oldmemo.migrations.Trust]:
372 key = "\n".join([ LegacyStorageImpl.KEY_TRUST, bare_jid, str(device_id) ])
373
374 return cast(
375 Optional[oldmemo.migrations.Trust],
376 await self.__storage.get(key, None)
377 )
378
379 async def deleteTrust(self, bare_jid: str, device_id: int) -> None:
380 key = "\n".join([ LegacyStorageImpl.KEY_TRUST, bare_jid, str(device_id) ])
381
382 try:
383 await self.__storage.remove(key)
384 except KeyError:
385 pass
386
387 async def listJIDs(self) -> Optional[List[str]]:
388 bare_jids = await self.__storage.get(LegacyStorageImpl.KEY_ALL_JIDS, None)
389
390 return None if bare_jids is None else list(bare_jids)
391
392 async def deleteJIDList(self) -> None:
393 try:
394 await self.__storage.remove(LegacyStorageImpl.KEY_ALL_JIDS)
395 except KeyError:
396 pass
397
398
399 async def download_oldmemo_bundle(
400 client: SatXMPPClient,
401 xep_0060: XEP_0060,
402 bare_jid: str,
403 device_id: int
404 ) -> oldmemo.oldmemo.BundleImpl:
405 """Download the oldmemo bundle corresponding to a specific device.
406
407 @param client: The client.
408 @param xep_0060: The XEP-0060 plugin instance to use for pubsub interactions.
409 @param bare_jid: The bare JID the device belongs to.
410 @param device_id: The id of the device.
411 @return: The bundle.
412 @raise BundleDownloadFailed: if the download failed. Feel free to raise a subclass
413 instead.
414 """
415 # Bundle downloads are needed by the session manager and for migrations from legacy,
416 # thus it is made a separate function.
417
418 namespace = oldmemo.oldmemo.NAMESPACE
419 node = f"eu.siacs.conversations.axolotl.bundles:{device_id}"
420
421 try:
422 items, __ = await xep_0060.getItems(client, jid.JID(bare_jid), node)
423 except Exception as e:
424 raise omemo.BundleDownloadFailed(
425 f"Bundle download failed for {bare_jid}: {device_id} under namespace"
426 f" {namespace}"
427 ) from e
428
429 if len(items) != 1:
430 raise omemo.BundleDownloadFailed(
431 f"Bundle download failed for {bare_jid}: {device_id} under namespace"
432 f" {namespace}: Unexpected number of items retrieved: {len(items)}."
433 )
434
435 element = next(iter(domish_to_etree(cast(domish.Element, items[0]))), None)
436 if element is None:
437 raise omemo.BundleDownloadFailed(
438 f"Bundle download failed for {bare_jid}: {device_id} under namespace"
439 f" {namespace}: Item download succeeded but parsing failed: {element}."
440 )
441
442 try:
443 return oldmemo.etree.parse_bundle(element, bare_jid, device_id)
444 except Exception as e:
445 raise omemo.BundleDownloadFailed(
446 f"Bundle parsing failed for {bare_jid}: {device_id} under namespace"
447 f" {namespace}"
448 ) from e
449
450
451 def make_session_manager(sat: SAT, profile: str) -> Type[omemo.SessionManager]:
452 """
453 @param sat: The SAT instance.
454 @param profile: The profile.
455 @return: A non-abstract subclass of :class:`~omemo.session_manager.SessionManager`
456 with XMPP interactions and trust handled via the SAT instance.
457 """
458
459 client = sat.getClient(profile)
460 xep_0060 = cast(XEP_0060, sat.plugins["XEP-0060"])
461
462 class SessionManagerImpl(omemo.SessionManager):
463 """
464 Session manager implementation handling XMPP interactions and trust via an
465 instance of :class:`~sat.core.sat_main.SAT`.
466 """
467
468 @staticmethod
469 async def _upload_bundle(bundle: omemo.Bundle) -> None:
470 if isinstance(bundle, twomemo.twomemo.BundleImpl):
471 element = twomemo.etree.serialize_bundle(bundle)
472
473 node = "urn:xmpp:omemo:2:bundles"
474 try:
475 await xep_0060.sendItem(
476 client,
477 client.jid.userhostJID(),
478 node,
479 etree_to_domish(element),
480 item_id=str(bundle.device_id),
481 extra={
482 xep_0060.EXTRA_PUBLISH_OPTIONS: {
483 xep_0060.OPT_MAX_ITEMS: "max"
484 },
485 xep_0060.EXTRA_ON_PRECOND_NOT_MET: "raise"
486 }
487 )
488 except (error.StanzaError, Exception) as e:
489 if (
490 isinstance(e, error.StanzaError)
491 and e.condition == "conflict"
492 and e.appCondition is not None
493 # pylint: disable=no-member
494 and e.appCondition.name == "precondition-not-met"
495 ):
496 # publish options couldn't be set on the fly, manually reconfigure
497 # the node and publish again
498 raise omemo.BundleUploadFailed(
499 f"precondition-not-met: {bundle}"
500 ) from e
501 # TODO: What can I do here? The correct node configuration is a
502 # MUST in the XEP.
503
504 raise omemo.BundleUploadFailed(
505 f"Bundle upload failed: {bundle}"
506 ) from e
507
508 return
509
510 if isinstance(bundle, oldmemo.oldmemo.BundleImpl):
511 element = oldmemo.etree.serialize_bundle(bundle)
512
513 node = f"eu.siacs.conversations.axolotl.bundles:{bundle.device_id}"
514 try:
515 await xep_0060.sendItem(
516 client,
517 client.jid.userhostJID(),
518 node,
519 etree_to_domish(element),
520 item_id=xep_0060.ID_SINGLETON,
521 extra={
522 xep_0060.EXTRA_PUBLISH_OPTIONS: { xep_0060.OPT_MAX_ITEMS: 1 },
523 xep_0060.EXTRA_ON_PRECOND_NOT_MET: "publish_without_options"
524 }
525 )
526 except Exception as e:
527 raise omemo.BundleUploadFailed(
528 f"Bundle upload failed: {bundle}"
529 ) from e
530
531 return
532
533 raise omemo.UnknownNamespace(f"Unknown namespace: {bundle.namespace}")
534
535 @staticmethod
536 async def _download_bundle(
537 namespace: str,
538 bare_jid: str,
539 device_id: int
540 ) -> omemo.Bundle:
541 if namespace == twomemo.twomemo.NAMESPACE:
542 node = "urn:xmpp:omemo:2:bundles"
543
544 try:
545 items, __ = await xep_0060.getItems(
546 client,
547 jid.JID(bare_jid),
548 node,
549 max_items=None,
550 item_ids=[ str(device_id) ]
551 )
552 except Exception as e:
553 raise omemo.BundleDownloadFailed(
554 f"Bundle download failed for {bare_jid}: {device_id} under"
555 f" namespace {namespace}"
556 ) from e
557
558 if len(items) != 1:
559 raise omemo.BundleDownloadFailed(
560 f"Bundle download failed for {bare_jid}: {device_id} under"
561 f" namespace {namespace}: Unexpected number of items retrieved:"
562 f" {len(items)}."
563 )
564
565 element = next(
566 iter(domish_to_etree(cast(domish.Element, items[0]))),
567 None
568 )
569 if element is None:
570 raise omemo.BundleDownloadFailed(
571 f"Bundle download failed for {bare_jid}: {device_id} under"
572 f" namespace {namespace}: Item download succeeded but parsing"
573 f" failed: {element}."
574 )
575
576 try:
577 return twomemo.etree.parse_bundle(element, bare_jid, device_id)
578 except Exception as e:
579 raise omemo.BundleDownloadFailed(
580 f"Bundle parsing failed for {bare_jid}: {device_id} under"
581 f" namespace {namespace}"
582 ) from e
583
584 if namespace == oldmemo.oldmemo.NAMESPACE:
585 return await download_oldmemo_bundle(
586 client,
587 xep_0060,
588 bare_jid,
589 device_id
590 )
591
592 raise omemo.UnknownNamespace(f"Unknown namespace: {namespace}")
593
594 @staticmethod
595 async def _delete_bundle(namespace: str, device_id: int) -> None:
596 if namespace == twomemo.twomemo.NAMESPACE:
597 node = "urn:xmpp:omemo:2:bundles"
598
599 try:
600 await xep_0060.retractItems(
601 client,
602 client.jid.userhostJID(),
603 node,
604 [ str(device_id) ],
605 notify=False
606 )
607 except Exception as e:
608 raise omemo.BundleDeletionFailed(
609 f"Bundle deletion failed for {device_id} under namespace"
610 f" {namespace}"
611 ) from e
612
613 return
614
615 if namespace == oldmemo.oldmemo.NAMESPACE:
616 node = f"eu.siacs.conversations.axolotl.bundles:{device_id}"
617
618 try:
619 await xep_0060.deleteNode(client, client.jid.userhostJID(), node)
620 except Exception as e:
621 raise omemo.BundleDeletionFailed(
622 f"Bundle deletion failed for {device_id} under namespace"
623 f" {namespace}"
624 ) from e
625
626 return
627
628 raise omemo.UnknownNamespace(f"Unknown namespace: {namespace}")
629
630 @staticmethod
631 async def _upload_device_list(
632 namespace: str,
633 device_list: Dict[int, Optional[str]]
634 ) -> None:
635 element: Optional[ET.Element] = None
636 node: Optional[str] = None
637
638 if namespace == twomemo.twomemo.NAMESPACE:
639 element = twomemo.etree.serialize_device_list(device_list)
640 node = TWOMEMO_DEVICE_LIST_NODE
641 if namespace == oldmemo.oldmemo.NAMESPACE:
642 element = oldmemo.etree.serialize_device_list(device_list)
643 node = OLDMEMO_DEVICE_LIST_NODE
644
645 if element is None or node is None:
646 raise omemo.UnknownNamespace(f"Unknown namespace: {namespace}")
647
648 try:
649 await xep_0060.sendItem(
650 client,
651 client.jid.userhostJID(),
652 node,
653 etree_to_domish(element),
654 item_id=xep_0060.ID_SINGLETON,
655 extra={
656 xep_0060.EXTRA_PUBLISH_OPTIONS: {
657 xep_0060.OPT_MAX_ITEMS: 1,
658 xep_0060.OPT_ACCESS_MODEL: "open"
659 },
660 xep_0060.EXTRA_ON_PRECOND_NOT_MET: "raise"
661 }
662 )
663 except (error.StanzaError, Exception) as e:
664 if (
665 isinstance(e, error.StanzaError)
666 and e.condition == "conflict"
667 and e.appCondition is not None
668 # pylint: disable=no-member
669 and e.appCondition.name == "precondition-not-met"
670 ):
671 # publish options couldn't be set on the fly, manually reconfigure the
672 # node and publish again
673 raise omemo.DeviceListUploadFailed(
674 f"precondition-not-met for namespace {namespace}"
675 ) from e
676 # TODO: What can I do here? The correct node configuration is a MUST
677 # in the XEP.
678
679 raise omemo.DeviceListUploadFailed(
680 f"Device list upload failed for namespace {namespace}"
681 ) from e
682
683 @staticmethod
684 async def _download_device_list(
685 namespace: str,
686 bare_jid: str
687 ) -> Dict[int, Optional[str]]:
688 node: Optional[str] = None
689
690 if namespace == twomemo.twomemo.NAMESPACE:
691 node = TWOMEMO_DEVICE_LIST_NODE
692 if namespace == oldmemo.oldmemo.NAMESPACE:
693 node = OLDMEMO_DEVICE_LIST_NODE
694
695 if node is None:
696 raise omemo.UnknownNamespace(f"Unknown namespace: {namespace}")
697
698 try:
699 items, __ = await xep_0060.getItems(client, jid.JID(bare_jid), node)
700 except exceptions.NotFound:
701 return {}
702 except Exception as e:
703 raise omemo.DeviceListDownloadFailed(
704 f"Device list download failed for {bare_jid} under namespace"
705 f" {namespace}"
706 ) from e
707
708 if len(items) != 1:
709 raise omemo.DeviceListDownloadFailed(
710 f"Device list download failed for {bare_jid} under namespace"
711 f" {namespace}: Unexpected number of items retrieved: {len(items)}."
712 )
713
714 element = next(iter(domish_to_etree(cast(domish.Element, items[0]))), None)
715 if element is None:
716 raise omemo.DeviceListDownloadFailed(
717 f"Device list download failed for {bare_jid} under namespace"
718 f" {namespace}: Item download succeeded but parsing failed:"
719 f" {element}."
720 )
721
722 try:
723 if namespace == twomemo.twomemo.NAMESPACE:
724 return twomemo.etree.parse_device_list(element)
725 if namespace == oldmemo.oldmemo.NAMESPACE:
726 return oldmemo.etree.parse_device_list(element)
727 except Exception as e:
728 raise omemo.DeviceListDownloadFailed(
729 f"Device list download failed for {bare_jid} under namespace"
730 f" {namespace}"
731 ) from e
732
733 raise omemo.UnknownNamespace(f"Unknown namespace: {namespace}")
734
735 @staticmethod
736 def _evaluate_custom_trust_level(trust_level_name: str) -> omemo.TrustLevel:
737 try:
738 return TrustLevel(trust_level_name).to_omemo_trust_level()
739 except ValueError as e:
740 raise omemo.UnknownTrustLevel(
741 f"Unknown trust level name {trust_level_name}"
742 ) from e
743
744 async def _make_trust_decision(
745 self,
746 undecided: FrozenSet[omemo.DeviceInformation],
747 identifier: Optional[str]
748 ) -> None:
749 if identifier is None:
750 raise omemo.TrustDecisionFailed(
751 "The identifier must contain the feedback JID."
752 )
753
754 # The feedback JID is transferred via the identifier
755 feedback_jid = jid.JID(identifier).userhostJID()
756
757 # Get the name of the trust model to use
758 trust_model = cast(str, sat.memory.getParamA(
759 PARAM_NAME,
760 PARAM_CATEGORY,
761 profile_key=cast(str, client.profile)
762 ))
763
764 # Under the BTBV trust model, if at least one device of a bare JID is manually
765 # trusted or distrusted, the trust model is "downgraded" to manual trust.
766 # Thus, we can separate bare JIDs into two pools here, one pool of bare JIDs
767 # for which BTBV is active, and one pool of bare JIDs for which manual trust
768 # is used.
769 bare_jids = { device.bare_jid for device in undecided }
770
771 btbv_bare_jids: Set[str] = set()
772 manual_trust_bare_jids: Set[str] = set()
773
774 if trust_model == "btbv":
775 # For each bare JID, decide whether BTBV or manual trust applies
776 for bare_jid in bare_jids:
777 # Get all known devices belonging to the bare JID
778 devices = await self.get_device_information(bare_jid)
779
780 # If the trust levels of all devices correspond to those used by BTBV,
781 # BTBV applies. Otherwise, fall back to manual trust.
782 if all(TrustLevel(device.trust_level_name) in {
783 TrustLevel.UNDECIDED,
784 TrustLevel.BLINDLY_TRUSTED
785 } for device in devices):
786 btbv_bare_jids.add(bare_jid)
787 else:
788 manual_trust_bare_jids.add(bare_jid)
789
790 if trust_model == "manual":
791 manual_trust_bare_jids = bare_jids
792
793 # With the JIDs sorted into their respective pools, the undecided devices can
794 # be categorized too
795 blindly_trusted_devices = \
796 { dev for dev in undecided if dev.bare_jid in btbv_bare_jids }
797 manually_trusted_devices = \
798 { dev for dev in undecided if dev.bare_jid in manual_trust_bare_jids }
799
800 # Blindly trust devices handled by BTBV
801 if len(blindly_trusted_devices) > 0:
802 for device in blindly_trusted_devices:
803 await self.set_trust(
804 device.bare_jid,
805 device.identity_key,
806 TrustLevel.BLINDLY_TRUSTED.name
807 )
808
809 blindly_trusted_devices_stringified = ", ".join([
810 f"device {device.device_id} of {device.bare_jid} under namespace"
811 f" {device.namespaces}"
812 for device
813 in blindly_trusted_devices
814 ])
815
816 client.feedback(
817 feedback_jid,
818 D_(
819 "Not all destination devices are trusted, unknown devices will be"
820 " blindly trusted due to the Blind Trust Before Verification"
821 " policy. If you want a more secure workflow, please activate the"
822 " \"manual\" policy in the settings' \"Security\" tab.\nFollowing"
823 " devices have been automatically trusted:"
824 f" {blindly_trusted_devices_stringified}."
825 )
826 )
827
828 # Prompt the user for manual trust decisions on the devices handled by manual
829 # trust
830 if len(manually_trusted_devices) > 0:
831 client.feedback(
832 feedback_jid,
833 D_(
834 "Not all destination devices are trusted, we can't encrypt"
835 " message in such a situation. Please indicate if you trust"
836 " those devices or not in the trust manager before we can"
837 " send this message."
838 )
839 )
840 await self.__prompt_manual_trust(
841 frozenset(manually_trusted_devices),
842 feedback_jid
843 )
844
845 @staticmethod
846 async def _send_message(message: omemo.Message, bare_jid: str) -> None:
847 element: Optional[ET.Element] = None
848
849 if message.namespace == twomemo.twomemo.NAMESPACE:
850 element = twomemo.etree.serialize_message(message)
851 if message.namespace == oldmemo.oldmemo.NAMESPACE:
852 element = oldmemo.etree.serialize_message(message)
853
854 if element is None:
855 raise omemo.UnknownNamespace(f"Unknown namespace: {message.namespace}")
856
857 # TODO: Untested
858 message_data = client.generateMessageXML(MessageData({
859 "from": client.jid,
860 "to": jid.JID(bare_jid),
861 "uid": str(uuid.uuid4()),
862 "message": {},
863 "subject": {},
864 "type": C.MESS_TYPE_CHAT,
865 "extra": {},
866 "timestamp": time.time()
867 }))
868
869 message_data["xml"].addChild(etree_to_domish(element))
870
871 try:
872 await client.send(message_data["xml"])
873 except Exception as e:
874 raise omemo.MessageSendingFailed() from e
875
876 async def __prompt_manual_trust(
877 self,
878 undecided: FrozenSet[omemo.DeviceInformation],
879 feedback_jid: jid.JID
880 ) -> None:
881 """Asks the user to decide on the manual trust level of a set of devices.
882
883 Blocks until the user has made a decision and updates the trust levels of all
884 devices using :meth:`set_trust`.
885
886 @param undecided: The set of devices to prompt manual trust for.
887 @param feedback_jid: The bare JID to redirect feedback to. In case of a one to
888 one message, the recipient JID. In case of a MUC message, the room JID.
889 @raise TrustDecisionFailed: if the user cancels the prompt.
890 """
891
892 # This session manager handles encryption with both twomemo and oldmemo, but
893 # both are currently registered as different plugins and the `deferXMLUI`
894 # below requires a single namespace identifying the encryption plugin. Thus,
895 # get the namespace of the requested encryption method from the encryption
896 # session using the feedback JID.
897 encryption = client.encryption.getSession(feedback_jid)
898 if encryption is None:
899 raise omemo.TrustDecisionFailed(
900 f"Encryption not requested for {feedback_jid.userhost()}."
901 )
902
903 namespace = encryption["plugin"].namespace
904
905 # Casting this to Any, otherwise all calls on the variable cause type errors
906 # pylint: disable=no-member
907 trust_ui = cast(Any, xml_tools.XMLUI(
908 panel_type=C.XMLUI_FORM,
909 title=D_("OMEMO trust management"),
910 submit_id=""
911 ))
912 trust_ui.addText(D_(
913 "This is OMEMO trusting system. You'll see below the devices of your "
914 "contacts, and a checkbox to trust them or not. A trusted device "
915 "can read your messages in plain text, so be sure to only validate "
916 "devices that you are sure are belonging to your contact. It's better "
917 "to do this when you are next to your contact and their device, so "
918 "you can check the \"fingerprint\" (the number next to the device) "
919 "yourself. Do *not* validate a device if the fingerprint is wrong!"
920 ))
921
922 own_device, __ = await self.get_own_device_information()
923
924 trust_ui.changeContainer("label")
925 trust_ui.addLabel(D_("This device ID"))
926 trust_ui.addText(str(own_device.device_id))
927 trust_ui.addLabel(D_("This device's fingerprint"))
928 trust_ui.addText(" ".join(self.format_identity_key(own_device.identity_key)))
929 trust_ui.addEmpty()
930 trust_ui.addEmpty()
931
932 # At least sort the devices by bare JID such that they aren't listed
933 # completely random
934 undecided_ordered = sorted(undecided, key=lambda device: device.bare_jid)
935
936 for index, device in enumerate(undecided_ordered):
937 trust_ui.addLabel(D_("Contact"))
938 trust_ui.addJid(jid.JID(device.bare_jid))
939 trust_ui.addLabel(D_("Device ID"))
940 trust_ui.addText(str(device.device_id))
941 trust_ui.addLabel(D_("Fingerprint"))
942 trust_ui.addText(" ".join(self.format_identity_key(device.identity_key)))
943 trust_ui.addLabel(D_("Trust this device?"))
944 trust_ui.addBool(f"trust_{index}", value=C.boolConst(False))
945 trust_ui.addEmpty()
946 trust_ui.addEmpty()
947
948 trust_ui_result = await xml_tools.deferXMLUI(
949 sat,
950 trust_ui,
951 action_extra={ "meta_encryption_trust": namespace },
952 profile=profile
953 )
954
955 if C.bool(trust_ui_result.get("cancelled", "false")):
956 raise omemo.TrustDecisionFailed("Trust UI cancelled.")
957
958 data_form_result = cast(Dict[str, str], xml_tools.XMLUIResult2DataFormResult(
959 trust_ui_result
960 ))
961
962 for key, value in data_form_result.items():
963 if not key.startswith("trust_"):
964 continue
965
966 device = undecided_ordered[int(key[len("trust_"):])]
967 trust = C.bool(value)
968
969 await self.set_trust(
970 device.bare_jid,
971 device.identity_key,
972 TrustLevel.TRUSTED.name if trust else TrustLevel.DISTRUSTED.name
973 )
974
975 return SessionManagerImpl
976
977
978 async def prepare_for_profile(
979 sat: SAT,
980 profile: str,
981 initial_own_label: Optional[str],
982 signed_pre_key_rotation_period: int = 7 * 24 * 60 * 60,
983 pre_key_refill_threshold: int = 99,
984 max_num_per_session_skipped_keys: int = 1000,
985 max_num_per_message_skipped_keys: Optional[int] = None
986 ) -> omemo.SessionManager:
987 """Prepare the OMEMO library (storage, backends, core) for a specific profile.
988
989 @param sat: The SAT instance.
990 @param profile: The profile.
991 @param initial_own_label: The initial (optional) label to assign to this device if
992 supported by any of the backends.
993 @param signed_pre_key_rotation_period: The rotation period for the signed pre key, in
994 seconds. The rotation period is recommended to be between one week (the default)
995 and one month.
996 @param pre_key_refill_threshold: The number of pre keys that triggers a refill to 100.
997 Defaults to 99, which means that each pre key gets replaced with a new one right
998 away. The threshold can not be configured to lower than 25.
999 @param max_num_per_session_skipped_keys: The maximum number of skipped message keys to
1000 keep around per session. Once the maximum is reached, old message keys are deleted
1001 to make space for newer ones. Accessible via
1002 :attr:`max_num_per_session_skipped_keys`.
1003 @param max_num_per_message_skipped_keys: The maximum number of skipped message keys to
1004 accept in a single message. When set to ``None`` (the default), this parameter
1005 defaults to the per-session maximum (i.e. the value of the
1006 ``max_num_per_session_skipped_keys`` parameter). This parameter may only be 0 if
1007 the per-session maximum is 0, otherwise it must be a number between 1 and the
1008 per-session maximum. Accessible via :attr:`max_num_per_message_skipped_keys`.
1009 @return: A session manager with ``urn:xmpp:omemo:2`` and
1010 ``eu.siacs.conversations.axolotl`` capabilities, specifically for the given
1011 profile.
1012 @raise BundleUploadFailed: if a bundle upload failed. Forwarded from
1013 :meth:`~omemo.session_manager.SessionManager.create`.
1014 @raise BundleDownloadFailed: if a bundle download failed. Forwarded from
1015 :meth:`~omemo.session_manager.SessionManager.create`.
1016 @raise BundleDeletionFailed: if a bundle deletion failed. Forwarded from
1017 :meth:`~omemo.session_manager.SessionManager.create`.
1018 @raise DeviceListUploadFailed: if a device list upload failed. Forwarded from
1019 :meth:`~omemo.session_manager.SessionManager.create`.
1020 @raise DeviceListDownloadFailed: if a device list download failed. Forwarded from
1021 :meth:`~omemo.session_manager.SessionManager.create`.
1022 """
1023
1024 client = sat.getClient(profile)
1025 xep_0060 = cast(XEP_0060, sat.plugins["XEP-0060"])
1026
1027 storage = StorageImpl(profile)
1028
1029 # TODO: Untested
1030 await oldmemo.migrations.migrate(
1031 LegacyStorageImpl(profile, client.jid.userhost()),
1032 storage,
1033 # TODO: Do we want BLINDLY_TRUSTED or TRUSTED here?
1034 TrustLevel.BLINDLY_TRUSTED.name,
1035 TrustLevel.UNDECIDED.name,
1036 TrustLevel.DISTRUSTED.name,
1037 lambda bare_jid, device_id: download_oldmemo_bundle(
1038 client,
1039 xep_0060,
1040 bare_jid,
1041 device_id
1042 )
120 ) 1043 )
121 return d 1044
122 1045 session_manager = await make_session_manager(sat, profile).create(
123 1046 [
124 class OmemoStorage(omemo.Storage): 1047 twomemo.Twomemo(
125 1048 storage,
126 def __init__(self, client, device_id, all_jids): 1049 max_num_per_session_skipped_keys,
127 self.own_bare_jid_s = client.jid.userhost() 1050 max_num_per_message_skipped_keys
128 self.device_id = device_id 1051 ),
129 self.all_jids = all_jids 1052 oldmemo.Oldmemo(
130 self.data = client._xep_0384_data 1053 storage,
131 1054 max_num_per_session_skipped_keys,
132 @property 1055 max_num_per_message_skipped_keys
133 def is_async(self): 1056 )
134 return True 1057 ],
135 1058 storage,
136 def setCb(self, deferred, callback): 1059 client.jid.userhost(),
137 """Associate Deferred and callback 1060 initial_own_label,
138 1061 TrustLevel.UNDECIDED.value,
139 callback of omemo.Storage expect a boolean with success state then result 1062 signed_pre_key_rotation_period,
140 Deferred on the other hand use 2 methods for callback and errback 1063 pre_key_refill_threshold,
141 This method use partial to call callback with boolean then result when 1064 omemo.AsyncFramework.TWISTED
142 Deferred is called 1065 )
143 """ 1066
144 deferred.addCallback(partial(callback, True)) 1067 # This shouldn't hurt here since we're not running on overly constrainted devices.
145 deferred.addErrback(partial(callback, False)) 1068 # TODO: Consider ensuring data consistency regularly/in response to certain events
146 1069 await session_manager.ensure_data_consistency()
147 def _callMainThread(self, callback, method, *args, check_jid=None): 1070
148 if check_jid is None: 1071 # TODO: Correct entering/leaving of the history synchronization mode isn't terribly
149 d = method(*args) 1072 # important for now, since it only prevents an extremely unlikely race condition of
150 else: 1073 # multiple devices choosing the same pre key for new sessions while the device was
151 check_jid_d = self._checkJid(check_jid) 1074 # offline. I don't believe other clients seriously defend against that race condition
152 check_jid_d.addCallback(lambda __: method(*args)) 1075 # either. In the long run, it might still be cool to have triggers for when history
153 d = check_jid_d 1076 # sync starts and ends (MAM, MUC catch-up, etc.) and to react to those triggers.
154 1077 await session_manager.after_history_sync()
155 if callback is not None: 1078
156 d.addCallback(partial(callback, True)) 1079 return session_manager
157 d.addErrback(partial(callback, False)) 1080
158 1081
159 def _call(self, callback, method, *args, check_jid=None): 1082 DEFAULT_TRUST_MODEL_PARAM = f"""
160 """Create Deferred and add Promise callback to it 1083 <params>
161 1084 <individual>
162 This method use reactor.callLater to launch Deferred in main thread 1085 <category name="{PARAM_CATEGORY}" label={quoteattr(D_('Security'))}>
163 @param check_jid: run self._checkJid before method 1086 <param name="{PARAM_NAME}"
164 """ 1087 label={quoteattr(D_('OMEMO default trust policy'))}
165 reactor.callFromThread( 1088 type="list" security="3">
166 self._callMainThread, callback, method, *args, check_jid=check_jid 1089 <option value="manual" label={quoteattr(D_('Manual trust (more secure)'))} />
1090 <option value="btbv"
1091 label={quoteattr(D_('Blind Trust Before Verification (more user friendly)'))}
1092 selected="true" />
1093 </param>
1094 </category>
1095 </individual>
1096 </params>
1097 """
1098
1099
1100 class OMEMO:
1101 """
1102 Plugin equipping Libervia with OMEMO capabilities under the (modern)
1103 ``urn:xmpp:omemo:2`` namespace and the (legacy) ``eu.siacs.conversations.axolotl``
1104 namespace. Both versions of the protocol are handled by this plugin and compatibility
1105 between the two is maintained. MUC messages are supported next to one to one messages.
1106 For trust management, the two trust models "BTBV" and "manual" are supported.
1107 """
1108
1109 # For MUC/MIX message stanzas, the <to/> affix is a MUST
1110 SCE_PROFILE_GROUPCHAT = SCEProfile(
1111 rpad_policy=SCEAffixPolicy.REQUIRED,
1112 time_policy=SCEAffixPolicy.OPTIONAL,
1113 to_policy=SCEAffixPolicy.REQUIRED,
1114 from_policy=SCEAffixPolicy.OPTIONAL,
1115 custom_policies={}
1116 )
1117
1118 # For everything but MUC/MIX message stanzas, the <to/> affix is a MAY
1119 SCE_PROFILE = SCEProfile(
1120 rpad_policy=SCEAffixPolicy.REQUIRED,
1121 time_policy=SCEAffixPolicy.OPTIONAL,
1122 to_policy=SCEAffixPolicy.OPTIONAL,
1123 from_policy=SCEAffixPolicy.OPTIONAL,
1124 custom_policies={}
1125 )
1126
1127 def __init__(self, sat: SAT) -> None:
1128 """
1129 @param sat: The SAT instance.
1130 """
1131
1132 self.__sat = sat
1133
1134 # Add configuration option to choose between manual trust and BTBV as the trust
1135 # model
1136 sat.memory.updateParams(DEFAULT_TRUST_MODEL_PARAM)
1137
1138 # Plugins
1139 self.__xep_0045 = cast(Optional[XEP_0045], sat.plugins.get("XEP-0045"))
1140 self.__xep_0334 = cast(XEP_0334, sat.plugins["XEP-0334"])
1141 self.__xep_0359 = cast(Optional[XEP_0359], sat.plugins.get("XEP-0359"))
1142 self.__xep_0420 = cast(XEP_0420, sat.plugins["XEP-0420"])
1143
1144 # In contrast to one to one messages, MUC messages are reflected to the sender.
1145 # Thus, the sender does not add messages to their local message log when sending
1146 # them, but when the reflection is received. This approach does not pair well with
1147 # OMEMO, since for security reasons it is forbidden to encrypt messages for the
1148 # own device. Thus, when the reflection of an OMEMO message is received, it can't
1149 # be decrypted and added to the local message log as usual. To counteract this,
1150 # the plaintext of encrypted messages sent to MUCs are cached in this field, such
1151 # that when the reflection is received, the plaintext can be looked up from the
1152 # cache and added to the local message log.
1153 # TODO: The old plugin expired this cache after some time. I'm not sure that's
1154 # really necessary.
1155 self.__muc_plaintext_cache: Dict[MUCPlaintextCacheKey, bytes] = {}
1156
1157 # Mapping from profile name to corresponding session manager
1158 self.__session_managers: Dict[str, omemo.SessionManager] = {}
1159
1160 # Calls waiting for a specific session manager to be built
1161 self.__session_manager_waiters: Dict[str, List[defer.Deferred]] = {}
1162
1163 # These triggers are used by oldmemo, which doesn't do SCE and only applies to
1164 # messages
1165 sat.trigger.add(
1166 "messageReceived",
1167 self.__message_received_trigger,
1168 priority=100050
167 ) 1169 )
168 1170 sat.trigger.add(
169 def _checkJid(self, bare_jid): 1171 "sendMessageData",
170 """Check if jid is known, and store it if not 1172 self.__send_message_data_trigger,
171 1173 priority=100050
172 @param bare_jid(unicode): bare jid to check 1174 )
173 @return (D): Deferred fired when jid is stored 1175
174 """ 1176 # These triggers are used by twomemo, which does do SCE
175 if bare_jid in self.all_jids: 1177 sat.trigger.add("send", self.__send_trigger, priority=0)
176 return defer.succeed(None) 1178 # TODO: Add new triggers here for freshly received and about-to-be-sent stanzas,
177 else: 1179 # including IQs.
178 self.all_jids.add(bare_jid) 1180
179 d = self.data.force(KEY_ALL_JIDS, self.all_jids) 1181 # Give twomemo a (slightly) higher priority than oldmemo
180 return d 1182 sat.registerEncryptionPlugin(self, "TWOMEMO", twomemo.twomemo.NAMESPACE, 101)
181 1183 sat.registerEncryptionPlugin(self, "OLDMEMO", oldmemo.oldmemo.NAMESPACE, 100)
182 def loadOwnData(self, callback): 1184
183 callback(True, {'own_bare_jid': self.own_bare_jid_s, 1185 xep_0163 = cast(XEP_0163, sat.plugins["XEP-0163"])
184 'own_device_id': self.device_id}) 1186 xep_0163.addPEPEvent(
185 1187 "TWOMEMO_DEVICES",
186 def storeOwnData(self, callback, own_bare_jid, own_device_id): 1188 TWOMEMO_DEVICE_LIST_NODE,
187 if own_bare_jid != self.own_bare_jid_s or own_device_id != self.device_id: 1189 lambda items_event, profile: defer.ensureDeferred(
188 raise exceptions.InternalError('bare jid or device id inconsistency!') 1190 self.__on_device_list_update(items_event, profile)
189 callback(True, None)
190
191 def loadState(self, callback):
192 self._call(callback, self.data.get, KEY_STATE)
193
194 def storeState(self, callback, state):
195 self._call(callback, self.data.force, KEY_STATE, state)
196
197 def loadSession(self, callback, bare_jid, device_id):
198 key = '\n'.join([KEY_SESSION, bare_jid, str(device_id)])
199 self._call(callback, self.data.get, key)
200
201 def storeSession(self, callback, bare_jid, device_id, session):
202 key = '\n'.join([KEY_SESSION, bare_jid, str(device_id)])
203 self._call(callback, self.data.force, key, session)
204
205 def deleteSession(self, callback, bare_jid, device_id):
206 key = '\n'.join([KEY_SESSION, bare_jid, str(device_id)])
207 self._call(callback, self.data.remove, key)
208
209 def loadActiveDevices(self, callback, bare_jid):
210 key = '\n'.join([KEY_ACTIVE_DEVICES, bare_jid])
211 self._call(callback, self.data.get, key, {})
212
213 def loadInactiveDevices(self, callback, bare_jid):
214 key = '\n'.join([KEY_INACTIVE_DEVICES, bare_jid])
215 self._call(callback, self.data.get, key, {})
216
217 def storeActiveDevices(self, callback, bare_jid, devices):
218 key = '\n'.join([KEY_ACTIVE_DEVICES, bare_jid])
219 self._call(callback, self.data.force, key, devices, check_jid=bare_jid)
220
221 def storeInactiveDevices(self, callback, bare_jid, devices):
222 key = '\n'.join([KEY_INACTIVE_DEVICES, bare_jid])
223 self._call(callback, self.data.force, key, devices, check_jid=bare_jid)
224
225 def storeTrust(self, callback, bare_jid, device_id, trust):
226 key = '\n'.join([KEY_TRUST, bare_jid, str(device_id)])
227 self._call(callback, self.data.force, key, trust)
228
229 def loadTrust(self, callback, bare_jid, device_id):
230 key = '\n'.join([KEY_TRUST, bare_jid, str(device_id)])
231 self._call(callback, self.data.get, key)
232
233 def listJIDs(self, callback):
234 if callback is not None:
235 callback(True, self.all_jids)
236
237 def _deleteJID_logResults(self, results):
238 failed = [success for success, __ in results if not success]
239 if failed:
240 log.warning(
241 "delete JID failed for {failed_count} on {total_count} operations"
242 .format(failed_count=len(failed), total_count=len(results)))
243 else:
244 log.info(
245 "Delete JID operation succeed ({total_count} operations)."
246 .format(total_count=len(results)))
247
248 def _deleteJID_gotDevices(self, results, bare_jid):
249 assert len(results) == 2
250 active_success, active_devices = results[0]
251 inactive_success, inactive_devices = results[0]
252 d_list = []
253 for success, devices in results:
254 if not success:
255 log.warning("Can't retrieve devices for {bare_jid}: {reason}"
256 .format(bare_jid=bare_jid, reason=active_devices))
257 else:
258 for device_id in devices:
259 for key in (KEY_SESSION, KEY_TRUST):
260 k = '\n'.join([key, bare_jid, str(device_id)])
261 d_list.append(self.data.remove(k))
262
263 d_list.append(self.data.remove(KEY_ACTIVE_DEVICES, bare_jid))
264 d_list.append(self.data.remove(KEY_INACTIVE_DEVICES, bare_jid))
265 d_list.append(lambda __: self.all_jids.discard(bare_jid))
266 # FIXME: there is a risk of race condition here,
267 # if self.all_jids is modified between discard and force)
268 d_list.append(lambda __: self.data.force(KEY_ALL_JIDS, self.all_jids))
269 d = defer.DeferredList(d_list)
270 d.addCallback(self._deleteJID_logResults)
271 return d
272
273 def _deleteJID(self, callback, bare_jid):
274 d_list = []
275
276 key = '\n'.join([KEY_ACTIVE_DEVICES, bare_jid])
277 d_list.append(self.data.get(key, []))
278
279 key = '\n'.join([KEY_INACTIVE_DEVICES, bare_jid])
280 d_inactive = self.data.get(key, {})
281 # inactive devices are returned as a dict mapping from devices_id to timestamp
282 # but we only need devices ids
283 d_inactive.addCallback(lambda devices: [k for k, __ in devices])
284
285 d_list.append(d_inactive)
286 d = defer.DeferredList(d_list)
287 d.addCallback(self._deleteJID_gotDevices, bare_jid)
288 if callback is not None:
289 self.setCb(d, callback)
290
291 def deleteJID(self, callback, bare_jid):
292 """Retrieve all (in)actives devices of bare_jid, and delete all related keys"""
293 reactor.callFromThread(self._deleteJID, callback, bare_jid)
294
295
296 class SatOTPKPolicy(omemo.DefaultOTPKPolicy):
297 pass
298
299
300 class OmemoSession:
301 """Wrapper to use omemo.OmemoSession with Deferred"""
302
303 def __init__(self, session):
304 self._session = session
305
306 @property
307 def republish_bundle(self):
308 return self._session.republish_bundle
309
310 @property
311 def public_bundle(self):
312 return self._session.public_bundle
313
314 @classmethod
315 def create(cls, client, storage, my_device_id = None):
316 omemo_session_p = omemo.SessionManager.create(
317 storage,
318 SatOTPKPolicy,
319 omemo_backend,
320 client.jid.userhost(),
321 my_device_id)
322 d = promise2Deferred(omemo_session_p)
323 d.addCallback(lambda session: cls(session))
324 return d
325
326 def newDeviceList(self, jid, devices):
327 jid = jid.userhost()
328 new_device_p = self._session.newDeviceList(jid, devices)
329 return promise2Deferred(new_device_p)
330
331 def getDevices(self, bare_jid=None):
332 bare_jid = bare_jid.userhost()
333 get_devices_p = self._session.getDevices(bare_jid=bare_jid)
334 return promise2Deferred(get_devices_p)
335
336 def buildSession(self, bare_jid, device, bundle):
337 bare_jid = bare_jid.userhost()
338 build_session_p = self._session.buildSession(bare_jid, int(device), bundle)
339 return promise2Deferred(build_session_p)
340
341 def deleteSession(self, bare_jid, device):
342 bare_jid = bare_jid.userhost()
343 delete_session_p = self._session.deleteSession(
344 bare_jid=bare_jid, device=int(device))
345 return promise2Deferred(delete_session_p)
346
347 def encryptMessage(self, bare_jids, message, bundles=None, expect_problems=None):
348 """Encrypt a message
349
350 @param bare_jids(iterable[jid.JID]): destinees of the message
351 @param message(unicode): message to encode
352 @param bundles(dict[jid.JID, dict[int, ExtendedPublicBundle]):
353 entities => devices => bundles map
354 @return D(dict): encryption data
355 """
356 bare_jids = [e.userhost() for e in bare_jids]
357 if bundles is not None:
358 bundles = {e.userhost(): v for e, v in bundles.items()}
359 encrypt_mess_p = self._session.encryptMessage(
360 bare_jids=bare_jids,
361 plaintext=message.encode(),
362 bundles=bundles,
363 expect_problems=expect_problems)
364 return promise2Deferred(encrypt_mess_p)
365
366 def encryptRatchetForwardingMessage(
367 self, bare_jids, bundles=None, expect_problems=None):
368 bare_jids = [e.userhost() for e in bare_jids]
369 if bundles is not None:
370 bundles = {e.userhost(): v for e, v in bundles.items()}
371 encrypt_ratchet_fwd_p = self._session.encryptRatchetForwardingMessage(
372 bare_jids=bare_jids,
373 bundles=bundles,
374 expect_problems=expect_problems)
375 return promise2Deferred(encrypt_ratchet_fwd_p)
376
377 def decryptMessage(self, bare_jid, device, iv, message, is_pre_key_message,
378 ciphertext, additional_information=None, allow_untrusted=False):
379 bare_jid = bare_jid.userhost()
380 decrypt_mess_p = self._session.decryptMessage(
381 bare_jid=bare_jid,
382 device=int(device),
383 iv=iv,
384 message=message,
385 is_pre_key_message=is_pre_key_message,
386 ciphertext=ciphertext,
387 additional_information=additional_information,
388 allow_untrusted=allow_untrusted
389 ) 1191 )
390 return promise2Deferred(decrypt_mess_p) 1192 )
391 1193 xep_0163.addPEPEvent(
392 def decryptRatchetForwardingMessage( 1194 "OLDMEMO_DEVICES",
393 self, bare_jid, device, iv, message, is_pre_key_message, 1195 OLDMEMO_DEVICE_LIST_NODE,
394 additional_information=None, allow_untrusted=False): 1196 lambda items_event, profile: defer.ensureDeferred(
395 bare_jid = bare_jid.userhost() 1197 self.__on_device_list_update(items_event, profile)
396 decrypt_ratchet_fwd_p = self._session.decryptRatchetForwardingMessage(
397 bare_jid=bare_jid,
398 device=int(device),
399 iv=iv,
400 message=message,
401 is_pre_key_message=is_pre_key_message,
402 additional_information=additional_information,
403 allow_untrusted=allow_untrusted
404 ) 1198 )
405 return promise2Deferred(decrypt_ratchet_fwd_p)
406
407 def setTrust(self, bare_jid, device, key, trusted):
408 bare_jid = bare_jid.userhost()
409 setTrust_p = self._session.setTrust(
410 bare_jid=bare_jid,
411 device=int(device),
412 key=key,
413 trusted=trusted,
414 ) 1199 )
415 return promise2Deferred(setTrust_p) 1200
416
417 def resetTrust(self, bare_jid, device):
418 bare_jid = bare_jid.userhost()
419 resetTrust_p = self._session.resetTrust(
420 bare_jid=bare_jid,
421 device=int(device),
422 )
423 return promise2Deferred(resetTrust_p)
424
425 def getTrustForJID(self, bare_jid):
426 bare_jid = bare_jid.userhost()
427 get_trust_p = self._session.getTrustForJID(bare_jid=bare_jid)
428 return promise2Deferred(get_trust_p)
429
430
431 class OMEMO:
432
433 params = """
434 <params>
435 <individual>
436 <category name="{category_name}" label="{category_label}">
437 <param name="{param_name}" label={param_label} type="list" security="3">
438 <option value="manual" label={opt_manual_lbl} />
439 <option value="btbv" label={opt_btbv_lbl} selected="true" />
440 </param>
441 </category>
442 </individual>
443 </params>
444 """.format(
445 category_name=PARAM_CATEGORY,
446 category_label=D_("Security"),
447 param_name=PARAM_NAME,
448 param_label=quoteattr(D_("OMEMO default trust policy")),
449 opt_manual_lbl=quoteattr(D_("Manual trust (more secure)")),
450 opt_btbv_lbl=quoteattr(
451 D_("Blind Trust Before Verification (more user friendly)")),
452 )
453
454 def __init__(self, host):
455 log.info(_("OMEMO plugin initialization (omemo module v{version})").format(
456 version=omemo.__version__))
457 version = tuple(map(int, omemo.__version__.split('.')[:3]))
458 if version < OMEMO_MIN_VER:
459 log.warning(_(
460 "Your version of omemo module is too old: {v[0]}.{v[1]}.{v[2]} is "
461 "minimum required, please update.").format(v=OMEMO_MIN_VER))
462 raise exceptions.CancelError("module is too old")
463 self.host = host
464 host.memory.updateParams(self.params)
465 self._p_hints = host.plugins["XEP-0334"]
466 self._p_carbons = host.plugins["XEP-0280"]
467 self._p = host.plugins["XEP-0060"]
468 self._m = host.plugins.get("XEP-0045")
469 self._sid = host.plugins.get("XEP-0359")
470 host.trigger.add("messageReceived", self._messageReceivedTrigger, priority=100050)
471 host.trigger.add("sendMessageData", self._sendMessageDataTrigger)
472 self.host.registerEncryptionPlugin(self, "OMEMO", NS_OMEMO, 100)
473 pep = host.plugins['XEP-0163']
474 pep.addPEPEvent(
475 "OMEMO_DEVICES", NS_OMEMO_DEVICES,
476 lambda itemsEvent, profile: defer.ensureDeferred(
477 self.onNewDevices(itemsEvent, profile))
478 )
479 try: 1201 try:
480 self.text_cmds = self.host.plugins[C.TEXT_CMDS] 1202 self.__text_commands = cast(TextCommands, sat.plugins[C.TEXT_CMDS])
481 except KeyError: 1203 except KeyError:
482 log.info(_("Text commands not available")) 1204 log.info(_("Text commands not available"))
483 else: 1205 else:
484 self.text_cmds.registerTextCommands(self) 1206 self.__text_commands.registerTextCommands(self)
485 1207
486 # Text commands # 1208 async def profileConnected( # pylint: disable=invalid-name
487 1209 self,
488 async def cmd_omemo_reset(self, client, mess_data): 1210 client: SatXMPPClient
489 """reset OMEMO session (use only if encryption is broken) 1211 ) -> None:
490 1212 """
491 @command(one2one): 1213 @param client: The client.
492 """ 1214 """
493 if not client.encryption.isEncryptionRequested(mess_data, NS_OMEMO): 1215
494 feedback = _( 1216 await self.__prepare_for_profile(cast(str, client.profile))
495 "You need to have OMEMO encryption activated to reset the session") 1217
496 self.text_cmds.feedBack(client, feedback, mess_data) 1218 async def cmd_omemo_reset(
1219 self,
1220 client: SatXMPPClient,
1221 mess_data: MessageData
1222 ) -> Literal[False]:
1223 """Reset all sessions of devices that belong to the recipient of ``mess_data``.
1224
1225 This must only be callable manually by the user. Use this when a session is
1226 apparently broken, i.e. sending and receiving encrypted messages doesn't work and
1227 something being wrong has been confirmed manually with the recipient.
1228
1229 @param client: The client.
1230 @param mess_data: The message data, whose ``to`` attribute will be the bare JID to
1231 reset all sessions with.
1232 @return: The constant value ``False``, indicating to the text commands plugin that
1233 the message is not supposed to be sent.
1234 """
1235
1236 twomemo_requested = \
1237 client.encryption.isEncryptionRequested(mess_data, twomemo.twomemo.NAMESPACE)
1238 oldmemo_requested = \
1239 client.encryption.isEncryptionRequested(mess_data, oldmemo.oldmemo.NAMESPACE)
1240
1241 if not (twomemo_requested or oldmemo_requested):
1242 self.__text_commands.feedBack(
1243 client,
1244 _("You need to have OMEMO encryption activated to reset the session"),
1245 mess_data
1246 )
497 return False 1247 return False
498 to_jid = mess_data["to"].userhostJID() 1248
499 session = client._xep_0384_session 1249 bare_jid = mess_data["to"].userhost()
500 devices = await session.getDevices(to_jid) 1250
501 1251 session_manager = await self.__prepare_for_profile(client.profile)
502 for device in devices['active']: 1252 devices = await session_manager.get_device_information(bare_jid)
503 log.debug(f"deleting session for device {device}") 1253
504 await session.deleteSession(to_jid, device=device) 1254 for device in devices:
505 1255 log.debug(f"Replacing sessions with device {device}")
506 log.debug("Sending an empty message to trigger key exchange") 1256 await session_manager.replace_sessions(device)
507 await client.sendMessage(to_jid, {'': ''}) 1257
508 1258 self.__text_commands.feedBack(
509 feedback = _("OMEMO session has been reset") 1259 client,
510 self.text_cmds.feedBack(client, feedback, mess_data) 1260 _("OMEMO session has been reset"),
1261 mess_data
1262 )
1263
511 return False 1264 return False
512 1265
513 async def trustUICb( 1266 async def getTrustUI( # pylint: disable=invalid-name
514 self, xmlui_data, trust_data, expect_problems=None, profile=C.PROF_KEY_NONE): 1267 self,
515 if C.bool(xmlui_data.get('cancelled', 'false')): 1268 client: SatXMPPClient,
1269 entity: jid.JID
1270 ) -> xml_tools.XMLUI:
1271 """
1272 @param client: The client.
1273 @param entity: The entity whose device trust levels to manage.
1274 @return: An XMLUI instance which opens a form to manage the trust level of all
1275 devices belonging to the entity.
1276 """
1277
1278 if entity.resource:
1279 raise ValueError("A bare JID is expected.")
1280
1281 bare_jids: Set[str]
1282 if self.__xep_0045 is not None and self.__xep_0045.isJoinedRoom(client, entity):
1283 bare_jids = self.__get_joined_muc_users(client, self.__xep_0045, entity)
1284 else:
1285 bare_jids = { entity.userhost() }
1286
1287 session_manager = await self.__prepare_for_profile(client.profile)
1288
1289 # At least sort the devices by bare JID such that they aren't listed completely
1290 # random
1291 devices = sorted(cast(Set[omemo.DeviceInformation], set()).union(*[
1292 await session_manager.get_device_information(bare_jid)
1293 for bare_jid
1294 in bare_jids
1295 ]), key=lambda device: device.bare_jid)
1296
1297 async def callback(
1298 data: Any,
1299 profile: str # pylint: disable=unused-argument
1300 ) -> Dict[Never, Never]:
1301 """
1302 @param data: The XMLUI result produces by the trust UI form.
1303 @param profile: The profile.
1304 @return: An empty dictionary. The type of the return value was chosen
1305 conservatively since the exact options are neither known not needed here.
1306 """
1307
1308 if C.bool(data.get("cancelled", "false")):
1309 return {}
1310
1311 data_form_result = cast(
1312 Dict[str, str],
1313 xml_tools.XMLUIResult2DataFormResult(data)
1314 )
1315 for key, value in data_form_result.items():
1316 if not key.startswith("trust_"):
1317 continue
1318
1319 device = devices[int(key[len("trust_"):])]
1320 trust = TrustLevel(value)
1321
1322 if TrustLevel(device.trust_level_name) is not trust:
1323 await session_manager.set_trust(
1324 device.bare_jid,
1325 device.identity_key,
1326 value
1327 )
1328
516 return {} 1329 return {}
517 client = self.host.getClient(profile) 1330
518 session = client._xep_0384_session 1331 submit_id = self.__sat.registerCallback(callback, with_data=True, one_shot=True)
519 stored_data = client._xep_0384_data 1332
520 manual_trust = await stored_data.get(KEY_MANUAL_TRUST, set()) 1333 result = xml_tools.XMLUI(
521 auto_trusted_cache = {} 1334 panel_type=C.XMLUI_FORM,
522 answer = xml_tools.XMLUIResult2DataFormResult(xmlui_data) 1335 title=D_("OMEMO trust management"),
523 blind_trust = C.bool(answer.get('blind_trust', C.BOOL_FALSE)) 1336 submit_id=submit_id
524 for key, value in answer.items():
525 if key.startswith('trust_'):
526 trust_id = key[6:]
527 else:
528 continue
529 data = trust_data[trust_id]
530 if blind_trust:
531 # user request to restore blind trust for this entity
532 # so if the entity is present in manual trust, we remove it
533 if data["jid"].full() in manual_trust:
534 manual_trust.remove(data["jid"].full())
535 await stored_data.aset(KEY_MANUAL_TRUST, manual_trust)
536 elif data["jid"].full() not in manual_trust:
537 # validating this trust UI implies that we activate manual mode for
538 # this entity (used for BTBV policy)
539 manual_trust.add(data["jid"].full())
540 await stored_data.aset(KEY_MANUAL_TRUST, manual_trust)
541 trust = C.bool(value)
542
543 if not trust:
544 # if device is not trusted, we check if it must be removed from auto
545 # trusted devices list
546 bare_jid_s = data['jid'].userhost()
547 key = f"{KEY_AUTO_TRUST}\n{bare_jid_s}"
548 if bare_jid_s not in auto_trusted_cache:
549 auto_trusted_cache[bare_jid_s] = await stored_data.get(
550 key, default=set())
551 auto_trusted = auto_trusted_cache[bare_jid_s]
552 if data['device'] in auto_trusted:
553 # as we don't trust this device anymore, we can remove it from the
554 # list of automatically trusted devices
555 auto_trusted.remove(data['device'])
556 await stored_data.aset(key, auto_trusted)
557 log.info(D_(
558 "device {device} from {peer_jid} is not an auto-trusted device "
559 "anymore").format(device=data['device'], peer_jid=bare_jid_s))
560
561 await session.setTrust(
562 data["jid"],
563 data["device"],
564 data["ik"],
565 trusted=trust,
566 )
567 if not trust and expect_problems is not None:
568 expect_problems.setdefault(data['jid'].userhost(), set()).add(
569 data['device']
570 )
571 return {}
572
573 async def getTrustUI(self, client, entity_jid=None, trust_data=None, submit_id=None):
574 """Generate a XMLUI to manage trust
575
576 @param entity_jid(None, jid.JID): jid of entity to manage
577 None to use trust_data
578 @param trust_data(None, dict): devices data:
579 None to use entity_jid
580 else a dict mapping from trust ids (unicode) to devices data,
581 where a device data must have the following keys:
582 - jid(jid.JID): bare jid of the device owner
583 - device(int): device id
584 - ik(bytes): identity key
585 and may have the following key:
586 - trusted(bool): True if device is trusted
587 @param submit_id(None, unicode): submit_id to use
588 if None set UI callback to trustUICb
589 @return D(xmlui): trust management form
590 """
591 # we need entity_jid xor trust_data
592 assert entity_jid and not trust_data or not entity_jid and trust_data
593 if entity_jid and entity_jid.resource:
594 raise ValueError("A bare jid is expected")
595
596 session = client._xep_0384_session
597 stored_data = client._xep_0384_data
598
599 if trust_data is None:
600 cache = client._xep_0384_cache.setdefault(entity_jid, {})
601 trust_data = {}
602 if self._m is not None and self._m.isJoinedRoom(client, entity_jid):
603 trust_jids = self.getJIDsForRoom(client, entity_jid)
604 else:
605 trust_jids = [entity_jid]
606 for trust_jid in trust_jids:
607 trust_session_data = await session.getTrustForJID(trust_jid)
608 bare_jid_s = trust_jid.userhost()
609 for device_id, trust_info in trust_session_data['active'].items():
610 if trust_info is None:
611 # device has never been (un)trusted, we have to retrieve its
612 # fingerprint (i.e. identity key or "ik") through public bundle
613 if device_id not in cache:
614 bundles, missing = await self.getBundles(client,
615 trust_jid,
616 [device_id])
617 if device_id not in bundles:
618 log.warning(_(
619 "Can't find bundle for device {device_id} of user "
620 "{bare_jid}, ignoring").format(device_id=device_id,
621 bare_jid=bare_jid_s))
622 continue
623 cache[device_id] = bundles[device_id]
624 # TODO: replace False below by None when undecided
625 # trusts are handled
626 trust_info = {
627 "key": cache[device_id].ik,
628 "trusted": False
629 }
630
631 ik = trust_info["key"]
632 trust_id = str(hash((bare_jid_s, device_id, ik)))
633 trust_data[trust_id] = {
634 "jid": trust_jid,
635 "device": device_id,
636 "ik": ik,
637 "trusted": trust_info["trusted"],
638 }
639
640 if submit_id is None:
641 submit_id = self.host.registerCallback(
642 lambda data, profile: defer.ensureDeferred(
643 self.trustUICb(data, trust_data=trust_data, profile=profile)),
644 with_data=True,
645 one_shot=True)
646 xmlui = xml_tools.XMLUI(
647 panel_type = C.XMLUI_FORM,
648 title = D_("OMEMO trust management"),
649 submit_id = submit_id
650 ) 1337 )
651 xmlui.addText(D_( 1338 # Casting this to Any, otherwise all calls on the variable cause type errors
1339 # pylint: disable=no-member
1340 trust_ui = cast(Any, result)
1341 trust_ui.addText(D_(
652 "This is OMEMO trusting system. You'll see below the devices of your " 1342 "This is OMEMO trusting system. You'll see below the devices of your "
653 "contacts, and a checkbox to trust them or not. A trusted device " 1343 "contacts, and a list selection to trust them or not. A trusted device "
654 "can read your messages in plain text, so be sure to only validate " 1344 "can read your messages in plain text, so be sure to only validate "
655 "devices that you are sure are belonging to your contact. It's better " 1345 "devices that you are sure are belonging to your contact. It's better "
656 "to do this when you are next to your contact and her/his device, so " 1346 "to do this when you are next to your contact and their device, so "
657 "you can check the \"fingerprint\" (the number next to the device) " 1347 "you can check the \"fingerprint\" (the number next to the device) "
658 "yourself. Do *not* validate a device if the fingerprint is wrong!")) 1348 "yourself. Do *not* validate a device if the fingerprint is wrong!"
659 1349 ))
660 xmlui.changeContainer("label") 1350
661 xmlui.addLabel(D_("This device ID")) 1351 own_device, __ = await session_manager.get_own_device_information()
662 xmlui.addText(str(client._xep_0384_device_id)) 1352
663 xmlui.addLabel(D_("This device fingerprint")) 1353 trust_ui.changeContainer("label")
664 ik_hex = session.public_bundle.ik.hex().upper() 1354 trust_ui.addLabel(D_("This device ID"))
665 fp_human = ' '.join([ik_hex[i:i+8] for i in range(0, len(ik_hex), 8)]) 1355 trust_ui.addText(str(own_device.device_id))
666 xmlui.addText(fp_human) 1356 trust_ui.addLabel(D_("This device's fingerprint"))
667 xmlui.addEmpty() 1357 trust_ui.addText(" ".join(session_manager.format_identity_key(
668 xmlui.addEmpty() 1358 own_device.identity_key
669 1359 )))
670 if entity_jid is not None: 1360 trust_ui.addEmpty()
671 omemo_policy = self.host.memory.getParamA( 1361 trust_ui.addEmpty()
672 PARAM_NAME, PARAM_CATEGORY, profile_key=client.profile 1362
1363 for index, device in enumerate(devices):
1364 trust_ui.addLabel(D_("Contact"))
1365 trust_ui.addJid(jid.JID(device.bare_jid))
1366 trust_ui.addLabel(D_("Device ID"))
1367 trust_ui.addText(str(device.device_id))
1368 trust_ui.addLabel(D_("Fingerprint"))
1369 trust_ui.addText(" ".join(session_manager.format_identity_key(
1370 device.identity_key
1371 )))
1372 trust_ui.addLabel(D_("Trust this device?"))
1373
1374 current_trust_level = TrustLevel(device.trust_level_name)
1375 avaiable_trust_levels = \
1376 { TrustLevel.DISTRUSTED, TrustLevel.TRUSTED, current_trust_level }
1377
1378 trust_ui.addList(
1379 f"trust_{index}",
1380 options=[ trust_level.name for trust_level in avaiable_trust_levels ],
1381 selected=current_trust_level.name,
1382 styles=[ "inline" ]
673 ) 1383 )
674 if omemo_policy == 'btbv': 1384
675 xmlui.addLabel(D_("Automatically trust new devices?")) 1385 twomemo_active = dict(device.active).get(twomemo.twomemo.NAMESPACE)
676 # blind trust is always disabled when UI is requested 1386 if twomemo_active is None:
677 # as submitting UI is a verification which should disable it. 1387 trust_ui.addEmpty()
678 xmlui.addBool("blind_trust", value=C.BOOL_FALSE) 1388 trust_ui.addLabel(D_("(not available for Twomemo)"))
679 xmlui.addEmpty() 1389 if twomemo_active is False:
680 xmlui.addEmpty() 1390 trust_ui.addEmpty()
681 1391 trust_ui.addLabel(D_("(inactive for Twomemo)"))
682 auto_trust_cache = {} 1392
683 1393 oldmemo_active = dict(device.active).get(oldmemo.oldmemo.NAMESPACE)
684 for trust_id, data in trust_data.items(): 1394 if oldmemo_active is None:
685 bare_jid_s = data['jid'].userhost() 1395 trust_ui.addEmpty()
686 if bare_jid_s not in auto_trust_cache: 1396 trust_ui.addLabel(D_("(not available for Oldmemo)"))
687 key = f"{KEY_AUTO_TRUST}\n{bare_jid_s}" 1397 if oldmemo_active is False:
688 auto_trust_cache[bare_jid_s] = await stored_data.get(key, set()) 1398 trust_ui.addEmpty()
689 xmlui.addLabel(D_("Contact")) 1399 trust_ui.addLabel(D_("(inactive for Oldmemo)"))
690 xmlui.addJid(data['jid']) 1400
691 xmlui.addLabel(D_("Device ID")) 1401 trust_ui.addEmpty()
692 xmlui.addText(str(data['device'])) 1402 trust_ui.addEmpty()
693 xmlui.addLabel(D_("Fingerprint")) 1403
694 ik_hex = data['ik'].hex().upper() 1404 return result
695 fp_human = ' '.join([ik_hex[i:i+8] for i in range(0, len(ik_hex), 8)]) 1405
696 xmlui.addText(fp_human) 1406 @staticmethod
697 xmlui.addLabel(D_("Trust this device?")) 1407 def __get_joined_muc_users(
698 xmlui.addBool("trust_{}".format(trust_id), 1408 client: SatXMPPClient,
699 value=C.boolConst(data.get('trusted', False))) 1409 xep_0045: XEP_0045,
700 if data['device'] in auto_trust_cache[bare_jid_s]: 1410 room_jid: jid.JID
701 xmlui.addEmpty() 1411 ) -> Set[str]:
702 xmlui.addLabel(D_("(automatically trusted)")) 1412 """
703 1413 @param client: The client.
704 1414 @param xep_0045: A MUC plugin instance.
705 xmlui.addEmpty() 1415 @param room_jid: The room JID.
706 xmlui.addEmpty() 1416 @return: A set containing the bare JIDs of the MUC participants.
707 1417 @raise InternalError: if the MUC is not joined or the entity information of a
708 return xmlui 1418 participant isn't available.
709 1419 """
710 async def profileConnected(self, client): 1420
711 if self._m is not None: 1421 bare_jids: Set[str] = set()
712 # we keep plain text message for MUC messages we send 1422
713 # as we can't encrypt for our own device 1423 try:
714 client._xep_0384_muc_cache = {} 1424 room = cast(muc.Room, xep_0045.getRoom(client, room_jid))
715 # and we keep them only for some time, in case something goes wrong 1425 except exceptions.NotFound as e:
716 # with the MUC 1426 raise exceptions.InternalError(
717 client._xep_0384_muc_cache_timer = None 1427 "Participant list of unjoined MUC requested."
718 1428 ) from e
719 # FIXME: is _xep_0384_ready needed? can we use profileConnecting? 1429
720 # Workflow should be checked 1430 for user in cast(Dict[str, muc.User], room.roster).values():
721 client._xep_0384_ready = defer.Deferred() 1431 entity = cast(Optional[SatXMPPEntity], user.entity)
722 # we first need to get devices ids (including our own) 1432 if entity is None:
723 persistent_dict = persistent.LazyPersistentBinaryDict("XEP-0384", client.profile) 1433 raise exceptions.InternalError(
724 client._xep_0384_data = persistent_dict 1434 f"Participant list of MUC requested, but the entity information of"
725 # all known devices of profile 1435 f" the participant {user} is not available."
726 devices = await self.getDevices(client) 1436 )
727 # and our own device id 1437
728 device_id = await persistent_dict.get(KEY_DEVICE_ID) 1438 bare_jids.add(entity.jid.userhost())
729 if device_id is None: 1439
730 log.info(_("We have no identity for this device yet, let's generate one")) 1440 return bare_jids
731 # we have a new device, we create device_id 1441
732 device_id = random.randint(1, 2**31-1) 1442 async def __prepare_for_profile(self, profile: str) -> omemo.SessionManager:
733 # we check that it's really unique 1443 """
734 while device_id in devices: 1444 @param profile: The profile to prepare for.
735 device_id = random.randint(1, 2**31-1) 1445 @return: A session manager instance for this profile. Creates a new instance if
736 # and we save it 1446 none was prepared before.
737 await persistent_dict.aset(KEY_DEVICE_ID, device_id) 1447 """
738 1448
739 log.debug(f"our OMEMO device id is {device_id}") 1449 try:
740 1450 # Try to return the session manager
741 if device_id not in devices: 1451 return self.__session_managers[profile]
742 log.debug(f"our device id ({device_id}) is not in the list, adding it") 1452 except KeyError:
743 devices.add(device_id) 1453 # If a session manager for that profile doesn't exist yet, check whether it is
744 await defer.ensureDeferred(self.setDevices(client, devices)) 1454 # currently being built. A session manager being built is signified by the
745 1455 # profile key existing on __session_manager_waiters.
746 all_jids = await persistent_dict.get(KEY_ALL_JIDS, set()) 1456 if profile in self.__session_manager_waiters:
747 1457 # If the session manager is being built, add ourselves to the waiting
748 omemo_storage = OmemoStorage(client, device_id, all_jids) 1458 # queue
749 omemo_session = await OmemoSession.create(client, omemo_storage, device_id) 1459 deferred = defer.Deferred()
750 client._xep_0384_cache = {} 1460 self.__session_manager_waiters[profile].append(deferred)
751 client._xep_0384_session = omemo_session 1461 return cast(omemo.SessionManager, await deferred)
752 client._xep_0384_device_id = device_id 1462
753 await omemo_session.newDeviceList(client.jid, devices) 1463 # If the session manager is not being built, do so here.
754 if omemo_session.republish_bundle: 1464 self.__session_manager_waiters[profile] = []
755 log.info(_("Saving public bundle for this device ({device_id})").format( 1465
756 device_id=device_id)) 1466 # Build and store the session manager
757 await defer.ensureDeferred( 1467 session_manager = await prepare_for_profile(
758 self.setBundle(client, omemo_session.public_bundle, device_id) 1468 self.__sat,
1469 profile,
1470 initial_own_label="Libervia"
759 ) 1471 )
760 client._xep_0384_ready.callback(None) 1472 self.__session_managers[profile] = session_manager
761 del client._xep_0384_ready 1473
762 1474 # Notify the waiters and delete them
763 1475 for waiter in self.__session_manager_waiters[profile]:
764 ## XMPP PEP nodes manipulation 1476 waiter.callback(session_manager)
765 1477 del self.__session_manager_waiters[profile]
766 # devices 1478
767 1479 return session_manager
768 def parseDevices(self, items): 1480
769 """Parse devices found in items 1481 async def __message_received_trigger(
770 1482 self,
771 @param items(iterable[domish.Element]): items as retrieved by getItems 1483 client: SatXMPPClient,
772 @return set[int]: parsed devices 1484 message_elt: domish.Element,
773 """ 1485 post_treat: defer.Deferred
774 devices = set() 1486 ) -> bool:
775 if len(items) > 1: 1487 """
776 log.warning(_("OMEMO devices list is stored in more that one items, " 1488 @param client: The client which received the message.
777 "this is not expected")) 1489 @param message_elt: The message element. Can be modified.
778 if items: 1490 @param post_treat: A deferred which evaluates to a :class:`MessageData` once the
1491 message has fully progressed through the message receiving flow. Can be used
1492 to apply treatments to the fully processed message, like marking it as
1493 encrypted.
1494 @return: Whether to continue the message received flow.
1495 """
1496
1497 muc_plaintext_cache_key: Optional[MUCPlaintextCacheKey] = None
1498
1499 sender_jid = jid.JID(message_elt["from"])
1500 feedback_jid: jid.JID
1501
1502 message_type = message_elt.getAttribute("type", "unknown")
1503 is_muc_message = message_type == C.MESS_TYPE_GROUPCHAT
1504 if is_muc_message:
1505 if self.__xep_0045 is None:
1506 log.warning(
1507 "Ignoring MUC message since plugin XEP-0045 is not available."
1508 )
1509 # Can't handle a MUC message without XEP-0045, let the flow continue
1510 # normally
1511 return True
1512
1513 room_jid = feedback_jid = sender_jid.userhostJID()
1514
779 try: 1515 try:
780 list_elt = next(items[0].elements(NS_OMEMO, 'list')) 1516 room = cast(muc.Room, self.__xep_0045.getRoom(client, room_jid))
781 except StopIteration: 1517 except exceptions.NotFound:
782 log.warning(_("no list element found in OMEMO devices list")) 1518 log.warning(
783 return devices 1519 f"Ignoring MUC message from a room that has not been joined:"
784 for device_elt in list_elt.elements(NS_OMEMO, 'device'): 1520 f" {room_jid}"
1521 )
1522 # Whatever, let the flow continue
1523 return True
1524
1525 sender_user = cast(Optional[muc.User], room.getUser(sender_jid.resource))
1526 if sender_user is None:
1527 log.warning(
1528 f"Ignoring MUC message from room {room_jid} since the sender's user"
1529 f" wasn't found {sender_jid.resource}"
1530 )
1531 # Whatever, let the flow continue
1532 return True
1533
1534 sender_user_jid = cast(Optional[jid.JID], sender_user.entity)
1535 if sender_user_jid is None:
1536 log.warning(
1537 f"Ignoring MUC message from room {room_jid} since the sender's bare"
1538 f" JID couldn't be found from its user information: {sender_user}"
1539 )
1540 # Whatever, let the flow continue
1541 return True
1542
1543 sender_jid = sender_user_jid
1544
1545 message_uid: Optional[str] = None
1546 if self.__xep_0359 is not None:
1547 message_uid = self.__xep_0359.getOriginId(message_elt)
1548 if message_uid is None:
1549 message_uid = message_elt.getAttribute("id")
1550 if message_uid is not None:
1551 muc_plaintext_cache_key = MUCPlaintextCacheKey(
1552 client,
1553 room_jid,
1554 message_uid
1555 )
1556 else:
1557 # I'm not sure why this check is required, this code is copied from the old
1558 # plugin.
1559 if sender_jid.userhostJID() == client.jid.userhostJID():
1560 # TODO: I've seen this cause an exception "builtins.KeyError: 'to'", seems
1561 # like "to" isn't always set.
1562 feedback_jid = jid.JID(message_elt["to"])
1563 else:
1564 feedback_jid = sender_jid
1565
1566 sender_bare_jid = sender_jid.userhost()
1567
1568 message: Optional[omemo.Message] = None
1569 encrypted_elt: Optional[domish.Element] = None
1570
1571 twomemo_encrypted_elt = cast(Optional[domish.Element], next(
1572 message_elt.elements(twomemo.twomemo.NAMESPACE, "encrypted"),
1573 None
1574 ))
1575
1576 oldmemo_encrypted_elt = cast(Optional[domish.Element], next(
1577 message_elt.elements(oldmemo.oldmemo.NAMESPACE, "encrypted"),
1578 None
1579 ))
1580
1581 session_manager = await self.__prepare_for_profile(cast(str, client.profile))
1582
1583 if twomemo_encrypted_elt is not None:
1584 try:
1585 message = twomemo.etree.parse_message(
1586 domish_to_etree(twomemo_encrypted_elt),
1587 sender_bare_jid
1588 )
1589 except (ValueError, XMLSchemaValidationError):
1590 log.warning(
1591 f"Ingoring malformed encrypted message for namespace"
1592 f" {twomemo.twomemo.NAMESPACE}: {twomemo_encrypted_elt.toXml()}"
1593 )
1594 else:
1595 encrypted_elt = twomemo_encrypted_elt
1596
1597 if oldmemo_encrypted_elt is not None:
1598 try:
1599 message = await oldmemo.etree.parse_message(
1600 domish_to_etree(oldmemo_encrypted_elt),
1601 sender_bare_jid,
1602 client.jid.userhost(),
1603 session_manager
1604 )
1605 except (ValueError, XMLSchemaValidationError):
1606 log.warning(
1607 f"Ingoring malformed encrypted message for namespace"
1608 f" {oldmemo.oldmemo.NAMESPACE}: {oldmemo_encrypted_elt.toXml()}"
1609 )
1610 except omemo.SenderNotFound:
1611 log.warning(
1612 f"Ingoring encrypted message for namespace"
1613 f" {oldmemo.oldmemo.NAMESPACE} by unknown sender:"
1614 f" {oldmemo_encrypted_elt.toXml()}"
1615 )
1616 else:
1617 encrypted_elt = oldmemo_encrypted_elt
1618
1619 if message is None or encrypted_elt is None:
1620 # None of our business, let the flow continue
1621 return True
1622
1623 message_elt.children.remove(encrypted_elt)
1624
1625 log.debug(
1626 f"{message.namespace} message of type {message_type} received from"
1627 f" {sender_bare_jid}"
1628 )
1629
1630 plaintext: Optional[bytes]
1631 device_information: omemo.DeviceInformation
1632
1633 if (
1634 muc_plaintext_cache_key is not None
1635 and muc_plaintext_cache_key in self.__muc_plaintext_cache
1636 ):
1637 # Use the cached plaintext
1638 plaintext = self.__muc_plaintext_cache.pop(muc_plaintext_cache_key)
1639
1640 # Since this message was sent by us, use the own device information here
1641 device_information, __ = await session_manager.get_own_device_information()
1642 else:
1643 try:
1644 plaintext, device_information = await session_manager.decrypt(message)
1645 except omemo.MessageNotForUs:
1646 # The difference between this being a debug or a warning is whether there
1647 # is a body included in the message. Without a body, we can assume that
1648 # it's an empty OMEMO message used for protocol stability reasons, which
1649 # is not expected to be sent to all devices of all recipients. If a body
1650 # is included, we can assume that the message carries content and we
1651 # missed out on something.
1652 if len(list(message_elt.elements(C.NS_CLIENT, "body"))) > 0:
1653 client.feedback(
1654 feedback_jid,
1655 D_(
1656 f"An OMEMO message from {sender_jid.full()} has not been"
1657 f" encrypted for our device, we can't decrypt it."
1658 ),
1659 { C.MESS_EXTRA_INFO: C.EXTRA_INFO_DECR_ERR }
1660 )
1661 log.warning("Message not encrypted for us.")
1662 else:
1663 log.debug("Message not encrypted for us.")
1664
1665 # No point in further processing this message.
1666 return False
1667 except Exception as e:
1668 log.warning(_("Can't decrypt message: {reason}\n{xml}").format(
1669 reason=e,
1670 xml=message_elt.toXml()
1671 ))
1672 client.feedback(
1673 feedback_jid,
1674 D_(
1675 f"An OMEMO message from {sender_jid.full()} can't be decrypted:"
1676 f" {e}"
1677 ),
1678 { C.MESS_EXTRA_INFO: C.EXTRA_INFO_DECR_ERR }
1679 )
1680 # No point in further processing this message
1681 return False
1682
1683 if message.namespace == twomemo.twomemo.NAMESPACE:
1684 if plaintext is not None:
1685 # XEP_0420.unpack_stanza handles the whole unpacking, including the
1686 # relevant modifications to the element
1687 sce_profile = \
1688 OMEMO.SCE_PROFILE_GROUPCHAT if is_muc_message else OMEMO.SCE_PROFILE
785 try: 1689 try:
786 device_id = int(device_elt['id']) 1690 affix_values = self.__xep_0420.unpack_stanza(
787 except KeyError: 1691 sce_profile,
788 log.warning(_('device element is missing "id" attribute: {elt}') 1692 message_elt,
789 .format(elt=device_elt.toXml())) 1693 plaintext
790 except ValueError: 1694 )
791 log.warning(_('invalid device id: {device_id}').format( 1695 except Exception as e:
792 device_id=device_elt['id'])) 1696 log.warning(D_(
793 else: 1697 f"Error unpacking SCE-encrypted message: {e}\n{plaintext}"
794 devices.add(device_id) 1698 ))
795 return devices 1699 client.feedback(
796 1700 feedback_jid,
797 async def getDevices(self, client, entity_jid=None): 1701 D_(
798 """Retrieve list of registered OMEMO devices 1702 f"An OMEMO message from {sender_jid.full()} was rejected:"
799 1703 f" {e}"
800 @param entity_jid(jid.JID, None): get devices from this entity 1704 ),
801 None to get our own devices 1705 { C.MESS_EXTRA_INFO: C.EXTRA_INFO_DECR_ERR }
802 @return (set(int)): list of devices 1706 )
803 """ 1707 # No point in further processing this message
804 if entity_jid is not None: 1708 return False
805 assert not entity_jid.resource 1709
1710 if affix_values.timestamp is not None:
1711 # TODO: affix_values.timestamp contains the timestamp included in the
1712 # encrypted element here. The XEP says it SHOULD be displayed with the
1713 # plaintext by clients.
1714 pass
1715
1716 if message.namespace == oldmemo.oldmemo.NAMESPACE:
1717 # Remove all body elements from the original element, since those act as
1718 # fallbacks in case the encryption protocol is not supported
1719 for child in message_elt.elements():
1720 if child.name == "body":
1721 message_elt.children.remove(child)
1722
1723 if plaintext is not None:
1724 # Add the decrypted body
1725 message_elt.addElement("body", content=plaintext.decode("utf-8"))
1726
1727 # Mark the message as trusted or untrusted. Undecided counts as untrusted here.
1728 trust_level = \
1729 TrustLevel(device_information.trust_level_name).to_omemo_trust_level()
1730 if trust_level is omemo.TrustLevel.TRUSTED:
1731 post_treat.addCallback(client.encryption.markAsTrusted)
1732 else:
1733 post_treat.addCallback(client.encryption.markAsUntrusted)
1734
1735 # Mark the message as originally encrypted
1736 post_treat.addCallback(
1737 client.encryption.markAsEncrypted,
1738 namespace=message.namespace
1739 )
1740
1741 # Message processed successfully, continue with the flow
1742 return True
1743
1744 async def __send_trigger(self, client: SatXMPPClient, stanza: domish.Element) -> bool:
1745 """
1746 @param client: The client sending this message.
1747 @param stanza: The stanza that is about to be sent. Can be modified.
1748 @return: Whether the send message flow should continue or not.
1749 """
1750
1751 # SCE is only applicable to message and IQ stanzas
1752 if stanza.name not in { "message", "iq" }:
1753 return True
1754
1755 # Get the intended recipient
1756 recipient = stanza.getAttribute("to", None)
1757 if recipient is None:
1758 if stanza.name == "message":
1759 # Message stanzas must have a recipient
1760 raise exceptions.InternalError(
1761 f"Message without recipient encountered. Blocking further processing"
1762 f" to avoid leaking plaintext data: {stanza.toXml()}"
1763 )
1764
1765 # IQs without a recipient are a thing, I believe those simply target the
1766 # server and are thus not eligible for e2ee anyway.
1767 return True
1768
1769 # Parse the JID
1770 recipient_bare_jid = jid.JID(recipient).userhostJID()
1771
1772 # Check whether encryption with twomemo is requested
1773 encryption = client.encryption.getSession(recipient_bare_jid)
1774
1775 if encryption is None:
1776 # Encryption is not requested for this recipient
1777 return True
1778
1779 if encryption["plugin"].namespace != twomemo.twomemo.NAMESPACE:
1780 # Encryption is requested for this recipient, but not with twomemo
1781 return True
1782
1783 # All pre-checks done, we can start encrypting!
1784 await self.__encrypt(
1785 client,
1786 twomemo.twomemo.NAMESPACE,
1787 stanza,
1788 recipient_bare_jid,
1789 stanza.getAttribute("type", "unkown") == C.MESS_TYPE_GROUPCHAT,
1790 stanza.getAttribute("id", None)
1791 )
1792
1793 # Add a store hint if this is a message stanza
1794 if stanza.name == "message":
1795 self.__xep_0334.addHintElements(stanza, [ "store" ])
1796
1797 # Let the flow continue.
1798 return True
1799
1800 async def __send_message_data_trigger(
1801 self,
1802 client: SatXMPPClient,
1803 mess_data: MessageData
1804 ) -> None:
1805 """
1806 @param client: The client sending this message.
1807 @param mess_data: The message data that is about to be sent. Can be modified.
1808 """
1809
1810 # Check whether encryption is requested for this message
806 try: 1811 try:
807 items, metadata = await self._p.getItems(client, entity_jid, NS_OMEMO_DEVICES) 1812 namespace = mess_data[C.MESS_KEY_ENCRYPTION]["plugin"].namespace
808 except exceptions.NotFound: 1813 except KeyError:
809 log.info(_("there is no node to handle OMEMO devices")) 1814 return
810 return set() 1815
811 1816 # If encryption is requested, check whether it's oldmemo
812 devices = self.parseDevices(items) 1817 if namespace != oldmemo.oldmemo.NAMESPACE:
813 return devices 1818 return
814 1819
815 async def setDevices(self, client, devices): 1820 # All pre-checks done, we can start encrypting!
816 log.debug(f"setting devices with {', '.join(str(d) for d in devices)}") 1821 stanza = mess_data["xml"]
817 list_elt = domish.Element((NS_OMEMO, 'list')) 1822 recipient_jid = mess_data["to"]
818 for device in devices: 1823 is_muc_message = mess_data["type"] == C.MESS_TYPE_GROUPCHAT
819 device_elt = list_elt.addElement('device') 1824 stanza_id = mess_data["uid"]
820 device_elt['id'] = str(device) 1825
1826 await self.__encrypt(
1827 client,
1828 oldmemo.oldmemo.NAMESPACE,
1829 stanza,
1830 recipient_jid,
1831 is_muc_message,
1832 stanza_id
1833 )
1834
1835 # Add a store hint
1836 self.__xep_0334.addHintElements(stanza, [ "store" ])
1837
1838 async def __encrypt(
1839 self,
1840 client: SatXMPPClient,
1841 namespace: Literal["urn:xmpp:omemo:2", "eu.siacs.conversations.axolotl"],
1842 stanza: domish.Element,
1843 recipient_jid: jid.JID,
1844 is_muc_message: bool,
1845 stanza_id: Optional[str]
1846 ) -> None:
1847 """
1848 @param client: The client.
1849 @param namespace: The namespace of the OMEMO version to use.
1850 @param stanza: The stanza. Twomemo will encrypt the whole stanza using SCE,
1851 oldmemo will encrypt only the body. The stanza is modified by this call.
1852 @param recipient_jid: The JID of the recipient. Can be a bare (aka "userhost") JID
1853 but doesn't have to.
1854 @param is_muc_message: Whether the stanza is a message stanza to a MUC room.
1855 @param stanza_id: The id of this stanza. Especially relevant for message stanzas
1856 to MUC rooms such that the outgoing plaintext can be cached for MUC message
1857 reflection handling.
1858
1859 @warning: The calling code MUST take care of adding the store message processing
1860 hint to the stanza if applicable! This can be done before or after this call,
1861 the order doesn't matter.
1862 """
1863
1864 muc_plaintext_cache_key: Optional[MUCPlaintextCacheKey] = None
1865
1866 recipient_bare_jids: Set[str]
1867 feedback_jid: jid.JID
1868
1869 if is_muc_message:
1870 if self.__xep_0045 is None:
1871 raise exceptions.InternalError(
1872 "Encryption of MUC message requested, but plugin XEP-0045 is not"
1873 " available."
1874 )
1875
1876 if stanza_id is None:
1877 raise exceptions.InternalError(
1878 "Encryption of MUC message requested, but stanza id not available."
1879 )
1880
1881 room_jid = feedback_jid = recipient_jid.userhostJID()
1882
1883 recipient_bare_jids = self.__get_joined_muc_users(
1884 client,
1885 self.__xep_0045,
1886 room_jid
1887 )
1888
1889 muc_plaintext_cache_key = MUCPlaintextCacheKey(
1890 client=client,
1891 room_jid=room_jid,
1892 message_uid=stanza_id
1893 )
1894 else:
1895 recipient_bare_jids = { recipient_jid.userhost() }
1896 feedback_jid = recipient_jid.userhostJID()
1897
1898 log.debug(
1899 f"Intercepting message that is to be encrypted by {namespace} for"
1900 f" {recipient_bare_jids}"
1901 )
1902
1903 def prepare_stanza() -> Optional[bytes]:
1904 """Prepares the stanza for encryption.
1905
1906 Does so by removing all parts that are not supposed to be sent in plain. Also
1907 extracts/prepares the plaintext to encrypt.
1908
1909 @return: The plaintext to encrypt. Returns ``None`` in case body-only
1910 encryption is requested and no body was found. The function should
1911 gracefully return in that case, i.e. it's not a critical error that should
1912 abort the message sending flow.
1913 """
1914
1915 if namespace == twomemo.twomemo.NAMESPACE:
1916 return self.__xep_0420.pack_stanza(
1917 OMEMO.SCE_PROFILE_GROUPCHAT if is_muc_message else OMEMO.SCE_PROFILE,
1918 stanza
1919 )
1920
1921 if namespace == oldmemo.oldmemo.NAMESPACE:
1922 plaintext: Optional[bytes] = None
1923
1924 for child in stanza.elements():
1925 if child.name == "body" and plaintext is None:
1926 plaintext = str(child).encode("utf-8")
1927
1928 # Any other sensitive elements to remove here?
1929 if child.name in { "body", "html" }:
1930 stanza.children.remove(child)
1931
1932 if plaintext is None:
1933 log.warning(
1934 "No body found in intercepted message to be encrypted with"
1935 " oldmemo."
1936 )
1937
1938 return plaintext
1939
1940 return assert_never(namespace)
1941
1942 # The stanza/plaintext preparation was moved into its own little function for type
1943 # safety reasons.
1944 plaintext = prepare_stanza()
1945 if plaintext is None:
1946 return
1947
1948 log.debug(f"Plaintext to encrypt: {plaintext}")
1949
1950 session_manager = await self.__prepare_for_profile(client.profile)
1951
821 try: 1952 try:
822 await self._p.sendItem( 1953 messages, encryption_errors = await session_manager.encrypt(
823 client, None, NS_OMEMO_DEVICES, list_elt, 1954 frozenset(recipient_bare_jids),
824 item_id=self._p.ID_SINGLETON, 1955 { namespace: plaintext },
825 extra={ 1956 backend_priority_order=[ namespace ],
826 self._p.EXTRA_PUBLISH_OPTIONS: {self._p.OPT_MAX_ITEMS: 1}, 1957 identifier=feedback_jid.userhost()
827 self._p.EXTRA_ON_PRECOND_NOT_MET: "publish_without_options",
828 }
829 ) 1958 )
830 except Exception as e: 1959 except Exception as e:
831 log.warning(_("Can't set devices: {reason}").format(reason=e)) 1960 msg = _(
832 1961 # pylint: disable=consider-using-f-string
833 # bundles 1962 "Can't encrypt message for {entities}: {reason}".format(
834 1963 entities=', '.join(recipient_bare_jids),
835 async def getBundles(self, client, entity_jid, devices_ids): 1964 reason=e
836 """Retrieve public bundles of an entity devices 1965 )
837
838 @param entity_jid(jid.JID): bare jid of entity
839 @param devices_id(iterable[int]): ids of the devices bundles to retrieve
840 @return (tuple(dict[int, ExtendedPublicBundle], list(int))):
841 - bundles collection:
842 * key is device_id
843 * value is parsed bundle
844 - set of bundles not found
845 """
846 assert not entity_jid.resource
847 bundles = {}
848 missing = set()
849 for device_id in devices_ids:
850 node = NS_OMEMO_BUNDLE.format(device_id=device_id)
851 try:
852 items, metadata = await self._p.getItems(client, entity_jid, node)
853 except exceptions.NotFound:
854 log.warning(_("Bundle missing for device {device_id}")
855 .format(device_id=device_id))
856 missing.add(device_id)
857 continue
858 except jabber_error.StanzaError as e:
859 log.warning(_("Can't get bundle for device {device_id}: {reason}")
860 .format(device_id=device_id, reason=e))
861 continue
862 if not items:
863 log.warning(_("no item found in node {node}, can't get public bundle "
864 "for device {device_id}").format(node=node,
865 device_id=device_id))
866 continue
867 if len(items) > 1:
868 log.warning(_("more than one item found in {node}, "
869 "this is not expected").format(node=node))
870 item = items[0]
871 try:
872 bundle_elt = next(item.elements(NS_OMEMO, 'bundle'))
873 signedPreKeyPublic_elt = next(bundle_elt.elements(
874 NS_OMEMO, 'signedPreKeyPublic'))
875 signedPreKeySignature_elt = next(bundle_elt.elements(
876 NS_OMEMO, 'signedPreKeySignature'))
877 identityKey_elt = next(bundle_elt.elements(
878 NS_OMEMO, 'identityKey'))
879 prekeys_elt = next(bundle_elt.elements(
880 NS_OMEMO, 'prekeys'))
881 except StopIteration:
882 log.warning(_("invalid bundle for device {device_id}, ignoring").format(
883 device_id=device_id))
884 continue
885
886 try:
887 spkPublic = base64.b64decode(str(signedPreKeyPublic_elt))
888 spkSignature = base64.b64decode(
889 str(signedPreKeySignature_elt))
890
891 ik = base64.b64decode(str(identityKey_elt))
892 spk = {
893 "key": spkPublic,
894 "id": int(signedPreKeyPublic_elt['signedPreKeyId'])
895 }
896 otpks = []
897 for preKeyPublic_elt in prekeys_elt.elements(NS_OMEMO, 'preKeyPublic'):
898 preKeyPublic = base64.b64decode(str(preKeyPublic_elt))
899 otpk = {
900 "key": preKeyPublic,
901 "id": int(preKeyPublic_elt['preKeyId'])
902 }
903 otpks.append(otpk)
904
905 except Exception as e:
906 log.warning(_("error while decoding key for device {device_id}: {msg}")
907 .format(device_id=device_id, msg=e))
908 continue
909
910 bundles[device_id] = ExtendedPublicBundle.parse(omemo_backend, ik, spk,
911 spkSignature, otpks)
912
913 return (bundles, missing)
914
915 async def setBundle(self, client, bundle, device_id):
916 """Set public bundle for this device.
917
918 @param bundle(ExtendedPublicBundle): bundle to publish
919 """
920 log.debug(_("updating bundle for {device_id}").format(device_id=device_id))
921 bundle = bundle.serialize(omemo_backend)
922 bundle_elt = domish.Element((NS_OMEMO, 'bundle'))
923 signedPreKeyPublic_elt = bundle_elt.addElement(
924 "signedPreKeyPublic",
925 content=b64enc(bundle["spk"]['key']))
926 signedPreKeyPublic_elt['signedPreKeyId'] = str(bundle["spk"]['id'])
927
928 bundle_elt.addElement(
929 "signedPreKeySignature",
930 content=b64enc(bundle["spk_signature"]))
931
932 bundle_elt.addElement(
933 "identityKey",
934 content=b64enc(bundle["ik"]))
935
936 prekeys_elt = bundle_elt.addElement('prekeys')
937 for otpk in bundle["otpks"]:
938 preKeyPublic_elt = prekeys_elt.addElement(
939 'preKeyPublic',
940 content=b64enc(otpk["key"]))
941 preKeyPublic_elt['preKeyId'] = str(otpk['id'])
942
943 node = NS_OMEMO_BUNDLE.format(device_id=device_id)
944 try:
945 await self._p.sendItem(
946 client, None, node, bundle_elt, item_id=self._p.ID_SINGLETON,
947 extra={
948 self._p.EXTRA_PUBLISH_OPTIONS: {self._p.OPT_MAX_ITEMS: 1},
949 self._p.EXTRA_ON_PRECOND_NOT_MET: "publish_without_options",
950 }
951 ) 1966 )
952 except Exception as e: 1967 log.warning(msg)
953 log.warning(_("Can't set bundle: {reason}").format(reason=e)) 1968 client.feedback(feedback_jid, msg, {
954 1969 C.MESS_EXTRA_INFO: C.EXTRA_INFO_ENCR_ERR
955 ## PEP node events callbacks 1970 })
956 1971 raise e
957 async def onNewDevices(self, itemsEvent, profile): 1972
958 log.debug("devices list has been updated") 1973 if len(encryption_errors) > 0:
959 client = self.host.getClient(profile) 1974 log.warning(
960 try: 1975 f"Ignored the following non-critical encryption errors:"
961 omemo_session = client._xep_0384_session 1976 f" {encryption_errors}"
962 except AttributeError:
963 await client._xep_0384_ready
964 omemo_session = client._xep_0384_session
965 entity = itemsEvent.sender
966
967 devices = self.parseDevices(itemsEvent.items)
968 await omemo_session.newDeviceList(entity, devices)
969
970 if entity == client.jid.userhostJID():
971 own_device = client._xep_0384_device_id
972 if own_device not in devices:
973 log.warning(_("Our own device is missing from devices list, fixing it"))
974 devices.add(own_device)
975 await self.setDevices(client, devices)
976
977 ## triggers
978
979 async def policyBTBV(self, client, feedback_jid, expect_problems, undecided):
980 session = client._xep_0384_session
981 stored_data = client._xep_0384_data
982 for pb in undecided.values():
983 peer_jid = jid.JID(pb.bare_jid)
984 device = pb.device
985 ik = pb.ik
986 key = f"{KEY_AUTO_TRUST}\n{pb.bare_jid}"
987 auto_trusted = await stored_data.get(key, default=set())
988 auto_trusted.add(device)
989 await stored_data.aset(key, auto_trusted)
990 await session.setTrust(peer_jid, device, ik, True)
991
992 user_msg = D_(
993 "Not all destination devices are trusted, unknown devices will be blind "
994 "trusted due to the OMEMO Blind Trust Before Verification policy. If you "
995 "want a more secure workflow, please activate \"manual\" OMEMO policy in "
996 "settings' \"Security\" tab.\nFollowing fingerprint have been automatically "
997 "trusted:\n{devices}"
998 ).format(
999 devices = ', '.join(
1000 f"- {pb.device} ({pb.bare_jid}): {pb.ik.hex().upper()}"
1001 for pb in undecided.values()
1002 ) 1977 )
1003 ) 1978
1004 client.feedback(feedback_jid, user_msg) 1979 encrypted_errors_stringified = ", ".join([
1005 1980 f"device {err.device_id} of {err.bare_jid} under namespace"
1006 async def policyManual(self, client, feedback_jid, expect_problems, undecided): 1981 f" {err.namespace}"
1007 trust_data = {} 1982 for err
1008 for trust_id, data in undecided.items(): 1983 in encryption_errors
1009 trust_data[trust_id] = { 1984 ])
1010 'jid': jid.JID(data.bare_jid), 1985
1011 'device': data.device,
1012 'ik': data.ik}
1013
1014 user_msg = D_("Not all destination devices are trusted, we can't encrypt "
1015 "message in such a situation. Please indicate if you trust "
1016 "those devices or not in the trust manager before we can "
1017 "send this message")
1018 client.feedback(feedback_jid, user_msg)
1019 xmlui = await self.getTrustUI(client, trust_data=trust_data, submit_id="")
1020
1021 answer = await xml_tools.deferXMLUI(
1022 self.host,
1023 xmlui,
1024 action_extra={
1025 "meta_encryption_trust": NS_OMEMO,
1026 },
1027 profile=client.profile)
1028 await self.trustUICb(answer, trust_data, expect_problems, client.profile)
1029
1030 async def handleProblems(
1031 self, client, feedback_jid, bundles, expect_problems, problems):
1032 """Try to solve problems found by EncryptMessage
1033
1034 @param feedback_jid(jid.JID): bare jid where the feedback message must be sent
1035 @param bundles(dict): bundles data as used in EncryptMessage
1036 already filled with known bundles, missing bundles
1037 need to be added to it
1038 This dict is updated
1039 @param problems(list): exceptions raised by EncryptMessage
1040 @param expect_problems(dict): known problems to expect, used in encryptMessage
1041 This dict will list devices where problems can be ignored
1042 (those devices won't receive the encrypted data)
1043 This dict is updated
1044 """
1045 # FIXME: not all problems are handled yet
1046 undecided = {}
1047 missing_bundles = {}
1048 found_bundles = None
1049 cache = client._xep_0384_cache
1050 for problem in problems:
1051 if isinstance(problem, omemo_excpt.TrustException):
1052 if problem.problem == 'undecided':
1053 undecided[str(hash(problem))] = problem
1054 elif problem.problem == 'untrusted':
1055 expect_problems.setdefault(problem.bare_jid, set()).add(
1056 problem.device)
1057 log.info(_(
1058 "discarding untrusted device {device_id} with key {device_key} "
1059 "for {entity}").format(
1060 device_id=problem.device,
1061 device_key=problem.ik.hex().upper(),
1062 entity=problem.bare_jid,
1063 )
1064 )
1065 else:
1066 log.error(
1067 f"Unexpected trust problem: {problem.problem!r} for device "
1068 f"{problem.device} for {problem.bare_jid}, ignoring device")
1069 expect_problems.setdefault(problem.bare_jid, set()).add(
1070 problem.device)
1071 elif isinstance(problem, omemo_excpt.MissingBundleException):
1072 pb_entity = jid.JID(problem.bare_jid)
1073 entity_cache = cache.setdefault(pb_entity, {})
1074 entity_bundles = bundles.setdefault(pb_entity, {})
1075 if problem.device in entity_cache:
1076 entity_bundles[problem.device] = entity_cache[problem.device]
1077 else:
1078 found_bundles, missing = await self.getBundles(
1079 client, pb_entity, [problem.device])
1080 entity_cache.update(bundles)
1081 entity_bundles.update(found_bundles)
1082 if problem.device in missing:
1083 missing_bundles.setdefault(pb_entity, set()).add(
1084 problem.device)
1085 expect_problems.setdefault(problem.bare_jid, set()).add(
1086 problem.device)
1087 elif isinstance(problem, omemo_excpt.NoEligibleDevicesException):
1088 if undecided or found_bundles:
1089 # we may have new devices after this run, so let's continue for now
1090 continue
1091 else:
1092 raise problem
1093 else:
1094 raise problem
1095
1096 for peer_jid, devices in missing_bundles.items():
1097 devices_s = [str(d) for d in devices]
1098 log.warning(
1099 _("Can't retrieve bundle for device(s) {devices} of entity {peer}, "
1100 "the message will not be readable on this/those device(s)").format(
1101 devices=", ".join(devices_s), peer=peer_jid.full()))
1102 client.feedback( 1986 client.feedback(
1103 feedback_jid, 1987 feedback_jid,
1104 D_("You're destinee {peer} has missing encryption data on some of " 1988 D_(
1105 "his/her device(s) (bundle on device {devices}), the message won't " 1989 "There were non-critical errors during encryption resulting in some"
1106 "be readable on this/those device.").format( 1990 " of your destinees' devices potentially not receiving the message."
1107 peer=peer_jid.full(), devices=", ".join(devices_s))) 1991 " This happens when the encryption data/key material of a device is"
1108 1992 " incomplete or broken, which shouldn't happen for actively used"
1109 if undecided: 1993 " devices, and can usually be ignored. The following devices are"
1110 omemo_policy = self.host.memory.getParamA( 1994 f" affected: {encrypted_errors_stringified}."
1111 PARAM_NAME, PARAM_CATEGORY, profile_key=client.profile 1995 )
1112 ) 1996 )
1113 if omemo_policy == 'btbv': 1997
1114 # we first separate entities which have been trusted manually 1998 message = next(message for message in messages if message.namespace == namespace)
1115 manual_trust = await client._xep_0384_data.get(KEY_MANUAL_TRUST) 1999
1116 if manual_trust: 2000 if namespace == twomemo.twomemo.NAMESPACE:
1117 manual_undecided = {} 2001 # Add the encrypted element
1118 for hash_, pb in undecided.items(): 2002 stanza.addChild(etree_to_domish(twomemo.etree.serialize_message(message)))
1119 if pb.bare_jid in manual_trust: 2003
1120 manual_undecided[hash_] = pb 2004 if namespace == oldmemo.oldmemo.NAMESPACE:
1121 for hash_ in manual_undecided: 2005 # Add the encrypted element
1122 del undecided[hash_] 2006 stanza.addChild(etree_to_domish(oldmemo.etree.serialize_message(message)))
1123 else: 2007
1124 manual_undecided = None 2008 if muc_plaintext_cache_key is not None:
1125 2009 self.__muc_plaintext_cache[muc_plaintext_cache_key] = plaintext
1126 if undecided: 2010
1127 # we do the automatic trust here 2011 async def __on_device_list_update(
1128 await self.policyBTBV( 2012 self,
1129 client, feedback_jid, expect_problems, undecided) 2013 items_event: pubsub.ItemsEvent,
1130 if manual_undecided: 2014 profile: str
1131 # here user has to manually trust new devices from entities already 2015 ) -> None:
1132 # verified 2016 """Handle device list updates fired by PEP.
1133 await self.policyManual( 2017
1134 client, feedback_jid, expect_problems, manual_undecided) 2018 @param items_event: The event.
1135 elif omemo_policy == 'manual': 2019 @param profile: The profile this event belongs to.
1136 await self.policyManual( 2020 """
1137 client, feedback_jid, expect_problems, undecided) 2021
2022 sender = cast(jid.JID, items_event.sender)
2023 items = cast(List[domish.Element], items_event.items)
2024
2025 if len(items) > 1:
2026 log.warning("Ignoring device list update with more than one element.")
2027 return
2028
2029 item = next(iter(items), None)
2030 if item is None:
2031 log.debug("Ignoring empty device list update.")
2032 return
2033
2034 item_elt = domish_to_etree(item)
2035
2036 device_list: Dict[int, Optional[str]] = {}
2037 namespace: Optional[str] = None
2038
2039 list_elt = item_elt.find(f"{{{twomemo.twomemo.NAMESPACE}}}devices")
2040 if list_elt is not None:
2041 try:
2042 device_list = twomemo.etree.parse_device_list(list_elt)
2043 except XMLSchemaValidationError:
2044 pass
1138 else: 2045 else:
1139 raise exceptions.InternalError(f"Unexpected OMEMO policy: {omemo_policy}") 2046 namespace = twomemo.twomemo.NAMESPACE
1140 2047
1141 async def encryptMessage(self, client, entity_bare_jids, message, feedback_jid=None): 2048 list_elt = item_elt.find(f"{{{oldmemo.oldmemo.NAMESPACE}}}list")
1142 if feedback_jid is None: 2049 if list_elt is not None:
1143 if len(entity_bare_jids) != 1: 2050 try:
1144 log.error( 2051 device_list = oldmemo.etree.parse_device_list(list_elt)
1145 "feedback_jid must be provided when message is encrypted for more " 2052 except XMLSchemaValidationError:
1146 "than one entities") 2053 pass
1147 feedback_jid = entity_bare_jids[0]
1148 omemo_session = client._xep_0384_session
1149 expect_problems = {}
1150 bundles = {}
1151 loop_idx = 0
1152 try:
1153 while True:
1154 if loop_idx > 10:
1155 msg = _("Too many iterations in encryption loop")
1156 log.error(msg)
1157 raise exceptions.InternalError(msg)
1158 # encryptMessage may fail, in case of e.g. trust issue or missing bundle
1159 try:
1160 if not message:
1161 encrypted = await omemo_session.encryptRatchetForwardingMessage(
1162 entity_bare_jids,
1163 bundles,
1164 expect_problems = expect_problems)
1165 else:
1166 encrypted = await omemo_session.encryptMessage(
1167 entity_bare_jids,
1168 message,
1169 bundles,
1170 expect_problems = expect_problems)
1171 except omemo_excpt.EncryptionProblemsException as e:
1172 # we know the problem to solve, we can try to fix them
1173 await self.handleProblems(
1174 client,
1175 feedback_jid=feedback_jid,
1176 bundles=bundles,
1177 expect_problems=expect_problems,
1178 problems=e.problems)
1179 loop_idx += 1
1180 else:
1181 break
1182 except Exception as e:
1183 msg = _("Can't encrypt message for {entities}: {reason}".format(
1184 entities=', '.join(e.full() for e in entity_bare_jids), reason=e))
1185 log.warning(msg)
1186 extra = {C.MESS_EXTRA_INFO: C.EXTRA_INFO_ENCR_ERR}
1187 client.feedback(feedback_jid, msg, extra)
1188 raise e
1189
1190 defer.returnValue(encrypted)
1191
1192 @defer.inlineCallbacks
1193 def _messageReceivedTrigger(self, client, message_elt, post_treat):
1194 try:
1195 encrypted_elt = next(message_elt.elements(NS_OMEMO, "encrypted"))
1196 except StopIteration:
1197 # no OMEMO message here
1198 defer.returnValue(True)
1199
1200 # we have an encrypted message let's decrypt it
1201
1202 from_jid = jid.JID(message_elt['from'])
1203
1204 if message_elt.getAttribute("type") == C.MESS_TYPE_GROUPCHAT:
1205 # with group chat, we must get the real jid for decryption
1206 # and use the room as feedback_jid
1207
1208 if self._m is None:
1209 # plugin XEP-0045 (MUC) is not available
1210 defer.returnValue(True)
1211
1212 room_jid = from_jid.userhostJID()
1213 feedback_jid = room_jid
1214 if self._sid is not None:
1215 mess_id = self._sid.getOriginId(message_elt)
1216 else: 2054 else:
1217 mess_id = None 2055 namespace = oldmemo.oldmemo.NAMESPACE
1218 2056
1219 if mess_id is None: 2057 if namespace is None:
1220 mess_id = message_elt.getAttribute('id') 2058 log.warning(
1221 cache_key = (room_jid, mess_id) 2059 f"Malformed device list update item:"
1222 2060 f" {ET.tostring(item_elt, encoding='unicode')}"
1223 try: 2061 )
1224 room = self._m.getRoom(client, room_jid)
1225 except exceptions.NotFound:
1226 log.warning(
1227 f"Received an OMEMO encrypted msg from a room {room_jid} which has "
1228 f"not been joined, ignoring")
1229 defer.returnValue(True)
1230
1231 user = room.getUser(from_jid.resource)
1232 if user is None:
1233 log.warning(f"Can't find user {user} in room {room_jid}, ignoring")
1234 defer.returnValue(True)
1235 if not user.entity:
1236 log.warning(
1237 f"Real entity of user {user} in room {room_jid} can't be established,"
1238 f" OMEMO encrypted message can't be decrypted")
1239 defer.returnValue(True)
1240
1241 # now we have real jid of the entity, we use it instead of from_jid
1242 from_jid = user.entity.userhostJID()
1243
1244 else:
1245 # we have a one2one message, we can user "from" and "to" normally
1246
1247 if from_jid.userhostJID() == client.jid.userhostJID():
1248 feedback_jid = jid.JID(message_elt['to'])
1249 else:
1250 feedback_jid = from_jid
1251
1252
1253 if (message_elt.getAttribute("type") == C.MESS_TYPE_GROUPCHAT
1254 and mess_id is not None
1255 and cache_key in client._xep_0384_muc_cache):
1256 plaintext = client._xep_0384_muc_cache.pop(cache_key)
1257 if not client._xep_0384_muc_cache:
1258 client._xep_0384_muc_cache_timer.cancel()
1259 client._xep_0384_muc_cache_timer = None
1260 else:
1261 try:
1262 omemo_session = client._xep_0384_session
1263 except AttributeError:
1264 # on startup, message can ve received before session actually exists
1265 # so we need to synchronise here
1266 yield client._xep_0384_ready
1267 omemo_session = client._xep_0384_session
1268
1269 device_id = client._xep_0384_device_id
1270 try:
1271 header_elt = next(encrypted_elt.elements(NS_OMEMO, 'header'))
1272 iv_elt = next(header_elt.elements(NS_OMEMO, 'iv'))
1273 except StopIteration:
1274 log.warning(_("Invalid OMEMO encrypted stanza, ignoring: {xml}")
1275 .format(xml=message_elt.toXml()))
1276 defer.returnValue(False)
1277 try:
1278 s_device_id = header_elt['sid']
1279 except KeyError:
1280 log.warning(_("Invalid OMEMO encrypted stanza, missing sender device ID, "
1281 "ignoring: {xml}")
1282 .format(xml=message_elt.toXml()))
1283 defer.returnValue(False)
1284 try:
1285 key_elt = next((e for e in header_elt.elements(NS_OMEMO, 'key')
1286 if int(e['rid']) == device_id))
1287 except StopIteration:
1288 log.warning(_("This OMEMO encrypted stanza has not been encrypted "
1289 "for our device (device_id: {device_id}, fingerprint: "
1290 "{fingerprint}): {xml}").format(
1291 device_id=device_id,
1292 fingerprint=omemo_session.public_bundle.ik.hex().upper(),
1293 xml=encrypted_elt.toXml()))
1294 user_msg = (D_("An OMEMO message from {sender} has not been encrypted for "
1295 "our device, we can't decrypt it").format(
1296 sender=from_jid.full()))
1297 extra = {C.MESS_EXTRA_INFO: C.EXTRA_INFO_DECR_ERR}
1298 client.feedback(feedback_jid, user_msg, extra)
1299 defer.returnValue(False)
1300 except ValueError as e:
1301 log.warning(_("Invalid recipient ID: {msg}".format(msg=e)))
1302 defer.returnValue(False)
1303 is_pre_key = C.bool(key_elt.getAttribute('prekey', 'false'))
1304 payload_elt = next(encrypted_elt.elements(NS_OMEMO, 'payload'), None)
1305 additional_information = {
1306 "from_storage": bool(message_elt.delay)
1307 }
1308
1309 kwargs = {
1310 "bare_jid": from_jid.userhostJID(),
1311 "device": s_device_id,
1312 "iv": base64.b64decode(bytes(iv_elt)),
1313 "message": base64.b64decode(bytes(key_elt)),
1314 "is_pre_key_message": is_pre_key,
1315 "additional_information": additional_information,
1316 }
1317
1318 try:
1319 if payload_elt is None:
1320 omemo_session.decryptRatchetForwardingMessage(**kwargs)
1321 plaintext = None
1322 else:
1323 kwargs["ciphertext"] = base64.b64decode(bytes(payload_elt))
1324 try:
1325 plaintext = yield omemo_session.decryptMessage(**kwargs)
1326 except omemo_excpt.TrustException:
1327 post_treat.addCallback(client.encryption.markAsUntrusted)
1328 kwargs['allow_untrusted'] = True
1329 plaintext = yield omemo_session.decryptMessage(**kwargs)
1330 else:
1331 post_treat.addCallback(client.encryption.markAsTrusted)
1332 plaintext = plaintext.decode()
1333 except Exception as e:
1334 log.warning(_("Can't decrypt message: {reason}\n{xml}").format(
1335 reason=e, xml=message_elt.toXml()))
1336 user_msg = (D_(
1337 "An OMEMO message from {sender} can't be decrypted: {reason}")
1338 .format(sender=from_jid.full(), reason=e))
1339 extra = {C.MESS_EXTRA_INFO: C.EXTRA_INFO_DECR_ERR}
1340 client.feedback(feedback_jid, user_msg, extra)
1341 defer.returnValue(False)
1342 finally:
1343 if omemo_session.republish_bundle:
1344 # we don't wait for the Deferred (i.e. no yield) on purpose
1345 # there is no need to block the whole message workflow while
1346 # updating the bundle
1347 defer.ensureDeferred(
1348 self.setBundle(client, omemo_session.public_bundle, device_id)
1349 )
1350
1351 message_elt.children.remove(encrypted_elt)
1352 if plaintext:
1353 message_elt.addElement("body", content=plaintext)
1354 post_treat.addCallback(client.encryption.markAsEncrypted, namespace=NS_OMEMO)
1355 defer.returnValue(True)
1356
1357 def getJIDsForRoom(self, client, room_jid):
1358 if self._m is None:
1359 exceptions.InternalError("XEP-0045 plugin missing, can't encrypt for group chat")
1360 room = self._m.getRoom(client, room_jid)
1361 return [u.entity.userhostJID() for u in room.roster.values()]
1362
1363 def _expireMUCCache(self, client):
1364 client._xep_0384_muc_cache_timer = None
1365 for (room_jid, uid), msg in client._xep_0384_muc_cache.items():
1366 client.feedback(
1367 room_jid,
1368 D_("Our message with UID {uid} has not been received in time, it has "
1369 "probably been lost. The message was: {msg!r}").format(
1370 uid=uid, msg=str(msg)))
1371
1372 client._xep_0384_muc_cache.clear()
1373 log.warning("Cache for OMEMO MUC has expired")
1374
1375 @defer.inlineCallbacks
1376 def _sendMessageDataTrigger(self, client, mess_data):
1377 encryption = mess_data.get(C.MESS_KEY_ENCRYPTION)
1378 if encryption is None or encryption['plugin'].namespace != NS_OMEMO:
1379 return 2062 return
1380 message_elt = mess_data["xml"] 2063
1381 if mess_data['type'] == C.MESS_TYPE_GROUPCHAT: 2064 session_manager = await self.__prepare_for_profile(profile)
1382 feedback_jid = room_jid = mess_data['to'] 2065
1383 to_jids = self.getJIDsForRoom(client, room_jid) 2066 await session_manager.update_device_list(
1384 else: 2067 namespace,
1385 feedback_jid = to_jid = mess_data["to"].userhostJID() 2068 sender.userhost(),
1386 to_jids = [to_jid] 2069 device_list
1387 log.debug("encrypting message") 2070 )
1388 body = None
1389 for child in list(message_elt.children):
1390 if child.name == "body":
1391 # we remove all unencrypted body,
1392 # and will only encrypt the first one
1393 if body is None:
1394 body = child
1395 message_elt.children.remove(child)
1396 elif child.name == "html":
1397 # we don't want any XHTML-IM element
1398 message_elt.children.remove(child)
1399
1400 if body is None:
1401 log.warning("No message found")
1402 return
1403
1404 body = str(body)
1405
1406 if mess_data['type'] == C.MESS_TYPE_GROUPCHAT:
1407 key = (room_jid, mess_data['uid'])
1408 # XXX: we can't encrypt message for our own device for security reason
1409 # so we keep the plain text version in cache until we receive the
1410 # message. We don't send it directly to bridge to keep a workflow
1411 # similar to plain text MUC, so when we see it in frontend we know
1412 # that it has been sent correctly.
1413 client._xep_0384_muc_cache[key] = body
1414 timer = client._xep_0384_muc_cache_timer
1415 if timer is None:
1416 client._xep_0384_muc_cache_timer = reactor.callLater(
1417 MUC_CACHE_TTL, self._expireMUCCache, client)
1418 else:
1419 timer.reset(MUC_CACHE_TTL)
1420
1421 encryption_data = yield defer.ensureDeferred(self.encryptMessage(
1422 client, to_jids, body, feedback_jid=feedback_jid))
1423
1424 encrypted_elt = message_elt.addElement((NS_OMEMO, 'encrypted'))
1425 header_elt = encrypted_elt.addElement('header')
1426 header_elt['sid'] = str(encryption_data['sid'])
1427
1428 for key_data in encryption_data['keys'].values():
1429 for rid, data in key_data.items():
1430 key_elt = header_elt.addElement(
1431 'key',
1432 content=b64enc(data['data'])
1433 )
1434 key_elt['rid'] = str(rid)
1435 if data['pre_key']:
1436 key_elt['prekey'] = 'true'
1437
1438 header_elt.addElement(
1439 'iv',
1440 content=b64enc(encryption_data['iv']))
1441 try:
1442 encrypted_elt.addElement(
1443 'payload',
1444 content=b64enc(encryption_data['payload']))
1445 except KeyError:
1446 pass