...
  View open merge request
Commits (9)
......@@ -4,11 +4,25 @@ pgp-mime-lib
Simple Python package to send
[PGP/MIME](https://www.ietf.org/rfc/rfc3156.txt) multipart/encrypted
and multipart/signed emails. Uses the
[python-gnupg](https://gnupg.readthedocs.io/en/latest/) Python module to talk to
gpg.
[pgpy](https://github.com/SecurityInnovation/PGPy) Python module that
implements the OpenPGP protocol.
The package is agnostic with respect to key management: recipients'
public keys are not permanently stored in a keyring, but are passed
along with each message by the caller.
Python compatibility: 2.7 / 3.5+
## Running SMTP tests
To have the test suite send you a signed and encrypted email, set up the following environment
variables before running the tests:
- `SMTP_SERVER`
- `SMTP_USER`
- `SMTP_PASSWORD`
- `SMTP_RECIPIENT`
- `SMTP_PUBLIC_KEY` (a PGP public key file)
In `tox.ini` you can use [setenv](https://tox.readthedocs.io/en/latest/config.html#conf-setenv) or
[passenv](https://tox.readthedocs.io/en/latest/config.html#conf-passenv).
......@@ -2,25 +2,26 @@
# storage (so, no keyserver interaction, no keyring management).
from __future__ import absolute_import
import contextlib
import os
import re
import shutil
import tempfile
from email import encoders
from email.mime.application import MIMEApplication
import gnupg
import pgpy
from pgpy.constants import SymmetricKeyAlgorithm
from ._compat import StringIO
from . import rfc3156
# Digest algorithm to use in signatures.
# TODO: not sure this is the one used by pgpy
DIGEST_ALGO = 'SHA256'
def _default_gpghome():
if 'GNUPGHOME' in os.environ:
return os.environ['GNUPGHOME']
return os.path.expanduser('~/.gnupg')
class Error(Exception):
pass
......@@ -35,94 +36,57 @@ class GPG(object):
"""
def __init__(self, key_id, public_keyring=None, secret_key_dir=None,
def __init__(self, key_id, public_keyring=None, secret_key_file=None,
passphrase=None):
if not public_keyring:
public_keyring = os.path.join(_default_gpghome(), 'pubring.gpg')
if not secret_key_dir:
secret_key_dir = os.path.join(_default_gpghome(), 'private-keys-v1.d')
if not secret_key_file:
secret_key_file = os.path.join(_default_gpghome(), 'private-keys-v1.d')
for f in (public_keyring, secret_key_dir):
for f in (public_keyring, secret_key_file):
if not os.path.exists(f):
raise Error('no such file or directory: %s' % f)
self.public_keyring = public_keyring
self.secret_key_dir = secret_key_dir
self.secret_key_file = secret_key_file
self.secret_key_id = key_id
self.passphrase = passphrase
self.public_key, _ = pgpy.PGPKey.from_file(self.public_keyring)
self.secret_key, _ = pgpy.PGPKey.from_file(self.secret_key_file)
def _encrypt(self, msg_str, public_key):
"""Encrypt msg_str to the given public_key.
Import the key (or keys) provided to us into a temporary keyring.
We would really like to call gpg with the --hidden-recipients-file option, to avoid
importing the public key, but we can't do it with the gnupg module.
"""
@contextlib.contextmanager
def _sane_gpg(self, public_keyring=None):
"""Create a temporary GPG environment.
pubkey = parse_public_key(public_key)
pgpmsg = pgpy.PGPMessage.new(msg_str, cleartext=True)
try:
result = str(pubkey.encrypt(pgpmsg, cipher=SymmetricKeyAlgorithm.AES256))
except NotImplementedError as exc:
raise Error("Error encrypting message (old key format?): {}".format(exc))
return result
If public_keyring is None, the temporary environment will
contain a writable public keyring, that will be a copy of the
default one. If we don't need to import keys, we can pass
self.public_keyring as an optimization (avoids a file copy).
def encrypt_message(self, msg, public_key):
"""Encrypt `msg` to `public_key`.
Note that we must always have our public key available for
signing.
Build a multipart/encrypted MIME message.
Args:
msg (email.Message) message to sign
public_key (str) public key of the recipient
Returns:
An email.Message object.
"""
with _temp_dir() as tmpdir:
if not public_keyring:
public_keyring = os.path.join(tmpdir, 'pubring.gpg')
shutil.copy(self.public_keyring, public_keyring)
os.chmod(public_keyring, 0o600)
# Create a gpg.conf with some options. Not all are used
# and for some of them there are equivalent command-line
# options available through the gnupg module.
with open(os.path.join(tmpdir, 'gpg.conf'), 'w') as fd:
fd.write('''
personal-digest-preferences SHA512 SHA384 SHA256 SHA224
cert-digest-algo SHA256
default-preference-list SHA512 SHA384 SHA256 SHA224 AES256 AES192 AES CAST5 ZLIB BZIP2 ZIP Uncompressed
sig-notation issuer-fpr@notations.openpgp.fifthhorseman.net=%%g
throw-keyids
default-key %s\n
''' % self.secret_key_id) # noqa
# symlink the private key directory
os.symlink(self.secret_key_dir, os.path.join(tmpdir, 'private-keys-v1.d'))
# Create the gnupg.GPG object to talk to gnupg.
gpg = gnupg.GPG(gpgbinary=_default_gpgbinary(),
gnupghome=tmpdir,
keyring=public_keyring)
# XXX this below might be wrong.
gpg.encoding = 'utf-8'
yield gpg
def _encrypt(self, gpg, msg_str, public_key):
"""Encrypt msg_str to the given public_key."""
# Import the key (or keys) provided to us into a temporary
# keyring. We would really like to call gpg with the
# --hidden-recipients-file option, to avoid importing the
# public key, but we can't do it with the gnupg module.
fp = _import_one_key(gpg, public_key)
s = gpg.encrypt(msg_str, fp, passphrase=self.passphrase,
always_trust=True, extra_args=["--throw-keyids"])
if not s:
raise Error('gpg --encrypt failed (no result)')
return str(s)
def _sign(self, gpg, msg_str):
"""Sign msg_str and return the detached signature."""
s = gpg.sign(msg_str, clearsign=False, detach=True, binary=False,
passphrase=self.passphrase)
if not s:
raise Error('gpg --sign failed (no result)')
if not s.data:
raise Error('gpg --sign failed (no result data)')
return s.data
def _encrypt_message(self, gpg, msg, public_key):
"""Build a multipart/encrypted MIME message."""
container = rfc3156.MultipartEncrypted('application/pgp-encrypted')
enc_msg = MIMEApplication(
self._encrypt(gpg, msg.as_string(unixfrom=False), public_key),
self._encrypt(msg.as_string(unixfrom=False), public_key),
_subtype='octet-stream',
_encoder=encoders.encode_noop)
enc_msg.add_header('Content-Disposition', 'attachment',
......@@ -136,30 +100,36 @@ default-key %s\n
return container
def _sign_message(self, gpg, msg):
"""Build a multipart/signed MIME message."""
container = rfc3156.MultipartSigned('application/pgp-signature',
'pgp-' + DIGEST_ALGO.lower())
def _sign(self, msg_str):
"""Sign msg_str and return the detached signature."""
# _openpgp_mangle_for_signature modifies msg as a side effect.
sig_msg = rfc3156.PGPSignature(
self._sign(gpg, _openpgp_mangle_for_signature(msg)))
if self.secret_key.is_protected:
with self.secret_key.unlock(self.passphrase) as unlocked:
return str(unlocked.sign(msg_str))
container.attach(msg)
container.attach(sig_msg)
return container
return str(self.secret_key.sign(msg_str))
def encrypt_message(self, msg, public_key):
"""Encrypt msg to public_key.
def sign_message(self, msg):
"""Sign the given message.
Build and returns a multipart/signed MIME message.
Args:
msg (email.Message) message to sign
public_key (str) public key of the recipient
Returns:
An email.Message object.
"""
with self._sane_gpg() as gpg:
return self._encrypt_message(gpg, msg, public_key)
container = rfc3156.MultipartSigned('application/pgp-signature',
'pgp-' + DIGEST_ALGO.lower())
# _openpgp_mangle_for_signature modifies msg as a side effect.
sig_msg = rfc3156.PGPSignature(
self._sign(_openpgp_mangle_for_signature(msg)))
container.attach(msg)
container.attach(sig_msg)
return container
def sign_and_encrypt_message(self, msg, public_key):
"""Sign and encrypt a message to public_key.
......@@ -170,19 +140,7 @@ default-key %s\n
Returns:
An email.Message object.
"""
with self._sane_gpg() as gpg:
return self._encrypt_message(gpg, self._sign_message(gpg, msg), public_key)
def sign_message(self, msg):
"""Sign the given message.
Args:
msg (email.Message) message to sign
Returns:
An email.Message object.
"""
with self._sane_gpg(self.public_keyring) as gpg:
return self._sign_message(gpg, msg)
return self.encrypt_message(self.sign_message(msg), public_key)
def _openpgp_mangle_for_signature(msg):
......@@ -200,76 +158,33 @@ def _openpgp_mangle_for_signature(msg):
g = rfc3156.RFC3156CompliantGenerator(
fp, mangle_from_=False, maxheaderlen=76)
g.flatten(msg)
s = re.sub('\r?\n', '\r\n', fp.getvalue())
s = re.sub(r'\r?\n', '\r\n', fp.getvalue())
if msg.is_multipart():
if not s.endswith('\r\n'):
s += '\r\n'
return s
def _default_gpghome():
if 'GNUPGHOME' in os.environ:
return os.environ['GNUPGHOME']
return os.path.expanduser('~/.gnupg')
def _default_gpgbinary():
return os.getenv('GNUPG')
def parse_public_key(public_key):
"""Parses a PGP public key from the byte string `public_key`.
The input is expected to contain a single PGP public key, either in ASCII-armored format or
not. The function will return a pgpy.PGPKey instance or raise an Error (possibly with a
descriptive message) if there was an error.
"""
@contextlib.contextmanager
def _temp_dir():
"""Create a temporary directory, as a context manager."""
tmpdir = tempfile.mkdtemp()
try:
yield tmpdir
finally:
shutil.rmtree(tmpdir, ignore_errors=True)
pubkey, others = pgpy.PGPKey.from_blob(public_key)
except (TypeError, ValueError, pgpy.errors.PGPError):
raise Error("This is not a PGP key")
if not pubkey.is_public:
raise Error("This is not a PGP public key")
def parse_public_key(public_key):
"""Parses a PGP public key.
if len(others.keys()) > 1:
raise Error("More than one PGP public key found")
The input is expected to contain a single PGP public key, either
in ASCII-armored format or not. The function will return the key
fingerprint, or raise an Error (possibly with a descriptive
message) if there was an error.
if pubkey.is_expired:
raise Error("Expired PGP public key")
"""
with _temp_dir() as tmpdir:
# Create empty pubrings.
for p in 'pubring.gpg', 'secring.gpg':
with open(os.path.join(tmpdir, p), 'w'):
pass
gpg = gnupg.GPG(gpgbinary=_default_gpgbinary(),
gnupghome=tmpdir)
gpg.encoding = 'utf-8'
return _import_one_key(gpg, public_key)
def _import_one_key(gpg, public_key):
"""Import a single PGP key and return its fingerprint."""
import_result = gpg.import_keys(public_key)
if not import_result:
raise Error('Generic GPG import error')
# Error handling is conservative: we are looking for exactly
# one new key. We also want to report meaningful error
# messages, so we are just going to bail out on the first bad
# status returned by gpg.
if import_result.count != 1:
raise Error('more than one public key found in input')
fp = None
for res in import_result.results:
if "Entirely new key" not in res["text"]:
raise Error('no valid keys found in input: %r' % res['text'])
if not res['fingerprint']:
continue
if fp:
raise Error('more than one public key found in input')
fp = res['fingerprint']
if not fp:
raise Error('no valid keys found in input')
return fp
return pubkey
import os
import shutil
import tempfile
import unittest
import smtplib
from email.mime.image import MIMEImage
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
import pgpy
import pytest
import pgp_mime_lib
import logging
logging.getLogger('gnupg').setLevel(logging.DEBUG)
logging.basicConfig()
fixtures_dir = os.path.join(os.path.dirname(__file__), 'fixtures')
secret_key_dir = os.path.join(fixtures_dir, "gnupghome/private-keys-v1.d")
class TestBase(unittest.TestCase):
def setUp(self):
self.dir = tempfile.mkdtemp()
# Load our test secret key.
self.public_keyring = os.path.join(
fixtures_dir, 'gnupghome/pubring.gpg')
self.secret_key_dir = secret_key_dir
self.secret_key = '0x56A1E35992BCB5CF'
def tearDown(self):
shutil.rmtree(self.dir)
def _gpg(self):
return pgp_mime_lib.GPG(self.secret_key, self.public_keyring, self.secret_key_dir)
class TestSign(TestBase):
def test_sign_ok(self):
result = self._gpg().sign_message(MIMEText('test message'))
self.assertTrue(result)
class TestEncrypt(TestBase):
def test_encrypt_and_sign_ok(self):
with open(os.path.join(fixtures_dir, 'pubkey1.asc')) as fd:
pubkey = fd.read()
msg = MIMEText('test message')
result = self._gpg().sign_and_encrypt_message(msg, pubkey)
self.assertTrue(result)
def test_encrypt_and_sign_ok_v2key(self):
with open(os.path.join(fixtures_dir, 'pubkey2.asc')) as fd:
pubkey = fd.read()
msg = MIMEText('test message')
result = self._gpg().sign_and_encrypt_message(msg, pubkey)
self.assertTrue(result)
def test_encrypt_fails_with_revoked_public_key(self):
with open(os.path.join(fixtures_dir, 'revoked.asc')) as fd:
pubkey = fd.read()
self.assertRaises(
pgp_mime_lib.Error,
self._gpg().encrypt_message, MIMEText('test_message'), pubkey)
def test_encrypt_fails_with_more_than_one_public_key(self):
with open(os.path.join(fixtures_dir, 'twokeys.asc')) as fd:
pubkey = fd.read()
self.assertRaises(
pgp_mime_lib.Error,
self._gpg().encrypt_message, MIMEText('test_message'), pubkey)
def test_encrypt_fails_with_secret_key(self):
# People will do this!
with open(os.path.join(fixtures_dir, 'secretkey.asc')) as fd:
pubkey = fd.read()
self.assertRaises(
pgp_mime_lib.Error,
self._gpg().encrypt_message, MIMEText('test_message'), pubkey)
def test_encrypt_fails_with_bad_public_key(self):
self.assertRaises(
pgp_mime_lib.Error,
self._gpg().encrypt_message, MIMEText('test_message'),
'this is not a pgp key')
def test_encrypt_fails_with_our_own_public_key(self):
# Being fed our own public key is pretty much the only way to
# get a 0 imported keys result from gpg.
with open(os.path.join(fixtures_dir, '92BCB5CF.asc')) as fd:
pubkey = fd.read()
self.assertRaises(
pgp_mime_lib.Error,
self._gpg().encrypt_message, MIMEText('test_message'),
pubkey)
def test_encrypt_and_sign_send_via_smtp(self):
# Send an email to yourself if you want to manually check the
# results, by defining some environment variables.
try:
smtp_server = os.environ['SMTP_SERVER']
smtp_user = os.environ['SMTP_USER']
smtp_password = os.environ['SMTP_PASSWORD']
smtp_recip = os.environ['SMTP_RECIPIENT']
smtp_pubkey = os.environ['SMTP_PUBLIC_KEY']
except KeyError:
raise unittest.SkipTest()
with open(smtp_pubkey) as fd:
pubkey = fd.read()
msg = MIMEText('test message')
result = self._gpg().sign_and_encrypt_message(msg, pubkey)
result['To'] = smtp_recip
result['Subject'] = 'test output'
s = smtplib.SMTP_SSL(smtp_server, 465)
s.login(smtp_user, smtp_password)
s.sendmail(smtp_recip, [smtp_recip], result.as_string(unixfrom=False))
s.quit()
class TestParsePublicKey(TestBase):
def test_parse_public_key_ok(self):
with open(os.path.join(fixtures_dir, 'pubkey1.asc')) as fd:
pubkey = fd.read()
result = pgp_mime_lib.parse_public_key(pubkey)
self.assertTrue(result)
def test_parse_public_key_fails_with_bad_public_key(self):
self.assertRaises(
pgp_mime_lib.Error, pgp_mime_lib.parse_public_key,
'this is not a pgp key')
def test_parse_public_key_fails_with_empty_data(self):
self.assertRaises(
pgp_mime_lib.Error, pgp_mime_lib.parse_public_key,
'')
def test_parse_public_key_fails_with_empty_pgp_key_block(self):
self.assertRaises(
pgp_mime_lib.Error, pgp_mime_lib.parse_public_key,
'''
def load_pubkey(filename):
"""Load a PGP public key from the fixtures directory"""
with open(os.path.join(fixtures_dir, filename), 'rb') as fd:
return fd.read()
@pytest.fixture
def gpg():
"""Provide a GPG instance preloaded with test keys as a fixture"""
public_keyring = os.path.join(fixtures_dir, 'gnupghome/pubring.gpg')
secret_key_file = os.path.join(fixtures_dir, 'gnupghome/secring.gpg')
secret_key = '0x56A1E35992BCB5CF'
return pgp_mime_lib.GPG(secret_key, public_keyring, secret_key_file)
def test_sign_and_verify(gpg):
"""Sign a message with our own private key; verify the signature"""
text = "test message"
result = gpg.sign_message(MIMEText(text))
assert result.is_multipart() is True
cleartext = None
for part in result.walk():
if part.get_content_type() == "text/plain":
cleartext = pgp_mime_lib._openpgp_mangle_for_signature(part)
if part.get_content_type() == "application/pgp-signature":
sig = pgpy.PGPSignature.from_blob(part.get_payload(decode=True))
assert bool(gpg.public_key.verify(cleartext, sig)) is True
def test_encrypt_and_sign_ok(gpg):
"""Sign and encrypt a message; ensure that is encrypted"""
pubkey = load_pubkey("pubkey1.asc")
msg = MIMEText('test message')
result = gpg.sign_and_encrypt_message(msg, pubkey)
assert result.is_multipart() is True
for part in result.walk():
content_type = part.get_content_type()
if content_type == "application/octet-stream":
encrypted = pgpy.PGPMessage.from_blob(part.get_payload(decode=True))
# at least ensure that we can load the message as a valid PGP message
assert encrypted.is_encrypted is True
def test_encrypt_and_sign_ok_v2key(gpg):
"""Sign and encrypt a message with a v2 key; ensure that is encrypted"""
pubkey = load_pubkey('pubkey2.asc')
msg = MIMEText('test message')
result = gpg.sign_and_encrypt_message(msg, pubkey)
assert result.is_multipart() is True
for part in result.walk():
content_type = part.get_content_type()
if content_type == "application/octet-stream":
encrypted = pgpy.PGPMessage.from_blob(part.get_payload(decode=True))
# at least ensure that we can load the message as a valid PGP message
assert encrypted.is_encrypted is True
def test_encrypt_fails_with_revoked_public_key(gpg):
pubkey = load_pubkey('revoked.asc')
with pytest.raises(pgp_mime_lib.Error):
gpg.encrypt_message(MIMEText('test_message'), pubkey)
def test_encrypt_fails_with_more_than_one_public_key(gpg):
pubkey = load_pubkey("twokeys.asc")
with pytest.raises(pgp_mime_lib.Error):
gpg.encrypt_message(MIMEText('test_message'), pubkey)
def test_encrypt_fails_with_secret_key(gpg):
# People will do this!
pubkey = load_pubkey("secretkey.asc")
with pytest.raises(pgp_mime_lib.Error):
gpg.encrypt_message(MIMEText('test_message'), pubkey)
def test_encrypt_fails_with_bad_public_key(gpg):
with pytest.raises(pgp_mime_lib.Error):
gpg.encrypt_message(
MIMEText('test_message'),
'this is not a pgp key'
)
@pytest.mark.skip("the pgpy implementation do not use a keyring at all")
def test_encrypt_fails_with_our_own_public_key(gpg):
# Being fed our own public key is pretty much the only way to
# get a 0 imported keys result from gpg.
pubkey = load_pubkey("92BCB5CF.asc")
with pytest.raises(pgp_mime_lib.Error):
gpg.encrypt_message(
MIMEText('test_message'),
pubkey
)
def test_encrypt_and_sign_send_via_smtp(gpg):
# Send an email to yourself if you want to manually check the
# results, by defining some environment variables.
try:
smtp_server = os.environ['SMTP_SERVER']
smtp_user = os.environ['SMTP_USER']
smtp_password = os.environ['SMTP_PASSWORD']
smtp_recip = os.environ['SMTP_RECIPIENT']
smtp_pubkey = os.environ['SMTP_PUBLIC_KEY']
except KeyError:
pytest.skip("SMTP test not configured.")
with open(smtp_pubkey) as fd:
pubkey = fd.read()
msg = MIMEText('test message')
result = gpg.sign_and_encrypt_message(msg, pubkey)
result['To'] = smtp_recip
result['Subject'] = 'test output'
s = smtplib.SMTP_SSL(smtp_server, 465)
s.login(smtp_user, smtp_password)
s.sendmail(smtp_recip, [smtp_recip], result.as_string(unixfrom=False))
s.quit()
def test_parse_public_key_ok():
pubkey = load_pubkey("pubkey1.asc")
result = pgp_mime_lib.parse_public_key(pubkey)
assert result.is_public is True
def test_parse_public_key_fails_with_bad_public_key():
with pytest.raises(pgp_mime_lib.Error):
pgp_mime_lib.parse_public_key('this is not a pgp key')
def test_parse_public_key_fails_with_empty_data():
with pytest.raises(pgp_mime_lib.Error):
pgp_mime_lib.parse_public_key('')
def test_parse_public_key_fails_with_empty_pgp_key_block():
with pytest.raises(pgp_mime_lib.Error):
pgp_mime_lib.parse_public_key('''
-----BEGIN PGP PUBLIC KEY BLOCK-----
Version: GnuPG v2
-----END PGP PUBLIC KEY BLOCK-----
''')
def test_parse_public_key_fails_with_revoked_public_key(self):
with open(os.path.join(fixtures_dir, 'revoked.asc')) as fd:
pubkey = fd.read()
self.assertRaises(
pgp_mime_lib.Error, pgp_mime_lib.parse_public_key,
pubkey)
def test_parse_public_key_fails_with_more_than_one_key(self):
with open(os.path.join(fixtures_dir, 'twokeys.asc')) as fd:
pubkey = fd.read()
self.assertRaises(
pgp_mime_lib.Error, pgp_mime_lib.parse_public_key,
pubkey)
def test_parse_public_key_fails_with_secret_key(self):
with open(os.path.join(fixtures_dir, 'secretkey.asc')) as fd:
pubkey = fd.read()
self.assertRaises(
pgp_mime_lib.Error, pgp_mime_lib.parse_public_key,
pubkey)
class TestMIME(TestBase):
def _encrypt(self, msg):
with open(os.path.join(fixtures_dir, 'pubkey1.asc')) as fd:
pubkey = fd.read()
result = self._gpg().sign_and_encrypt_message(msg, pubkey)
self.assertTrue(result)
def test_simple_text_message(self):
self._encrypt(MIMEText('test message'))
def test_base64_text_message(self):
self._encrypt(MIMEText(u'test message', 'plain', 'utf-8'))
def test_8bit_text_message(self):
m = MIMEText(u'test message')
m.replace_header('Content-Transfer-Encoding', '8bit')
self._encrypt(m)
def test_multipart_message_with_image(self):
msg = MIMEMultipart()
msg.preamble = 'This is a message with an image attachment'
msg.attach(MIMEImage('GIF89ZZ', 'gif'))
self._encrypt(msg)
def test_multipart_alternative(self):
msg = MIMEMultipart('alternative')
msg.attach(MIMEText('text version', 'plain'))
msg.attach(MIMEText('html version', 'html'))
self._encrypt(msg)
''')
def test_parse_public_key_fails_with_revoked_public_key():
pubkey = load_pubkey("revoked.asc")
with pytest.raises(pgp_mime_lib.Error):
pgp_mime_lib.parse_public_key(pubkey)
def test_parse_public_key_fails_with_more_than_one_key():
pubkey = load_pubkey("twokeys.asc")
with pytest.raises(pgp_mime_lib.Error):
pgp_mime_lib.parse_public_key(pubkey)
def test_parse_public_key_fails_with_secret_key():
pubkey = load_pubkey("secretkey.asc")
with pytest.raises(pgp_mime_lib.Error):
pgp_mime_lib.parse_public_key(pubkey)
@pytest.fixture
def encrypt_test_msg():
def _encrypt_test_msg(gpg, msg):
pubkey = load_pubkey("pubkey1.asc")
return gpg.sign_and_encrypt_message(msg, pubkey)
return _encrypt_test_msg
def test_simple_text_message(gpg, encrypt_test_msg):
result = encrypt_test_msg(gpg, MIMEText('test message'))
assert result.is_multipart() is True
for part in result.walk():
content_type = part.get_content_type()
if content_type == "application/octet-stream":
encrypted = pgpy.PGPMessage.from_blob(part.get_payload(decode=True))
# at least ensure that we can load the message as a valid PGP message
assert encrypted.is_encrypted is True
def test_base64_text_message(gpg, encrypt_test_msg):
result = encrypt_test_msg(gpg, MIMEText(u'test message', 'plain', 'utf-8'))
assert result.is_multipart() is True
for part in result.walk():
content_type = part.get_content_type()
if content_type == "application/octet-stream":
encrypted = pgpy.PGPMessage.from_blob(part.get_payload(decode=True))
# at least ensure that we can load the message as a valid PGP message
assert encrypted.is_encrypted is True
def test_8bit_text_message(gpg, encrypt_test_msg):
m = MIMEText(u'test message')
m.replace_header('Content-Transfer-Encoding', '8bit')
assert encrypt_test_msg(gpg, m) is not None
def test_multipart_message_with_image(gpg, encrypt_test_msg):
msg = MIMEMultipart()
msg.preamble = 'This is a message with an image attachment'
msg.attach(MIMEImage('GIF89ZZ', 'gif'))
assert encrypt_test_msg(gpg, msg) is not None
def test_multipart_alternative(gpg, encrypt_test_msg):
msg = MIMEMultipart('alternative')
msg.attach(MIMEText('text version', 'plain'))
msg.attach(MIMEText('html version', 'html'))
assert encrypt_test_msg(gpg, msg) is not None
[[package]]
category = "main"
description = "Fast ASN.1 parser and serializer with definitions for private keys, public keys, certificates, CRL, OCSP, CMS, PKCS#3, PKCS#7, PKCS#8, PKCS#12, PKCS#5, X.509 and TSP"
name = "asn1crypto"
optional = false
python-versions = "*"
version = "0.24.0"
[[package]]
category = "main"
description = "Foreign Function Interface for Python calling C code."
name = "cffi"
optional = false
python-versions = "*"
version = "1.12.1"
[package.dependencies]
pycparser = "*"
[[package]]
category = "main"
description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers."
name = "cryptography"
optional = false
python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*"
version = "2.5"
[package.dependencies]
asn1crypto = ">=0.21.0"
cffi = ">=1.8,<1.11.3 || >1.11.3"
six = ">=1.4.1"
[package.dependencies.enum34]
python = "<3"
version = "*"
[package.dependencies.ipaddress]
python = "<3"
version = "*"
[[package]]
category = "main"
description = "Python 3.4 Enum backported to 3.3, 3.2, 3.1, 2.7, 2.6, 2.5, and 2.4"
marker = "python_version < \"3\""
name = "enum34"
optional = false
python-versions = "*"
version = "1.1.6"
[[package]]
category = "dev"
description = "A platform independent file lock."
......@@ -6,13 +55,36 @@ optional = false
python-versions = "*"
version = "3.0.10"
[[package]]
category = "main"
description = "IPv4/IPv6 manipulation library"
marker = "python_version < \"3\""
name = "ipaddress"
optional = false
python-versions = "*"
version = "1.0.22"
[[package]]
category = "main"
description = "Pretty Good Privacy for Python"
name = "pgpy"
optional = false
python-versions = "*"
version = "0.4.3"
[package.dependencies]
cryptography = ">=1.1"
pyasn1 = "*"
singledispatch = "*"
six = ">=1.9.0"
[[package]]
category = "dev"
description = "plugin and hook calling mechanisms for python"
name = "pluggy"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
version = "0.8.1"
version = "0.9.0"
[[package]]
category = "dev"
......@@ -20,23 +92,42 @@ description = "library with cross-python path, ini-parsing, io, code, log facili
name = "py"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
version = "1.7.0"
version = "1.8.0"
[[package]]
category = "main"
description = "A wrapper for the Gnu Privacy Guard (GPG or GnuPG)"
name = "python-gnupg"
description = "ASN.1 types and codecs"
name = "pyasn1"
optional = false
python-versions = "*"
version = "0.4.4"
version = "0.4.5"
[[package]]
category = "dev"
category = "main"
description = "C parser in Python"
name = "pycparser"
optional = false
python-versions = "*"
version = "2.19"
[[package]]
category = "main"
description = "This library brings functools.singledispatch from Python 3.4 to Python 2.6-3.3."
name = "singledispatch"
optional = false
python-versions = "*"
version = "3.4.0.3"
[package.dependencies]
six = "*"
[[package]]
category = "main"
description = "Python 2 and 3 compatibility utilities"
name = "six"
optional = false
python-versions = ">=2.6, !=3.0.*, !=3.1.*"
version = "1.12.0"
python-versions = "*"
version = "1.10.0"
[[package]]
category = "dev"
......@@ -69,18 +160,26 @@ description = "Virtual Python Environment builder"
name = "virtualenv"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
version = "16.4.0"
version = "16.4.1"
[metadata]
content-hash = "28aa155b6cccb2f93f6e632be14599cf66f63da25a8598a423cab7aae6a6a9eb"
content-hash = "e10b8a2c076b0235fbf1a0fc20b8d5c721d998346a56e7b262af2296b5beb544"
python-versions = "~2.7 || ^3.5"
[metadata.hashes]
asn1crypto = ["2f1adbb7546ed199e3c90ef23ec95c5cf3585bac7d11fb7eb562a3fe89c64e87", "9d5c20441baf0cb60a4ac34cc447c6c189024b6b4c6cd7877034f4965c464e49"]
cffi = ["0b5f895714a7a9905148fc51978c62e8a6cbcace30904d39dcd0d9e2265bb2f6", "27cdc7ba35ee6aa443271d11583b50815c4bb52be89a909d0028e86c21961709", "2d4a38049ea93d5ce3c7659210393524c1efc3efafa151bd85d196fa98fce50a", "3262573d0d60fc6b9d0e0e6e666db0e5045cbe8a531779aa0deb3b425ec5a282", "358e96cfffc185ab8f6e7e425c7bb028931ed08d65402fbcf3f4e1bff6e66556", "37c7db824b5687fbd7ea5519acfd054c905951acc53503547c86be3db0580134", "39b9554dfe60f878e0c6ff8a460708db6e1b1c9cc6da2c74df2955adf83e355d", "42b96a77acf8b2d06821600fa87c208046decc13bd22a4a0e65c5c973443e0da", "5b37dde5035d3c219324cac0e69d96495970977f310b306fa2df5910e1f329a1", "5d35819f5566d0dd254f273d60cf4a2dcdd3ae3003dfd412d40b3fe8ffd87509", "5df73aa465e53549bd03c819c1bc69fb85529a5e1a693b7b6cb64408dd3970d1", "7075b361f7a4d0d4165439992d0b8a3cdfad1f302bf246ed9308a2e33b046bd3", "7678b5a667b0381c173abe530d7bdb0e6e3b98e062490618f04b80ca62686d96", "7dfd996192ff8a535458c17f22ff5eb78b83504c34d10eefac0c77b1322609e2", "8a3be5d31d02c60f84c4fd4c98c5e3a97b49f32e16861367f67c49425f955b28", "9812e53369c469506b123aee9dcb56d50c82fad60c5df87feb5ff59af5b5f55c", "9b6f7ba4e78c52c1a291d0c0c0bd745d19adde1a9e1c03cb899f0c6efd6f8033", "a85bc1d7c3bba89b3d8c892bc0458de504f8b3bcca18892e6ed15b5f7a52ad9d", "aa6b9c843ad645ebb12616de848cc4e25a40f633ccc293c3c9fe34107c02c2ea", "bae1aa56ee00746798beafe486daa7cfb586cd395c6ce822ba3068e48d761bc0", "bae96e26510e4825d5910a196bf6b5a11a18b87d9278db6d08413be8ea799469", "bd78df3b594013b227bf31d0301566dc50ba6f40df38a70ded731d5a8f2cb071", "c2711197154f46d06f73542c539a0ff5411f1951fab391e0a4ac8359badef719", "d998c20e3deed234fca993fd6c8314cb7cbfda05fd170f1bd75bb5d7421c3c5a", "df4f840d77d9e37136f8e6b432fecc9d6b8730f18f896e90628712c793466ce6", "f5653c2581acb038319e6705d4e3593677676df14b112f13e0b5b44b6a18df1a", "f7c7aa485a2e2250d455148470ffd0195eecc3d845122635202d7467d6f7b4cf", "f9e2c66a6493147de835f207f198540a56b26745ce4f272fbc7c2f2cfebeb729"]
cryptography = ["05b3ded5e88747d28ee3ef493f2b92cbb947c1e45cf98cfef22e6d38bb67d4af", "06826e7f72d1770e186e9c90e76b4f84d90cdb917b47ff88d8dc59a7b10e2b1e", "08b753df3672b7066e74376f42ce8fc4683e4fd1358d34c80f502e939ee944d2", "2cd29bd1911782baaee890544c653bb03ec7d95ebeb144d714b0f5c33deb55c7", "31e5637e9036d966824edaa91bf0aa39dc6f525a1c599f39fd5c50340264e079", "42fad67d7072216a49e34f923d8cbda9edacbf6633b19a79655e88a1b4857063", "4946b67235b9d2ea7d31307be9d5ad5959d6c4a8f98f900157b47abddf698401", "522fdb2809603ee97a4d0ef2f8d617bc791eb483313ba307cb9c0a773e5e5695", "6f841c7272645dd7c65b07b7108adfa8af0aaea57f27b7f59e01d41f75444c85", "7d335e35306af5b9bc0560ca39f740dfc8def72749645e193dd35be11fb323b3", "8504661ffe324837f5c4607347eeee4cf0fcad689163c6e9c8d3b18cf1f4a4ad", "9260b201ce584d7825d900c88700aa0bd6b40d4ebac7b213857bd2babee9dbca", "9a30384cc402eac099210ab9b8801b2ae21e591831253883decdb4513b77a3cd", "9e29af877c29338f0cab5f049ccc8bd3ead289a557f144376c4fbc7d1b98914f", "ab50da871bc109b2d9389259aac269dd1b7c7413ee02d06fe4e486ed26882159", "b13c80b877e73bcb6f012813c6f4a9334fcf4b0e96681c5a15dac578f2eedfa0", "bfe66b577a7118e05b04141f0f1ed0959552d45672aa7ecb3d91e319d846001e", "e091bd424567efa4b9d94287a952597c05d22155a13716bf5f9f746b9dc906d3", "fa2b38c8519c5a3aa6e2b4e1cf1a549b54acda6adb25397ff542068e73d1ed00"]
enum34 = ["2d81cbbe0e73112bdfe6ef8576f2238f2ba27dd0d55752a776c41d38b7da2850", "644837f692e5f550741432dd3f223bbb9852018674981b1664e5dc339387588a", "6bd0f6ad48ec2aa117d3d141940d484deccda84d4fcd884f5c3d93c23ecd8c79", "8ad8c4783bf61ded74527bffb48ed9b54166685e4230386a9ed9b1279e2df5b1"]
filelock = ["b8d5ca5ca1c815e1574aee746650ea7301de63d87935b3463d26368b76e31633", "d610c1bb404daf85976d7a82eb2ada120f04671007266b708606565dd03b5be6"]
pluggy = ["8ddc32f03971bfdf900a81961a48ccf2fb677cf7715108f85295c67405798616", "980710797ff6a041e9a73a5787804f848996ecaa6f8a1b1e08224a5894f2074a"]
py = ["bf92637198836372b520efcba9e020c330123be8ce527e535d185ed4b6f45694", "e76826342cefe3c3d5f7e8ee4316b80d1dd8a300781612ddbc765c17ba25a6c6"]
python-gnupg = ["45daf020b370bda13a1429c859fcdff0b766c0576844211446f9266cae97fb0e", "85c231850a0275c9722f06e34b45a22510b83a6a6e88f93b5ae32ba04c95056c"]
six = ["3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", "d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73"]
ipaddress = ["64b28eec5e78e7510698f6d4da08800a5c575caa4a286c93d651c5d3ff7b6794", "b146c751ea45cad6188dd6cf2d9b757f6f4f8d6ffb96a023e6f2e26eea02a72c"]
pgpy = ["04412dddd6882ac0c5d5daf4326c28d481421851a68e25e7ac8e06cc9dc2b902"]
pluggy = ["19ecf9ce9db2fce065a7a0586e07cfb4ac8614fe96edf628a264b1c70116cf8f", "84d306a647cc805219916e62aab89caa97a33a1dd8c342e87a37f91073cd4746"]
py = ["64f65755aee5b381cea27766a3a147c3f15b9b6b9ac88676de66ba2ae36793fa", "dc639b046a6e2cff5bbe40194ad65936d6ba360b52b3c3fe1d08a82dd50b5e53"]
pyasn1 = ["061442c60842f6d11051d4fdae9bc197b64bd41573a12234a753a0cb80b4f30b", "0ee2449bf4c4e535823acc25624c45a8b454f328d59d3f3eeb82d3567100b9bd", "5f9fb05c33e53b9a6ee3b1ed1d292043f83df465852bec876e93b47fd2df7eed", "65201d28e081f690a32401e6253cca4449ccacc8f3988e811fae66bd822910ee", "79b336b073a52fa3c3d8728e78fa56b7d03138ef59f44084de5f39650265b5ff", "8ec20f61483764de281e0b4aba7d12716189700debcfa9e7935780850bf527f3", "9458d0273f95d035de4c0d5e0643f25daba330582cc71bb554fe6969c015042a", "98d97a1833a29ca61cd04a60414def8f02f406d732f9f0bcb49f769faff1b699", "b00d7bfb6603517e189d1ad76967c7e805139f63e43096e5f871d1277f50aea5", "b06c0cfd708b806ea025426aace45551f91ea7f557e0c2d4fbd9a4b346873ce0", "d14d05984581770333731690f5453efd4b82e1e5d824a1d7976b868a2e5c38e8", "da2420fe13a9452d8ae97a0e478adde1dee153b11ba832a95b223a2ba01c10f7", "da6b43a8c9ae93bc80e2739efb38cc776ba74a886e3e9318d65fe81a8b8a2c6e"]
pycparser = ["a988718abfad80b6b157acce7bf130a30876d27603738ac39f140993246b25b3"]
singledispatch = ["5b06af87df13818d14f08a028e42f566640aef80805c3b50c5056b086e3c2b9c", "833b46966687b3de7f438c761ac475213e53b306740f1abfaa86e1d1aae56aa8"]
six = ["0ff78c403d9bccf5a425a6d31a12aa6b47f1c21ca4dc2573a7e2f32a97335eb1", "105f8d68616f8248e24bf0e9372ef04d3cc10104f1980f54d57b2ce73a5ad56a"]
toml = ["229f81c57791a41d65e399fc06bf0848bab550a9dfd5ed66df18ce5f05e73d5c", "235682dd292d5899d361a811df37e04a8828a5b1da3115886b73cf81ebc9100e", "f1db651f9657708513243e61e6cc67d101a39bad662eaa9b5546f789338e07a3"]
tox = ["04f8f1aa05de8e76d7a266ccd14e0d665d429977cd42123bc38efa9b59964e9e", "25ef928babe88c71e3ed3af0c464d1160b01fca2dd1870a5bb26c2dea61a17fc"]
virtualenv = ["8b9abfc51c38b70f61634bf265e5beacf6fae11fc25d355d1871f49b8e45f0db", "cceab52aa7d4df1e1871a70236eb2b89fcfe29b6b43510d9738689787c513261"]
virtualenv = ["5a3ecdfbde67a4a3b3111301c4d64a5b71cf862c8c42958d30cf3253df1f29dd", "dffd40d19ab0168c02cf936de59590a3c0f2c8c4a36f363fcf3dae18728dc94e"]
......@@ -6,7 +6,8 @@ authors = ["AI <info@autistici.org>"]
[tool.poetry.dependencies]
python = "~2.7 || ^3.5"
python-gnupg = "^0.4.4"
pgpy = "^0.4.3"
six = { version = "1.10.0", python = "~2.7" }
[tool.poetry.dev-dependencies]
tox = "^3.7"
......
......@@ -10,7 +10,8 @@ setup(
author_email="info@autistici.org",
url="https://git.autistici.org/ai/pgp-mime-lib.git",
install_requires=[
"python-gnupg",
"pgpy",
"six==1.10.0;python_version<'3.4'",
],
packages=find_packages(),
zip_safe=False,
......