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.
This commit is contained in:
Goffi 2020-03-27 10:02:14 +01:00
commit cabaa16279
2 changed files with 201 additions and 59 deletions

View file

@ -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

View file

@ -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.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 = """
<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 @@ 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')