Skip to content

Commit

Permalink
Add config options for how much to obfuscate email addresses in 3rd p…
Browse files Browse the repository at this point in the history
…arty invites (#311)

When inviting a user via their email address using Sydent, a third party invite event is injected into the room using an obfuscated version of the invitee's email address (to prevent This PR adds two new config options to sydent:

* `email.third_party_invite_username_obfuscate_characters` - for obfuscating the text before the `@` sign
* `email.third_party_invite_domain_obfuscate_characters - for obfuscating the text after the `@` sign

Instead of only truncating the string, I decided to keep the old behaviour of redacting based on string length (only if the string's length is <= the configured threshold). The old behaviour ensured that a full email address is never shown, even if it is very short (e.g. a@a.co), which is a property I believe we want to uphold.
  • Loading branch information
anoadragon453 authored and turt2live committed Sep 11, 2020
1 parent 8a2ca19 commit ab3dc76
Show file tree
Hide file tree
Showing 4 changed files with 85 additions and 18 deletions.
1 change: 1 addition & 0 deletions changelog.d/311.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add config options for controlling how email addresses are obfuscated in third party invites.
47 changes: 30 additions & 17 deletions sydent/http/servlets/store_invite_servlet.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,44 +126,57 @@ def render_POST(self, request):
"token": token,
"public_key": pubKeyBase64,
"public_keys": keysToReturn,
"display_name": self.redact(address),
"display_name": self.redact_email_address(address),
}

return resp

def redact(self, address):
def redact_email_address(self, address):
"""
Redacts the content of a 3PID address. If the address is an email address,
then redacts both the address's localpart and domain independently. Otherwise,
redacts the whole address.
Redacts the content of a 3PID address. Redacts both the email's username and
domain independently.
:param address: The address to redact.
:type address: unicode
:return: The redacted address.
:rtype: unicode
"""
return u"@".join(map(self._redact, address.split(u"@", 1)))
# Extract strings from the address
username, domain = address.split(u"@", 1)

def _redact(self, s):
# Obfuscate strings
redacted_username = self._redact(username, self.sydent.username_obfuscate_characters)
redacted_domain = self._redact(domain, self.sydent.domain_obfuscate_characters)

return redacted_username + u"@" + redacted_domain

def _redact(self, s, characters_to_reveal):
"""
Redacts the content of a 3PID address. If the address is an email address,
then redacts both the address's localpart and domain independently. Otherwise,
redacts the whole address.
Redacts the content of a string, using a given amount of characters to reveal.
If the string is shorter than the given threshold, redact it based on length.
:param s: The address to redact.
:param s: The string to redact.
:type s: unicode
:return: The redacted address.
:param characters_to_reveal: How many characters of the string to leave before
the '...'
:type characters_to_reveal: int
:return: The redacted string.
:rtype: unicode
"""
if len(s) > 5:
return s[:3] + u"..."
elif len(s) > 1:
return s[0] + u"..."
else:
# If the string is shorter than the defined threshold, redact based on length
if len(s) <= characters_to_reveal:
if len(s) > 5:
return s[3] + u"..."
if len(s) > 1:
return s[0] + u"..."
return u"..."

# Otherwise truncate it and add an ellipses
return s[:characters_to_reveal] + u"..."

def _randomString(self, length):
"""
Generate a random string of the given length.
Expand Down
28 changes: 28 additions & 0 deletions sydent/sydent.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,27 @@
'email.smtppassword': '',
'email.hostname': '',
'email.tlsmode': '0',
# When a user is invited to a room via their email address, that invite is
# displayed in the room list using an obfuscated version of the user's email
# address. These config options determine how much of the email address to
# obfuscate. Note that the '@' sign is always included.
#
# If the string is longer than a configured limit below, it is truncated to that limit
# with '...' added. Otherwise:
#
# * If the string is longer than 5 characters, it is truncated to 3 characters + '...'
# * If the string is longer than 1 character, it is truncated to 1 character + '...'
# * If the string is 1 character long, it is converted to '...'
#
# This ensures that a full email address is never shown, even if it is extremely
# short.
#
# The number of characters from the beginning to reveal of the email's username
# portion (left of the '@' sign)
'email.third_party_invite_username_obfuscate_characters': '3',
# The number of characters from the beginning to reveal of the email's domain
# portion (right of the '@' sign)
'email.third_party_invite_domain_obfuscate_characters': '3',
},
'sms': {
'bodyTemplate': 'Your code is {token}',
Expand Down Expand Up @@ -186,6 +207,13 @@ def __init__(self, cfg, reactor=twisted.internet.reactor):
self.cfg.get("general", "delete_tokens_on_bind")
)

self.username_obfuscate_characters = int(self.cfg.get(
"email", "email.third_party_invite_username_obfuscate_characters"
))
self.domain_obfuscate_characters = int(self.cfg.get(
"email", "email.third_party_invite_domain_obfuscate_characters"
))

# See if a pepper already exists in the database
# Note: This MUST be run before we start serving requests, otherwise lookups for
# 3PID hashes may come in before we've completed generating them
Expand Down
27 changes: 26 additions & 1 deletion tests/test_invites.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,22 @@
from tests.utils import make_sydent
from twisted.web.client import Response
from twisted.trial import unittest
from sydent.http.servlets.store_invite_servlet import StoreInviteServlet


class ThreepidInvitesTestCase(unittest.TestCase):
"""Tests features related to storing and delivering 3PID invites."""

def setUp(self):
# Create a new sydent
self.sydent = make_sydent()
config = {
"email": {
# Used by test_invited_email_address_obfuscation
"email.third_party_invite_username_obfuscate_characters": "6",
"email.third_party_invite_domain_obfuscate_characters": "8",
},
}
self.sydent = make_sydent(test_config=config)

def test_delete_on_bind(self):
"""Tests that 3PID invite tokens are deleted upon delivery after a successful
Expand Down Expand Up @@ -65,6 +73,23 @@ def post_json_get_nothing(uri, post_json, opts):
# Check that we didn't get any result.
self.assertEqual(len(rows), 0, rows)

def test_invited_email_address_obfuscation(self):
"""Test that email addresses included in third-party invites are properly
obfuscated according to the relevant config options
"""
store_invite_servlet = StoreInviteServlet(self.sydent)

email_address = "1234567890@1234567890.com"
redacted_address = store_invite_servlet.redact_email_address(email_address)

self.assertEqual(redacted_address, "123456...@12345678...")

# Even short addresses are redacted
short_email_address = "1@1.com"
redacted_address = store_invite_servlet.redact_email_address(short_email_address)

self.assertEqual(redacted_address, "...@1...")


class ThreepidInvitesNoDeleteTestCase(unittest.TestCase):
"""Test that invite tokens are not deleted when that is disabled.
Expand Down

0 comments on commit ab3dc76

Please sign in to comment.