diff options
author | Patrick Totzke <patricktotzke@gmail.com> | 2013-02-19 10:11:11 +0000 |
---|---|---|
committer | Patrick Totzke <patricktotzke@gmail.com> | 2013-02-19 10:11:11 +0000 |
commit | ae4a4b28c2f461219344c2b8503dab2a9a8c1d3b (patch) | |
tree | eb2c19eaf288c6812158610ee496db0c0e1e8d39 | |
parent | 17ac6c9539986f3a8e3c079e7cfe56812d7da384 (diff) | |
parent | 15cd4894ed3ad9a9b16484ea0b1dc58e6975040f (diff) |
Merge branch '0.3.3-feature-cryptomails-543'
-rw-r--r-- | alot/buffers.py | 24 | ||||
-rw-r--r-- | alot/commands/envelope.py | 92 | ||||
-rw-r--r-- | alot/completion.py | 27 | ||||
-rw-r--r-- | alot/crypto.py | 85 | ||||
-rw-r--r-- | alot/db/envelope.py | 43 | ||||
-rw-r--r-- | alot/errors.py | 16 | ||||
-rw-r--r-- | alot/helper.py | 1 | ||||
-rw-r--r-- | docs/source/crypto/index.rst | 7 | ||||
-rw-r--r-- | docs/source/usage/modes/envelope.rst | 37 |
9 files changed, 313 insertions, 19 deletions
diff --git a/alot/buffers.py b/alot/buffers.py index 555268e1..8777e246 100644 --- a/alot/buffers.py +++ b/alot/buffers.py @@ -149,9 +149,27 @@ class EnvelopeBuffer(Buffer): description = 'Yes' sign_key = self.envelope.sign_key if sign_key is not None and len(sign_key.subkeys) > 0: - description += ', with key ' + sign_key.subkeys[0].keyid + description += ', with key ' + sign_key.uids[0].uid lines.append(('GPG sign', description)) + if self.envelope.encrypt: + description = 'Yes' + encrypt_keys = self.envelope.encrypt_keys.values() + if len(encrypt_keys) == 1: + description += ', with key ' + elif len(encrypt_keys) > 1: + description += ', with keys ' + first_key = True + for key in encrypt_keys: + if key is not None: + if first_key: + first_key = False + else: + description += ', ' + if len(key.subkeys) > 0: + description += key.uids[0].uid + lines.append(('GPG encrypt', description)) + # add header list widget iff header values exists if lines: key_att = settings.get_theming_attribute('envelope', 'header_key') @@ -377,8 +395,8 @@ class ThreadBuffer(Buffer): # let urwid.ListBox focus this widget: # The first parameter is a "size" tuple: that needs only to # be iterable an is *never* used. i is the integer index - # to focus. offset_inset is may be used to shift the visible area - # so that the focus lies at given offset + # to focus. offset_inset is may be used to shift the + # visible area so that the focus lies at given offset self.body.change_focus((0, 0), i, offset_inset=0, coming_from='above') diff --git a/alot/commands/envelope.py b/alot/commands/envelope.py index 6354f2e0..88e7ab43 100644 --- a/alot/commands/envelope.py +++ b/alot/commands/envelope.py @@ -12,7 +12,7 @@ from twisted.internet.defer import inlineCallbacks import datetime from alot.account import SendingMailFailed, StoreMailError -from alot.errors import GPGProblem +from alot.errors import GPGProblem, GPGCode from alot import buffers from alot import commands from alot import crypto @@ -22,6 +22,8 @@ from alot.helper import string_decode from alot.settings import settings from alot.utils.booleanaction import BooleanAction +import gpgme + MODE = 'envelope' @@ -439,7 +441,7 @@ class SignCommand(Command): if len(self.keyid) > 0: keyid = str(' '.join(self.keyid)) try: - key = crypto.get_key(keyid) + key = crypto.get_key(keyid, validate=True, sign=True) except GPGProblem, e: envelope.sign = False ui.notify(e.message, priority='error') @@ -448,3 +450,89 @@ class SignCommand(Command): # reload buffer ui.current_buffer.rebuild() + + +@registerCommand(MODE, 'encrypt', forced={'action': 'encrypt'}, arguments=[ + (['keyids'], {'nargs':argparse.REMAINDER, + 'help': 'keyid of the key to encrypt with'})], + help='request encryption of message before sendout') +@registerCommand(MODE, 'unencrypt', forced={'action': 'unencrypt'}, + help='remove request to encrypt message before sending') +@registerCommand(MODE, 'toggleencrypt', forced={'action': 'toggleencrypt'}, + arguments=[ + (['keyids'], {'nargs': argparse.REMAINDER, + 'help':'keyid of the key to encrypt with'})], + help='toggle whether message should be encrypted before sendout') +@registerCommand(MODE, 'rmencrypt', forced={'action': 'rmencrypt'}, + arguments=[ + (['keyids'], {'nargs': argparse.REMAINDER, + 'help':'keyid of the key to encrypt with'})], + help='do not encrypt to given recipient key') +class EncryptCommand(Command): + def __init__(self, action=None, keyids=None, **kwargs): + """ + :param action: wether to encrypt/unencrypt/toggleencrypt + :type action: str + :param keyid: the id of the key to encrypt + :type keyid: str + """ + + self.encrypt_keys = keyids + self.action = action + Command.__init__(self, **kwargs) + + @inlineCallbacks + def apply(self, ui): + envelope = ui.current_buffer.envelope + if self.action == 'rmencrypt': + try: + for keyid in self.encrypt_keys: + tmp_key = crypto.get_key(keyid) + del envelope.encrypt_keys[crypto.hash_key(tmp_key)] + except GPGProblem as e: + ui.notify(e.message, priority='error') + if not envelope.encrypt_keys: + envelope.encrypt = False + ui.current_buffer.rebuild() + return + elif self.action == 'encrypt': + encrypt = True + elif self.action == 'unencrypt': + encrypt = False + elif self.action == 'toggleencrypt': + encrypt = not envelope.encrypt + envelope.encrypt = encrypt + if encrypt: + if not self.encrypt_keys: + for recipient in envelope.headers['To'][0].split(','): + if not recipient: + continue + match = re.search("<(.*@.*)>", recipient) + if match: + recipient = match.group(0) + self.encrypt_keys.append(recipient) + + logging.debug("encryption keys: " + str(self.encrypt_keys)) + for keyid in self.encrypt_keys: + try: + key = crypto.get_key(keyid, validate=True, encrypt=True) + except GPGProblem as e: + if e.code == GPGCode.AMBIGUOUS_NAME: + possible_keys = crypto.list_keys(hint=keyid) + tmp_choices = [k.uids[0].uid for k in possible_keys] + choices = {str(len(tmp_choices) - x) : tmp_choices[x] + for x in range(0, len(tmp_choices))} + keyid = yield ui.choice("This keyid was ambiguous. " + + "Which key do you want to use?", + choices, cancel=None) + if keyid: + self.encrypt_keys.append(keyid) + continue + else: + ui.notify(e.message, priority='error') + continue + envelope.encrypt_keys[crypto.hash_key(key)] = key + if not envelope.encrypt_keys: + envelope.encrypt = False + #reload buffer + ui.current_buffer.rebuild() diff --git a/alot/completion.py b/alot/completion.py index 70ac8489..ccb46b22 100644 --- a/alot/completion.py +++ b/alot/completion.py @@ -7,6 +7,7 @@ import glob import logging import argparse +import alot.crypto as crypto import alot.commands as commands from alot.buffers import EnvelopeBuffer from alot.settings import settings @@ -313,6 +314,8 @@ class CommandCompleter(Completer): self._contactscompleter = ContactsCompleter(abooks) self._pathcompleter = PathCompleter() self._accountscompleter = AccountCompleter() + self._secretkeyscompleter = CryptoKeyCompleter(private=True) + self._publickeyscompleter = CryptoKeyCompleter(private=False) def complete(self, line, pos): # remember how many preceding space characters we see until the command @@ -421,6 +424,12 @@ class CommandCompleter(Completer): elif self.mode == 'envelope' and cmd == 'attach': res = self._pathcompleter.complete(params, localpos) + elif self.mode == 'envelope' and cmd in ['sign', 'togglesign']: + res = self._secretkeyscompleter.complete(params, localpos) + elif self.mode == 'envelope' and cmd in ['encrypt', + 'rmencrypt', + 'toggleencrypt']: + res = self._publickeyscompleter.complete(params, localpos) # thread elif self.mode == 'thread' and cmd == 'save': res = self._pathcompleter.complete(params, localpos) @@ -505,3 +514,21 @@ class PathCompleter(Completer): return escaped_path, len(escaped_path) return map(prep, glob.glob(deescape(prefix) + '*')) + + +class CryptoKeyCompleter(StringlistCompleter): + """completion for gpg keys""" + + def __init__(self, private=False): + """ + :param private: return private keys + :type private: bool + """ + keys = crypto.list_keys(private=private) + resultlist = [] + for k in keys: + for s in k.subkeys: + resultlist.append(s.keyid) + for u in k.uids: + resultlist.append(u.email) + StringlistCompleter.__init__(self, resultlist, match_anywhere=True) diff --git a/alot/crypto.py b/alot/crypto.py index a457dd8e..3a130478 100644 --- a/alot/crypto.py +++ b/alot/crypto.py @@ -2,10 +2,11 @@ # This file is released under the GNU GPL, version 3 or a later revision. # For further details see the COPYING file import re +import logging from email.generator import Generator from cStringIO import StringIO -from alot.errors import GPGProblem +from alot.errors import GPGProblem, GPGCode from email.mime.multipart import MIMEMultipart import gpgme @@ -65,7 +66,8 @@ def _hash_algo_name(hash_algo): return mapping[hash_algo] else: raise GPGProblem(("Invalid hash_algo passed to hash_algo_name." - " Please report this as a bug in alot.")) + " Please report this as a bug in alot."), + code=GPGCode.INVALID_HASH) def RFC3156_micalg_from_algo(hash_algo): @@ -106,7 +108,7 @@ def RFC3156_canonicalize(text): return text -def get_key(keyid): +def get_key(keyid, validate=False, encrypt=False, sign=False): """ Gets a key from the keyring by filtering for the specified keyid, but only if the given keyid is specific enough (if it matches multiple @@ -118,17 +120,34 @@ def get_key(keyid): ctx = gpgme.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: - # Deferred import to avoid a circular import dependency - from alot.db.errors import GPGProblem - raise GPGProblem(("More than one key found matching this filter." - " Please be more specific (use a key ID like 4AC8EE1D).")) + raise GPGProblem(("More than one key found matching this filter." + + " Please be more specific (use a key ID like " + + "4AC8EE1D)."), + code=GPGCode.AMBIGUOUS_NAME) + elif e.code == gpgme.ERR_INV_VALUE or e.code == gpgme.ERR_EOF: + raise GPGProblem("Can not find key for \'" + keyid + "\'.", + code=GPGCode.NOT_FOUND) else: raise e return key +def list_keys(hint=None, private=False): + """ + Returns a list of all keys containing keyid. + + :param keyid: The part we search for + :param private: Whether secret keys are listed + :rtype: list + """ + ctx = gpgme.Context() + return ctx.keylist(hint, private) + + def detached_signature_for(plaintext_str, key=None): """ Signs the given plaintext string and returns the detached signature. @@ -150,3 +169,55 @@ def detached_signature_for(plaintext_str, key=None): signature_data.seek(0, 0) signature = signature_data.read() return sigs, signature + + +def encrypt(plaintext_str, keys=None): + """ + Encrypts the given plaintext string and returns a PGP/MIME compatible + string + + :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 + """ + 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, 0) + encrypted = encrypted_data.read() + return encrypted + + +def hash_key(key): + """ + Returns a hash of the given key. This is a workaround for + https://bugs.launchpad.net/pygpgme/+bug/1089865 + and can be removed if the missing feature is added to pygpgme + + :param key: the key we want a hash of + :rtype: a has of the key as string + """ + hash_str = "" + for tmp_key in key.subkeys: + hash_str += tmp_key.keyid + return hash_str + +def validate_key(key, sign=False, encrypt=False): + if key.revoked: + raise GPGProblem("The key \"" + key.uids[0].uid + "\" is revoked.", + code=GPGCode.KEY_REVOKED) + elif key.expired: + raise GPGProblem("The key \"" + key.uids[0].uid + "\" is expired.", + code=GPGCode.KEY_EXPIRED) + elif key.invalid: + raise GPGProblem("The key \"" + key.uids[0].uid + "\" is invalid.", + code=GPGCode.KEY_INVALID) + if encrypt and not key.can_encrypt: + raise GPGProblem("The key \"" + key.uids[0].uid + "\" can not " + + "encrypt.", code=GPGCode.KEY_CANNOT_ENCRYPT) + if sign and not key.can_sign: + raise GPGProblem("The key \"" + key.uids[0].uid + "\" can not sign.", + code=GPGCode.KEY_CANNOT_SIGN) diff --git a/alot/db/envelope.py b/alot/db/envelope.py index 96abbd40..2e8f0366 100644 --- a/alot/db/envelope.py +++ b/alot/db/envelope.py @@ -58,6 +58,7 @@ class Envelope(object): self.sign = sign self.sign_key = sign_key self.encrypt = encrypt + self.encrypt_keys = {} self.tags = tags # tags to add after successful sendout self.sent_time = None self.modified_since_sent = False @@ -188,8 +189,9 @@ class Envelope(object): raise GPGProblem(str(e)) micalg = crypto.RFC3156_micalg_from_algo(signatures[0].hash_algo) - outer_msg = MIMEMultipart('signed', micalg=micalg, - protocol='application/pgp-signature') + unencrypted_msg = MIMEMultipart('signed', micalg=micalg, + protocol= + 'application/pgp-signature') # wrap signature in MIMEcontainter stype = 'pgp-signature; name="signature.asc"' @@ -200,11 +202,40 @@ class Envelope(object): signature_mime.set_charset('us-ascii') # add signed message and signature to outer message - outer_msg.attach(inner_msg) - outer_msg.attach(signature_mime) - outer_msg['Content-Disposition'] = 'inline' + unencrypted_msg.attach(inner_msg) + unencrypted_msg.attach(signature_mime) + unencrypted_msg['Content-Disposition'] = 'inline' else: - outer_msg = inner_msg + unencrypted_msg = inner_msg + + if self.encrypt: + plaintext = crypto.email_as_string(unencrypted_msg) + logging.debug('encrypting plaintext: ' + plaintext) + + try: + encrypted_str = crypto.encrypt(plaintext, + self.encrypt_keys.values()) + except gpgme.GpgmeError as e: + raise GPGProblem(str(e)) + + outer_msg = MIMEMultipart('encrypted', + protocol='application/pgp-encrypted') + + version_str = 'Version: 1' + encryption_mime = MIMEApplication(_data=version_str, + _subtype='pgp-encrypted', + _encoder=encode_7or8bit) + encryption_mime.set_charset('us-ascii') + + encrypted_mime = MIMEApplication(_data=encrypted_str, + _subtype='octet-stream', + _encoder=encode_7or8bit) + encrypted_mime.set_charset('us-ascii') + outer_msg.attach(encryption_mime) + outer_msg.attach(encrypted_mime) + + else: + outer_msg = unencrypted_msg headers = self.headers.copy() # add Message-ID diff --git a/alot/errors.py b/alot/errors.py index 881acf1f..a4169c3c 100644 --- a/alot/errors.py +++ b/alot/errors.py @@ -2,7 +2,21 @@ # This file is released under the GNU GPL, version 3 or a later revision. # For further details see the COPYING file +class GPGCode: + AMBIGUOUS_NAME = 1 + NOT_FOUND = 2 + BAD_PASSPHRASE = 3 + KEY_REVOKED = 4 + KEY_EXPIRED = 5 + KEY_INVALID = 6 + KEY_CANNOT_ENCRYPT = 7 + KEY_CANNOT_SIGN = 8 + INVALID_HASHH = 9 + class GPGProblem(Exception): """GPG Error""" - pass + def __init__(self, message, code): + self.code = code + self.message = message + Exception(message) diff --git a/alot/helper.py b/alot/helper.py index 2df22493..91837f4c 100644 --- a/alot/helper.py +++ b/alot/helper.py @@ -29,6 +29,7 @@ def split_commandline(s, comments=False, posix=True): """ # shlex seems to remove unescaped quotes s = s.replace('\'', '\\\'') + s = s.replace('\"', '\\\"') # encode s to utf-8 for shlex if isinstance(s, unicode): s = s.encode('utf-8') diff --git a/docs/source/crypto/index.rst b/docs/source/crypto/index.rst index 4c79f4b2..52fb356a 100644 --- a/docs/source/crypto/index.rst +++ b/docs/source/crypto/index.rst @@ -3,6 +3,7 @@ Cryptography ************ At the moment alot only supports signing of outgoing mails via PGP/MIME (:rfc:`3156`). +Encryption via PGP/MIME (:rfc:`3156`) is in an experimental stadium. .. note:: To use GPG with alot, you need to have `gpg-agent` running. @@ -37,4 +38,10 @@ instead of the default graphical pinentry. You can do that by setting up your pinentry-program /usr/bin/pinentry-curses +.. rubric:: Encrypt outgoing emails +You can use the commands `encrypt` and `unencrypt` in envelope mode to +encrypt the mail. You have to give a hint string as argument to the `encrypt` +command. This hint would normally be a fingerprint of the key. + +Encryption is done after signing (if signing is enabled) the email. diff --git a/docs/source/usage/modes/envelope.rst b/docs/source/usage/modes/envelope.rst index b3f478be..d7f9c4b8 100644 --- a/docs/source/usage/modes/envelope.rst +++ b/docs/source/usage/modes/envelope.rst @@ -5,6 +5,13 @@ Commands in `envelope` mode --------------------------- The following commands are available in envelope mode +.. _cmd.envelope.unencrypt: + +.. describe:: unencrypt + + remove request to encrypt message before sending + + .. _cmd.envelope.set: .. describe:: set @@ -19,6 +26,16 @@ The following commands are available in envelope mode optional arguments :---append: keep previous values. +.. _cmd.envelope.encrypt: + +.. describe:: encrypt + + request encryption of message before sendout + + argument + keyid of the key to encrypt with + + .. _cmd.envelope.togglesign: .. describe:: togglesign @@ -73,6 +90,16 @@ The following commands are available in envelope mode file(s) to attach (accepts wildcads) +.. _cmd.envelope.rmencrypt: + +.. describe:: rmencrypt + + do not encrypt to given recipient key + + argument + keyid of the key to encrypt with + + .. _cmd.envelope.refine: .. describe:: refine @@ -83,6 +110,16 @@ The following commands are available in envelope mode header to refine +.. _cmd.envelope.toggleencrypt: + +.. describe:: toggleencrypt + + toggle whether message should be encrypted before sendout + + argument + keyid of the key to encrypt with + + .. _cmd.envelope.save: .. describe:: save |