Mercurial > libervia-backend
changeset 3237:b0c57c9a4bd8
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.
| author | Goffi <goffi@goffi.org> |
|---|---|
| date | Fri, 27 Mar 2020 10:02:14 +0100 |
| parents | 9477f3197981 |
| children | 199fc4c551e9 |
| files | sat/core/sat_main.py sat/plugins/plugin_xep_0384.py |
| diffstat | 2 files changed, 200 insertions(+), 58 deletions(-) [+] |
line wrap: on
line diff
--- 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
--- 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 <http://www.gnu.org/licenses/>. +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 = """ + <params> + <individual> + <category name="{category_name}" label="{category_label}"> + <param name="{param_name}" label={param_label} type="list" security="3"> + <option value="manual" label={opt_manual_lbl} /> + <option value="btbv" label={opt_btbv_lbl} selected="true" /> + </param> + </category> + </individual> + </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')
