summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPatrick Totzke <patricktotzke@gmail.com>2013-02-19 10:11:11 +0000
committerPatrick Totzke <patricktotzke@gmail.com>2013-02-19 10:11:11 +0000
commitae4a4b28c2f461219344c2b8503dab2a9a8c1d3b (patch)
treeeb2c19eaf288c6812158610ee496db0c0e1e8d39
parent17ac6c9539986f3a8e3c079e7cfe56812d7da384 (diff)
parent15cd4894ed3ad9a9b16484ea0b1dc58e6975040f (diff)
Merge branch '0.3.3-feature-cryptomails-543'
-rw-r--r--alot/buffers.py24
-rw-r--r--alot/commands/envelope.py92
-rw-r--r--alot/completion.py27
-rw-r--r--alot/crypto.py85
-rw-r--r--alot/db/envelope.py43
-rw-r--r--alot/errors.py16
-rw-r--r--alot/helper.py1
-rw-r--r--docs/source/crypto/index.rst7
-rw-r--r--docs/source/usage/modes/envelope.rst37
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