Commit a7f9c02a authored by sand's avatar sand

Merge branch 'py3' into 'master'

Py3

See merge request !1
parents 25e74028 82e0b917
Pipeline #2259 passed with stage
in 1 minute and 7 seconds
......@@ -2,3 +2,4 @@
*.pyc
.coverage
.tox
/htmlcov/
image: "ai/test:base"
run_tests:
script: "tox -e py27"
script: "tox"
# Package to send PGP-encrypted emails to users using ephemeral key
# storage (so, no keyserver interaction, no keyring management).
from __future__ import absolute_import
import contextlib
import gnupg
import os
import re
import shutil
import tempfile
from cStringIO import StringIO
from email import encoders
from email.mime.multipart import MIMEMultipart
from email.mime.application import MIMEApplication
from pgp_mime_lib import rfc3156
import gnupg
from ._compat import StringIO
from . import rfc3156
# Digest algorithm to use in signatures.
......@@ -33,20 +34,20 @@ class GPG(object):
public keyring which is deleted afterwards.
"""
def __init__(self, key_id, public_keyring=None, secret_keyring=None,
def __init__(self, key_id, public_keyring=None, secret_key_dir=None,
passphrase=None):
if not public_keyring:
public_keyring = os.path.join(_default_gpghome(), 'pubring.gpg')
if not secret_keyring:
secret_keyring = os.path.join(_default_gpghome(), 'secring.gpg')
for f in (public_keyring, secret_keyring):
if not secret_key_dir:
secret_key_dir = os.path.join(_default_gpghome(), 'private-keys-v1.d')
for f in (public_keyring, secret_key_dir):
if not os.path.exists(f):
raise Error('no such file or directory: %s' % f)
self.public_keyring = public_keyring
self.secret_keyring = secret_keyring
self.secret_key_dir = secret_key_dir
self.secret_key_id = key_id
self.passphrase = passphrase
......@@ -67,7 +68,8 @@ class GPG(object):
if not public_keyring:
public_keyring = os.path.join(tmpdir, 'pubring.gpg')
shutil.copy(self.public_keyring, public_keyring)
os.chmod(public_keyring, 0600)
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.
......@@ -77,14 +79,18 @@ 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
throw-keyids
default-key %s\n
''' % self.secret_key_id)
''' % 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(binary=_default_gpgbinary(),
homedir=tmpdir,
keyring=public_keyring,
secring=self.secret_keyring)
gpg = gnupg.GPG(gpgbinary=_default_gpgbinary(),
gnupghome=tmpdir,
keyring=public_keyring)
# XXX this below might be wrong.
gpg.encoding = 'utf-8'
yield gpg
......@@ -96,15 +102,14 @@ default-key %s\n
# 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,
throw_keyids=True, always_trust=True)
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, digest_algo=DIGEST_ALGO,
clearsign=False, detach=True, binary=False,
s = gpg.sign(msg_str, clearsign=False, detach=True, binary=False,
passphrase=self.passphrase)
if not s:
raise Error('gpg --sign failed (no result)')
......@@ -133,7 +138,8 @@ default-key %s\n
def _sign_message(self, gpg, msg):
"""Build a multipart/signed MIME message."""
container = rfc3156.MultipartSigned('application/pgp-signature', 'pgp-' + DIGEST_ALGO.lower())
container = rfc3156.MultipartSigned('application/pgp-signature',
'pgp-' + DIGEST_ALGO.lower())
# _openpgp_mangle_for_signature modifies msg as a side effect.
sig_msg = rfc3156.PGPSignature(
......@@ -177,7 +183,7 @@ default-key %s\n
"""
with self._sane_gpg(self.public_keyring) as gpg:
return self._sign_message(gpg, msg)
def _openpgp_mangle_for_signature(msg):
"""Return a message suitable for signing.
......@@ -233,10 +239,10 @@ def parse_public_key(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') as fd:
with open(os.path.join(tmpdir, p), 'w'):
pass
gpg = gnupg.GPG(binary=_default_gpgbinary(),
homedir=tmpdir)
gpg = gnupg.GPG(gpgbinary=_default_gpgbinary(),
gnupghome=tmpdir)
gpg.encoding = 'utf-8'
return _import_one_key(gpg, public_key)
......@@ -251,10 +257,13 @@ def _import_one_key(gpg, public_key):
# 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 res['status'].strip() != 'Entirely new key':
raise Error('no valid keys found in input: %s' % res['status'])
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:
......
import sys
import base64
PY2 = sys.version_info[0] == 2
if not PY2:
from io import StringIO
string_types = (str,)
else:
from cStringIO import StringIO
string_types = (str, unicode)
def base64_encodestring(data):
if PY2:
return base64.encodestring(data)
return base64.encodebytes(data)
......@@ -20,9 +20,7 @@
"""
Implements RFC 3156: MIME Security with OpenPGP.
"""
import base64
from StringIO import StringIO
from __future__ import absolute_import
from email.mime.application import MIMEApplication
from email.mime.multipart import MIMEMultipart
......@@ -34,6 +32,8 @@ from email.generator import (
_make_boundary,
)
from ._compat import StringIO, string_types, base64_encodestring
#
# A generator that solves http://bugs.python.org/issue14983
......@@ -66,7 +66,7 @@ class RFC3156CompliantGenerator(Generator):
subparts = msg.get_payload()
if subparts is None:
subparts = []
elif isinstance(subparts, basestring):
elif isinstance(subparts, string_types):
# e.g. a non-strict parse of a message with no starting boundary.
self._fp.write(subparts)
return
......@@ -120,18 +120,18 @@ class RFC3156CompliantGenerator(Generator):
# solution, but a bit modified.
#
def _bencode(s):
def _bencode(data):
"""
Encode C{s} in base64.
Encode C{data} in base64.
:param s: The string to be encoded.
:type s: str
:param data: The string to be encoded.
:type data: bytes
"""
# We can't quite use base64.encodestring() since it tacks on a "courtesy
# newline". Blech!
if not s:
return s
value = base64.encodestring(s)
if not data:
return data
value = base64_encodestring(data)
return value[:-1]
......
......@@ -3,7 +3,6 @@ import shutil
import tempfile
import unittest
import smtplib
import gnupg
from email.mime.image import MIMEImage
from email.mime.multipart import MIMEMultipart
......@@ -11,8 +10,13 @@ from email.mime.text import MIMEText
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):
......@@ -22,16 +26,15 @@ class TestBase(unittest.TestCase):
# Load our test secret key.
self.public_keyring = os.path.join(
fixtures_dir, 'secret/pubring.gpg')
self.secret_keyring = os.path.join(
fixtures_dir, 'secret/secring.gpg')
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_keyring)
return pgp_mime_lib.GPG(self.secret_key, self.public_keyring, self.secret_key_dir)
class TestSign(TestBase):
......@@ -188,7 +191,7 @@ class TestMIME(TestBase):
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'
......
[[package]]
category = "dev"
description = "A platform independent file lock."
name = "filelock"
optional = false
python-versions = "*"
version = "3.0.10"
[[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"
[[package]]
category = "dev"
description = "library with cross-python path, ini-parsing, io, code, log facilities"
name = "py"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
version = "1.7.0"
[[package]]
category = "main"
description = "A wrapper for the Gnu Privacy Guard (GPG or GnuPG)"
name = "python-gnupg"
optional = false
python-versions = "*"
version = "0.4.4"
[[package]]
category = "dev"
description = "Python 2 and 3 compatibility utilities"
name = "six"
optional = false
python-versions = ">=2.6, !=3.0.*, !=3.1.*"
version = "1.12.0"
[[package]]
category = "dev"
description = "Python Library for Tom's Obvious, Minimal Language"
name = "toml"
optional = false
python-versions = "*"
version = "0.10.0"
[[package]]
category = "dev"
description = "virtualenv-based automation of test activities"
name = "tox"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
version = "3.7.0"
[package.dependencies]
filelock = ">=3.0.0,<4"
pluggy = ">=0.3.0,<1"
py = ">=1.4.17,<2"
setuptools = ">=30.0.0"
six = ">=1.0.0,<2"
toml = ">=0.9.4"
virtualenv = ">=1.11.2"
[[package]]
category = "dev"
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"
[metadata]
content-hash = "28aa155b6cccb2f93f6e632be14599cf66f63da25a8598a423cab7aae6a6a9eb"
python-versions = "~2.7 || ^3.5"
[metadata.hashes]
filelock = ["b8d5ca5ca1c815e1574aee746650ea7301de63d87935b3463d26368b76e31633", "d610c1bb404daf85976d7a82eb2ada120f04671007266b708606565dd03b5be6"]
pluggy = ["8ddc32f03971bfdf900a81961a48ccf2fb677cf7715108f85295c67405798616", "980710797ff6a041e9a73a5787804f848996ecaa6f8a1b1e08224a5894f2074a"]
py = ["bf92637198836372b520efcba9e020c330123be8ce527e535d185ed4b6f45694", "e76826342cefe3c3d5f7e8ee4316b80d1dd8a300781612ddbc765c17ba25a6c6"]
python-gnupg = ["45daf020b370bda13a1429c859fcdff0b766c0576844211446f9266cae97fb0e", "85c231850a0275c9722f06e34b45a22510b83a6a6e88f93b5ae32ba04c95056c"]
six = ["3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", "d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73"]
toml = ["229f81c57791a41d65e399fc06bf0848bab550a9dfd5ed66df18ce5f05e73d5c", "235682dd292d5899d361a811df37e04a8828a5b1da3115886b73cf81ebc9100e", "f1db651f9657708513243e61e6cc67d101a39bad662eaa9b5546f789338e07a3"]
tox = ["04f8f1aa05de8e76d7a266ccd14e0d665d429977cd42123bc38efa9b59964e9e", "25ef928babe88c71e3ed3af0c464d1160b01fca2dd1870a5bb26c2dea61a17fc"]
virtualenv = ["8b9abfc51c38b70f61634bf265e5beacf6fae11fc25d355d1871f49b8e45f0db", "cceab52aa7d4df1e1871a70236eb2b89fcfe29b6b43510d9738689787c513261"]
[tool.poetry]
name = "pgp_mime_lib"
version = "0.1.1"
description = "Create PGP/MIME emails."
authors = ["AI <info@autistici.org>"]
[tool.poetry.dependencies]
python = "~2.7 || ^3.5"
python-gnupg = "^0.4.4"
[tool.poetry.dev-dependencies]
tox = "^3.7"
[build-system]
requires = ["poetry>=0.12"]
build-backend = "poetry.masonry.api"
......@@ -9,11 +9,9 @@ setup(
author="Autistici/Inventati",
author_email="info@autistici.org",
url="https://git.autistici.org/ai/pgp-mime-lib.git",
install_requires=["gnupg"],
setup_requires=[],
zip_safe=False,
install_requires=[
"python-gnupg",
],
packages=find_packages(),
package_data={},
entry_points={},
zip_safe=False,
)
[tox]
envlist = py27
envlist = py27, py3, flake8
[testenv]
deps=
nose
coverage
commands=
/usr/bin/env LANG=en_US.UTF-8 \
nosetests -v \
--with-coverage --cover-package=pgp_mime_lib \
--cover-erase --cover-html --cover-html-dir=htmlcov \
[]
pytest
pytest-cov
setenv =
LANG=en_US.UTF-8
GNUPG=/usr/bin/gpg
commands =
# run pytest with -x --pdb to drop into pdb at the first failure
pytest --cov=pgp_mime_lib --cov-report html:htmlcov --cov-report term {posargs}
[testenv:flake8]
basepython = python3
deps =
flake8
flake8-builtins
flake8-bugbear
skip_install = true
commands = flake8 pgp_mime_lib/ setup.py
[flake8]
exclude = .tox/*, .git/*, build/*, pgp_mime_lib/_compat.py
max-line-length = 100
select =
E,F,W,C90,B,B902,C
[pytest]
norecursedirs = .tox
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment