From 2c0e342e5e00cc4213ed88769bd35b1d958589aa Mon Sep 17 00:00:00 2001 From: fkrone Date: Mon, 25 May 2015 20:24:37 +0200 Subject: [PATCH] Support for decrypting PGP encrypted mails. However, it has some drawbacks and might cause some security issues. So before using it please read carefully through the installation instructions. --- GnuPG/__init__.py | 32 +++++ INSTALL.md | 190 ++++++++++++++++++++++------ README.md | 7 +- gpg-mailgate.conf.sample | 38 ++++-- gpg-mailgate.py | 262 ++++++++++++++++++++++++++++++++++++--- 5 files changed, 458 insertions(+), 71 deletions(-) diff --git a/GnuPG/__init__.py b/GnuPG/__init__.py index 595838e..c9bbee0 100644 --- a/GnuPG/__init__.py +++ b/GnuPG/__init__.py @@ -24,6 +24,20 @@ import shutil import random import string +def private_keys( keyhome ): + cmd = ['/usr/bin/gpg', '--homedir', keyhome, '--list-secret-keys', '--with-colons'] + p = subprocess.Popen( cmd, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE ) + p.wait() + keys = dict() + for line in p.stdout.readlines(): + if line[0:3] == 'uid' or line[0:3] == 'sec': + if ('<' not in line or '>' not in line): + continue + email = line.split('<')[1].split('>')[0] + fingerprint = line.split(':')[4] + keys[fingerprint] = email + return keys + def public_keys( keyhome ): cmd = ['/usr/bin/gpg', '--homedir', keyhome, '--list-keys', '--with-colons'] p = subprocess.Popen( cmd, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE ) @@ -116,3 +130,21 @@ class GPGEncryptor: cmd.append('Charset: ' + self._charset) return cmd + +class GPGDecryptor: + def __init__(self, keyhome): + self._keyhome = keyhome + self._message = '' + + def update(self, message): + self._message += message + + def decrypt(self): + p = subprocess.Popen( self._command(), stdin=subprocess.PIPE, stdout=subprocess.PIPE,stderr=subprocess.PIPE ) + decdata = p.communicate(input=self._message)[0] + return (decdata, p.returncode) + + def _command(self): + cmd = ["/usr/bin/gpg", "--trust-model", "always", "--homedir", self._keyhome, "--batch", "--yes", "--no-secmem-warning", "-a", "-d"] + + return cmd \ No newline at end of file diff --git a/INSTALL.md b/INSTALL.md index df78209..39b045f 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -1,25 +1,49 @@ - 1. Ensure that GPG is installed and configured. Also make sure public keys for - all of your potential recipients are available in the GPG home directory - used for `keyhome` in step 2. +# Installation instructions +## Content +- General information +- Install GPG-Mailgate +- Install GPG-Mailgate-Web +- Install Register-handler - 2. Configure `/etc/gpg-mailgate.conf` based on the provided - `gpg-mailgate.conf.sample` +## General information +GPG-Mailgate is divided in 3 main parts: GPG-Mailgate itself, GPG-Mailgate-Web and Register-handler. Some parts of the GPG-Mailgate project depend on other parts of the project. You will find information about these dependencies at the beginning of every installation part. - 3. Install some python dependencies `apt-get install python-m2crypto python-markdown python-requests python-mysqldb` (for linux distributions based on Debian. If you have a non Debian based distribution, the install command might be different) +These instructions show you how to set up GPG-Mailgate in an easy way. If you are a more advanced user, feel free to experiment with the settings. For these instructions a home directory for the user `nobody` is set. Sadly this is an odd workaround but no better solution was found. - 4. Place `gpg-mailgate.py` and `register-handler.py` in `/usr/local/bin/` +These instructions are based on an installation on an Ubuntu 14.04 LTS virtual machine. For other Linux distributions and other versions these instructions might need to be adapted to your distribution (e.g. installation of packages and used directories). - 5. Make sure that `gpg-mailgate.py` and `register-handler.py` are executable +## Install GPG-Mailgate +### Requirements +- Python 2.X is already installed (GPG-Mailgate is not Python 3 compatible) +- Postfix is already installed and configured. It is recommended that you have already tested your configuration so we can exclude this as a main cause of problems +- GnuPG is already installed and configured + +### Installation + +1. Install the Python-M2Crypto module: + + apt-get install python-m2crypto + +2. Set the home directory for the user `nobody` (sadly this workaround is needed as there is no better solution at this point). If you get an error that the user is currently used by a process, you might need to kill the process manually. + + usermod -d /var/gpgmailgate nobody + +3. Create dedicated directories for storing PGP keys and S/MIME certificates and make the user `nobody` owner of these: + + mkdir -p /var/gpgmailgate/.gnupg + mkdir -p /var/gpgmailgate/smime + chown -R nobody:nogroup /var/gpgmailgate/ + +4. Place the `gpg-mailgate.py` in `/usr/local/bin/`, make the user `nobody` owner of the file and make it executable: - chmod u+x /usr/local/bin/gpg-mailgate.py - chmod u+x /usr/local/bin/register-handler.py chown nobody:nogroup /usr/local/bin/gpg-mailgate.py - chown nobody:nogroup /usr/local/bin/register-handler.py - - 6. Place the GnuPG directory in `/usr/local/lib/python2.7/dist-packages` (replace 2.7 with your - Python version) - - 7. Add the following to the end of `/etc/postfix/master.cf` + chmod u+x /usr/local/bin/gpg-mailgate.py + +5. Place the `GnuPG` directory in `/usr/local/lib/python2.7/dist-packages` (replace 2.7 with your Python 2 version) + +6. Configure `/etc/gpg-mailgate.conf` based on the provided `gpg-mailgate.conf.sample`. Change the settings according to your configuration. If you follow this guide and have a standard configuration for postfix, you don't need to change much. + +7. Add the following to the end of `/etc/postfix/master.cf` gpg-mailgate unix - n n - - pipe flags= user=nobody argv=/usr/local/bin/gpg-mailgate.py ${recipient} @@ -34,41 +58,127 @@ -o mynetworks=127.0.0.0/8 -o smtpd_authorized_xforward_hosts=127.0.0.0/8 - 8. Add the following to `/etc/postfix/main.cf` + If you use Postfix versions from 2.5 onwards, it is recommended to change `${recipient}` to `${original_recipient}` in line two of the lines above. + +8. Add the following line to `/etc/postfix/main.cf` content_filter = gpg-mailgate + +9. Restart Postfix + +You are now ready to go. To add a public key for encryption just use the following command: - 9. Add `register: |/usr/local/bin/register-handler.py` to `/etc/aliases` - - 10. Update postfix's alias database with `postalias /etc/aliases` + sudo -u nobody /usr/bin/gpg --homedir=/var/gpgmailgate/.gnupg --import /some/public.key - 11. Restart postfix. +- Replace `/some/public.key` with the location of a public key +- `/some/public.key` can be deleted after importation +- Confirm that it's working: +`sudo -u nobody /usr/bin/gpg --list-keys --homedir=/var/gpgmailgate/.gnupg` - 12. Setup a place to store public keys and certificates with these example commands: +Please also test your installation before using it. - usermod -d /var/gpg nobody +GPG-Mailgate is also able to handle S/MIME certificates for encrypting mails. However, it is best to use it in combination with Register-Handler described later to add new certificates. If you try to add them manually it might fail. The certificates are stored in `/var/gpgmailgate/smime` in PKCS7 format and are named like `User@example.com` (the user part is case sensitive, the domain part should be in lower case). - If you encounter any errors when using this command you might need to kill active processes from nobody +####Additional settings +Most mail servers do not handle mail addresses case sensitive. If you know that all your recipient mail servers do not care about case sensitivity then you can set `mail_case_insensitive` in the settings to `yes` so looking up PGP keys or S/MIME certificates does also happen case insensitive. +If your recipients have problems to decrypt mails encrypted by GPG-Mailgate they might use a piece of software that does not support PGP/MIME encrypted mails. You can tell GPG-Mailgate to use the legacy PGP/INLINE format by adding the recipient to the `pgp_style` map in the following format: +`User@example.com=inline` - mkdir -p /var/gpg/.gnupg - mkdir -p /var/smime/certs - chown -R nobody /var/gpg - chown -R nobody /var/smime - chmod 700 /var/gpg/.gnupg - sudo -u nobody /usr/bin/gpg --homedir=/var/gpg/.gnupg --import /some/public.key - - Replace `/some/public.key` with the location of a public key - - `/some/public.key` can be deleted after importation - - Confirm that it's working: `sudo -u nobody /usr/bin/gpg --list-keys --homedir=/var/gpg/.gnupg` +### Mail decryption +GPG-Mailgate does not only feature encryption of mails but also decryption of PGP encrypted mails. +#### Important notice +**Read carefully before setting up and using this functionality!** - 13. Create directories for storing email templates: +With this functionality you could use GPG-Mailgate to decrypt incoming PGP encrypted mails (it is also capable of decrypting outgoing mails if the necessary key is present). To use this, you need to store your private keys on the server. This means that anyone who is able to obtain admin rights on the server is able to get the private keys stored on the server and is able to decrypt any mail encrypted with the corresponding public key. **If the server gets compromised in any kind and the attacker may have gained access to the server's file system, the keys have to be regarded as compromised as well!** If this happens you have to revoke your keys, notify everyone who has your public key (key servers as well) not to use this key any longer. You also need to create a new key pair for encrypted communication. +#### Limitations +There are two main types of PGP encryption: PGP/MIME and PGP/INLINE. PGP/MIME is standardized while PGP/INLINE isn't completely (even though some people claim so). Decrypting PGP/MIME encrypted mails works in most cases while decrypting PGP/INLINE encrypted mails may fail more often. The reason is that most clients are implementing PGP/INLINE in their own way. GPG-Mailgate is able to decrypt mails which are encrypted PGP/INLINE by GPG-Mailgate on the sender's side. Furthermore it should be able to decrypt PGP/INLINE encrypted mails encrypted by Enigmail. For PGP/INLINE the mail's structure may not be preserved due to how PGP/INLINE is implemented on most clients. If you receive a PGP/INLINE encrypted mail that could not be decrypted by GPG-Mailgate you may ask the sender to use PGP/MIME instead. Furthermore file types might get lost when using PGP/INLINE. + +#### Setting up decryption +You need the recipient's private key for whom you want to decrypt mails. To add the private key, use the following command: +`sudo -u nobody /usr/bin/gpg --homedir=/var/gpgmailgate/.gnupg --import /some/private.key` +From now on PGP encrypted mails will be decrypted for the recipients for whom the keys are imported. + +You also can remove a private key by using the following command. Replace `user@example.com` with the user's address for whom you want to remove the key: +`sudo -u nobody /usr/bin/gpg --homedir=/var/gpgmailgate/.gnupg --delete-secret-keys user@example.com` + +## Install GPG-Mailgate-Web +### Requirements +- A webserver is installed and reachable +- The webserver is able to handle PHP scripts +- MySQL is installed +- Python 2.X is already installed + +### Installation +All files you need can be found in the [gpg-mailgate-web] (gpg-mailgate-web/) directory. + +1. Install the Python-mysqldb and Python-markdown modules: + + apt-get install python-mysqldb python-markdown + +2. Create a new database for GPG-Mailgate-Web. + +3. Import the schema file `schema.sql` into the newly created database. + +4. Edit the config file located at `/etc/gpg-mailgate.conf`. Set `enabled = yes` in `[database]` and fill in the necessary settings for the database connection. + +5. Copy the files located in the [public_html] (gpg-mailgate-web/public_html) directory onto your webserver. They can also be placed in a subdirectory on your webserver. + +6. On your webserver move the `config.sample.php` file to `config.php` and edit the configuration file. + +7. Create directories for storing email templates: + + mkdir -p /var/gpgmailgate/cron_templates + +8. Copy the templates found in the [cron_templates] (cron_templates/) directory into the newly created directory and transfer ownership: + + chown -R nobody:nogroup /var/gpgmailgate/cron_templates + +9. Copy `cron.py` to `/usr/local/bin/gpgmw-cron.py`. Make it executable and and transfer ownership to `nobody`: + + chown nobody:nogroup /usr/local/bin/gpgmw-cron.py + chmod u+x /usr/local/bin/gpgmw-cron.py + +10. Create `/etc/cron.d/gpgmw` with contents: +`*/3 * * * * nobody /usr/bin/python /usr/local/bin/gpgmw-cron.py > /dev/null` + for executing the cron job automatically. + +11. Test your installation. + +### GPG-Mailgate-Web as keyserver +GPG-Mailgate-Web can also be used as a keyserver. For more information have a look at GPG-Mailgate-Web's [readme] (gpg-mailgate-web/README). + +## Install Register-handler +### Requirements +- Already set up and working GPG-Mailgate-Web. It should be reachable from the machine that will run register-handler +- Postfix is already installed and configured. It is recommended that you have already tested your configuration so we can exclude this as a main cause of problems. Your Postfix configuration should also support aliases + +### Installation + +1. Install the Python-requests module: + + apt-get install python-requests + +2. Create directories for storing email templates: + mkdir -p /var/gpgmailgate/register_templates - mkdir -p /var/gpgmailgate/cron_templates - chown -R nobody /var/gpgmailgate + +3. Copy the templates found in the [register_templates] (register_templates/) directory into the newly created directory and transfer ownership: - - Place the corresponding directories from this project in the created ones - - Edit them if you want to + chown -R nobody:nogroup /var/gpgmailgate/register_templates + +4. Copy `register-handler.py` to `/usr/local/bin/register-handler.py`. Make it executable and own it to `nobody`: - 14. [Install gpg-mailgate-web] (gpg-mailgate-web/README) - \ No newline at end of file + chown nobody:nogroup /usr/local/bin/register-handler.py + chmod a+x /usr/local/bin/register-handler.py + +5. Edit the config file located at `/etc/gpg-mailgate.conf`. Set the parameter `webpanel_url` in `[mailregister]` to the url of your GPG-Mailgate-Web panel (the URL should be the same as the one you use to access the panel with your web browser). Also set the parameter `register_email` to the email address you want the user to see when receiving mails from the register-handler (it does not have to be an existing address but it is recommended). Register-handler will send users mails when they are registering S/MIME certificates or when neither a S/MIME certificate nor a PGP key was found in a mail sent to the register-handler. + +6. Add `register: |/usr/local/bin/register-handler.py` to `/etc/aliases` + +7. Update postfix's alias database with `postalias /etc/aliases` + +8. Restart postfix. + +9. Test your installation. diff --git a/README.md b/README.md index ffa12a5..31cfb5e 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ **Please note: This fork is currently WIP. It is not recommended for use at the moment.** -gpg-mailgate is a content filter for Postfix that automatically encrypts unencrypted incoming email using PGP or S/MIME for select recipients. +gpg-mailgate is a content filter for Postfix that automatically encrypts unencrypted incoming email using PGP or S/MIME for select recipients. It is also able to decrypt incoming PGP mails. For installation instructions, please refer to the included INSTALL file. @@ -10,6 +10,7 @@ For installation instructions, please refer to the included INSTALL file. - Correctly displays attachments and general email content; currently will only display first part of multipart messages - Public keys are stored in a dedicated gpg-home-directory - Encrypts both matching incoming and outgoing mail (this means gpg-mailgate can be used to encrypt outgoing mail for software that doesn't support PGP or S/MIME) +- Decrypt PGP encrypted mails for present private keys (but no signature check and it does not always work with PGP/INLINE encrypted mails) - Easy installation - gpg-mailgate-web extension is a web interface allowing any user to upload PGP keys so that emails sent to them from your mail server will be encrypted (see gpg-mailgate-web directory for details) - people can submit their public key like to any keyserver to gpg-mailgate with the gpg-mailgate-web extension @@ -37,7 +38,7 @@ This is a combined work of many developers and contributors: # To Do * clean up code -* rewrite and improve installation instructions -* rewrite readme of gpg-mailgate-web in markdown * rename from gpg-mailgate to openpgp-s-mime-mailgate or something..... +* find a better solution for an own user instead of the user `nobody` +* make PGP/INLINE decryption more reliable * even more magical stuff diff --git a/gpg-mailgate.conf.sample b/gpg-mailgate.conf.sample index 7797a71..8a81801 100644 --- a/gpg-mailgate.conf.sample +++ b/gpg-mailgate.conf.sample @@ -1,12 +1,22 @@ [default] -# whether gpg-mailgate should add a header after it has processed an email -# this may be useful for debugging purposes +# Whether gpg-mailgate should add a header after it has processed an email +# This may be useful for debugging purposes add_header = yes -# whether we should only sign emails if they are explicitly defined in -# the key mappings below ([keymap] section) -# this means gpg-mailgate won't automatically detect PGP recipients -keymap_only = no +# Whether we should only encrypt emails if they are explicitly defined in +# the key mappings below ([enc_keymap] section) +# This means gpg-mailgate won't automatically detect PGP recipients for encrypting +enc_keymap_only = no + +# Whether we should only decrypt emails if they are explicitly defined in +# the key mappings below ([dec_keymap] section) +# This means gpg-mailgate won't automatically detect PGP recipients for decrypting +dec_keymap_only = no + +# If dec_keymap_only is set to yes and recipients have private keys present for decrypting +# but are not on in the keymap, this can cause that mails for them will be +# encrypted. Set this to no if you want this behaviour. +failsave_dec = yes # Convert encrypted text/plain email to MIME-attached encrypt style. # (Default is to use older inline-style PGP encoding.) @@ -22,11 +32,11 @@ mail_case_insensitive = no [gpg] # the directory where gpg-mailgate public keys are stored # (see INSTALL for details) -keyhome = /var/gpg/.gnupg +keyhome = /var/gpgmailgate/.gnupg [smime] # the directory for the S/MIME certificate files -cert_path = /var/smime/certs +cert_path = /var/gpgmailgate/smime [mailregister] # settings for the register-handler @@ -62,7 +72,7 @@ host = localhost username = gpgmw password = password -[keymap] +[enc_keymap] # You can find these by running the following command: # gpg --list-keys --keyid-format long user@example.com # Which will return output similar to: @@ -72,6 +82,16 @@ password = password # You want the AAAAAAAAAAAAAAAA not BBBBBBBBBBBBBBBB. #you@domain.tld = 12345678 +[dec_keymap] +# You can find these by running the following command: +# gpg --list-secret-keys --keyid-format long user@example.com +# Which will return output similar to: +# sec 1024D/AAAAAAAAAAAAAAAA 2007-10-22 +# uid Joe User +# ssb 2048g/BBBBBBBBBBBBBBBB 2007-10-22 +# You want the AAAAAAAAAAAAAAAA not BBBBBBBBBBBBBBBB. +#you@domain.tld = 12345678 + [pgp_style] # Here a PGP style (inline or PGP/MIME) could be defined for recipients. # This overwrites the setting mime_conversion for the defined recipients. diff --git a/gpg-mailgate.py b/gpg-mailgate.py index eb11a7e..5509a05 100755 --- a/gpg-mailgate.py +++ b/gpg-mailgate.py @@ -22,17 +22,18 @@ from ConfigParser import RawConfigParser from email.mime.base import MIMEBase from email.mime.multipart import MIMEMultipart +import copy import email import email.message -import re +import email.utils import GnuPG +import os +import re import smtplib import sys import syslog import traceback -import email.utils -import os -import copy + # imports for S/MIME from M2Crypto import BIO, Rand, SMIME, X509 @@ -47,7 +48,7 @@ for sect in _cfg.sections(): for (name, value) in _cfg.items(sect): cfg[sect][name] = value -def log(msg): +def log( msg ): if 'logging' in cfg and 'file' in cfg['logging']: if cfg['logging'].get('file') == "syslog": syslog.syslog(syslog.LOG_INFO | syslog.LOG_MAIL, msg) @@ -64,6 +65,218 @@ raw_message = email.message_from_string( raw ) from_addr = raw_message['From'] to_addrs = sys.argv[1:] +def gpg_decrypt( raw_message, recipients ): + + gpg_to = list() + ungpg_to = list() + + # This is needed to avoid encryption if decryption is set to keymap only, + # private key is present but not in keymap. + noenc_to = list() + + if not get_bool_from_cfg('gpg', 'keyhome'): + log("No valid entry for gpg keyhome. Decryption aborted.") + return recipients + + keys = GnuPG.private_keys( cfg['gpg']['keyhome'] ) + + for fingerprint in keys: + keys[fingerprint] = sanitize_case_sense(keys[fingerprint]) + + for to in recipients: + if to in keys.values() and not get_bool_from_cfg('default', 'dec_keymap_only', 'yes'): + gpg_to.append(to) + elif get_bool_from_cfg('dec_keymap', to): + log("Decrypt keymap has key '%s'" % cfg['dec_keymap'][to] ) + # Check we've got a matching key! If not, decline to attempt decryption. The key is checked for safty reasons. + if not cfg['dec_keymap'][to] in keys: + log("Key '%s' in decryption keymap not found in keyring for email address '%s'. Won't decrypt." % (cfg['dec_keymap'][to], to)) + # Avoid unwanted encryption if set + if to in keys.values() and get_bool_from_cfg('default', 'failsave_dec', 'yes'): + noenc_to.append(to) + else: + ungpg_to.append(to) + else: + gpg_to.append(to) + else: + if verbose: + log("Recipient (%s) not in PGP domain list for decrypting." % to) + # Avoid unwanted encryption if set + if to in keys.values() and get_bool_from_cfg('default', 'failsave_dec', 'yes'): + noenc_to.append(to) + else: + ungpg_to.append(to) + + if gpg_to != list(): + send_msg( gpg_decrypt_all_payloads( raw_message ).as_string(), gpg_to ) + + if noenc_to != list(): + log("Do not try to encrypt mails for: %s" % ', '.join( noenc_to )) + send_msg(raw_message.as_string(), noenc_to) + + return ungpg_to + +def gpg_decrypt_all_payloads( message ): + + # We don't want to modify the original message + decrypted_message = copy.deepcopy(message) + + # Check if message is PGP/MIME encrypted + if not (message.get_param('protocol') is None) and message.get_param('protocol') == 'application/pgp-encrypted' and message.is_multipart(): + decrypted_message = decrypt_mime(decrypted_message) + + # At this point the message could only be PGP/INLINE encrypted, unencrypted or + # encrypted with a mechanism not covered by GPG-Mailgate + else: + # Check if message is PGP/INLINE encrypted and has attachments (or unencrypted with attachments) + if message.is_multipart(): + + # Set message's payload to list so payloads can be attached later on + decrypted_message.set_payload(list()) + + # We only need to hand over the original message here. Not needed for other decrypt implementations. + decrypted_message, success = decrypt_inline_with_attachments(message, False, decrypted_message) + + # Add header here to avoid it being appended several times + if get_bool_from_cfg('default', 'add_header', 'yes') and success: + decrypted_message['X-GPG-Mailgate'] = 'Decrypted by GPG Mailgate' + + # Check if message is PGP/INLINE encrypted without attachments (or unencrypted without attachments) + else: + decrypted_message = decrypt_inline_without_attachments(decrypted_message) + + return decrypted_message + +def decrypt_mime( decrypted_message ): + # Please note: Signatures will disappear while decrypting and will not be checked + + # Getting the part which should be PGP encrypted (according to RFC) + msg_content = decrypted_message.get_payload(1).get_payload() + + if "-----BEGIN PGP MESSAGE-----" in msg_content and "-----END PGP MESSAGE-----" in msg_content: + start = msg_content.find("-----BEGIN PGP MESSAGE-----") + end = msg_content.find("-----END PGP MESSAGE-----") + decrypted_payload, decrypt_success = decrypt_payload(msg_content[start:end + 25]) + + if decrypt_success: + # Making decrypted_message a "normal" unencrypted message + decrypted_message.del_param('protocol') + decrypted_message.set_type(decrypted_payload.get_content_type()) + + # Restore Content-Disposition header from original message + if not (decrypted_payload.get('Content-Disposition') is None): + if not (decrypted_message.get('Content-Disposition') is None): + decrypted_message.replace_header('Content-Disposition', decrypted_payload.get('Content-Disposition')) + else: + decrypted_message.set_param(decrypted_payload.get('Content-Disposition'), "", 'Content-Disposition') + + if decrypted_payload.is_multipart(): + # Clear message's original payload and insert the decrypted payloads + decrypted_message.set_payload(list()) + decrypted_message = generate_message_from_payloads( decrypted_payload, decrypted_message ) + decrypted_message.preamble = "BLA" + else: + decrypted_message.set_payload(decrypted_payload.get_payload()) + decrypted_message.preamble = None + + + if get_bool_from_cfg('default', 'add_header', 'yes'): + decrypted_message['X-GPG-Mailgate'] = 'Decrypted by GPG Mailgate' + + # If decryption fails, decrypted_message is equal to the original message + return decrypted_message + +def decrypt_inline_with_attachments( payloads, success, message = None ): + + if message is None: + message = email.mime.multipart.MIMEMultipart(payloads.get_content_subtype()) + + for payload in payloads.get_payload(): + if( type( payload.get_payload() ) == list ): + # Take care of cascaded MIME messages + submessage, subsuccess = decrypt_inline_with_attachments( payload, success ) + message.attach(submessage) + success = success or subsuccess + else: + msg_content = payload.get_payload() + + # Getting values for different implementations as PGP/INLINE is not implemented + # the same on different clients + pgp_inline_tags = "-----BEGIN PGP MESSAGE-----" in msg_content and "-----END PGP MESSAGE-----" in msg_content + attachment_filename = payload.get_filename() + + if pgp_inline_tags or not (attachment_filename is None) and not (re.search('.\.pgp$', attachment_filename) is None): + if pgp_inline_tags: + start = msg_content.find("-----BEGIN PGP MESSAGE-----") + end = msg_content.find("-----END PGP MESSAGE-----") + decrypted_payload, decrypt_success = decrypt_payload(msg_content[start:end + 25]) + # Some implementations like Enigmail have strange interpretations of PGP/INLINE + # This tries to cope with it as good as possible. + else: + build_message = """ +-----BEGIN PGP MESSAGE----- + +%s +-----END PGP MESSAGE-----""" % msg_content + + decrypted_payload, decrypt_success = decrypt_payload(build_message) + + # Was at least one decryption successful? + success = success or decrypt_success + + if decrypt_success: + + if not (attachment_filename is None): + attachment_filename = re.sub('\.pgp$', '', attachment_filename) + payload.set_param('filename', attachment_filename, 'Content-Disposition') + payload.set_param('name', attachment_filename, 'Content-Type') + # Need this nasty hack to avoid double blank lines at beginning of message + payload.set_payload(decrypted_payload.as_string()[1:]) + + message.attach(payload) + else: + # Message could not be decrypted, so non-decrypted message is attached + message.attach(payload) + else: + # There was no encrypted payload found, so the original payload is attached + message.attach(payload) + + return message, success + +def decrypt_inline_without_attachments( decrypted_message ): + + msg_content = decrypted_message.get_payload() + if "-----BEGIN PGP MESSAGE-----" in msg_content and "-----END PGP MESSAGE-----" in msg_content: + start = msg_content.find("-----BEGIN PGP MESSAGE-----") + end = msg_content.find("-----END PGP MESSAGE-----") + decrypted_payload, decrypt_success = decrypt_payload(msg_content[start:end + 25]) + + if decrypt_success: + # Need this nasty hack to avoid double blank lines at beginning of message + decrypted_message.set_payload(decrypted_payload.as_string()[1:]) + + if get_bool_from_cfg('default', 'add_header', 'yes'): + decrypted_message['X-GPG-Mailgate'] = 'Decrypted by GPG Mailgate' + + # If message was not encrypted, this will just return the original message + return decrypted_message + +def decrypt_payload( payload ): + + gpg = GnuPG.GPGDecryptor( cfg['gpg']['keyhome'] ) + gpg.update( payload ) + decrypted_data, returncode = gpg.decrypt() + if verbose: + log("Return code from decryption=%d (0 indicates success)." % returncode) + if returncode != 0: + log("Decrytion failed with return code %d. Decryption aborted." % returncode) + return payload, False + + # Decryption always generate a new message + decrypted_msg = email.message_from_string(decrypted_data) + + return decrypted_msg, True + def gpg_encrypt( raw_message, recipients ): if not get_bool_from_cfg('gpg', 'keyhome'): @@ -80,25 +293,26 @@ def gpg_encrypt( raw_message, recipients ): for to in recipients: # Check if recipient is in keymap - if get_bool_from_cfg('keymap', to): - log("Keymap has key '%s'" % cfg['keymap'][to]) + if get_bool_from_cfg('enc_keymap', to): + log("Encrypt keymap has key '%s'" % cfg['enc_keymap'][to] ) # Check we've got a matching key! - if cfg['keymap'][to] in keys: - gpg_to.append( (to, cfg['keymap'][to]) ) + if cfg['enc_keymap'][to] in keys: + gpg_to.append( (to, cfg['enc_keymap'][to]) ) continue else: - log("Key '%s' in keymap not found in keyring for email address '%s'." % (cfg['keymap'][to], to)) + log("Key '%s' in encrypt keymap not found in keyring for email address '%s'." % (cfg['enc_keymap'][to], to)) - if to in keys.values() and not get_bool_from_cfg('default', 'keymap_only', 'yes'): + if to in keys.values() and not get_bool_from_cfg('default', 'enc_keymap_only', 'yes'): gpg_to.append( (to, to) ) else: if verbose: - log("Recipient (%s) not in PGP domain list." % to) + log("Recipient (%s) not in PGP domain list for encrypting." % to) ungpg_to.append(to) if gpg_to != list(): log("Encrypting email to: %s" % ' '.join( map(lambda x: x[0], gpg_to) )) + # Getting PGP style for recipient gpg_to_smtp_mime = list() gpg_to_cmdline_mime = list() @@ -106,6 +320,7 @@ def gpg_encrypt( raw_message, recipients ): gpg_to_cmdline_inline = list() for rcpt in gpg_to: + # Checking pre defined styles in settings first if get_bool_from_cfg('pgp_style', rcpt[0], 'mime'): gpg_to_smtp_mime.append(rcpt[0]) gpg_to_cmdline_mime.extend(rcpt[1].split(',')) @@ -117,6 +332,7 @@ def gpg_encrypt( raw_message, recipients ): if get_bool_from_cfg('pgp_style', rcpt[0]): log("Style %s for recipient %s is not known. Use default as fallback." % (cfg['pgp_style'][rcpt[0]], rcpt[0])) + # If no style is in settings defined for recipient, use default from settings if get_bool_from_cfg('default', 'mime_conversion', 'yes'): gpg_to_smtp_mime.append(rcpt[0]) gpg_to_cmdline_mime.extend(rcpt[1].split(',')) @@ -125,6 +341,7 @@ def gpg_encrypt( raw_message, recipients ): gpg_to_cmdline_inline.extend(rcpt[1].split(',')) if gpg_to_smtp_mime != list(): + # Encrypt mail with PGP/MIME raw_message_mime = copy.deepcopy(raw_message) if get_bool_from_cfg('default', 'add_header', 'yes'): @@ -136,6 +353,7 @@ def gpg_encrypt( raw_message, recipients ): send_msg( raw_message_mime.as_string(), gpg_to_smtp_mime ) if gpg_to_smtp_inline != list(): + # Encrypt mail with PGP/INLINE raw_message_inline = copy.deepcopy(raw_message) if get_bool_from_cfg('default', 'add_header', 'yes'): @@ -150,6 +368,7 @@ def gpg_encrypt( raw_message, recipients ): def encrypt_all_payloads_inline( message, gpg_to_cmdline ): + # This breaks cascaded MIME messages. Blame PGP/INLINE. encrypted_payloads = list() if type( message.get_payload() ) == str: return encrypt_payload( message, gpg_to_cmdline ).get_payload() @@ -337,18 +556,18 @@ def sanitize_case_sense( address ): return address -def generate_message_from_payloads( payloads, submsg = None ): +def generate_message_from_payloads( payloads, message = None ): - if submsg == None: - submsg = email.mime.multipart.MIMEMultipart(payloads.get_content_subtype()) + if message == None: + message = email.mime.multipart.MIMEMultipart(payloads.get_content_subtype()) for payload in payloads.get_payload(): if( type( payload.get_payload() ) == list ): - submsg.attach(attach_payload_list_to_message(payload, email.mime.multipart.MIMEMultipart(payload.get_content_subtype()))) + message.attach(generate_message_from_payloads(payload)) else: - submsg.attach(payload) + message.attach(payload) - return submsg + return message def get_first_payload( payloads ): @@ -377,6 +596,11 @@ def sort_recipients( raw_message, from_addr, to_addrs ): for recipient in to_addrs: recipients_left.append(sanitize_case_sense(recipient)) + # Decrypt mails for recipients with known private PGP keys + recipients_left = gpg_decrypt(raw_message, recipients_left) + if recipients_left == list(): + return + # There is no need for nested encryption first_payload = get_first_payload(raw_message) if first_payload.get_content_type() == 'application/pkcs7-mime': @@ -398,7 +622,7 @@ def sort_recipients( raw_message, from_addr, to_addrs ): send_msg(raw_message.as_string(), recipients_left) return - # Encrypt mails for recipients with known PGP keys + # Encrypt mails for recipients with known public PGP keys recipients_left = gpg_encrypt(raw_message, recipients_left) if recipients_left == list(): return