Commit 78a371b5 authored by sand's avatar sand

migrate from python-gnupg to pgpy

parent bb7b0f81
......@@ -11,6 +11,8 @@ import tempfile
from email import encoders
from email.mime.application import MIMEApplication
import pgpy
from pgpy.constants import SymmetricKeyAlgorithm
import gnupg
from ._compat import StringIO
......@@ -35,21 +37,23 @@ 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)
@contextlib.contextmanager
def _sane_gpg(self, public_keyring=None):
......@@ -83,9 +87,6 @@ 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,
......@@ -94,35 +95,41 @@ default-key %s\n
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):
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.
"""
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
# accetta testo, ritorna la signatura binary
def _sign(self, 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):
print("we're about to sign %r" % msg_str)
if self.secret_key.is_protected:
with self.secret_key.unlock(self.passphrase) as unlocked:
return str(unlocked.sign(msg_str))
return str(self.secret_key.sign(msg_str))
def _encrypt_message(self, 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,14 +143,15 @@ default-key %s\n
return container
def _sign_message(self, gpg, msg):
def _sign_message(self, msg):
"""Build a multipart/signed MIME message."""
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(gpg, _openpgp_mangle_for_signature(msg)))
self._sign(_openpgp_mangle_for_signature(msg)))
container.attach(msg)
container.attach(sig_msg)
......@@ -158,8 +166,7 @@ default-key %s\n
Returns:
An email.Message object.
"""
with self._sane_gpg() as gpg:
return self._encrypt_message(gpg, msg, public_key)
return self._encrypt_message(msg, public_key)
def sign_and_encrypt_message(self, msg, public_key):
"""Sign and encrypt a message to public_key.
......@@ -170,8 +177,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)
return self._encrypt_message(self._sign_message(msg), public_key)
def sign_message(self, msg):
"""Sign the given message.
......@@ -181,8 +187,7 @@ default-key %s\n
Returns:
An email.Message object.
"""
with self._sane_gpg(self.public_keyring) as gpg:
return self._sign_message(gpg, msg)
return self._sign_message(msg)
def _openpgp_mangle_for_signature(msg):
......@@ -213,10 +218,6 @@ def _default_gpghome():
return os.path.expanduser('~/.gnupg')
def _default_gpgbinary():
return os.getenv('GNUPG')
@contextlib.contextmanager
def _temp_dir():
"""Create a temporary directory, as a context manager."""
......@@ -236,40 +237,18 @@ def parse_public_key(public_key):
message) if there was an error.
"""
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
try:
pubkey, others = pgpy.PGPKey.from_blob(public_key)
except Exception as exc:
raise Error("this is not a pgp key")
if not pubkey.is_public:
raise Error("This is not a pgp public key")
if len(others.keys()) > 1:
raise Error("More than one public key found")
if pubkey.is_expired:
raise Error("Expired public key")
return pubkey
This diff is collapsed.
[[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,6 +55,29 @@ 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"
......@@ -22,6 +94,22 @@ optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
version = "1.7.0"
[[package]]
category = "main"
description = "ASN.1 types and codecs"
name = "pyasn1"
optional = false
python-versions = "*"
version = "0.4.5"
[[package]]
category = "main"
description = "C parser in Python"
name = "pycparser"
optional = false
python-versions = "*"
version = "2.19"
[[package]]
category = "main"
description = "A wrapper for the Gnu Privacy Guard (GPG or GnuPG)"
......@@ -31,7 +119,18 @@ python-versions = "*"
version = "0.4.4"
[[package]]
category = "dev"
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
......@@ -72,14 +171,23 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
version = "16.4.0"
[metadata]
content-hash = "28aa155b6cccb2f93f6e632be14599cf66f63da25a8598a423cab7aae6a6a9eb"
content-hash = "9b8b06a7283d2179400ca0616f786297b93b65c232477bfb3aee28499b974d75"
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"]
ipaddress = ["64b28eec5e78e7510698f6d4da08800a5c575caa4a286c93d651c5d3ff7b6794", "b146c751ea45cad6188dd6cf2d9b757f6f4f8d6ffb96a023e6f2e26eea02a72c"]
pgpy = ["04412dddd6882ac0c5d5daf4326c28d481421851a68e25e7ac8e06cc9dc2b902"]
pluggy = ["8ddc32f03971bfdf900a81961a48ccf2fb677cf7715108f85295c67405798616", "980710797ff6a041e9a73a5787804f848996ecaa6f8a1b1e08224a5894f2074a"]
py = ["bf92637198836372b520efcba9e020c330123be8ce527e535d185ed4b6f45694", "e76826342cefe3c3d5f7e8ee4316b80d1dd8a300781612ddbc765c17ba25a6c6"]
pyasn1 = ["061442c60842f6d11051d4fdae9bc197b64bd41573a12234a753a0cb80b4f30b", "0ee2449bf4c4e535823acc25624c45a8b454f328d59d3f3eeb82d3567100b9bd", "5f9fb05c33e53b9a6ee3b1ed1d292043f83df465852bec876e93b47fd2df7eed", "65201d28e081f690a32401e6253cca4449ccacc8f3988e811fae66bd822910ee", "79b336b073a52fa3c3d8728e78fa56b7d03138ef59f44084de5f39650265b5ff", "8ec20f61483764de281e0b4aba7d12716189700debcfa9e7935780850bf527f3", "9458d0273f95d035de4c0d5e0643f25daba330582cc71bb554fe6969c015042a", "98d97a1833a29ca61cd04a60414def8f02f406d732f9f0bcb49f769faff1b699", "b00d7bfb6603517e189d1ad76967c7e805139f63e43096e5f871d1277f50aea5", "b06c0cfd708b806ea025426aace45551f91ea7f557e0c2d4fbd9a4b346873ce0", "d14d05984581770333731690f5453efd4b82e1e5d824a1d7976b868a2e5c38e8", "da2420fe13a9452d8ae97a0e478adde1dee153b11ba832a95b223a2ba01c10f7", "da6b43a8c9ae93bc80e2739efb38cc776ba74a886e3e9318d65fe81a8b8a2c6e"]
pycparser = ["a988718abfad80b6b157acce7bf130a30876d27603738ac39f140993246b25b3"]
python-gnupg = ["45daf020b370bda13a1429c859fcdff0b766c0576844211446f9266cae97fb0e", "85c231850a0275c9722f06e34b45a22510b83a6a6e88f93b5ae32ba04c95056c"]
singledispatch = ["5b06af87df13818d14f08a028e42f566640aef80805c3b50c5056b086e3c2b9c", "833b46966687b3de7f438c761ac475213e53b306740f1abfaa86e1d1aae56aa8"]
six = ["3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", "d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73"]
toml = ["229f81c57791a41d65e399fc06bf0848bab550a9dfd5ed66df18ce5f05e73d5c", "235682dd292d5899d361a811df37e04a8828a5b1da3115886b73cf81ebc9100e", "f1db651f9657708513243e61e6cc67d101a39bad662eaa9b5546f789338e07a3"]
tox = ["04f8f1aa05de8e76d7a266ccd14e0d665d429977cd42123bc38efa9b59964e9e", "25ef928babe88c71e3ed3af0c464d1160b01fca2dd1870a5bb26c2dea61a17fc"]
......
......@@ -7,6 +7,7 @@ authors = ["AI <info@autistici.org>"]
[tool.poetry.dependencies]
python = "~2.7 || ^3.5"
python-gnupg = "^0.4.4"
pgpy = "^0.4.3"
[tool.poetry.dev-dependencies]
tox = "^3.7"
......
......@@ -10,6 +10,7 @@ setup(
author_email="info@autistici.org",
url="https://git.autistici.org/ai/pgp-mime-lib.git",
install_requires=[
"pgpy",
"python-gnupg",
],
packages=find_packages(),
......
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