summaryrefslogtreecommitdiff
path: root/alot/crypto.py
diff options
context:
space:
mode:
authorDaniel Kahn Gillmor <dkg@fifthhorseman.net>2016-11-30 02:30:44 -0500
committerDylan Baker <dylan@pnwbakers.com>2017-08-14 09:30:34 -0700
commitb0e2f322aa571a5e1999c069779f589e282a566c (patch)
treed5b905ff596a045ef4c8e5c9540772218b026230 /alot/crypto.py
parentc377ee5bd6e2b64be8bbdd5df72ac3ca50373134 (diff)
convert from pygpgme to the python "gpg" module
This converts from the now abandoned pygpgme project for wrapping gpgme, to the upstream gpgme python bindings (which are descended from the pyme project, before they became official). Largely this change should not be user visible, but there are a couple cases where the new bindings provide slightly more detailed error messages, and alot directly presents those messages to users. This patch has been significantly revised and updated by Dylan Baker, but was originally authored by Daniel Kahn Gillmor. Fixes #1069
Diffstat (limited to 'alot/crypto.py')
-rw-r--r--alot/crypto.py236
1 files changed, 113 insertions, 123 deletions
diff --git a/alot/crypto.py b/alot/crypto.py
index 4da32eea..3700aeef 100644
--- a/alot/crypto.py
+++ b/alot/crypto.py
@@ -1,43 +1,13 @@
+# encoding=utf-8
# Copyright (C) 2011-2012 Patrick Totzke <patricktotzke@gmail.com>
+# Copyright © 2017 Dylan Baker <dylan@pnwbakers.com>
# This file is released under the GNU GPL, version 3 or a later revision.
# For further details see the COPYING file
from __future__ import absolute_import
-import os
-from cStringIO import StringIO
-import gpgme
-from .errors import GPGProblem, GPGCode
-
-
-def _hash_algo_name(hash_algo):
- """
- Re-implements GPGME's hash_algo_name as long as pygpgme doesn't wrap that
- function.
+import gpg
- :param hash_algo: GPGME hash_algo
- :rtype: str
- """
- mapping = {
- gpgme.MD_MD5: "MD5",
- gpgme.MD_SHA1: "SHA1",
- gpgme.MD_RMD160: "RIPEMD160",
- gpgme.MD_MD2: "MD2",
- gpgme.MD_TIGER: "TIGER192",
- gpgme.MD_HAVAL: "HAVAL",
- gpgme.MD_SHA256: "SHA256",
- gpgme.MD_SHA384: "SHA384",
- gpgme.MD_SHA512: "SHA512",
- gpgme.MD_MD4: "MD4",
- gpgme.MD_CRC32: "CRC32",
- gpgme.MD_CRC32_RFC1510: "CRC32RFC1510",
- gpgme.MD_CRC24_RFC2440: "CRC24RFC2440",
- }
- if hash_algo in mapping:
- return mapping[hash_algo]
- else:
- raise GPGProblem(("Invalid hash_algo passed to hash_algo_name."
- " Please report this as a bug in alot."),
- code=GPGCode.INVALID_HASH)
+from .errors import GPGProblem, GPGCode
def RFC3156_micalg_from_algo(hash_algo):
@@ -47,12 +17,16 @@ def RFC3156_micalg_from_algo(hash_algo):
GPGME returns hash algorithm names such as "SHA256", but RFC3156 says that
programs need to use names such as "pgp-sha256" instead.
- :param hash_algo: GPGME hash_algo
+ :param str hash_algo: GPGME hash_algo
+ :returns: the lowercase name of of the algorithm with "pgp-" prepended
:rtype: str
"""
# hash_algo will be something like SHA256, but we need pgp-sha256.
- hash_algo = _hash_algo_name(hash_algo)
- return 'pgp-' + hash_algo.lower()
+ algo = gpg.core.hash_algo_name(hash_algo)
+ if algo is None:
+ raise GPGProblem('Unknown hash algorithm {}'.format(algo),
+ code=GPGCode.INVALID_HASH_ALGORITHM)
+ return 'pgp-' + algo.lower()
def get_key(keyid, validate=False, encrypt=False, sign=False,
@@ -81,69 +55,94 @@ def get_key(keyid, validate=False, encrypt=False, sign=False,
:param signed_only: only return keys whose uid is signed (trusted to
belong to the key)
:type signed_only: bool
- :rtype: gpgme.Key
+ :returns: A gpg key matching the given parameters
+ :rtype: gpg.gpgme._gpgme_key
+ :raises ~alot.errors.GPGProblem: if the keyid is ambiguous
+ :raises ~alot.errors.GPGProblem: if there is no key that matches the
+ parameters
+ :raises ~alot.errors.GPGProblem: if a key is found, but signed_only is true
+ and the key is unused
"""
- ctx = gpgme.Context()
+ ctx = gpg.core.Context()
try:
key = ctx.get_key(keyid)
if validate:
validate_key(key, encrypt=encrypt, sign=sign)
- except gpgme.GpgmeError as e:
- if e.code == gpgme.ERR_AMBIGUOUS_NAME:
+ except gpg.errors.KeyNotFound:
+ raise GPGProblem('Cannot find key for "{}".'.format(keyid),
+ code=GPGCode.NOT_FOUND)
+ except gpg.errors.GPGMEError as e:
+ if e.getcode() == gpg.errors.AMBIGUOUS_NAME:
# When we get here it means there were multiple keys returned by
# gpg for given keyid. Unfortunately gpgme returns invalid and
# expired keys together with valid keys. If only one key is valid
# for given operation maybe we can still return it instead of
# raising exception
- keys = list_keys(hint=keyid)
+
valid_key = None
- for k in keys:
- try:
- validate_key(k, encrypt=encrypt, sign=sign)
- except GPGProblem:
- # if the key is invalid for given action skip it
- continue
-
- if valid_key:
- # we have already found one valid key and now we find
- # another? We really received an ambiguous keyid
+
+ # Catching exceptions for list_keys
+ try:
+ for k in list_keys(hint=keyid):
+ try:
+ validate_key(k, encrypt=encrypt, sign=sign)
+ except GPGProblem:
+ # if the key is invalid for given action skip it
+ continue
+
+ if valid_key:
+ # we have already found one valid key and now we find
+ # another? We really received an ambiguous keyid
+ raise GPGProblem(
+ "More than one key found matching this filter. "
+ "Please be more specific "
+ "(use a key ID like 4AC8EE1D).",
+ code=GPGCode.AMBIGUOUS_NAME)
+ valid_key = k
+ except gpg.errors.GPGMEError as e:
+ # This if will be triggered if there is no key matching at all.
+ if e.getcode() == gpg.errors.AMBIGUOUS_NAME:
raise GPGProblem(
- "More than one key found matching this filter. Please "
- "be more specific (use a key ID like 4AC8EE1D).",
- code=GPGCode.AMBIGUOUS_NAME)
- valid_key = k
+ 'Can not find any key for "{}".'.format(keyid),
+ code=GPGCode.NOT_FOUND)
+ raise
if not valid_key:
# there were multiple keys found but none of them are valid for
# given action (we don't have private key, they are expired
- # etc)
+ # etc), or there was no key at all
raise GPGProblem(
'Can not find usable key for "{}".'.format(keyid),
code=GPGCode.NOT_FOUND)
return valid_key
- elif e.code == gpgme.ERR_INV_VALUE or e.code == gpgme.ERR_EOF:
- raise GPGProblem('Can not find key for "{}".'.format(keyid),
- code=GPGCode.NOT_FOUND)
+ elif e.getcode() == gpg.errors.INV_VALUE:
+ raise GPGProblem(
+ 'Can not find usable key for "{}".'.format(keyid),
+ code=GPGCode.NOT_FOUND)
else:
raise e
if signed_only and not check_uid_validity(key, keyid):
- raise GPGProblem('Can not find a trusworthy key for "{}".'.format(keyid),
+ raise GPGProblem('Cannot find a trusworthy key for "{}".'.format(keyid),
code=GPGCode.NOT_FOUND)
return key
def list_keys(hint=None, private=False):
"""
- Returns a iterator of all keys containing the fingerprint, or all keys if
+ Returns a generator of all keys containing the fingerprint, or all keys if
hint is None.
+ The generator may raise exceptions of :class:gpg.errors.GPGMEError, and it
+ is the caller's responsibility to handle them.
+
:param hint: Part of a fingerprint to usee to search
:type hint: str or None
:param private: Whether to return public keys or secret keys
:type private: bool
- :rtype: :class:`gpgme.KeyIter`
+ :returns: A generator that yields keys.
+ :rtype: Generator[gpg.gpgme.gpgme_key_t, None, None]
"""
- ctx = gpgme.Context()
+ ctx = gpg.core.Context()
return ctx.keylist(hint, private)
@@ -154,81 +153,69 @@ def detached_signature_for(plaintext_str, key=None):
A detached signature in GPG speak is a separate blob of data containing
a signature for the specified plaintext.
- :param plaintext_str: text to sign
- :param key: gpgme_key_t object representing the key to use
- :rtype: tuple of gpgme.NewSignature array and str
+ :param str plaintext_str: text to sign
+ :param key: key to sign with
+ :type key: gpg.gpgme._gpgme_key
+ :returns: A list of signature and the signed blob of data
+ :rtype: tuple[list[gpg.results.NewSignature], str]
"""
- ctx = gpgme.Context()
- ctx.armor = True
+ ctx = gpg.core.Context(armor=True)
if key is not None:
ctx.signers = [key]
- plaintext_data = StringIO(plaintext_str)
- signature_data = StringIO()
- sigs = ctx.sign(plaintext_data, signature_data, gpgme.SIG_MODE_DETACH)
- signature_data.seek(0, os.SEEK_SET)
- signature = signature_data.read()
- return sigs, signature
+ (sigblob, sign_result) = ctx.sign(plaintext_str, mode=gpg.constants.SIG_MODE_DETACH)
+ return sign_result.signatures, sigblob
def encrypt(plaintext_str, keys=None):
- """
- Encrypts the given plaintext string and returns a PGP/MIME compatible
- string
+ """Encrypt data and return the encrypted form.
- :param plaintext_str: the mail to encrypt
- :param key: gpgme_key_t object representing the key to use
- :rtype: a string holding the encrypted mail
+ :param str plaintext_str: the mail to encrypt
+ :param key: optionally, a list of keys to encrypt with
+ :type key: list[gpg.gpgme.gpgme_key_t] or None
+ :returns: encrypted mail
+ :rtype: str
"""
- plaintext_data = StringIO(plaintext_str)
- encrypted_data = StringIO()
- ctx = gpgme.Context()
- ctx.armor = True
- ctx.encrypt(keys, gpgme.ENCRYPT_ALWAYS_TRUST, plaintext_data,
- encrypted_data)
- encrypted_data.seek(0, os.SEEK_SET)
- encrypted = encrypted_data.read()
- return encrypted
+ ctx = gpg.core.Context(armor=True)
+ out = ctx.encrypt(plaintext_str, recipients=keys, always_trust=True)[0]
+ return out
def verify_detached(message, signature):
- '''Verifies whether the message is authentic by checking the
- signature.
+ """Verifies whether the message is authentic by checking the signature.
- :param message: the message as `str`
- :param signature: a `str` containing an OpenPGP signature
- :returns: a list of :class:`gpgme.Signature`
+ :param str message: The message to be verified, in canonical form.
+ :param str signature: the OpenPGP signature to verify
+ :returns: a list of signatures
+ :rtype: list[gpg.results.Signature]
:raises: :class:`~alot.errors.GPGProblem` if the verification fails
- '''
- message_data = StringIO(message)
- signature_data = StringIO(signature)
- ctx = gpgme.Context()
-
- status = ctx.verify(signature_data, message_data, None)
- if isinstance(status[0].status, gpgme.GpgmeError):
- raise GPGProblem(status[0].status.message, code=status[0].status.code)
- return status
+ """
+ ctx = gpg.core.Context()
+ try:
+ verify_results = ctx.verify(message, signature)[1]
+ return verify_results.signatures
+ except gpg.errors.BadSignatures as e:
+ raise GPGProblem(str(e), code=GPGCode.BAD_SIGNATURE)
+ except gpg.errors.GPGMEError as e:
+ raise GPGProblem(e.message, code=e.getcode())
def decrypt_verify(encrypted):
- '''Decrypts the given ciphertext string and returns both the
+ """Decrypts the given ciphertext string and returns both the
signatures (if any) and the plaintext.
- :param encrypted: the mail to decrypt
- :returns: a tuple (sigs, plaintext) with sigs being a list of a
- :class:`gpgme.Signature` and plaintext is a `str` holding
- the decrypted mail
+ :param str encrypted: the mail to decrypt
+ :returns: the signatures and decrypted plaintext data
+ :rtype: tuple[list[gpg.resuit.Signature], str]
:raises: :class:`~alot.errors.GPGProblem` if the decryption fails
- '''
- encrypted_data = StringIO(encrypted)
- plaintext_data = StringIO()
- ctx = gpgme.Context()
+ """
+ ctx = gpg.core.Context()
try:
- sigs = ctx.decrypt_verify(encrypted_data, plaintext_data)
- except gpgme.GpgmeError as e:
- raise GPGProblem(e.message, code=e.code)
+ (plaintext, _, verify_result) = ctx.decrypt(encrypted, verify=True)
+ except gpg.errors.GPGMEError as e:
+ raise GPGProblem(e.message, code=e.getcode())
+ # what if the signature is bad?
- plaintext_data.seek(0, os.SEEK_SET)
- return sigs, plaintext_data.read()
+ return verify_result.signatures, plaintext
def hash_key(key):
@@ -253,12 +240,16 @@ def validate_key(key, sign=False, encrypt=False):
signing or encrypting. Raise GPGProblem otherwise.
:param key: the GPG key to check
- :type key: gpgme.Key
+ :type key: gpg.gpgme._gpgme_key
:param sign: whether the key should be able to sign
:type sign: bool
:param encrypt: whether the key should be able to encrypt
:type encrypt: bool
-
+ :raises ~alot.errors.GPGProblem: If the key is revoked, expired, or invalid
+ :raises ~alot.errors.GPGProblem: If encrypt is true and the key cannot be
+ used to encrypt
+ :raises ~alot.errors.GPGProblem: If sign is true and th key cannot be used
+ to encrypt
"""
if key.revoked:
raise GPGProblem('The key "{}" is revoked.'.format(key.uids[0].uid),
@@ -285,16 +276,15 @@ def check_uid_validity(key, email):
email is assumed to belong to the key.
:param key: the GPG key to which the email should belong
- :type key: gpgme.Key
+ :type key: gpg.gpgme._gpgme_key
:param email: the email address that should belong to the key
:type email: str
:returns: whether the key can be assumed to belong to the given email
:rtype: bool
-
"""
for key_uid in key.uids:
if email == key_uid.email and not key_uid.revoked and \
not key_uid.invalid and \
- key_uid.validity >= gpgme.VALIDITY_FULL:
+ key_uid.validity >= gpg.constants.validity.FULL:
return True
return False