summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPatrick Totzke <patricktotzke@gmail.com>2013-06-16 21:20:44 +0100
committerPatrick Totzke <patricktotzke@gmail.com>2013-06-16 21:20:44 +0100
commit7e6e40ee39854b7ab05eabc1d1b20c13a6c6bfd6 (patch)
tree0897550431a6ad6b7f5dbe1199a1000b6cdc5e71
parent998c89b9544e2c676bb8ad4ad7e0396eaa070a32 (diff)
parentdf954de61c64a29846ffe426a630cef891ec6902 (diff)
Merge branch '0.3.4-feature-gpg-decode-616'
-rw-r--r--alot/crypto.py46
-rw-r--r--alot/db/message.py4
-rw-r--r--alot/db/utils.py196
-rw-r--r--alot/errors.py3
-rw-r--r--alot/widgets/thread.py6
5 files changed, 247 insertions, 8 deletions
diff --git a/alot/crypto.py b/alot/crypto.py
index 6281ecfd..8346b4ec 100644
--- a/alot/crypto.py
+++ b/alot/crypto.py
@@ -2,7 +2,7 @@
# 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
+import os
from email.generator import Generator
from cStringIO import StringIO
@@ -166,7 +166,7 @@ def detached_signature_for(plaintext_str, key=None):
plaintext_data = StringIO(plaintext_str)
signature_data = StringIO()
sigs = ctx.sign(plaintext_data, signature_data, gpgme.SIG_MODE_DETACH)
- signature_data.seek(0, 0)
+ signature_data.seek(0, os.SEEK_SET)
signature = signature_data.read()
return sigs, signature
@@ -186,11 +186,51 @@ def encrypt(plaintext_str, keys=None):
ctx.armor = True
ctx.encrypt(keys, gpgme.ENCRYPT_ALWAYS_TRUST, plaintext_data,
encrypted_data)
- encrypted_data.seek(0, 0)
+ encrypted_data.seek(0, os.SEEK_SET)
encrypted = encrypted_data.read()
return encrypted
+def verify_detached(message, 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`
+ :raises: :class:`~alot.errors.GPGProblem` if the verification fails
+ '''
+ message_data = StringIO(message)
+ signature_data = StringIO(signature)
+ ctx = gpgme.Context()
+ try:
+ return ctx.verify(signature_data, message_data, None)
+ except gpgme.GpgmeError as e:
+ raise GPGProblem(e.message, code=e.code)
+
+
+def decrypt_verify(encrypted):
+ '''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
+ :raises: :class:`~alot.errors.GPGProblem` if the decryption fails
+ '''
+ encrypted_data = StringIO(encrypted)
+ plaintext_data = StringIO()
+ ctx = gpgme.Context()
+ try:
+ sigs = ctx.decrypt_verify(encrypted_data, plaintext_data)
+ except gpgme.GpgmeError as e:
+ raise GPGProblem(e.message, code=e.code)
+
+ plaintext_data.seek(0, os.SEEK_SET)
+ return sigs, plaintext_data.read()
+
+
def hash_key(key):
"""
Returns a hash of the given key. This is a workaround for
diff --git a/alot/db/message.py b/alot/db/message.py
index 65ef2dc8..002f7cf9 100644
--- a/alot/db/message.py
+++ b/alot/db/message.py
@@ -10,7 +10,7 @@ from notmuch import NullPointerError
import alot.helper as helper
from alot.settings import settings
-from utils import extract_headers, extract_body
+from utils import extract_headers, extract_body, message_from_file
from alot.db.utils import decode_header
from attachment import Attachment
@@ -69,7 +69,7 @@ class Message(object):
if not self._email:
try:
f_mail = open(path)
- self._email = email.message_from_file(f_mail)
+ self._email = message_from_file(f_mail)
f_mail.close()
except IOError:
self._email = email.message_from_string(warning)
diff --git a/alot/db/utils.py b/alot/db/utils.py
index 1280667a..0c508b79 100644
--- a/alot/db/utils.py
+++ b/alot/db/utils.py
@@ -11,14 +11,210 @@ charset.add_charset('utf-8', charset.QP, charset.QP, 'utf-8')
from email.iterators import typed_subpart_iterator
import logging
import mailcap
+from cStringIO import StringIO
+import alot.crypto as crypto
import alot.helper as helper
+from alot.errors import GPGProblem
from alot.settings import settings
from alot.helper import string_sanitize
from alot.helper import string_decode
from alot.helper import parse_mailcap_nametemplate
from alot.helper import split_commandstring
+X_SIGNATURE_VALID_HEADER = 'X-Alot-OpenPGP-Signature-Valid'
+X_SIGNATURE_MESSAGE_HEADER = 'X-Alot-OpenPGP-Signature-Message'
+
+
+def add_signature_headers(mail, sigs, error_msg):
+ '''Add pseudo headers to the mail indicating whether the signature
+ verification was successful.
+
+ :param mail: :class:`email.message.Message` the message to entitle
+ :param sigs: list of :class:`gpgme.Signature`
+ :param error_msg: `str` containing an error message, the empty
+ string indicating no error
+ '''
+ sig_from = ''
+
+ if len(sigs) == 0:
+ error_msg = error_msg or 'no signature found'
+ else:
+ try:
+ sig_from = crypto.get_key(sigs[0].fpr).uids[0].uid
+ except:
+ sig_from = sigs[0].fpr
+
+ mail.add_header(
+ X_SIGNATURE_VALID_HEADER,
+ 'False' if error_msg else 'True',
+ )
+ mail.add_header(
+ X_SIGNATURE_MESSAGE_HEADER,
+ 'Invalid: {0}'.format(error_msg)
+ if error_msg else
+ 'Valid: {0}'.format(sig_from),
+ )
+
+
+def get_params(mail, failobj=None, header='content-type', unquote=True):
+ '''Get Content-Type parameters as dict.
+
+ RFC 2045 specifies that parameter names are case-insensitive, so
+ we normalize them here.
+
+ :param mail: :class:`email.message.Message`
+ :param failobj: object to return if no such header is found
+ :param header: the header to search for parameters, default
+ :param unquote: unquote the values
+ :returns: a `dict` containing the parameters
+ '''
+ return {k.lower():v for k, v in mail.get_params(failobj, header, unquote)}
+
+
+def message_from_file(handle):
+ '''Reads a mail from the given file-like object and returns an email
+ object, very much like email.message_from_file. In addition to
+ that OpenPGP encrypted data is detected and decrypted. If this
+ succeeds, any mime messages found in the recovered plaintext
+ message are added to the returned message object.
+
+ :param handle: a file-like object
+ :returns: :class:`email.message.Message` possibly augmented with decrypted data
+ '''
+ m = email.message_from_file(handle)
+
+ # make sure noone smuggles a token in (data from m is untrusted)
+ del m[X_SIGNATURE_VALID_HEADER]
+ del m[X_SIGNATURE_MESSAGE_HEADER]
+
+ p = get_params(m)
+ app_pgp_sig = 'application/pgp-signature'
+ app_pgp_enc = 'application/pgp-encrypted'
+
+ # handle OpenPGP signed data
+ if (m.is_multipart() and
+ m.get_content_subtype() == 'signed' and
+ p.get('protocol', None) == app_pgp_sig):
+ # RFC 3156 is quite strict:
+ # * exactly two messages
+ # * the second is of type 'application/pgp-signature'
+ # * the second contains the detached signature
+
+ malformed = False
+ if len(m.get_payload()) != 2:
+ malformed = 'expected exactly two messages, got {0}'.format(
+ len(m.get_payload()))
+
+ ct = m.get_payload(1).get_content_type()
+ if ct != app_pgp_sig:
+ malformed = 'expected Content-Type: {0}, got: {1}'.format(
+ app_pgp_sig, ct)
+
+ # TODO: RFC 3156 says the alg has to be lower case, but I've
+ # seen a message with 'PGP-'. maybe we should be more
+ # permissive here, or maybe not, this is crypto stuff...
+ if not p.get('micalg', 'nothing').startswith('pgp-'):
+ malformed = 'expected micalg=pgp-..., got: {0}'.format(
+ p.get('micalg', 'nothing'))
+
+ sigs = []
+ if not malformed:
+ try:
+ sigs = crypto.verify_detached(m.get_payload(0).as_string(),
+ m.get_payload(1).get_payload())
+ except GPGProblem as e:
+ malformed = str(e)
+
+ add_signature_headers(m, sigs, malformed)
+
+ # handle OpenPGP encrypted data
+ elif (m.is_multipart() and
+ m.get_content_subtype() == 'encrypted' and
+ p.get('protocol', None) == app_pgp_enc and
+ 'Version: 1' in m.get_payload(0).get_payload()):
+ # RFC 3156 is quite strict:
+ # * exactly two messages
+ # * the first is of type 'application/pgp-encrypted'
+ # * the first contains 'Version: 1'
+ # * the second is of type 'application/octet-stream'
+ # * the second contains the encrypted and possibly signed data
+ malformed = False
+
+ ct = m.get_payload(0).get_content_type()
+ if ct != app_pgp_enc:
+ malformed = 'expected Content-Type: {0}, got: {1}'.format(app_pgp_enc, ct)
+
+ want = 'application/octet-stream'
+ ct = m.get_payload(1).get_content_type()
+ if ct != want:
+ malformed = 'expected Content-Type: {0}, got: {1}'.format(want, ct)
+
+ if not malformed:
+ try:
+ sigs, d = crypto.decrypt_verify(m.get_payload(1).get_payload())
+ except GPGProblem as e:
+ # signature verification failures end up here too if
+ # the combined method is used, currently this prevents
+ # the interpretation of the recovered plain text
+ # mail. maybe that's a feature.
+ malformed = str(e)
+ else:
+ # parse decrypted message
+ n = message_from_string(d)
+
+ # add the decrypted message to m. note that n contains
+ # all the attachments, no need to walk over n here.
+ m.attach(n)
+
+ # add any defects found
+ m.defects.extend(n.defects)
+
+ # there are two methods for both signed and encrypted
+ # data, one is called 'RFC 1847 Encapsulation' by
+ # RFC 3156, and one is the 'Combined method'.
+ if len(sigs) == 0:
+ # 'RFC 1847 Encapsulation', the signature is a
+ # detached signature found in the recovered mime
+ # message of type multipart/signed.
+ if X_SIGNATURE_VALID_HEADER in n:
+ for k in (X_SIGNATURE_VALID_HEADER,
+ X_SIGNATURE_MESSAGE_HEADER):
+ m[k] = n[k]
+ else:
+ # an encrypted message without signatures
+ # should arouse some suspicion, better warn
+ # the user
+ add_signature_headers(m, [], 'no signature found')
+ else:
+ # 'Combined method', the signatures are returned
+ # by the decrypt_verify function.
+
+ # note that if we reached this point, we know the
+ # signatures are valid. if they were not valid,
+ # the else block of the current try would not have
+ # been executed
+ add_signature_headers(m, sigs, '')
+
+ if malformed:
+ msg = 'Malformed OpenPGP message: {0}'.format(malformed)
+ m.attach(email.message_from_string(msg))
+
+ return m
+
+
+def message_from_string(s):
+ '''Reads a mail from the given string. This is the equivalent of
+ :func:`email.message_from_string` which does nothing but to wrap
+ the given string in a StringIO object and to call
+ :func:`email.message_from_file`.
+
+ Please refer to the documentation of :func:`message_from_file` for
+ details.
+
+ '''
+ return message_from_file(StringIO(s))
+
def extract_headers(mail, headers=None):
"""
diff --git a/alot/errors.py b/alot/errors.py
index ce88f38e..435a4bf9 100644
--- a/alot/errors.py
+++ b/alot/errors.py
@@ -19,5 +19,4 @@ class GPGProblem(Exception):
"""GPG Error"""
def __init__(self, message, code):
self.code = code
- self.message = message
- Exception(message)
+ super(GPGProblem, self).__init__(message)
diff --git a/alot/widgets/thread.py b/alot/widgets/thread.py
index 17b43883..32229d2e 100644
--- a/alot/widgets/thread.py
+++ b/alot/widgets/thread.py
@@ -8,7 +8,7 @@ import urwid
import logging
from alot.settings import settings
-from alot.db.utils import decode_header
+from alot.db.utils import decode_header, X_SIGNATURE_MESSAGE_HEADER
from alot.helper import tag_cmp
from alot.widgets.globals import TagWidget
from alot.widgets.globals import AttachmentWidget
@@ -303,6 +303,10 @@ class MessageTree(CollapsibleTree):
values.append(t)
lines.append((key, ', '.join(values)))
+ # OpenPGP pseudo headers
+ if mail[X_SIGNATURE_MESSAGE_HEADER]:
+ lines.append(('PGP-Signature', mail[X_SIGNATURE_MESSAGE_HEADER]))
+
key_att = settings.get_theming_attribute('thread', 'header_key')
value_att = settings.get_theming_attribute('thread', 'header_value')
gaps_att = settings.get_theming_attribute('thread', 'header')