Skip to content

Commit

Permalink
Add exception handlers for failed encryption only for the encrypting …
Browse files Browse the repository at this point in the history
…mail backend
  • Loading branch information
blag committed Mar 30, 2017
1 parent fecf3cd commit fd943ac
Show file tree
Hide file tree
Showing 3 changed files with 139 additions and 19 deletions.
42 changes: 24 additions & 18 deletions email_extras/backends.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@
from django.core.mail.message import EmailMultiAlternatives
from django.utils.encoding import smart_text

from .handlers import (handle_failed_message_encryption,
handle_failed_alternative_encryption,
handle_failed_attachment_encryption)
from .settings import (GNUPG_HOME, GNUPG_ENCODING, USE_GNUPG, encrypt_kwargs)
from .utils import EncryptionFailedError

Expand All @@ -39,14 +42,6 @@ def open(self, body):
webbrowser.open("file://" + temp.name)


class AttachmentEncryptionFailedError(EncryptionFailedError):
pass


class AlternativeEncryptionFailedError(EncryptionFailedError):
pass


if USE_GNUPG:
from gnupg import GPG

Expand Down Expand Up @@ -102,12 +97,22 @@ def encrypt_attachment(address, attachment, use_asc):
try:
encrypted_content = encrypt(content, address)
except EncryptionFailedError as e:
# SECURITY: We could include a piece of the content here, but that
# would leak information in logs and to the admins. So instead, we
# only try to include the filename.
raise AttachmentEncryptionFailedError(
"Encrypting attachment to %s failed: %s (%s)", address,
filename, e.msg)
# This function will need to decide what to do. Possibilities include
# one or more of:
#
# * Mail admins (possibly without encrypting the message to them)
# * Remove the offending key automatically
# * Set the body to a blank string
# * Set the body to the cleartext
# * Set the body to the cleartext, with a warning message prepended
# * Set the body to a custom error string
# * Reraise the exception
#
# However, the behavior will be very site-specific, because each site
# will have different attackers, different threat profiles, different
# compliance requirements, and different policies.
#
handle_failed_attachment_encryption(e)
else:
if use_asc and filename is not None:
filename += ".asc"
Expand Down Expand Up @@ -145,7 +150,10 @@ def encrypt_messages(email_messages):
continue

# Replace the message body with the encrypted message body
new_msg.body = encrypt(new_msg.body, address)
try:
new_msg.body = encrypt(new_msg.body, address)
except EncryptionFailedError as e:
handle_failed_message_encryption(e)

# If the message has alternatives, encrypt them all
alternatives = []
Expand All @@ -159,9 +167,7 @@ def encrypt_messages(email_messages):
encrypted_alternative = encrypt(alt, address,
**encrypt_kwargs)
except EncryptionFailedError as e:
raise AlternativeEncryptionFailedError(
"Encrypting alternative to %s failed: %s (%s)",
address, alt, e.msg)
handle_failed_alternative_encryption(e)
else:
alternatives.append((encrypted_alternative,
"application/gpg-encrypted"))
Expand Down
109 changes: 109 additions & 0 deletions email_extras/handlers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import inspect

from django.conf import settings
from django.core.mail import mail_admins

from .models import Address
from .settings import FAILURE_HANDLERS


ADMIN_ADDRESSES = [admin[1] for admin in settings.ADMINS]


def get_variable_from_exception(exception, variable_name):
"""
Grab the variable from closest frame in the stack
"""
for frame in reversed(inspect.trace()):
try:
# From http://stackoverflow.com/a/9059407/6461688
frame_variable = frame[0].f_locals[variable_name]
except KeyError:
pass
else:
return frame_variable
else:
raise KeyError("Variable '%s' not in any stack frames", variable_name)


def default_handle_failed_encryption(exception):
"""
Handle failures when trying to encrypt alternative content for messages
"""
raise exception


def default_handle_failed_alternative_encryption(exception):
"""
Handle failures when trying to encrypt alternative content for messages
"""
raise exception


def default_handle_failed_attachment_encryption(exception):
"""
Handle failures when trying to encrypt alternative content for messages
"""
raise exception


def force_mail_admins(unencrypted_message, address):
"""
Mail admins when encryption fails, and send the message unencrypted if
the recipient is an admin
"""

if address in ADMIN_ADDRESSES:
# We assume that it is more important to mail the admin *without*
# encrypting the message
force_send_message(unencrypted_message)
else:
mail_admins(
"Failed encryption attempt",
"""
There was a problem encrypting an email message.
Subject: "{subject}"
Address: "{address}"
""")


def force_delete_key(address):
"""
Delete the key from the keyring and the Key and Address objects from the
database
"""
address_object = Address.objects.get(address=address)
address_object.key.delete()
address_object.delete()


def force_send_message(unencrypted_message):
"""
Send the message unencrypted
"""
unencrypted_message.do_not_encrypt_this_message = True
unencrypted_message.send()


def import_function(key):
mod, _, function = FAILURE_HANDLERS[key].rpartition('.')
try:
# Python 3.4+
from importlib import import_module
except ImportError:
# Python < 3.4
# From http://stackoverflow.com/a/8255024/6461688
mod = __import__(mod, globals(), locals(), [function])
else:
mod = import_module(mod)
return getattr(mod, function)

exception_handlers = {
'message': 'handle_failed_message_encryption',
'alternative': 'handle_failed_alternative_encryption',
'attachment': 'handle_failed_attachment_encryption',
}

for key, value in exception_handlers.items():
locals()[value] = import_function(key)
7 changes: 6 additions & 1 deletion email_extras/settings.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@

from django.conf import settings
from django.core.exceptions import ImproperlyConfigured

Expand All @@ -7,6 +6,12 @@
USE_GNUPG = getattr(settings, "EMAIL_EXTRAS_USE_GNUPG", GNUPG_HOME is not None)

ALWAYS_TRUST = getattr(settings, "EMAIL_EXTRAS_ALWAYS_TRUST_KEYS", False)
FAILURE_HANDLERS = {
'message': 'email_extras.handlers.default_handle_failed_encryption',
'alternative': 'email_extras.handlers.default_handle_failed_alternative_encryption',
'attachment': 'email_extras.handlers.default_handle_failed_attachment_encryption',
}
FAILURE_HANDLERS.update(getattr(settings, "EMAIL_EXTRAS_FAILURE_HANDLERS", {}))
GNUPG_ENCODING = getattr(settings, "EMAIL_EXTRAS_GNUPG_ENCODING", None)
SIGNING_KEY_DATA = {
'key_type': "DSA",
Expand Down

0 comments on commit fd943ac

Please sign in to comment.