# HG changeset patch # User Goffi # Date 1585299734 -3600 # Node ID b0c57c9a4bd8d447929cf10f90a1f4a9e23fd0b3 # Parent 9477f319798179fbe5bf21dbd19e0e1329a0ae06 plugin XEP-0384: OMEMO trust policy: OMEMO trust policy can now be specified. For now there are 2 policies: - `manual`: each new device fingerprint must be explicitly trusted or not before the device can be used, and the message sent - `BTBV` (Blind Trust Before Verification): each new device fingerprint is automically trusted, until user manually trust or not a device, in which case the behaviour becomes the same as for `manual` for the entity. When using the Trust UI, user can put the entity back to blind trust if they wish. A message is send as feedback to user when a new device is/must be trusted, trying to explain clearly what's happening to the user. Devices which have been automically trusted are marked, so user can know which ones may cause security issue. diff -r 9477f3197981 -r b0c57c9a4bd8 sat/core/sat_main.py --- a/sat/core/sat_main.py Fri Mar 27 09:55:16 2020 +0100 +++ b/sat/core/sat_main.py Fri Mar 27 10:02:14 2020 +0100 @@ -882,7 +882,8 @@ def _encryptionTrustUIGet(self, to_jid_s, namespace, profile_key): client = self.getClient(profile_key) to_jid = jid.JID(to_jid_s) - d = client.encryption.getTrustUI(to_jid, namespace=namespace or None) + d = defer.ensureDeferred( + client.encryption.getTrustUI(to_jid, namespace=namespace or None)) d.addCallback(lambda xmlui: xmlui.toXml()) return d diff -r 9477f3197981 -r b0c57c9a4bd8 sat/plugins/plugin_xep_0384.py --- a/sat/plugins/plugin_xep_0384.py Fri Mar 27 09:55:16 2020 +0100 +++ b/sat/plugins/plugin_xep_0384.py Fri Mar 27 10:02:14 2020 +0100 @@ -16,6 +16,11 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . +import logging +import random +import base64 +from functools import partial +from xml.sax.saxutils import quoteattr from sat.core.i18n import _, D_ from sat.core.constants import Const as C from sat.core.log import getLogger @@ -25,11 +30,7 @@ from twisted.words.protocols.jabber import jid from twisted.words.protocols.jabber import error as jabber_error from sat.memory import persistent -from functools import partial from sat.tools import xml_tools -import logging -import random -import base64 try: import omemo from omemo import exceptions as omemo_excpt @@ -69,6 +70,11 @@ KEY_DEVICE_ID = "DEVICE_ID" KEY_SESSION = "SESSION" KEY_TRUST = "TRUST" +# devices which have been automatically trusted by policy like BTBV +KEY_AUTO_TRUST = "AUTO_TRUST" +# list of peer bare jids where trust UI has been used at least once +# this is useful to activate manual trust with BTBV policy +KEY_MANUAL_TRUST = "MANUAL_TRUST" KEY_ACTIVE_DEVICES = "DEVICES" KEY_INACTIVE_DEVICES = "INACTIVE_DEVICES" KEY_ALL_JIDS = "ALL_JIDS" @@ -76,6 +82,9 @@ # expressed in seconds, reset on each new MUC message MUC_CACHE_TTL = 60 * 5 +PARAM_CATEGORY = "Security" +PARAM_NAME = "omemo_policy" + # we want to manage log emitted by omemo module ourselves @@ -111,15 +120,11 @@ class OmemoStorage(omemo.Storage): - def __init__(self, client, device_id, all_jids, persistent_dict): - """ - @param persistent_dict(persistent.LazyPersistentBinaryDict): object which will - store data in SàT database - """ + def __init__(self, client, device_id, all_jids): self.own_bare_jid_s = client.jid.userhost() self.device_id = device_id self.all_jids = all_jids - self.data = persistent_dict + self.data = client._xep_0384_data @property def is_async(self): @@ -262,7 +267,7 @@ return d def deleteJID(self, callback, bare_jid): - """Retrieve all (in)actives of bare_jid, and delete all related keys""" + """Retrieve all (in)actives devices of bare_jid, and delete all related keys""" d_list = [] key = '\n'.join([KEY_ACTIVE_DEVICES, bare_jid]) @@ -392,6 +397,27 @@ class OMEMO: + params = """ + + + + + + + + """.format( + category_name=PARAM_CATEGORY, + category_label=D_("Security"), + param_name=PARAM_NAME, + param_label=quoteattr(D_("OMEMO default trust policy")), + opt_manual_lbl=quoteattr(D_("Manual trust (more secure)")), + opt_btbv_lbl=quoteattr( + D_("Blind Trust Before Verification (more user friendly)")), + ) + def __init__(self, host): log.info(_("OMEMO plugin initialization (omemo module v{version})").format( version=omemo.__version__)) @@ -402,6 +428,7 @@ "minimum required, please update.").format(v=OMEMO_MIN_VER)) raise exceptions.CancelError("module is too old") self.host = host + host.memory.updateParams(self.params) self._p_hints = host.plugins["XEP-0334"] self._p_carbons = host.plugins["XEP-0280"] self._p = host.plugins["XEP-0060"] @@ -450,22 +477,55 @@ self.text_cmds.feedBack(client, feedback, mess_data) return False - @defer.inlineCallbacks - def trustUICb(self, xmlui_data, trust_data, expect_problems=None, - profile=C.PROF_KEY_NONE): + async def trustUICb( + self, xmlui_data, trust_data, expect_problems=None, profile=C.PROF_KEY_NONE): if C.bool(xmlui_data.get('cancelled', 'false')): - defer.returnValue({}) + return {} client = self.host.getClient(profile) session = client._xep_0384_session + stored_data = client._xep_0384_data + manual_trust = await stored_data.get(KEY_MANUAL_TRUST, set()) + auto_trusted_cache = {} answer = xml_tools.XMLUIResult2DataFormResult(xmlui_data) + blind_trust = C.bool(answer.get('blind_trust', C.BOOL_FALSE)) for key, value in answer.items(): if key.startswith('trust_'): trust_id = key[6:] else: continue data = trust_data[trust_id] + if blind_trust: + # user request to restore blind trust for this entity + # so if the entity is present in manual trust, we remove it + if data["jid"].full() in manual_trust: + manual_trust.remove(data["jid"].full()) + await stored_data.aset(KEY_MANUAL_TRUST, manual_trust) + elif data["jid"].full() not in manual_trust: + # validating this trust UI implies that we activate manual mode for + # this entity (used for BTBV policy) + manual_trust.add(data["jid"].full()) + await stored_data.aset(KEY_MANUAL_TRUST, manual_trust) trust = C.bool(value) - yield session.setTrust( + + if not trust: + # if device is not trusted, we check if it must be removed from auto + # trusted devices list + bare_jid_s = data['jid'].userhost() + key = f"{KEY_AUTO_TRUST}\n{bare_jid_s}" + if bare_jid_s not in auto_trusted_cache: + auto_trusted_cache[bare_jid_s] = await stored_data.get( + key, default=set()) + auto_trusted = auto_trusted_cache[bare_jid_s] + if data['device'] in auto_trusted: + # as we don't trust this device anymore, we can remove it from the + # list of automatically trusted devices + auto_trusted.remove(data['device']) + await stored_data.aset(key, auto_trusted) + log.info(D_( + "device {device} from {peer_jid} is not an auto-trusted device " + "anymore").format(device=data['device'], peer_jid=bare_jid_s)) + + await session.setTrust( data["jid"], data["device"], data["ik"], @@ -475,10 +535,9 @@ expect_problems.setdefault(data['jid'].userhost(), set()).add( data['device'] ) - defer.returnValue({}) + return {} - @defer.inlineCallbacks - def getTrustUI(self, client, entity_jid=None, trust_data=None, submit_id=None): + async def getTrustUI(self, client, entity_jid=None, trust_data=None, submit_id=None): """Generate a XMLUI to manage trust @param entity_jid(None, jid.JID): jid of entity to manage @@ -502,6 +561,7 @@ raise ValueError("A bare jid is expected") session = client._xep_0384_session + stored_data = client._xep_0384_data if trust_data is None: cache = client._xep_0384_cache.setdefault(entity_jid, {}) @@ -511,14 +571,14 @@ else: trust_jids = [entity_jid] for trust_jid in trust_jids: - trust_session_data = yield session.getTrustForJID(trust_jid) + trust_session_data = await session.getTrustForJID(trust_jid) bare_jid_s = trust_jid.userhost() for device_id, trust_info in trust_session_data['active'].items(): if trust_info is None: # device has never been (un)trusted, we have to retrieve its # fingerprint (i.e. identity key or "ik") through public bundle if device_id not in cache: - bundles, missing = yield self.getBundles(client, + bundles, missing = await self.getBundles(client, trust_jid, [device_id]) if device_id not in bundles: @@ -545,10 +605,11 @@ } if submit_id is None: - submit_id = self.host.registerCallback(partial(self.trustUICb, - trust_data=trust_data), - with_data=True, - one_shot=True) + submit_id = self.host.registerCallback( + lambda data, profile: defer.ensureDeferred( + self.trustUICb(data, trust_data=trust_data, profile=profile)), + with_data=True, + one_shot=True) xmlui = xml_tools.XMLUI( panel_type = C.XMLUI_FORM, title = D_("OMEMO trust management"), @@ -573,8 +634,25 @@ xmlui.addEmpty() xmlui.addEmpty() + if entity_jid is not None: + omemo_policy = self.host.memory.getParamA( + PARAM_NAME, PARAM_CATEGORY, profile_key=client.profile + ) + if omemo_policy == 'btbv': + xmlui.addLabel(D_("Automatically trust new devices?")) + # blind trust is always disabled when UI is requested + # as submitting UI is a verification which should disable it. + xmlui.addBool("blind_trust", value=C.BOOL_FALSE) + xmlui.addEmpty() + xmlui.addEmpty() + + auto_trust_cache = {} for trust_id, data in trust_data.items(): + bare_jid_s = data['jid'].userhost() + if bare_jid_s not in auto_trust_cache: + key = f"{KEY_AUTO_TRUST}\n{bare_jid_s}" + auto_trust_cache[bare_jid_s] = await stored_data.get(key, set()) xmlui.addLabel(D_("Contact")) xmlui.addJid(data['jid']) xmlui.addLabel(D_("Device ID")) @@ -586,11 +664,15 @@ xmlui.addLabel(D_("Trust this device?")) xmlui.addBool("trust_{}".format(trust_id), value=C.boolConst(data.get('trusted', False))) + if data['device'] in auto_trust_cache[bare_jid_s]: + xmlui.addEmpty() + xmlui.addLabel(D_("(automatically trusted)")) + xmlui.addEmpty() xmlui.addEmpty() - defer.returnValue(xmlui) + return xmlui @defer.inlineCallbacks def profileConnected(self, client): @@ -607,6 +689,7 @@ client._xep_0384_ready = defer.Deferred() # we first need to get devices ids (including our own) persistent_dict = persistent.LazyPersistentBinaryDict("XEP-0384", client.profile) + client._xep_0384_data = persistent_dict # all known devices of profile devices = yield self.getDevices(client) # and our own device id @@ -630,7 +713,7 @@ all_jids = yield persistent_dict.get(KEY_ALL_JIDS, set()) - omemo_storage = OmemoStorage(client, device_id, all_jids, persistent_dict) + omemo_storage = OmemoStorage(client, device_id, all_jids) omemo_session = yield OmemoSession.create(client, omemo_storage, device_id) client._xep_0384_cache = {} client._xep_0384_session = omemo_session @@ -862,8 +945,59 @@ ## triggers - @defer.inlineCallbacks - def handleProblems(self, client, feedback_jid, bundles, expect_problems, problems): + async def policyBTBV(self, client, feedback_jid, expect_problems, undecided): + session = client._xep_0384_session + stored_data = client._xep_0384_data + for pb in undecided.values(): + peer_jid = jid.JID(pb.bare_jid) + device = pb.device + ik = pb.ik + key = f"{KEY_AUTO_TRUST}\n{pb.bare_jid}" + auto_trusted = await stored_data.get(key, default=set()) + auto_trusted.add(device) + await stored_data.aset(key, auto_trusted) + await session.setTrust(peer_jid, device, ik, True) + + user_msg = D_( + "Not all destination devices are trusted, unknown devices will be blind " + "trusted due to the OMEMO Blind Trust Before Verification policy. If you " + "want a more secure workflow, please activate \"manual\" OMEMO policy in " + "settings' \"Security\" tab.\nFollowing fingerprint have been automatically " + "trusted:\n{devices}" + ).format( + devices = ', '.join( + f"- {pb.device} ({pb.bare_jid}): {pb.ik.hex().upper()}" + for pb in undecided.values() + ) + ) + client.feedback(feedback_jid, user_msg) + + async def policyManual(self, client, feedback_jid, expect_problems, undecided): + trust_data = {} + for trust_id, data in undecided.items(): + trust_data[trust_id] = { + 'jid': jid.JID(data.bare_jid), + 'device': data.device, + 'ik': data.ik} + + user_msg = D_("Not all destination devices are trusted, we can't encrypt " + "message in such a situation. Please indicate if you trust " + "those devices or not in the trust manager before we can " + "send this message") + client.feedback(feedback_jid, user_msg) + xmlui = await self.getTrustUI(client, trust_data=trust_data, submit_id="") + + answer = await xml_tools.deferXMLUI( + self.host, + xmlui, + action_extra={ + "meta_encryption_trust": NS_OMEMO, + }, + profile=client.profile) + await self.trustUICb(answer, trust_data, expect_problems, client.profile) + + async def handleProblems( + self, client, feedback_jid, bundles, expect_problems, problems): """Try to solve problems found by EncryptMessage @param feedback_jid(jid.JID): bare jid where the feedback message must be sent @@ -910,7 +1044,7 @@ if problem.device in entity_cache: entity_bundles[problem.device] = entity_cache[problem.device] else: - found_bundles, missing = yield self.getBundles( + found_bundles, missing = await self.getBundles( client, pb_entity, [problem.device]) entity_cache.update(bundles) entity_bundles.update(found_bundles) @@ -942,31 +1076,38 @@ peer=peer_jid.full(), devices=", ".join(devices_s))) if undecided: - trust_data = {} - for trust_id, data in undecided.items(): - trust_data[trust_id] = { - 'jid': jid.JID(data.bare_jid), - 'device': data.device, - 'ik': data.ik} + omemo_policy = self.host.memory.getParamA( + PARAM_NAME, PARAM_CATEGORY, profile_key=client.profile + ) + if omemo_policy == 'btbv': + # we first separate entities which have been trusted manually + manual_trust = await client._xep_0384_data.get(KEY_MANUAL_TRUST) + if manual_trust: + manual_undecided = {} + for hash_, pb in undecided.items(): + if pb.bare_jid in manual_trust: + manual_undecided[hash_] = pb + for hash_ in manual_undecided: + del undecided[hash_] + else: + manual_undecided = None - user_msg = D_("Not all destination devices are trusted, we can't encrypt " - "message in such a situation. Please indicate if you trust " - "those devices or not in the trust manager before we can " - "send this message") - client.feedback(feedback_jid, user_msg) - xmlui = yield self.getTrustUI(client, trust_data=trust_data, submit_id="") + if undecided: + # we do the automatic trust here + await self.policyBTBV( + client, feedback_jid, expect_problems, undecided) + if manual_undecided: + # here user has to manually trust new devices from entities already + # verified + await self.policyManual( + client, feedback_jid, expect_problems, manual_undecided) + elif omemo_policy == 'manual': + await self.policyManual( + client, feedback_jid, expect_problems, undecided) + else: + raise exceptions.InternalError(f"Unexpected OMEMO policy: {omemo_policy}") - answer = yield xml_tools.deferXMLUI( - self.host, - xmlui, - action_extra={ - "meta_encryption_trust": NS_OMEMO, - }, - profile=client.profile) - yield self.trustUICb(answer, trust_data, expect_problems, client.profile) - - @defer.inlineCallbacks - def encryptMessage(self, client, entity_bare_jids, message, feedback_jid=None): + async def encryptMessage(self, client, entity_bare_jids, message, feedback_jid=None): if feedback_jid is None: if len(entity_bare_jids) != 1: log.error( @@ -985,14 +1126,14 @@ raise exceptions.InternalError(msg) # encryptMessage may fail, in case of e.g. trust issue or missing bundle try: - encrypted = yield omemo_session.encryptMessage( + encrypted = await omemo_session.encryptMessage( entity_bare_jids, message, bundles, expect_problems = expect_problems) except omemo_excpt.EncryptionProblemsException as e: # we know the problem to solve, we can try to fix them - yield self.handleProblems( + await self.handleProblems( client, feedback_jid=feedback_jid, bundles=bundles, @@ -1240,8 +1381,8 @@ if self._sid is not None: self._sid.addOriginId(message_elt, mess_data['uid']) - encryption_data = yield self.encryptMessage( - client, to_jids, body, feedback_jid=feedback_jid) + encryption_data = yield defer.ensureDeferred(self.encryptMessage( + client, to_jids, body, feedback_jid=feedback_jid)) encrypted_elt = message_elt.addElement((NS_OMEMO, 'encrypted')) header_elt = encrypted_elt.addElement('header')