Mercurial > libervia-backend
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 |