Skip to content

Commit d197e18

Browse files
committed
Add exception handlers for failed encryption only for the encrypting mail backend
1 parent dcc76db commit d197e18

File tree

3 files changed

+139
-19
lines changed

3 files changed

+139
-19
lines changed

email_extras/backends.py

Lines changed: 24 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@
1313
from django.core.mail.message import EmailMultiAlternatives
1414
from django.utils.encoding import smart_text
1515

16+
from .handlers import (handle_failed_message_encryption,
17+
handle_failed_alternative_encryption,
18+
handle_failed_attachment_encryption)
1619
from .settings import (GNUPG_HOME, GNUPG_ENCODING, USE_GNUPG, encrypt_kwargs)
1720
from .utils import EncryptionFailedError
1821

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

4144

42-
class AttachmentEncryptionFailedError(EncryptionFailedError):
43-
pass
44-
45-
46-
class AlternativeEncryptionFailedError(EncryptionFailedError):
47-
pass
48-
49-
5045
if USE_GNUPG:
5146
from gnupg import GPG
5247

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

147152
# Replace the message body with the encrypted message body
148-
new_msg.body = encrypt(new_msg.body, address)
153+
try:
154+
new_msg.body = encrypt(new_msg.body, address)
155+
except EncryptionFailedError as e:
156+
handle_failed_message_encryption(e)
149157

150158
# If the message has alternatives, encrypt them all
151159
alternatives = []
@@ -159,9 +167,7 @@ def encrypt_messages(email_messages):
159167
encrypted_alternative = encrypt(alt, address,
160168
**encrypt_kwargs)
161169
except EncryptionFailedError as e:
162-
raise AlternativeEncryptionFailedError(
163-
"Encrypting alternative to %s failed: %s (%s)",
164-
address, alt, e.msg)
170+
handle_failed_alternative_encryption(e)
165171
else:
166172
alternatives.append((encrypted_alternative,
167173
"application/gpg-encrypted"))

email_extras/handlers.py

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import inspect
2+
3+
from django.conf import settings
4+
from django.core.mail import mail_admins
5+
6+
from .models import Address
7+
from .settings import FAILURE_HANDLERS
8+
9+
10+
ADMIN_ADDRESSES = [admin[1] for admin in settings.ADMINS]
11+
12+
13+
def get_variable_from_exception(exception, variable_name):
14+
"""
15+
Grab the variable from closest frame in the stack
16+
"""
17+
for frame in reversed(inspect.trace()):
18+
try:
19+
# From http://stackoverflow.com/a/9059407/6461688
20+
frame_variable = frame[0].f_locals[variable_name]
21+
except KeyError:
22+
pass
23+
else:
24+
return frame_variable
25+
else:
26+
raise KeyError("Variable '%s' not in any stack frames", variable_name)
27+
28+
29+
def default_handle_failed_encryption(exception):
30+
"""
31+
Handle failures when trying to encrypt alternative content for messages
32+
"""
33+
raise exception
34+
35+
36+
def default_handle_failed_alternative_encryption(exception):
37+
"""
38+
Handle failures when trying to encrypt alternative content for messages
39+
"""
40+
raise exception
41+
42+
43+
def default_handle_failed_attachment_encryption(exception):
44+
"""
45+
Handle failures when trying to encrypt alternative content for messages
46+
"""
47+
raise exception
48+
49+
50+
def force_mail_admins(unencrypted_message, address):
51+
"""
52+
Mail admins when encryption fails, and send the message unencrypted if
53+
the recipient is an admin
54+
"""
55+
56+
if address in ADMIN_ADDRESSES:
57+
# We assume that it is more important to mail the admin *without*
58+
# encrypting the message
59+
force_send_message(unencrypted_message)
60+
else:
61+
mail_admins(
62+
"Failed encryption attempt",
63+
"""
64+
There was a problem encrypting an email message.
65+
66+
Subject: "{subject}"
67+
Address: "{address}"
68+
""")
69+
70+
71+
def force_delete_key(address):
72+
"""
73+
Delete the key from the keyring and the Key and Address objects from the
74+
database
75+
"""
76+
address_object = Address.objects.get(address=address)
77+
address_object.key.delete()
78+
address_object.delete()
79+
80+
81+
def force_send_message(unencrypted_message):
82+
"""
83+
Send the message unencrypted
84+
"""
85+
unencrypted_message.do_not_encrypt_this_message = True
86+
unencrypted_message.send()
87+
88+
89+
def import_function(key):
90+
mod, _, function = FAILURE_HANDLERS[key].rpartition('.')
91+
try:
92+
# Python 3.4+
93+
from importlib import import_module
94+
except ImportError:
95+
# Python < 3.4
96+
# From http://stackoverflow.com/a/8255024/6461688
97+
mod = __import__(mod, globals(), locals(), [function])
98+
else:
99+
mod = import_module(mod)
100+
return getattr(mod, function)
101+
102+
exception_handlers = {
103+
'message': 'handle_failed_message_encryption',
104+
'alternative': 'handle_failed_alternative_encryption',
105+
'attachment': 'handle_failed_attachment_encryption',
106+
}
107+
108+
for key, value in exception_handlers.items():
109+
locals()[value] = import_function(key)

email_extras/settings.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
21
from django.conf import settings
32
from django.core.exceptions import ImproperlyConfigured
43

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

98
ALWAYS_TRUST = getattr(settings, "EMAIL_EXTRAS_ALWAYS_TRUST_KEYS", False)
9+
FAILURE_HANDLERS = {
10+
'message': 'email_extras.handlers.default_handle_failed_encryption',
11+
'alternative': 'email_extras.handlers.default_handle_failed_alternative_encryption',
12+
'attachment': 'email_extras.handlers.default_handle_failed_attachment_encryption',
13+
}
14+
FAILURE_HANDLERS.update(getattr(settings, "EMAIL_EXTRAS_FAILURE_HANDLERS", {}))
1015
GNUPG_ENCODING = getattr(settings, "EMAIL_EXTRAS_GNUPG_ENCODING", None)
1116
SIGNING_KEY_DATA = {
1217
'key_type': "RSA",

0 commit comments

Comments
 (0)