From 48866fbf53241587b1540551a98d787af3ba9787 Mon Sep 17 00:00:00 2001 From: Antoine Catton Date: Wed, 29 Jul 2015 20:36:47 -0600 Subject: [PATCH 01/16] There's no problem with encrypt being public --- tests/test_zeyple.py | 4 ++-- zeyple/zeyple.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/test_zeyple.py b/tests/test_zeyple.py index a436494..785261e 100644 --- a/tests/test_zeyple.py +++ b/tests/test_zeyple.py @@ -41,7 +41,7 @@ def test_user_key(self): def test_encrypt_with_plain_text(self): """Encrypts plain text""" - encrypted = self.zeyple._encrypt( + encrypted = self.zeyple.encrypt( 'The key is under the carpet.', [LINUS_ID] ) assert is_encrypted(encrypted) @@ -49,7 +49,7 @@ def test_encrypt_with_plain_text(self): def test_encrypt_with_unicode(self): """Encrypts Unicode text""" - encrypted = self.zeyple._encrypt('héhé', [LINUS_ID]) + encrypted = self.zeyple.encrypt('héhé', [LINUS_ID]) assert is_encrypted(encrypted) def test_process_message_with_simple_message(self): diff --git a/zeyple/zeyple.py b/zeyple/zeyple.py index 9dd4cdf..916287a 100644 --- a/zeyple/zeyple.py +++ b/zeyple/zeyple.py @@ -56,7 +56,7 @@ def process_message(self, message, recipients): if message.is_multipart(): logging.warn("Message is multipart, ignoring") else: - payload = self._encrypt(message.get_payload(), [key_id]) + payload = self.encrypt(message.get_payload(), [key_id]) # replace message body with encrypted payload message.set_payload(payload) @@ -110,7 +110,7 @@ def _user_key(self, email): return None - def _encrypt(self, message, key_ids): + def encrypt(self, message, key_ids): """Encrypts the message with the given keys""" try: From 86a80b4d0716eedce31a3845b6ed1da58e67e8bb Mon Sep 17 00:00:00 2001 From: Antoine Catton Date: Wed, 29 Jul 2015 20:38:57 -0600 Subject: [PATCH 02/16] Make load_configuration() return config --- tests/test_zeyple.py | 2 +- zeyple/zeyple.py | 23 ++++++++++++----------- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/tests/test_zeyple.py b/tests/test_zeyple.py index 785261e..238291e 100644 --- a/tests/test_zeyple.py +++ b/tests/test_zeyple.py @@ -27,7 +27,7 @@ def tearDown(self): def test_config(self): """Parses the configuration file properly""" - log_file = self.zeyple._config.get('zeyple', 'log_file') + log_file = self.zeyple.config.get('zeyple', 'log_file') assert log_file == '/tmp/zeyple.log' def test_user_key(self): diff --git a/zeyple/zeyple.py b/zeyple/zeyple.py index 916287a..63f1d8e 100644 --- a/zeyple/zeyple.py +++ b/zeyple/zeyple.py @@ -25,9 +25,9 @@ class Zeyple: """Zeyple Encrypts Your Precious Log Emails""" def __init__(self): - self._load_configuration() + self.config = self.load_configuration() - log_file = self._config.get('zeyple', 'log_file') + log_file = self.config.get('zeyple', 'log_file') logging.basicConfig( filename=log_file, level=logging.DEBUG, format='%(asctime)s %(process)s %(levelname)s %(message)s' @@ -35,7 +35,7 @@ def __init__(self): logging.info("Zeyple ready to encrypt outgoing emails") # tells gpgme.Context() where are the keys - os.environ['GNUPGHOME'] = self._config.get('gpg', 'home') + os.environ['GNUPGHOME'] = self.config.get('gpg', 'home') def process_message(self, message, recipients): """Encrypts the message with recipient keys""" @@ -70,8 +70,8 @@ def process_message(self, message, recipients): return sent_messages def _add_zeyple_header(self, message): - if self._config.has_option('zeyple', 'add_header') and \ - self._config.getboolean('zeyple', 'add_header'): + if self.config.has_option('zeyple', 'add_header') and \ + self.config.getboolean('zeyple', 'add_header'): message.add_header( 'X-Zeyple', "processed by {0} v{1}".format(__title__, __version__) @@ -81,21 +81,22 @@ def _send_message(self, message, recipient): """Sends the given message through the SMTP relay""" logging.info("Sending message %s", message['Message-id']) - smtp = smtplib.SMTP(self._config.get('relay', 'host'), - self._config.get('relay', 'port')) + smtp = smtplib.SMTP(self.config.get('relay', 'host'), + self.config.get('relay', 'port')) smtp.sendmail(message['From'], recipient, message.as_string()) smtp.quit() logging.info("Message %s sent", message['Message-id']) - def _load_configuration(self, filename='zeyple.conf'): + def load_configuration(self, filename='zeyple.conf'): """Reads and parses the config file""" - self._config = SafeConfigParser() - self._config.read(['/etc/' + filename, filename]) - if not self._config.sections(): + config = SafeConfigParser() + config.read(['/etc/' + filename, filename]) + if not config.sections(): raise IOError('Cannot open config file.') + return config def _user_key(self, email): """Returns the GPG key for the given email address""" From 44ee5d6832f6a5f0bcfa30c39c838deafde6a0fe Mon Sep 17 00:00:00 2001 From: Antoine Catton Date: Wed, 29 Jul 2015 21:01:52 -0600 Subject: [PATCH 03/16] Define the gpg context as a property * Set the protocol to the default protocol (it doesn't really matter) * Pass the home_dir to gpgme * Give the ability to override the gpg executable --- zeyple/zeyple.py | 30 +++++++++++++++++++++--------- 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/zeyple/zeyple.py b/zeyple/zeyple.py index 63f1d8e..fbc149d 100644 --- a/zeyple/zeyple.py +++ b/zeyple/zeyple.py @@ -34,8 +34,22 @@ def __init__(self): ) logging.info("Zeyple ready to encrypt outgoing emails") - # tells gpgme.Context() where are the keys - os.environ['GNUPGHOME'] = self.config.get('gpg', 'home') + @property + def gpg(self): + protocol = gpgme.PROTOCOL_OpenPGP + + if self.config.has_option('gpg', 'executable'): + executable = self.config.get('gpg', 'executable') + else: + executable = None # Default value + + home_dir = self.config.get('gpg', 'home') + + ctx = gpgme.Context() + ctx.set_engine_info(protocol, executable, home_dir) + ctx.armor = True + + return ctx def process_message(self, message, recipients): """Encrypts the message with recipient keys""" @@ -101,8 +115,7 @@ def load_configuration(self, filename='zeyple.conf'): def _user_key(self, email): """Returns the GPG key for the given email address""" logging.info("Trying to encrypt for %s", email) - gpg = gpgme.Context() - keys = [key for key in gpg.keylist(email)] + keys = [key for key in self.gpg.keylist(email)] if keys: key = keys.pop() # NOTE: looks like keys[0] is the master key @@ -123,13 +136,12 @@ def encrypt(self, message, key_ids): plaintext = BytesIO(message) ciphertext = BytesIO() - gpg = gpgme.Context() - gpg.armor = True + self.gpg.armor = True - recipient = [gpg.get_key(key_id) for key_id in key_ids] + recipient = [self.gpg.get_key(key_id) for key_id in key_ids] - gpg.encrypt(recipient, gpgme.ENCRYPT_ALWAYS_TRUST, - plaintext, ciphertext) + self.gpg.encrypt(recipient, gpgme.ENCRYPT_ALWAYS_TRUST, + plaintext, ciphertext) return ciphertext.getvalue() From 6644b1ed51cc2cc9d86aa5b168c7f4b6e64b80c0 Mon Sep 17 00:00:00 2001 From: Antoine Catton Date: Wed, 29 Jul 2015 21:28:15 -0600 Subject: [PATCH 04/16] Remove unused import --- zeyple/zeyple.py | 1 - 1 file changed, 1 deletion(-) diff --git a/zeyple/zeyple.py b/zeyple/zeyple.py index fbc149d..1fd4332 100644 --- a/zeyple/zeyple.py +++ b/zeyple/zeyple.py @@ -2,7 +2,6 @@ # -*- coding: utf-8 -*- import sys -import os import logging import email import smtplib From 97014898d487ae5f8eb04ee97bf628e3b2997047 Mon Sep 17 00:00:00 2001 From: Antoine Catton Date: Wed, 29 Jul 2015 21:28:55 -0600 Subject: [PATCH 05/16] Fix #2 --- tests/test_zeyple.py | 6 ++---- zeyple/zeyple.py | 15 +++++++++++---- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/tests/test_zeyple.py b/tests/test_zeyple.py index 238291e..96c4300 100644 --- a/tests/test_zeyple.py +++ b/tests/test_zeyple.py @@ -109,7 +109,5 @@ def test_process_message_with_multipart_message(self): """), ["torvalds@linux-foundation.org"]) assert emails[0]['X-Zeyple'] is not None - assert emails[0].is_multipart() - for part in emails[0].walk(): - assert not is_encrypted(part.as_string().encode('utf-8')) - + assert not emails[0].is_multipart() # GPG encrypt the multipart + assert is_encrypted(emails[0].get_payload().encode('utf-8')) diff --git a/zeyple/zeyple.py b/zeyple/zeyple.py index 1fd4332..6fcc843 100644 --- a/zeyple/zeyple.py +++ b/zeyple/zeyple.py @@ -4,6 +4,7 @@ import sys import logging import email +import email.mime.multipart import smtplib import gpgme from io import BytesIO @@ -67,12 +68,18 @@ def process_message(self, message, recipients): logging.info("Key ID: %s", key_id) if key_id: if message.is_multipart(): - logging.warn("Message is multipart, ignoring") + mime = email.mime.multipart.MIMEMultipart( + 'mixed', + None, # The boundary will be autogenerated + message.get_payload(), + ) + payload = mime.as_string() else: - payload = self.encrypt(message.get_payload(), [key_id]) + payload = message.get_payload() - # replace message body with encrypted payload - message.set_payload(payload) + encrypted_payload = self.encrypt(payload, [key_id]) + # replace message body with encrypted payload + message.set_payload(encrypted_payload) else: logging.warn("No keys found, message will be sent unencrypted") From abf5ab4eb144f387d8fdd30d2774ba215d1389bf Mon Sep 17 00:00:00 2001 From: Antoine Catton Date: Wed, 29 Jul 2015 21:47:17 -0600 Subject: [PATCH 06/16] This is testing python, not zeyple --- tests/test_zeyple.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/tests/test_zeyple.py b/tests/test_zeyple.py index 96c4300..5f777ce 100644 --- a/tests/test_zeyple.py +++ b/tests/test_zeyple.py @@ -24,12 +24,6 @@ def setUp(self): def tearDown(self): os.remove('zeyple.conf') - def test_config(self): - """Parses the configuration file properly""" - - log_file = self.zeyple.config.get('zeyple', 'log_file') - assert log_file == '/tmp/zeyple.log' - def test_user_key(self): """Returns the right ID for the given email address""" From b2669d50e2d1ae799932e99b7e6281988c179b13 Mon Sep 17 00:00:00 2001 From: Antoine Catton Date: Wed, 29 Jul 2015 21:56:11 -0600 Subject: [PATCH 07/16] Always use os.path.join instead of concatenation --- zeyple/zeyple.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/zeyple/zeyple.py b/zeyple/zeyple.py index 6fcc843..603a3b8 100644 --- a/zeyple/zeyple.py +++ b/zeyple/zeyple.py @@ -2,6 +2,7 @@ # -*- coding: utf-8 -*- import sys +import os import logging import email import email.mime.multipart @@ -113,7 +114,10 @@ def load_configuration(self, filename='zeyple.conf'): """Reads and parses the config file""" config = SafeConfigParser() - config.read(['/etc/' + filename, filename]) + config.read([ + os.path.join('/etc/', filename), + filename, + ]) if not config.sections(): raise IOError('Cannot open config file.') return config From 70fba5961a2487a1199bed9ab2a170b546b9f60e Mon Sep 17 00:00:00 2001 From: Antoine Catton Date: Wed, 29 Jul 2015 22:01:28 -0600 Subject: [PATCH 08/16] Fix style: the PEP-0008 requires two space before an inline comment --- tests/test_zeyple.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_zeyple.py b/tests/test_zeyple.py index 5f777ce..8e52e72 100644 --- a/tests/test_zeyple.py +++ b/tests/test_zeyple.py @@ -19,7 +19,7 @@ def setUp(self): shutil.copyfile('tests/zeyple.conf', 'zeyple.conf') os.system("gpg --recv-keys %s 2> /dev/null" % LINUS_ID) self.zeyple = zeyple.Zeyple() - self.zeyple._send_message = Mock() # don't try to send emails + self.zeyple._send_message = Mock() # don't try to send emails def tearDown(self): os.remove('zeyple.conf') From db7c9add8842cb803a4ba421de77d8fefccc55e5 Mon Sep 17 00:00:00 2001 From: Antoine Catton Date: Wed, 29 Jul 2015 22:07:45 -0600 Subject: [PATCH 09/16] Make the tests use a temporary directory and temporary gpg homedir --- tests/test_zeyple.py | 37 +++++++++++++++++++++++++++++++++---- tests/zeyple.conf | 11 ----------- zeyple/zeyple.py | 10 +++++----- 3 files changed, 38 insertions(+), 20 deletions(-) delete mode 100644 tests/zeyple.conf diff --git a/tests/test_zeyple.py b/tests/test_zeyple.py index 8e52e72..a4b7160 100644 --- a/tests/test_zeyple.py +++ b/tests/test_zeyple.py @@ -4,8 +4,11 @@ import unittest from mock import Mock import os +import subprocess import shutil import six +from six.moves.configparser import ConfigParser +import tempfile from textwrap import dedent from zeyple import zeyple @@ -16,13 +19,39 @@ def is_encrypted(string): class ZeypleTest(unittest.TestCase): def setUp(self): - shutil.copyfile('tests/zeyple.conf', 'zeyple.conf') - os.system("gpg --recv-keys %s 2> /dev/null" % LINUS_ID) - self.zeyple = zeyple.Zeyple() + self.tmpdir = tempfile.mkdtemp() + + self.conffile = os.path.join(self.tmpdir, 'zeyple.conf') + self.homedir = os.path.join(self.tmpdir, 'gpg') + self.logfile = os.path.join(self.tmpdir, 'zeyple.log') + + config = ConfigParser() + + config.add_section('zeyple') + config.set('zeyple', 'log_file', self.logfile) + config.set('zeyple', 'add_header', 'true') + + config.add_section('gpg') + config.set('gpg', 'home', self.homedir) + + config.add_section('relay') + config.set('relay', 'host', 'example.net') + config.set('relay', 'port', '2525') + + with open(self.conffile, 'w') as fp: + config.write(fp) + + os.mkdir(self.homedir, 0700) + subprocess.check_call(['gpg', '--homedir', self.homedir, + '--keyserver', 'pgp.mit.edu', + '--recv-keys', LINUS_ID], + stderr=open('/dev/null')) + + self.zeyple = zeyple.Zeyple(self.conffile) self.zeyple._send_message = Mock() # don't try to send emails def tearDown(self): - os.remove('zeyple.conf') + shutil.rmtree(self.tmpdir) def test_user_key(self): """Returns the right ID for the given email address""" diff --git a/tests/zeyple.conf b/tests/zeyple.conf deleted file mode 100644 index 59ea273..0000000 --- a/tests/zeyple.conf +++ /dev/null @@ -1,11 +0,0 @@ -[zeyple] -log_file = /tmp/zeyple.log -add_header = true - -[gpg] -home = ~/.gnupg - -[relay] -host = localhost -port = 10026 - diff --git a/zeyple/zeyple.py b/zeyple/zeyple.py index 603a3b8..efcaf8b 100644 --- a/zeyple/zeyple.py +++ b/zeyple/zeyple.py @@ -25,8 +25,8 @@ class Zeyple: """Zeyple Encrypts Your Precious Log Emails""" - def __init__(self): - self.config = self.load_configuration() + def __init__(self, config_fname='zeyple.conf'): + self.config = self.load_configuration(config_fname) log_file = self.config.get('zeyple', 'log_file') logging.basicConfig( @@ -110,13 +110,13 @@ def _send_message(self, message, recipient): logging.info("Message %s sent", message['Message-id']) - def load_configuration(self, filename='zeyple.conf'): + def load_configuration(self, fname): """Reads and parses the config file""" config = SafeConfigParser() config.read([ - os.path.join('/etc/', filename), - filename, + os.path.join('/etc/', fname), + fname, ]) if not config.sections(): raise IOError('Cannot open config file.') From f202c68d7174ca8175e42feeebb6aabc80d0a163 Mon Sep 17 00:00:00 2001 From: Antoine Catton Date: Wed, 29 Jul 2015 22:32:03 -0600 Subject: [PATCH 10/16] Use test keys (for decoding later) --- tests/keys.gpg | 61 ++++++++++++++++++++++++++++++++++++++++++++ tests/keys/test1.key | 33 ++++++++++++++++++++++++ tests/keys/test2.key | 32 +++++++++++++++++++++++ tests/test_zeyple.py | 30 ++++++++++++---------- 4 files changed, 143 insertions(+), 13 deletions(-) create mode 100644 tests/keys.gpg create mode 100644 tests/keys/test1.key create mode 100644 tests/keys/test2.key diff --git a/tests/keys.gpg b/tests/keys.gpg new file mode 100644 index 0000000..7632338 --- /dev/null +++ b/tests/keys.gpg @@ -0,0 +1,61 @@ +-----BEGIN PGP PRIVATE KEY BLOCK----- +Comment: THESE PRIVATE KEYS ARE FOR TESTING, DO NOT IMPORT THEM!! + +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 +=+1Yx +-----END PGP PRIVATE KEY BLOCK----- diff --git a/tests/keys/test1.key b/tests/keys/test1.key new file mode 100644 index 0000000..93750c7 --- /dev/null +++ b/tests/keys/test1.key @@ -0,0 +1,33 @@ +-----BEGIN PGP PRIVATE KEY BLOCK----- +Comment: NEVER USE THIS KEY, THIS KEY IS FOR TESTING + +lQHYBFW5pC8BBADJRSpFia07qiS5lEOyBjAQzn16b4WR1iHktGAXV/LD4D3NE2Hc +i0HHs9CxJ1xqbAcblltxCb6kFq+kHA1gzeNYbUBmfu0XciU850s8JCtbWilKLKRD +cYX20Tgof5APqINEswfTytyVFSiK6bMlGMZjj9R2McoPPb20lbnwJ1npxwARAQAB +AAP8Cmg0tuwN9KGxKCf035E7xQT85WrN9SLup7hRFM0QYpniuC95GF1IJVE0f22o +mYVEvxqTDmyIbN7JJMZ8175toVEqT4TO/eND79PW+7MVINlmbz712xfPu2oVYGEe +Avx/jTcOpmKPHnY+F9z0InRnWAGzuxbHCXQs8ECREOW3NqkCAM5UULZ0LrXlvgOu +o8NHx17SW+t6kwGvZzJExFG7VvJvzuZdW/jm5u1mlRfS7G61F5DMSw7fmftUApKr +HlI9V20CAPm5D+y9ConXKUJ6XXLRIPDkl+B9duWVJ4t3FcTx8xcqHObj4ZEzhIC1 +JFoYIEbRG0859+ZwJOrm+zAD8wYhwYMCAL4Ssmcy3BHcwCK7zzRopxhvfiN6KGAh +BzMy6oVKbp3khMYBDe4S5c2xcO70G2cvHNZf74476cvwhddDmSu7LtiibbQLWmV5 +cGxlIFRlc3SIuAQTAQIAIgUCVbmkLwIbAwYLCQgHAwIGFQgCCQoLBBYCAwECHgEC +F4AACgkQnAqeRfWkB0pGyQQAgmL0yxG8gz4dqtqc5Lp12jRXQb9/cKScYlD3ozSI +ZY5gxdMZgS8/wVWIFqW1b4m+DQBmaobNX0uIU2afvNsFxSyR2U67lU0dU/bxGrsG +G9cnkijV/JcNU2/OLDwsAC5KOcqScSJCstNweUj5npgpw/2ru89/NoY9nzyOqfSg +TIqdAdgEVbmkLwEEAMMButbh59FnHI4Z+1xBqHmJFD/H5+rD9Dr7pjbaXMfsJ6Cy +HXUMBqrZEfZpE9mfXMrmUaMB2EIVd0mb+mvUxqToCrR5/Kg2BaOr48IBzSm80HQv +HXa5i2ac2Qnf45APAhns2XIo2/haqQt94hbkmV4+90a4aA9dZCtvfap+y0wNABEB +AAEAA/kB3Uoemah9p3VN2YCTpFAP9hLNZ6P3ag9eIf+ik3S4IkagYhuR0yncJ85P +aLfxyHTavsLQjWkL0V3jhFwrpuw4Qp7i3HG6umoV87hWVXIE0HXOXhJmGrNzxINb +2HTdjo1q4oVC55UZIMO3RvPPt1AlRx67hXfi8nPqxVlfsrzeOQIA0DUHnnHPSJr7 +JBrlNsUOqELQodEmUAw6+53+gTQfU1qHCqN2W5np7l9iu5Gq2UCxY3UEG+yPPQyD +WL3AP6UEOQIA78T/5kxeXgSsfbmE8DQFxoCRLqyykCax5EByku3yAmF/F/IOYyHU +dn9eVNnEaerng6EMkIkV9xCTuNtWb89OdQIAlRfXV/23R+krjbCzDkNNVag6l8d2 +E0nG4k4uS6iazFcghnutTzcoKG1uMOMSlLJ11Q5m3g9ebEz8uUXeaT81bKIMiJ8E +GAECAAkFAlW5pC8CGwwACgkQnAqeRfWkB0rBhgP+LtZy+NiQGNSUZ2LxlYgwsbYT +jSOAOYwVODpaIAyTQzvCKxPhZM7RMUlWPpamZQrUofZTNmsu7Cnj5ThE9WgcsXPg +VIoxy7jXGU5g0TZuJXEsHV4jGYvIBfcY/R4LjTskLL20CX8EWIv9U59PY4lSV+mZ +nHv89db50/2kNSibsZU= +=VY60 +-----END PGP PRIVATE KEY BLOCK----- diff --git a/tests/keys/test2.key b/tests/keys/test2.key new file mode 100644 index 0000000..db23c6e --- /dev/null +++ b/tests/keys/test2.key @@ -0,0 +1,32 @@ +-----BEGIN PGP PRIVATE KEY BLOCK----- +Comment: NEVER USE THIS KEY, THIS KEY IS FOR TESTING + +lQHYBFW5pF4BBADfRJzouz2MH3YZ7qTBSzfxzIHYrtfZoetG0rFp4B5FPNggXz0P +tDIVSqwpkb3Yp5PDWR0u/1RyEeSHh9YL2EMezpi8ZGQFn9Cjmkc48sluaSWLj5Cc +hedaCV/t5/IfvvsQdD8PyZGC4Os+nuwryUVM9Hc2UwShMEPNaBmhdFDXXwARAQAB +AAP8DeAsxEYGwDOgWmI7eQvcsTldhILxRURL4/3qKsNT/keWwwRIPjabujkG1BqL +qvBXPZfHOYmCzQgRpN6rTdcl7KF0pGCaksgY3KakiOp3/wNIe1RuN+bLj7fHCttT +PT9FNQKkqKdTGCXn3ViYTqDBuXmXSteel8IcIRjh8TyGBBECAOHeDNVzmEpn+6Ei +Womq7LWUQLKLaJkgEuhwJIJOQMTFbCBgyoWfdJVSke422V2TpYGPMbLCGxf/vttR +nCHWOfcCAP0NybCRjsrHE7v5982QTZQsP86Hmg9hnMahzygJyFF71LvZCP2Q6xj/ +vNCInpgr84s+ulbiFWsM2+PEbkHes9kCAN0d2z67anUiMW/jBrmvQSYcEeCwX2Xn +OJzXZQ2l5GN49rVKoTXCx10kGPwozrJa3/hDprNZn0ibJl6GrhJe30mlWrQZT3Ro +ZXIgS2V5IGZvciBaZXlwbGUgVGVzdIi4BBMBAgAiBQJVuaReAhsDBgsJCAcDAgYV +CAIJCgsEFgIDAQIeAQIXgAAKCRAf9YefAE74fTswA/4h4b6XExYtim+xSRHkFbFx +QrL+vyqNu27ZXwimaZsSDOYpupp0wbB+vNde8SFMQfYmOK8dCrrNpVGf6Rc35WWK +LpsH5yYUkAMU8+kwRycb16FoGxQFTmR/FylBuFAh3PwzhlsCu77+Y3JX3S+Kkv+O +SD0C4PHoAXIJdXpNjEek1J0B2ARVuaReAQQAq8UgO4MmyqP8Qg07KWBNaE8wKEBa +tRxeGHIY9mubUwqK0GQxZoHSdCE7NOtE8z81VptJvtCfNlqtNualGwoV8KKby13a +TeIPZoTW0J3XyQvweCcBJlsbzv14MVnp89Qscs0lCLNftg4pGXxEuFJ5/swRInnR +88wCvq1t/4/rH1EAEQEAAQAD+wVm8hYxEB6iJOg6ZIyO9OxFhksTyA6HXX25E+gn +FvfX/mjip8uUpGbm1yb5Sb45NA8c2+dChilWVXmDLDjjGrC9I94qWtg+4b9ANImz +NzvMwqug4uFb1MhXpMdSxkAeKEhogXkp3i4Ck8F0eUI0ks8n7N5g+DAoQ0pPACQ1 +auHJAgDFoSS+Fe0oCmlqylSQMfxTug1T4qJgGt6K7LnLkj8KlizccXXrF3KLFdh5 +dzdXOtifbXBv3vyy0nlLkrA7CyCpAgDegLzcKPh5zpIjEHSRzxh4IIJSwNXV8Tiu +ZWcPtRsA1s34SukTyWeLvIo/T2YkpsDbXEGDvQOOUcgeECH5ZyppAf4pXfFUzfMx +NySYStM9CCIY6r+QnK3/UX5rlnbJu98h2w1wF0GtdIG3D0y2nPDUdCK9ejnClTam +GMG2cYTZnMzVn1mInwQYAQIACQUCVbmkXgIbDAAKCRAf9YefAE74fZ4oBACYkp4O +gsy+aMUejadahvm9+bfzqU1tQ9iWlG/9Xz9pjj5sJbOrHD7z0qRpLMYAZru2llgw +T7vdp9JzmUeTdGY7/4Ce8+rw6ZxRpGRBagdWFChgDbb0f+uT8xu3Ef+Fk4aOZ/IR +XCVN6qkCuKUdfQB6xMxKEQnL2wRruHuCFgk1Xw== +=gOA4 diff --git a/tests/test_zeyple.py b/tests/test_zeyple.py index a4b7160..d097089 100644 --- a/tests/test_zeyple.py +++ b/tests/test_zeyple.py @@ -12,7 +12,11 @@ from textwrap import dedent from zeyple import zeyple -LINUS_ID = '79BE3E4300411886' +KEYS_FNAME = os.path.join(os.path.dirname(__file__), 'keys.gpg') +TEST1_ID = 'D6513C04E24C1F83' +TEST1_EMAIL = 'test1@zeyple.example.com' +TEST2_ID = '0422F1C597FB1687' +TEST2_EMAIL = 'test2@zeyple.example.com' def is_encrypted(string): return string.startswith(six.b('-----BEGIN PGP MESSAGE-----')) @@ -42,10 +46,10 @@ def setUp(self): config.write(fp) os.mkdir(self.homedir, 0700) - subprocess.check_call(['gpg', '--homedir', self.homedir, - '--keyserver', 'pgp.mit.edu', - '--recv-keys', LINUS_ID], - stderr=open('/dev/null')) + subprocess.check_call( + ['gpg', '--homedir', self.homedir, '--import', KEYS_FNAME], + #stderr=open('/dev/null'), + ) self.zeyple = zeyple.Zeyple(self.conffile) self.zeyple._send_message = Mock() # don't try to send emails @@ -58,21 +62,21 @@ def test_user_key(self): assert self.zeyple._user_key('non_existant@example.org') is None - user_key = self.zeyple._user_key('torvalds@linux-foundation.org') - assert user_key == LINUS_ID + user_key = self.zeyple._user_key(TEST1_EMAIL) + assert user_key == TEST1_ID def test_encrypt_with_plain_text(self): """Encrypts plain text""" encrypted = self.zeyple.encrypt( - 'The key is under the carpet.', [LINUS_ID] + 'The key is under the carpet.', [TEST1_ID] ) assert is_encrypted(encrypted) def test_encrypt_with_unicode(self): """Encrypts Unicode text""" - encrypted = self.zeyple.encrypt('héhé', [LINUS_ID]) + encrypted = self.zeyple.encrypt('héhé', [TEST1_ID]) assert is_encrypted(encrypted) def test_process_message_with_simple_message(self): @@ -81,14 +85,14 @@ def test_process_message_with_simple_message(self): emails = self.zeyple.process_message(dedent("""\ Received: by example.org (Postfix, from userid 0) id DD3B67981178; Thu, 6 Sep 2012 23:35:37 +0000 (UTC) - To: torvalds@linux-foundation.org + To: """ + TEST1_EMAIL + """ Subject: Hello with Unicode héüøœ©ßð®å¥¹²æ¿áßö«ç Message-Id: <20120906233537.DD3B67981178@example.org> Date: Thu, 6 Sep 2012 23:35:37 +0000 (UTC) From: root@example.org (root) test ðßïð - """), ["torvalds@linux-foundation.org"]) + """), [TEST1_EMAIL]) assert emails[0]['X-Zeyple'] is not None assert is_encrypted(emails[0].get_payload().encode('utf-8')) @@ -101,7 +105,7 @@ def test_process_message_with_multipart_message(self): Received: by example.org (Postfix, from userid 0) id CE9876C78258; Sat, 8 Sep 2012 13:00:18 +0000 (UTC) Date: Sat, 08 Sep 2012 13:00:18 +0000 - To: torvalds@linux-foundation.org + To: """ + TEST1_EMAIL + """ Subject: test User-Agent: Heirloom mailx 12.4 7/29/08 MIME-Version: 1.0 @@ -129,7 +133,7 @@ def test_process_message_with_multipart_message(self): Yy90ZXN0JyB3d3ctZGF0YQo= --=_504b4162.Gyt30puFsMOHWjpCATT1XRbWoYI1iR/sT4UX78zEEMJbxu+h-- - """), ["torvalds@linux-foundation.org"]) + """), [TEST1_EMAIL]) assert emails[0]['X-Zeyple'] is not None assert not emails[0].is_multipart() # GPG encrypt the multipart From d3793df36da333ea5aaa79240fa2e2e161f6689c Mon Sep 17 00:00:00 2001 From: Antoine Catton Date: Wed, 29 Jul 2015 22:45:20 -0600 Subject: [PATCH 11/16] Octal literals needs to be lead by 0o in Python 3 (This is backward compatible with python 2) --- tests/test_zeyple.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_zeyple.py b/tests/test_zeyple.py index a4b7160..cd0ceb9 100644 --- a/tests/test_zeyple.py +++ b/tests/test_zeyple.py @@ -41,7 +41,7 @@ def setUp(self): with open(self.conffile, 'w') as fp: config.write(fp) - os.mkdir(self.homedir, 0700) + os.mkdir(self.homedir, 0o700) subprocess.check_call(['gpg', '--homedir', self.homedir, '--keyserver', 'pgp.mit.edu', '--recv-keys', LINUS_ID], From f378afa716af69305c183bb49548698765895368 Mon Sep 17 00:00:00 2001 From: Antoine Catton Date: Wed, 29 Jul 2015 23:11:58 -0600 Subject: [PATCH 12/16] Propose a better alternative to #10 --- tests/test_zeyple.py | 34 +++++++++++++++++++++++++++++++++- zeyple/zeyple.py | 23 +++++++++++++---------- 2 files changed, 46 insertions(+), 11 deletions(-) diff --git a/tests/test_zeyple.py b/tests/test_zeyple.py index d097089..d4aca84 100644 --- a/tests/test_zeyple.py +++ b/tests/test_zeyple.py @@ -48,7 +48,7 @@ def setUp(self): os.mkdir(self.homedir, 0700) subprocess.check_call( ['gpg', '--homedir', self.homedir, '--import', KEYS_FNAME], - #stderr=open('/dev/null'), + stderr=open('/dev/null'), ) self.zeyple = zeyple.Zeyple(self.conffile) @@ -57,6 +57,14 @@ def setUp(self): def tearDown(self): shutil.rmtree(self.tmpdir) + def decrypt(self, data): + gpg = subprocess.Popen( + ['gpg', '--homedir', self.homedir, '--decrypt'], + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + ) + return gpg.communicate(data.encode('ascii'))[0] + def test_user_key(self): """Returns the right ID for the given email address""" @@ -138,3 +146,27 @@ def test_process_message_with_multipart_message(self): assert emails[0]['X-Zeyple'] is not None assert not emails[0].is_multipart() # GPG encrypt the multipart assert is_encrypted(emails[0].get_payload().encode('utf-8')) + + def test_process_message_with_multiple_recipients(self): + """Encrypt a message with multiple recipients""" + + content = "Content" + + emails = self.zeyple.process_message(dedent("""\ + Received: by example.org (Postfix, from userid 0) + id DD3B67981178; Thu, 6 Sep 2012 23:35:37 +0000 (UTC) + To: """ + ', '.join([TEST1_EMAIL, TEST2_EMAIL]) + """ + Subject: もしもし with Unicode + Message-Id: <20120906233537.DD3B67981178@example.org> + Date: Thu, 6 Sep 2012 23:35:37 +0000 (UTC) + From: root@example.org (root) + + """ + content + """ + """), [TEST1_EMAIL, TEST2_EMAIL]) + + assert len(emails) == 2 # It had two recipients + + for m in emails: + assert m['X-Zeyple'] is not None + payload = self.decrypt(m.get_payload()).strip() + assert payload == six.b(content) diff --git a/zeyple/zeyple.py b/zeyple/zeyple.py index efcaf8b..0452b28 100644 --- a/zeyple/zeyple.py +++ b/zeyple/zeyple.py @@ -8,6 +8,7 @@ import email.mime.multipart import smtplib import gpgme +import copy from io import BytesIO try: from configparser import SafeConfigParser # Python 3 @@ -52,11 +53,12 @@ def gpg(self): return ctx - def process_message(self, message, recipients): + def process_message(self, message_data, recipients): """Encrypts the message with recipient keys""" - message = email.message_from_string(message) - logging.info("Processing outgoing message %s", message['Message-id']) + in_message = email.message_from_string(message_data) + logging.info( + "Processing outgoing message %s", in_message['Message-id']) if not recipients: logging.warn("Cannot find any recipients, ignoring") @@ -64,29 +66,30 @@ def process_message(self, message, recipients): sent_messages = [] for recipient in recipients: logging.info("Recipient: %s", recipient) + out_message = copy.copy(in_message) key_id = self._user_key(recipient) logging.info("Key ID: %s", key_id) if key_id: - if message.is_multipart(): + if in_message.is_multipart(): mime = email.mime.multipart.MIMEMultipart( 'mixed', None, # The boundary will be autogenerated - message.get_payload(), + in_message.get_payload(), ) payload = mime.as_string() else: - payload = message.get_payload() + payload = in_message.get_payload() encrypted_payload = self.encrypt(payload, [key_id]) # replace message body with encrypted payload - message.set_payload(encrypted_payload) + out_message.set_payload(encrypted_payload) else: logging.warn("No keys found, message will be sent unencrypted") - self._add_zeyple_header(message) - self._send_message(message, recipient) - sent_messages.append(message) + self._add_zeyple_header(out_message) + self._send_message(out_message, recipient) + sent_messages.append(out_message) return sent_messages From 35cd5fdd9b617ccd1a2168ac552942d3fadfe4ed Mon Sep 17 00:00:00 2001 From: Antoine Catton Date: Thu, 30 Jul 2015 22:08:35 -0600 Subject: [PATCH 13/16] Fix bad abreviation fname -> filename --- zeyple/zeyple.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/zeyple/zeyple.py b/zeyple/zeyple.py index 0452b28..ad32b1c 100644 --- a/zeyple/zeyple.py +++ b/zeyple/zeyple.py @@ -113,13 +113,13 @@ def _send_message(self, message, recipient): logging.info("Message %s sent", message['Message-id']) - def load_configuration(self, fname): + def load_configuration(self, filename): """Reads and parses the config file""" config = SafeConfigParser() config.read([ - os.path.join('/etc/', fname), - fname, + os.path.join('/etc/', filename), + filename, ]) if not config.sections(): raise IOError('Cannot open config file.') From ce71bff7eb27032b24a1a29211d4ece53780f057 Mon Sep 17 00:00:00 2001 From: Antoine Catton Date: Thu, 30 Jul 2015 22:13:49 -0600 Subject: [PATCH 14/16] Remove wrongfully committed test keys --- tests/keys/test1.key | 33 --------------------------------- tests/keys/test2.key | 32 -------------------------------- 2 files changed, 65 deletions(-) delete mode 100644 tests/keys/test1.key delete mode 100644 tests/keys/test2.key diff --git a/tests/keys/test1.key b/tests/keys/test1.key deleted file mode 100644 index 93750c7..0000000 --- a/tests/keys/test1.key +++ /dev/null @@ -1,33 +0,0 @@ ------BEGIN PGP PRIVATE KEY BLOCK----- -Comment: NEVER USE THIS KEY, THIS KEY IS FOR TESTING - -lQHYBFW5pC8BBADJRSpFia07qiS5lEOyBjAQzn16b4WR1iHktGAXV/LD4D3NE2Hc -i0HHs9CxJ1xqbAcblltxCb6kFq+kHA1gzeNYbUBmfu0XciU850s8JCtbWilKLKRD -cYX20Tgof5APqINEswfTytyVFSiK6bMlGMZjj9R2McoPPb20lbnwJ1npxwARAQAB -AAP8Cmg0tuwN9KGxKCf035E7xQT85WrN9SLup7hRFM0QYpniuC95GF1IJVE0f22o -mYVEvxqTDmyIbN7JJMZ8175toVEqT4TO/eND79PW+7MVINlmbz712xfPu2oVYGEe -Avx/jTcOpmKPHnY+F9z0InRnWAGzuxbHCXQs8ECREOW3NqkCAM5UULZ0LrXlvgOu -o8NHx17SW+t6kwGvZzJExFG7VvJvzuZdW/jm5u1mlRfS7G61F5DMSw7fmftUApKr -HlI9V20CAPm5D+y9ConXKUJ6XXLRIPDkl+B9duWVJ4t3FcTx8xcqHObj4ZEzhIC1 -JFoYIEbRG0859+ZwJOrm+zAD8wYhwYMCAL4Ssmcy3BHcwCK7zzRopxhvfiN6KGAh -BzMy6oVKbp3khMYBDe4S5c2xcO70G2cvHNZf74476cvwhddDmSu7LtiibbQLWmV5 -cGxlIFRlc3SIuAQTAQIAIgUCVbmkLwIbAwYLCQgHAwIGFQgCCQoLBBYCAwECHgEC -F4AACgkQnAqeRfWkB0pGyQQAgmL0yxG8gz4dqtqc5Lp12jRXQb9/cKScYlD3ozSI -ZY5gxdMZgS8/wVWIFqW1b4m+DQBmaobNX0uIU2afvNsFxSyR2U67lU0dU/bxGrsG -G9cnkijV/JcNU2/OLDwsAC5KOcqScSJCstNweUj5npgpw/2ru89/NoY9nzyOqfSg -TIqdAdgEVbmkLwEEAMMButbh59FnHI4Z+1xBqHmJFD/H5+rD9Dr7pjbaXMfsJ6Cy -HXUMBqrZEfZpE9mfXMrmUaMB2EIVd0mb+mvUxqToCrR5/Kg2BaOr48IBzSm80HQv -HXa5i2ac2Qnf45APAhns2XIo2/haqQt94hbkmV4+90a4aA9dZCtvfap+y0wNABEB -AAEAA/kB3Uoemah9p3VN2YCTpFAP9hLNZ6P3ag9eIf+ik3S4IkagYhuR0yncJ85P -aLfxyHTavsLQjWkL0V3jhFwrpuw4Qp7i3HG6umoV87hWVXIE0HXOXhJmGrNzxINb -2HTdjo1q4oVC55UZIMO3RvPPt1AlRx67hXfi8nPqxVlfsrzeOQIA0DUHnnHPSJr7 -JBrlNsUOqELQodEmUAw6+53+gTQfU1qHCqN2W5np7l9iu5Gq2UCxY3UEG+yPPQyD -WL3AP6UEOQIA78T/5kxeXgSsfbmE8DQFxoCRLqyykCax5EByku3yAmF/F/IOYyHU -dn9eVNnEaerng6EMkIkV9xCTuNtWb89OdQIAlRfXV/23R+krjbCzDkNNVag6l8d2 -E0nG4k4uS6iazFcghnutTzcoKG1uMOMSlLJ11Q5m3g9ebEz8uUXeaT81bKIMiJ8E -GAECAAkFAlW5pC8CGwwACgkQnAqeRfWkB0rBhgP+LtZy+NiQGNSUZ2LxlYgwsbYT -jSOAOYwVODpaIAyTQzvCKxPhZM7RMUlWPpamZQrUofZTNmsu7Cnj5ThE9WgcsXPg -VIoxy7jXGU5g0TZuJXEsHV4jGYvIBfcY/R4LjTskLL20CX8EWIv9U59PY4lSV+mZ -nHv89db50/2kNSibsZU= -=VY60 ------END PGP PRIVATE KEY BLOCK----- diff --git a/tests/keys/test2.key b/tests/keys/test2.key deleted file mode 100644 index db23c6e..0000000 --- a/tests/keys/test2.key +++ /dev/null @@ -1,32 +0,0 @@ ------BEGIN PGP PRIVATE KEY BLOCK----- -Comment: NEVER USE THIS KEY, THIS KEY IS FOR TESTING - -lQHYBFW5pF4BBADfRJzouz2MH3YZ7qTBSzfxzIHYrtfZoetG0rFp4B5FPNggXz0P -tDIVSqwpkb3Yp5PDWR0u/1RyEeSHh9YL2EMezpi8ZGQFn9Cjmkc48sluaSWLj5Cc -hedaCV/t5/IfvvsQdD8PyZGC4Os+nuwryUVM9Hc2UwShMEPNaBmhdFDXXwARAQAB -AAP8DeAsxEYGwDOgWmI7eQvcsTldhILxRURL4/3qKsNT/keWwwRIPjabujkG1BqL -qvBXPZfHOYmCzQgRpN6rTdcl7KF0pGCaksgY3KakiOp3/wNIe1RuN+bLj7fHCttT -PT9FNQKkqKdTGCXn3ViYTqDBuXmXSteel8IcIRjh8TyGBBECAOHeDNVzmEpn+6Ei -Womq7LWUQLKLaJkgEuhwJIJOQMTFbCBgyoWfdJVSke422V2TpYGPMbLCGxf/vttR -nCHWOfcCAP0NybCRjsrHE7v5982QTZQsP86Hmg9hnMahzygJyFF71LvZCP2Q6xj/ -vNCInpgr84s+ulbiFWsM2+PEbkHes9kCAN0d2z67anUiMW/jBrmvQSYcEeCwX2Xn -OJzXZQ2l5GN49rVKoTXCx10kGPwozrJa3/hDprNZn0ibJl6GrhJe30mlWrQZT3Ro -ZXIgS2V5IGZvciBaZXlwbGUgVGVzdIi4BBMBAgAiBQJVuaReAhsDBgsJCAcDAgYV -CAIJCgsEFgIDAQIeAQIXgAAKCRAf9YefAE74fTswA/4h4b6XExYtim+xSRHkFbFx -QrL+vyqNu27ZXwimaZsSDOYpupp0wbB+vNde8SFMQfYmOK8dCrrNpVGf6Rc35WWK -LpsH5yYUkAMU8+kwRycb16FoGxQFTmR/FylBuFAh3PwzhlsCu77+Y3JX3S+Kkv+O -SD0C4PHoAXIJdXpNjEek1J0B2ARVuaReAQQAq8UgO4MmyqP8Qg07KWBNaE8wKEBa -tRxeGHIY9mubUwqK0GQxZoHSdCE7NOtE8z81VptJvtCfNlqtNualGwoV8KKby13a -TeIPZoTW0J3XyQvweCcBJlsbzv14MVnp89Qscs0lCLNftg4pGXxEuFJ5/swRInnR -88wCvq1t/4/rH1EAEQEAAQAD+wVm8hYxEB6iJOg6ZIyO9OxFhksTyA6HXX25E+gn -FvfX/mjip8uUpGbm1yb5Sb45NA8c2+dChilWVXmDLDjjGrC9I94qWtg+4b9ANImz -NzvMwqug4uFb1MhXpMdSxkAeKEhogXkp3i4Ck8F0eUI0ks8n7N5g+DAoQ0pPACQ1 -auHJAgDFoSS+Fe0oCmlqylSQMfxTug1T4qJgGt6K7LnLkj8KlizccXXrF3KLFdh5 -dzdXOtifbXBv3vyy0nlLkrA7CyCpAgDegLzcKPh5zpIjEHSRzxh4IIJSwNXV8Tiu -ZWcPtRsA1s34SukTyWeLvIo/T2YkpsDbXEGDvQOOUcgeECH5ZyppAf4pXfFUzfMx -NySYStM9CCIY6r+QnK3/UX5rlnbJu98h2w1wF0GtdIG3D0y2nPDUdCK9ejnClTam -GMG2cYTZnMzVn1mInwQYAQIACQUCVbmkXgIbDAAKCRAf9YefAE74fZ4oBACYkp4O -gsy+aMUejadahvm9+bfzqU1tQ9iWlG/9Xz9pjj5sJbOrHD7z0qRpLMYAZru2llgw -T7vdp9JzmUeTdGY7/4Ce8+rw6ZxRpGRBagdWFChgDbb0f+uT8xu3Ef+Fk4aOZ/IR -XCVN6qkCuKUdfQB6xMxKEQnL2wRruHuCFgk1Xw== -=gOA4 From a01900771d05a87d1448d3ef4e53dfd981c10121 Mon Sep 17 00:00:00 2001 From: Antoine Catton Date: Sun, 2 Aug 2015 22:39:51 -0600 Subject: [PATCH 15/16] Force all data to be binary and get rid of is_encrypted() By forcing binary data, we don't have to worry about encoding. Everything is just bytes. --- tests/test_zeyple.py | 46 +++++++++++++++++++++----------------------- zeyple/zeyple.py | 41 +++++++++++++++++++++++++++++---------- 2 files changed, 53 insertions(+), 34 deletions(-) diff --git a/tests/test_zeyple.py b/tests/test_zeyple.py index fb794c6..d97f8c1 100644 --- a/tests/test_zeyple.py +++ b/tests/test_zeyple.py @@ -1,6 +1,9 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- +# BBB: Python 2.7 support +from __future__ import unicode_literals + import unittest from mock import Mock import os @@ -18,9 +21,6 @@ TEST2_ID = '0422F1C597FB1687' TEST2_EMAIL = 'test2@zeyple.example.com' -def is_encrypted(string): - return string.startswith(six.b('-----BEGIN PGP MESSAGE-----')) - class ZeypleTest(unittest.TestCase): def setUp(self): self.tmpdir = tempfile.mkdtemp() @@ -63,7 +63,7 @@ def decrypt(self, data): stdin=subprocess.PIPE, stdout=subprocess.PIPE, ) - return gpg.communicate(data.encode('ascii'))[0] + return gpg.communicate(data)[0] def test_user_key(self): """Returns the right ID for the given email address""" @@ -75,35 +75,34 @@ def test_user_key(self): def test_encrypt_with_plain_text(self): """Encrypts plain text""" + content = 'The key is under the carpet.'.encode('ascii') + encrypted = self.zeyple.encrypt(content, [TEST1_ID]) + assert self.decrypt(encrypted) == content - encrypted = self.zeyple.encrypt( - 'The key is under the carpet.', [TEST1_ID] - ) - assert is_encrypted(encrypted) - - def test_encrypt_with_unicode(self): - """Encrypts Unicode text""" - - encrypted = self.zeyple.encrypt('héhé', [TEST1_ID]) - assert is_encrypted(encrypted) + def test_encrypt_binary_data(self): + """Encrypt binary data. (Simulate encrypting non ascii characters""" + content = b'\xff\x80' + encrypted = self.zeyple.encrypt(content, [TEST1_ID]) + assert self.decrypt(encrypted) == content def test_process_message_with_simple_message(self): """Encrypts simple messages""" + content = "test" emails = self.zeyple.process_message(dedent("""\ Received: by example.org (Postfix, from userid 0) id DD3B67981178; Thu, 6 Sep 2012 23:35:37 +0000 (UTC) To: """ + TEST1_EMAIL + """ - Subject: Hello with Unicode héüøœ©ßð®å¥¹²æ¿áßö«ç + Subject: Hello Message-Id: <20120906233537.DD3B67981178@example.org> Date: Thu, 6 Sep 2012 23:35:37 +0000 (UTC) From: root@example.org (root) - test ðßïð - """), [TEST1_EMAIL]) + """ + content).encode('ascii'), [TEST1_EMAIL]) assert emails[0]['X-Zeyple'] is not None - assert is_encrypted(emails[0].get_payload().encode('utf-8')) + payload = emails[0].get_payload().encode('ascii') + assert self.decrypt(payload) == content.encode('ascii') def test_process_message_with_multipart_message(self): """Ignores multipart messages""" @@ -141,11 +140,11 @@ def test_process_message_with_multipart_message(self): Yy90ZXN0JyB3d3ctZGF0YQo= --=_504b4162.Gyt30puFsMOHWjpCATT1XRbWoYI1iR/sT4UX78zEEMJbxu+h-- - """), [TEST1_EMAIL]) + """).encode('ascii'), [TEST1_EMAIL]) assert emails[0]['X-Zeyple'] is not None assert not emails[0].is_multipart() # GPG encrypt the multipart - assert is_encrypted(emails[0].get_payload().encode('utf-8')) + assert self.decrypt(emails[0].get_payload().encode('ascii')) def test_process_message_with_multiple_recipients(self): """Encrypt a message with multiple recipients""" @@ -156,17 +155,16 @@ def test_process_message_with_multiple_recipients(self): Received: by example.org (Postfix, from userid 0) id DD3B67981178; Thu, 6 Sep 2012 23:35:37 +0000 (UTC) To: """ + ', '.join([TEST1_EMAIL, TEST2_EMAIL]) + """ - Subject: もしもし with Unicode + Subject: Hello Message-Id: <20120906233537.DD3B67981178@example.org> Date: Thu, 6 Sep 2012 23:35:37 +0000 (UTC) From: root@example.org (root) - """ + content + """ - """), [TEST1_EMAIL, TEST2_EMAIL]) + """ + content).encode('ascii'), [TEST1_EMAIL, TEST2_EMAIL]) assert len(emails) == 2 # It had two recipients for m in emails: assert m['X-Zeyple'] is not None - payload = self.decrypt(m.get_payload()).strip() + payload = self.decrypt(m.get_payload().encode('ascii')) assert payload == six.b(content) diff --git a/zeyple/zeyple.py b/zeyple/zeyple.py index ad32b1c..bf0cafd 100644 --- a/zeyple/zeyple.py +++ b/zeyple/zeyple.py @@ -15,6 +15,28 @@ except ImportError: from ConfigParser import SafeConfigParser # Python 2 +# Boiler plate to avoid dependency from six +# BBB: Python 2.7 support +PY3K = sys.version_info > (3, 0) +binary_string = bytes if PY3K else str +if PY3K: + message_from_binary = email.message_from_bytes +else: + message_from_binary = email.message_from_string + + +def as_binary_string(email): + if PY3K: + return email.as_bytes() + else: + return email.as_string() + + +def get_binary_payload(email): + payload = email.get_payload() + if not isinstance(payload, binary_string): + payload = payload.encode() + return payload __title__ = 'Zeyple' __version__ = '1.0.0' @@ -55,8 +77,9 @@ def gpg(self): def process_message(self, message_data, recipients): """Encrypts the message with recipient keys""" + assert isinstance(message_data, binary_string) - in_message = email.message_from_string(message_data) + in_message = message_from_binary(message_data) logging.info( "Processing outgoing message %s", in_message['Message-id']) @@ -77,9 +100,9 @@ def process_message(self, message_data, recipients): None, # The boundary will be autogenerated in_message.get_payload(), ) - payload = mime.as_string() + payload = as_binary_string(mime) else: - payload = in_message.get_payload() + payload = get_binary_payload(in_message) encrypted_payload = self.encrypt(payload, [key_id]) # replace message body with encrypted payload @@ -139,13 +162,8 @@ def _user_key(self, email): def encrypt(self, message, key_ids): """Encrypts the message with the given keys""" + assert isinstance(message, binary_string) - try: - message = message.decode('utf-8', 'replace') - except AttributeError: - pass - - message = message.encode('utf-8') plaintext = BytesIO(message) ciphertext = BytesIO() @@ -161,7 +179,10 @@ def encrypt(self, message, key_ids): if __name__ == '__main__': recipients = sys.argv[1:] - message = sys.stdin.read() + + # BBB: Python 2.7 support + binary_stdin = sys.stdin.buffer if PY3K else sys.stdin + message = binary_stdin.read() zeyple = Zeyple() zeyple.process_message(message, recipients) From 1720c0249763fb5f90227f477eb230551cc5e4e2 Mon Sep 17 00:00:00 2001 From: Antoine Catton Date: Sun, 2 Aug 2015 23:52:09 -0600 Subject: [PATCH 16/16] Behave according to the RFCs Fix #2 --- zeyple/zeyple.py | 50 +++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 47 insertions(+), 3 deletions(-) diff --git a/zeyple/zeyple.py b/zeyple/zeyple.py index bf0cafd..e304dd6 100644 --- a/zeyple/zeyple.py +++ b/zeyple/zeyple.py @@ -6,6 +6,8 @@ import logging import email import email.mime.multipart +import email.mime.application +import email.encoders import smtplib import gpgme import copy @@ -89,7 +91,6 @@ def process_message(self, message_data, recipients): sent_messages = [] for recipient in recipients: logging.info("Recipient: %s", recipient) - out_message = copy.copy(in_message) key_id = self._user_key(recipient) logging.info("Key ID: %s", key_id) @@ -105,8 +106,24 @@ def process_message(self, message_data, recipients): payload = get_binary_payload(in_message) encrypted_payload = self.encrypt(payload, [key_id]) - # replace message body with encrypted payload - out_message.set_payload(encrypted_payload) + + version = self.get_version_part() + encrypted = self.get_encrypted_part(encrypted_payload) + + out_message = copy.copy(in_message) + out_message.preamble = "This is an OpenPGP/MIME encrypted " \ + "message (RFC 4880 and 3156)" + + if 'Content-Type' not in out_message: + out_message['Content-Type'] = 'multipart/encrypted' + else: + out_message.replace_header( + 'Content-Type', + 'multipart/encrypted', + ) + out_message.set_param('protocol', 'application/pgp-encrypted') + out_message.set_payload([version, encrypted]) + else: logging.warn("No keys found, message will be sent unencrypted") @@ -116,6 +133,33 @@ def process_message(self, message_data, recipients): return sent_messages + def get_version_part(self): + ret = email.mime.application.MIMEApplication( + 'Version: 1\n', + 'pgp-encrypted', + email.encoders.encode_noop, + ) + ret.add_header( + 'Content-Description', + "PGP/MIME version identification", + ) + return ret + + def get_encrypted_part(self, payload): + ret = email.mime.application.MIMEApplication( + payload, + 'octet-stream', + email.encoders.encode_noop, + name="encrypted.asc", + ) + ret.add_header('Content-Description', "OpenPGP encrypted message") + ret.add_header( + 'Content-Disposition', + 'inline', + filename='encrypted.asc', + ) + return ret + def _add_zeyple_header(self, message): if self.config.has_option('zeyple', 'add_header') and \ self.config.getboolean('zeyple', 'add_header'):