Skip to content

Commit

Permalink
convert from pygpgme to the python "gpg" module
Browse files Browse the repository at this point in the history
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
  • Loading branch information
dkg authored and dcbaker committed Aug 14, 2017
1 parent c377ee5 commit b0e2f32
Show file tree
Hide file tree
Showing 13 changed files with 190 additions and 170 deletions.
2 changes: 1 addition & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ install:
# alot when rebuilding the documentation. Notmuch would have to be
# installed by hand in order to get a recent enough version on travis.
printf '%s = None\n' NotmuchError NullPointerError > notmuch.py
touch gpgme.py
touch gpg.py
# install sphinx for building the html docs
pip install sphinx
else
Expand Down
1 change: 1 addition & 0 deletions NEWS
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ next:
* bug fix: GPG signatures are acutally verified
* feature: option to use linewise focussing in thread mode
* feature: add support to move to next or previous message matching a notmuch query in a thread buffer
* feature: Convert from deprecated pygppme module to upstream gpg wrappers

0.5:
* save command prompt, recipient and sender history across program restarts
Expand Down
2 changes: 1 addition & 1 deletion alot/commands/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ def _get_keys(ui, encrypt_keyids, block_error=False, signed_only=False):
to the key)
:type signed_only: bool
:returns: the available keys indexed by their key hash
:rtype: dict(str->gpgme.Key)
:rtype: dict(str->gpg key object)
"""
keys = {}
Expand Down
236 changes: 113 additions & 123 deletions alot/crypto.py
Original file line number Diff line number Diff line change
@@ -1,43 +1,13 @@
# encoding=utf-8
# Copyright (C) 2011-2012 Patrick Totzke <[email protected]>
# Copyright © 2017 Dylan Baker <[email protected]>
# 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):
Expand All @@ -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,
Expand Down Expand Up @@ -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)


Expand All @@ -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):
Expand All @@ -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),
Expand All @@ -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
Loading

0 comments on commit b0e2f32

Please sign in to comment.