diff options
Diffstat (limited to 'alot/crypto.py')
-rw-r--r-- | alot/crypto.py | 236 |
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 |