Skip to content

Commit

Permalink
Merge pull request #1239 from meskio/session-key
Browse files Browse the repository at this point in the history
crypto: Use session-key to decrypt messages if present in the index
  • Loading branch information
lucc committed Oct 10, 2018
2 parents 4a67118 + 01b8ff8 commit 34b3805
Show file tree
Hide file tree
Showing 5 changed files with 60 additions and 15 deletions.
31 changes: 30 additions & 1 deletion alot/crypto.py
Original file line number Diff line number Diff line change
Expand Up @@ -206,15 +206,22 @@ def verify_detached(message, signature):
raise GPGProblem(str(e), code=e.getcode())


def decrypt_verify(encrypted):
def decrypt_verify(encrypted, session_keys=None):
"""Decrypts the given ciphertext string and returns both the
signatures (if any) and the plaintext.
:param bytes encrypted: the mail to decrypt
:param list[str] session_keys: a list OpenPGP session keys
:returns: the signatures and decrypted plaintext data
:rtype: tuple[list[gpg.resuit.Signature], str]
:raises: :class:`~alot.errors.GPGProblem` if the decryption fails
"""
if session_keys is not None:
try:
return _decrypt_verify_session_keys(encrypted, session_keys)
except GPGProblem:
pass

ctx = gpg.core.Context()
try:
plaintext, _, verify_result = ctx.decrypt(encrypted, verify=True)
Expand All @@ -228,6 +235,28 @@ def decrypt_verify(encrypted):
return sigs, plaintext


def _decrypt_verify_session_keys(encrypted, session_keys):
"""Decrypts the given ciphertext string using the session_keys
and returns both the signatures (if any) and the plaintext.
:param bytes encrypted: the mail to decrypt
:param list[str] session_keys: a list OpenPGP session keys
:returns: the signatures and decrypted plaintext data
:rtype: tuple[list[gpg.resuit.Signature], str]
:raises: :class:`~alot.errors.GPGProblem` if the decryption fails
"""
for key in session_keys:
ctx = gpg.core.Context()
ctx.set_ctx_flag("override-session-key", key)
try:
(plaintext, _, verify_result) = ctx.decrypt(
encrypted, verify=True)
except gpg.errors.GPGMEError as e:
continue
return verify_result.signatures, plaintext
raise GPGProblem("No valid session key", code=GPGCode.NOT_FOUND)


