diff --git a/sat/core/sat_main.py b/sat/core/sat_main.py
index 76a0e70b..efe783b9 100644
--- a/sat/core/sat_main.py
+++ b/sat/core/sat_main.py
@@ -882,7 +882,8 @@ class SAT(service.Service):
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 --git a/sat/plugins/plugin_xep_0384.py b/sat/plugins/plugin_xep_0384.py
index 532df1fd..c5ffe7de 100644
--- a/sat/plugins/plugin_xep_0384.py
+++ b/sat/plugins/plugin_xep_0384.py
@@ -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.xish import domish
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_STATE = "STATE"
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 @@ KEY_ALL_JIDS = "ALL_JIDS"
# 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 @@ def promise2Deferred(promise_):
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 @@ class OmemoStorage(omemo.Storage):
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 OmemoSession:
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 @@ class OMEMO:
"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 @@ class OMEMO:
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 @@ class OMEMO:
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 @@ class OMEMO:
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 @@ class OMEMO:
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 @@ class OMEMO:
}
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 @@ class OMEMO:
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 @@ class OMEMO:
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 @@ class OMEMO:
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 @@ class OMEMO:
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 @@ class OMEMO:
## 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 @@ class OMEMO:
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 @@ class OMEMO:
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 @@ class OMEMO:
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 @@ class OMEMO:
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')