diff options
author | Dylan Baker <dylan@pnwbakers.com> | 2017-07-14 14:27:04 -0700 |
---|---|---|
committer | Dylan Baker <dylan@pnwbakers.com> | 2017-08-17 10:59:49 -0700 |
commit | c64d4507d858f1d443eee0926000e14fe9755adc (patch) | |
tree | 6273853837ddf0fbf94d12b57ccd0d17a1234cf4 | |
parent | e024d670a3ba363df3b15b8d45fc87ac80c3de6b (diff) |
db/utils: Allow encrypted messages to be put in mixed payloads as well
Since a multipart/mixed can contain anything that a normal message
could, this should be allowed. The only case that I can think of this
actually happening is if an email server takes the original message,
puts it in a multipart/mixed, and then attaches it's own message, like
on of the annoying "this mail scanned for viruses by <product>".
-rw-r--r-- | NEWS | 1 | ||||
-rw-r--r-- | alot/db/utils.py | 188 | ||||
-rw-r--r-- | tests/db/utils_test.py | 2 |
3 files changed, 103 insertions, 88 deletions
@@ -6,6 +6,7 @@ next: * feature: option to use linewise focussing in thread mode * feature: add support to move to next or previous message matching a notmuch query in a thread buffer * feature: Convert from deprecated pygppme module to upstream gpg wrappers +* feature: Verify signatures/decrypt messages in multipart/mixed payloads 0.5: * save command prompt, recipient and sender history across program restarts diff --git a/alot/db/utils.py b/alot/db/utils.py index 198966ba..c24ffef1 100644 --- a/alot/db/utils.py +++ b/alot/db/utils.py @@ -1,4 +1,6 @@ +# encoding=utf-8 # Copyright (C) 2011-2012 Patrick Totzke <patricktotzke@gmail.com> +# Copyright © 2017 Dylan Baker <dylan@pnwbakers.com> # This file is released under the GNU GPL, version 3 or a later revision. # For further details see the COPYING file from __future__ import absolute_import @@ -138,6 +140,78 @@ def _handle_signatures(original, message, params): add_signature_headers(original, sigs, malformed) +def _handle_encrypted(original, message): + """Handle encrypted messages helper. + + RFC 3156 is quite strict: + * exactly two messages + * the first is of type 'application/pgp-encrypted' + * the first contains 'Version: 1' + * the second is of type 'application/octet-stream' + * the second contains the encrypted and possibly signed data + + :param original: The original top-level mail. This is required to attache + special headers to + :type original: :class:`email.message.Message` + :param message: The multipart/signed payload to verify + :type message: :class:`email.message.Message` + """ + malformed = False + + ct = message.get_payload(0).get_content_type() + if ct != _APP_PGP_ENC: + malformed = u'expected Content-Type: {0}, got: {1}'.format( + _APP_PGP_ENC, ct) + + want = 'application/octet-stream' + ct = message.get_payload(1).get_content_type() + if ct != want: + malformed = u'expected Content-Type: {0}, got: {1}'.format(want, ct) + + if not malformed: + try: + sigs, d = crypto.decrypt_verify(message.get_payload(1).get_payload()) + 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 = unicode(e) + else: + n = message_from_string(d) + + # add the decrypted message to message. note that n contains all + # the attachments, no need to walk over n here. + original.attach(n) + + original.defects.extend(n.defects) + + # there are two methods for both signed and encrypted data, one is + # called 'RFC 1847 Encapsulation' by RFC 3156, and one is the + # 'Combined method'. + if not sigs: + # 'RFC 1847 Encapsulation', the signature is a detached + # signature found in the recovered mime message of type + # multipart/signed. + if X_SIGNATURE_VALID_HEADER in n: + for k in (X_SIGNATURE_VALID_HEADER, + X_SIGNATURE_MESSAGE_HEADER): + original[k] = n[k] + else: + # 'Combined method', the signatures are returned by the + # decrypt_verify function. + + # note that if we reached this point, we know the signatures + # are valid. if they were not valid, the else block of the + # current try would not have been executed + add_signature_headers(original, sigs, '') + + if malformed: + msg = u'Malformed OpenPGP message: {0}'.format(malformed) + content = email.message_from_string(msg.encode('utf-8')) + content.set_charset('utf-8') + original.attach(content) + + def message_from_file(handle): '''Reads a mail from the given file-like object and returns an email object, very much like email.message_from_file. In addition to @@ -155,92 +229,34 @@ def message_from_file(handle): del m[X_SIGNATURE_VALID_HEADER] del m[X_SIGNATURE_MESSAGE_HEADER] - p = get_params(m) - - # handle OpenPGP signed data - if (m.is_multipart() and - m.get_content_subtype() == 'signed' and - p.get('protocol') == _APP_PGP_SIG): - _handle_signatures(m, m, p) - elif (m.is_multipart() and m.get_content_subtype() == 'mixed' and - m.get_payload(0).get_content_subtype() == 'signed'): - p = get_params(m.get_payload(0)) - if p.get('protocol') == _APP_PGP_SIG: - _handle_signatures(m, m.get_payload(0), p) - - # XXX: It is presumably possible to put an encrypted message in a - # multipart/mixed, but I have no such examples and haven't looked through - # RFC's thuroughly - # handle OpenPGP encrypted data - elif (m.is_multipart() and - m.get_content_subtype() == 'encrypted' and - p.get('protocol') == _APP_PGP_ENC and - 'Version: 1' in m.get_payload(0).get_payload()): - # RFC 3156 is quite strict: - # * exactly two messages - # * the first is of type 'application/pgp-encrypted' - # * the first contains 'Version: 1' - # * the second is of type 'application/octet-stream' - # * the second contains the encrypted and possibly signed data - malformed = False - - ct = m.get_payload(0).get_content_type() - if ct != _APP_PGP_ENC: - malformed = u'expected Content-Type: {0}, got: {1}'.format( - _APP_PGP_ENC, ct) - - want = 'application/octet-stream' - ct = m.get_payload(1).get_content_type() - if ct != want: - malformed = u'expected Content-Type: {0}, got: {1}'.format(want, - ct) - - if not malformed: - try: - sigs, d = crypto.decrypt_verify(m.get_payload(1).get_payload()) - 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 = unicode(e) - else: - # parse decrypted message - n = message_from_string(d) - - # add the decrypted message to m. note that n contains - # all the attachments, no need to walk over n here. - m.attach(n) - - # add any defects found - m.defects.extend(n.defects) - - # there are two methods for both signed and encrypted - # data, one is called 'RFC 1847 Encapsulation' by - # RFC 3156, and one is the 'Combined method'. - if len(sigs) == 0: - # 'RFC 1847 Encapsulation', the signature is a - # detached signature found in the recovered mime - # message of type multipart/signed. - if X_SIGNATURE_VALID_HEADER in n: - for k in (X_SIGNATURE_VALID_HEADER, - X_SIGNATURE_MESSAGE_HEADER): - m[k] = n[k] - else: - # 'Combined method', the signatures are returned - # by the decrypt_verify function. - - # note that if we reached this point, we know the - # signatures are valid. if they were not valid, - # the else block of the current try would not have - # been executed - add_signature_headers(m, sigs, '') - - if malformed: - msg = u'Malformed OpenPGP message: {0}'.format(malformed) - content = email.message_from_string(msg.encode('utf-8')) - content.set_charset('utf-8') - m.attach(content) + if m.is_multipart(): + p = get_params(m) + + # handle OpenPGP signed data + if (m.get_content_subtype() == 'signed' and + p.get('protocol') == _APP_PGP_SIG): + _handle_signatures(m, m, p) + + # handle OpenPGP encrypted data + 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) + + # It is also possible to put either of the abov into a multipart/mixed + # segment + elif m.get_content_subtype() == 'mixed': + sub = m.get_payload(0) + + if sub.is_multipart(): + p = get_params(sub) + + if (sub.get_content_subtype() == 'signed' and + p.get('protocol') == _APP_PGP_SIG): + _handle_signatures(m, sub, p) + elif (sub.get_content_subtype() == 'encrypted' and + p.get('protocol') == _APP_PGP_ENC): + _handle_encrypted(m, sub) return m diff --git a/tests/db/utils_test.py b/tests/db/utils_test.py index d2c92577..26768597 100644 --- a/tests/db/utils_test.py +++ b/tests/db/utils_test.py @@ -603,7 +603,6 @@ class TestMessageFromFile(TestCaseClassCleanup): self.assertIn(utils.X_SIGNATURE_VALID_HEADER, m) self.assertIn(utils.X_SIGNATURE_MESSAGE_HEADER, m) - @unittest.expectedFailure def test_encrypted_unsigned_in_multipart_mixed(self): """It is valid to encapsulate a multipart/encrypted payload inside a multipart/mixed payload, verify that works. @@ -615,7 +614,6 @@ class TestMessageFromFile(TestCaseClassCleanup): self.assertNotIn(utils.X_SIGNATURE_VALID_HEADER, m) self.assertNotIn(utils.X_SIGNATURE_MESSAGE_HEADER, m) - @unittest.expectedFailure def test_encrypted_signed_in_multipart_mixed(self): """It is valid to encapsulate a multipart/encrypted payload inside a multipart/mixed payload, verify that works when the multipart/encrypted |