def validate_key(key, sign=False, encrypt=False):
"""Assert that a key is valide and optionally that it can be used for
signing or encrypting. Raise GPGProblem otherwise.
Expand Down
8 changes: 7 additions & 1 deletion alot/db/message.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,11 @@ def __init__(self, dbman, msg, thread=None):
self._attachments = None # will be read upon first use
self._tags = set(msg.get_tags())

self._session_keys = []
for name, value in msg.get_properties("session-key", exact=True):
if name == "session-key":
self._session_keys.append(value)

try:
sender = decode_header(msg.get_header('From'))
if not sender:
Expand Down Expand Up @@ -102,7 +107,8 @@ def get_email(self):
if not self._email:
try:
with open(path, 'rb') as f:
self._email = utils.decrypted_message_from_bytes(f.read())
self._email = utils.decrypted_message_from_bytes(
f.read(), self._session_keys)
except IOError:
self._email = email.message_from_string(
warning, policy=email.policy.SMTP)
Expand Down
31 changes: 19 additions & 12 deletions alot/db/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ def _handle_signatures(original, message, params):
add_signature_headers(original, sigs, malformed)


def _handle_encrypted(original, message):
def _handle_encrypted(original, message, session_keys=None):
"""Handle encrypted messages helper.
RFC 3156 is quite strict:
Expand All @@ -155,6 +155,8 @@ def _handle_encrypted(original, message):
:type original: :class:`email.message.Message`
:param message: The multipart/signed payload to verify
:type message: :class:`email.message.Message`
:param session_keys: a list OpenPGP session keys
:type session_keys: [str]
"""
malformed = False

Expand All @@ -172,14 +174,14 @@ def _handle_encrypted(original, message):
# This should be safe because PGP uses US-ASCII characters only
payload = message.get_payload(1).get_payload().encode('ascii')
try:
sigs, d = crypto.decrypt_verify(payload)
sigs, d = crypto.decrypt_verify(payload, session_keys)
except GPGProblem as e:
# signature verification failures end up here too if the combined
# method is used, currently this prevents the interpretation of the
# recovered plain text mail. maybe that's a feature.
malformed = str(e)
else:
n = decrypted_message_from_bytes(d)
n = decrypted_message_from_bytes(d, session_keys)

# add the decrypted message to message. note that n contains all
# the attachments, no need to walk over n here.
Expand Down Expand Up @@ -214,26 +216,29 @@ def _handle_encrypted(original, message):
original.attach(content)


def decrypted_message_from_file(handle):
def decrypted_message_from_file(handle, session_keys=None):
'''Reads a mail from the given file-like object and returns an email
object, very much like email.message_from_file. In addition to
that OpenPGP encrypted data is detected and decrypted. If this
succeeds, any mime messages found in the recovered plaintext
message are added to the returned message object.
:param handle: a file-like object
:param session_keys: a list OpenPGP session keys
:returns: :class:`email.message.Message` possibly augmented with
decrypted data
'''
return decrypted_message_from_message(email.message_from_file(handle))
return decrypted_message_from_message(email.message_from_file(handle),
session_keys)


def decrypted_message_from_message(m):
def decrypted_message_from_message(m, session_keys=None):
'''Detect and decrypt OpenPGP encrypted data in an email object. If this
succeeds, any mime messages found in the recovered plaintext
message are added to the returned message object.
:param m: an email object
:param session_keys: a list OpenPGP session keys
:returns: :class:`email.message.Message` possibly augmented with
decrypted data
'''
Expand All @@ -253,7 +258,7 @@ def decrypted_message_from_message(m):
elif (m.get_content_subtype() == 'encrypted' and
p.get('protocol') == _APP_PGP_ENC and
'Version: 1' in m.get_payload(0).get_payload()):
_handle_encrypted(m, m)
_handle_encrypted(m, m, session_keys)

# It is also possible to put either of the abov into a multipart/mixed
# segment
Expand All @@ -268,12 +273,12 @@ def decrypted_message_from_message(m):
_handle_signatures(m, sub, p)
elif (sub.get_content_subtype() == 'encrypted' and
p.get('protocol') == _APP_PGP_ENC):
_handle_encrypted(m, sub)
_handle_encrypted(m, sub, session_keys)

return m


def decrypted_message_from_string(s):
def decrypted_message_from_string(s, session_keys=None):
'''Reads a mail from the given string. This is the equivalent of
:func:`email.message_from_string` which does nothing but to wrap
the given string in a StringIO object and to call
Expand All @@ -283,16 +288,18 @@ def decrypted_message_from_string(s):
details.
'''
return decrypted_message_from_file(io.StringIO(s))
return decrypted_message_from_file(io.StringIO(s), session_keys)


def decrypted_message_from_bytes(bytestring):
def decrypted_message_from_bytes(bytestring, session_keys=None):
"""Create a Message from bytes.
:param bytes bytestring: an email message as raw bytes
:param session_keys: a list OpenPGP session keys
"""
return decrypted_message_from_message(
email.message_from_bytes(bytestring, policy=email.policy.SMTP))
email.message_from_bytes(bytestring, policy=email.policy.SMTP),
session_keys)


def extract_headers(mail, headers=None):
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@
['alot = alot.__main__:main'],
},
install_requires=[
'notmuch>=0.26',
'notmuch>=0.27',
'urwid>=1.3.0',
'urwidtrees>=1.0',
'twisted>=10.2.0',
Expand Down
3 changes: 3 additions & 0 deletions tests/db/message_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,9 @@ def get_filename(self):
def get_tags(self):
return self.mock_tags

def get_properties(self, prop, exact=False):
return []


class TestMessage(unittest.TestCase):

Expand Down

0 comments on commit 34b3805

Please sign in to comment.