Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add config options for how much to obfuscate email addresses in 3rd party invites #311

Merged
merged 6 commits into from
Sep 1, 2020
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
22 changes: 22 additions & 0 deletions sydent/sydent.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,21 @@
'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 given username or domain is shorter than the threshold defined here,
# the string is then redacted based on its length. This ensure that a full email
anoadragon453 marked this conversation as resolved.
Show resolved Hide resolved
# 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 @@ -182,6 +197,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