Compare commits

..

2 Commits

Author SHA1 Message Date
muppeth e9ad17e7bc Merge branch 'master' into php_update 2022-03-15 10:37:25 +00:00
muppeth c6b0f921c9
compatibility update for php7/8 2021-10-21 01:52:48 +02:00
69 changed files with 2317 additions and 2823 deletions

View File

@ -1,24 +1,22 @@
#
# gpg-mailgate
# gpg-mailgate
#
# This file is part of the gpg-mailgate source code.
# This file is part of the gpg-mailgate source code.
#
# gpg-mailgate is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# gpg-mailgate is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# gpg-mailgate source code is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
# gpg-mailgate source code is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with gpg-mailgate source code. If not, see <http://www.gnu.org/licenses/>.
# You should have received a copy of the GNU General Public License
# along with gpg-mailgate source code. If not, see <http://www.gnu.org/licenses/>.
#
"""GnuPG wrapper module."""
import os
import os.path
import subprocess
@ -26,7 +24,6 @@ import shutil
import random
import string
import sys
import logging
LINE_FINGERPRINT = 'fpr'
@ -34,164 +31,137 @@ LINE_USER_ID = 'uid'
POS_FINGERPRINT = 9
LOG = logging.getLogger(__name__)
def build_command(key_home, *args, **kwargs):
cmd = ["gpg", '--homedir', key_home] + list(args)
return cmd
def private_keys( keyhome ):
cmd = build_command(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 _build_command(key_home, *args, **kwargs):
cmd = ["gpg", '--homedir', key_home] + list(args)
return cmd
def public_keys( keyhome ):
cmd = build_command(keyhome, '--list-keys', '--with-colons')
p = subprocess.Popen( cmd, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE )
p.wait()
keys = dict()
fingerprint = None
email = None
for line in p.stdout.readlines():
line = line.decode('utf-8')
if line[0:3] == LINE_FINGERPRINT:
fingerprint = line.split(':')[POS_FINGERPRINT]
if line[0:3] == LINE_USER_ID:
if ('<' not in line or '>' not in line):
continue
email = line.split('<')[1].split('>')[0]
if not (fingerprint is None or email is None):
keys[fingerprint] = email
fingerprint = None
email = None
return keys
def public_keys(keyhome):
"""List public keys from keyring KEYHOME."""
cmd = _build_command(keyhome, '--list-keys', '--with-colons')
p = subprocess.Popen(cmd, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
p.wait()
# confirms a key has a given email address
def confirm_key( content, email ):
tmpkeyhome = ''
keys = dict()
fingerprint = None
email = None
for line in p.stdout.readlines():
line = line.decode(sys.getdefaultencoding())
if line[0:3] == LINE_FINGERPRINT:
fingerprint = line.split(':')[POS_FINGERPRINT]
if line[0:3] == LINE_USER_ID:
if ('<' not in line or '>' not in line):
continue
email = line.split('<')[1].split('>')[0]
if not (fingerprint is None or email is None):
keys[fingerprint] = email
fingerprint = None
email = None
while True:
tmpkeyhome = '/tmp/' + ''.join(random.choice(string.ascii_uppercase + string.digits) for x in range(12))
if not os.path.exists(tmpkeyhome):
break
p.stdout.close()
p.stderr.close()
return keys
os.mkdir(tmpkeyhome)
localized_env = os.environ.copy()
localized_env["LANG"] = "C"
p = subprocess.Popen( build_command(tmpkeyhome, '--import', '--batch'), stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=localized_env )
result = p.communicate(input=content)[1]
confirmed = False
for line in result.split("\n"):
if 'imported' in line and '<' in line and '>' in line:
if line.split('<')[1].split('>')[0].lower() == email.lower():
confirmed = True
break
else:
break # confirmation failed
def _to_bytes(s) -> bytes:
if isinstance(s, str):
return bytes(s, sys.getdefaultencoding())
else:
return s
# Confirms a key has a given email address by importing it into a temporary
# keyring. If this operation succeeds and produces a message mentioning the
# expected email, a key is confirmed.
def confirm_key(content, email):
"""Verify that the key CONTENT is assigned to identity EMAIL."""
tmpkeyhome = ''
content = _to_bytes(content)
expected_email = _to_bytes(email.lower())
while True:
tmpkeyhome = '/tmp/' + ''.join(random.choice(string.ascii_uppercase + string.digits) for x in range(12))
if not os.path.exists(tmpkeyhome):
break
# let only the owner access the directory, otherwise gpg would complain
os.mkdir(tmpkeyhome, mode=0o700)
localized_env = os.environ.copy()
localized_env["LANG"] = "C"
p = subprocess.Popen(_build_command(tmpkeyhome, '--import', '--batch'), stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=localized_env)
result = p.communicate(input=content)[1]
confirmed = False
for line in result.split(b"\n"):
if b'imported' in line and b'<' in line and b'>' in line:
if line.split(b'<')[1].split(b'>')[0].lower() == expected_email:
confirmed = True
break
else:
break # confirmation failed
# cleanup
shutil.rmtree(tmpkeyhome)
return confirmed
# cleanup
shutil.rmtree(tmpkeyhome)
return confirmed
# adds a key and ensures it has the given email address
def add_key(keyhome, content):
"""Register new key CONTENT in the keyring KEYHOME."""
if isinstance(content, str):
content = bytes(content, sys.getdefaultencoding())
p = subprocess.Popen(_build_command(keyhome, '--import', '--batch'), stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
p.communicate(input=content)
p.wait()
def add_key( keyhome, content ):
p = subprocess.Popen( build_command(keyhome, '--import', '--batch'), stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE )
p.communicate(input=content)
p.wait()
def delete_key( keyhome, email ):
from email.utils import parseaddr
result = parseaddr(email)
def delete_key(keyhome, email):
"""Remove key assigned to identity EMAIL from keyring KEYHOME."""
from email.utils import parseaddr
result = parseaddr(email)
if result[1]:
# delete all keys matching this email address
p = subprocess.Popen(_build_command(keyhome, '--delete-key', '--batch', '--yes', result[1]), stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
p.wait()
return True
return False
if result[1]:
# delete all keys matching this email address
p = subprocess.Popen( build_command(keyhome, '--delete-key', '--batch', '--yes', result[1]), stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE )
p.wait()
return True
return False
class GPGEncryptor:
"""A wrapper for 'gpg -e' command."""
def __init__(self, keyhome, recipients = None, charset = None):
self._keyhome = keyhome
self._message = b''
self._recipients = list()
self._charset = charset
if recipients != None:
self._recipients.extend(recipients)
def __init__(self, keyhome, recipients=None, charset=None):
"""Initialise the wrapper."""
self._keyhome = keyhome
self._message = b''
self._recipients = list()
self._charset = charset
if recipients is not None:
self._recipients.extend(recipients)
def update(self, message):
self._message += message
def update(self, message):
"""Append MESSAGE to buffer about to be encrypted."""
self._message += message
def encrypt(self):
p = subprocess.Popen( self._command(), stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE )
encdata = p.communicate(input=self._message)[0]
return (encdata, p.returncode)
def encrypt(self):
"""Feed GnuPG with the message."""
p = subprocess.Popen(self._command(), stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
encdata = p.communicate(input=self._message)[0]
return (encdata, p.returncode)
def _command(self):
cmd = build_command(self._keyhome, "--trust-model", "always", "--batch", "--yes", "--pgp7", "--no-secmem-warning", "-a", "-e")
def _command(self):
cmd = _build_command(self._keyhome, "--trust-model", "always", "--batch", "--yes", "--pgp7", "--no-secmem-warning", "-a", "-e")
# add recipients
for recipient in self._recipients:
cmd.append("-r")
cmd.append(recipient)
# add recipients
for recipient in self._recipients:
cmd.append("-r")
cmd.append(recipient)
# add on the charset, if set
if self._charset:
cmd.append("--comment")
cmd.append('Charset: ' + self._charset)
LOG.debug(f'Built command: {cmd!r}')
return cmd
# add on the charset, if set
if self._charset:
cmd.append("--comment")
cmd.append('Charset: ' + self._charset)
return cmd
class GPGDecryptor:
"""A wrapper for 'gpg -d' command."""
def __init__(self, keyhome):
self._keyhome = keyhome
self._message = ''
def __init__(self, keyhome):
"""Initialise the wrapper."""
self._keyhome = keyhome
self._message = ''
def update(self, message):
self._message += message
def update(self, message):
"""Append encrypted content to be decrypted."""
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 decrypt(self):
"""Decrypt the message."""
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):
return _build_command(self._keyhome, "--trust-model", "always", "--batch", "--yes", "--no-secmem-warning", "-a", "-d")
def _command(self):
return build_command(self._keyhome, "--trust-model", "always", "--batch", "--yes", "--no-secmem-warning", "-a", "-d")

View File

@ -1,14 +1,11 @@
# Installation instructions
## Content
- General information
- Install GPG-Mailgate
- Install GPG-Mailgate-Web
- Install Register-handler
## 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.
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.
@ -16,10 +13,8 @@ These instructions show you how to set up GPG-Mailgate in an easy way. If you ar
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).
## Install GPG-Mailgate
### Requirements
- Python 3.x is already installed
- 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
@ -27,133 +22,104 @@ These instructions are based on an installation on an Ubuntu 14.04 LTS virtual m
1. Install the Python-M2Crypto module:
```
apt-get install python-m2crypto
```
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
```
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/
```
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:
```
chown nobody:nogroup /usr/local/bin/gpg-mailgate.py
chmod u+x /usr/local/bin/gpg-mailgate.py
```
chown nobody:nogroup /usr/local/bin/gpg-mailgate.py
chmod u+x /usr/local/bin/gpg-mailgate.py
5. Place the `GnuPG` directory in `/usr/local/lib/python3.x/dist-packages` (replace 3.x with your Python version)
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.
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. Configure logging by copying `gpg-lacre-logging.conf.sample` to `/etc/gpg-lacre-logging.conf` and editing it according to your needs. The path to this file is included in `[logging]` section of `gpg-mailgate.conf` file, so if you place it somewhere else, make sure to update the path too. See also: [Configuration file format](https://docs.python.org/3/library/logging.config.html#configuration-file-format).
7. Add the following to the end of `/etc/postfix/master.cf`
8. 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}
```
gpg-mailgate unix - n n - - pipe
flags= user=nobody argv=/usr/local/bin/gpg-mailgate.py ${recipient}
127.0.0.1:10028 inet n - n - 10 smtpd
-o content_filter=
-o receive_override_options=no_unknown_recipient_checks,no_header_body_checks
-o smtpd_helo_restrictions=
-o smtpd_client_restrictions=
-o smtpd_sender_restrictions=
-o smtpd_recipient_restrictions=permit_mynetworks,reject
-o mynetworks=127.0.0.0/8
-o smtpd_authorized_xforward_hosts=127.0.0.0/8
127. 0. 0. 1:10028 inet n - n - 10 smtpd
-o content_filter=
-o receive_override_options=no_unknown_recipient_checks,no_header_body_checks
-o smtpd_helo_restrictions=
-o smtpd_client_restrictions=
-o smtpd_sender_restrictions=
-o smtpd_recipient_restrictions=permit_mynetworks,reject
-o mynetworks=127. 0. 0. 0/8
-o smtpd_authorized_xforward_hosts=127. 0. 0. 0/8
```
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.
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`
9. Add the following line to `/etc/postfix/main.cf`
content_filter = gpg-mailgate
```
content_filter = gpg-mailgate
```
9. Optional: GPG can automatically download new public keys for automatic signature verification. To enable automatic create the file `/var/gpgmailgate/.gnupg/gpg.conf`. Add the following line to the file:
10. Optional: GPG can automatically download new public keys for automatic signature verification. To enable automatic create the file `/var/gpgmailgate/.gnupg/gpg.conf`. Add the following line to the file:
keyserver-options auto-key-retrieve
```
keyserver-options auto-key-retrieve
```
11. Restart Postfix
10. Restart Postfix
You are now ready to go. To add a public key for encryption just use the following command:
```
sudo -u nobody /usr/bin/gpg --homedir=/var/gpgmailgate/.gnupg --import /some/public.key
```
sudo -u nobody /usr/bin/gpg --homedir=/var/gpgmailgate/.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:
- Confirm that it's working:
`sudo -u nobody /usr/bin/gpg --list-keys --homedir=/var/gpgmailgate/.gnupg`
Please also test your installation before using it.
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).
#### Additional settings
####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:
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`
### 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!**
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 clear standardized (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. Due to this limitations decrypting PGP/INLINE encrypted mails is disabled by default. If you want to take the risk you can set `no_inline_dec` to `no` in the `[default]` section. You have been warned.
#### Setting up decryption
You need the recipient's private key for whom you want to decrypt mails. Only unprotected keys are supported. Keys protected by a passphrase could not be used. To add the private key, use the following command:
You need the recipient's private key for whom you want to decrypt mails. Only unprotected keys are supported. Keys protected by a passphrase could not be used. 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:
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 3.x is already installed
- Python 2.X is already installed
### Installation
All files you need can be found in the [gpg-mailgate-web](gpg-mailgate-web/) directory.
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
```
apt-get install python-mysqldb python-markdown
2. Create a new database for GPG-Mailgate-Web.
@ -161,43 +127,34 @@ apt-get install python-mysqldb python-markdown
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.
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:
```
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
```
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
```
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`
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).
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
@ -205,29 +162,21 @@ GPG-Mailgate-Web can also be used as a keyserver. For more information have a lo
1. Install the Python-requests module:
```
apt-get install python-requests
```
apt-get install python-requests
2. Create directories for storing email templates:
mkdir -p /var/gpgmailgate/register_templates
3. Copy the templates found in the [register_templates] (register_templates/) directory into the newly created directory and transfer ownership:
```
mkdir -p /var/gpgmailgate/register_templates
```
3. Copy the templates found in the [register_templates](register_templates/) directory into the newly created directory and transfer ownership:
```
chown -R nobody:nogroup /var/gpgmailgate/register_templates
```
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`:
```
chown nobody:nogroup /usr/local/bin/register-handler.py
chmod a+x /usr/local/bin/register-handler.py
```
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`
@ -236,4 +185,4 @@ chmod a+x /usr/local/bin/register-handler.py
8. Restart postfix.
9. Test your installation.
9. Test your installation.

View File

@ -1,5 +1,5 @@
.POSIX:
.PHONY: test e2etest unittest crontest daemontest pre-clean clean restore-keyhome
.PHONY: test unittest pre-clean clean
#
# On systems where Python 3.x binary has a different name, just
@ -12,13 +12,6 @@
#
PYTHON = python3
TEST_DB = test/lacre.db
#
# Main goal to run tests.
#
test: e2etest unittest daemontest crontest
#
# Run a set of end-to-end tests.
#
@ -26,46 +19,19 @@ test: e2etest unittest daemontest crontest
# file. Basically this is just a script that feeds GPG Mailgate with
# known input and checks whether output meets expectations.
#
e2etest: test/tmp test/logs pre-clean restore-keyhome
test: test/tmp test/logs pre-clean
$(PYTHON) test/e2e_test.py
#
# Run a basic cron-job test.
#
# We use PYTHONPATH to make sure that cron.py can import GnuPG
# package. We also set GPG_MAILGATE_CONFIG env. variable to make sure
# it slurps the right config.
#
crontest: clean-db $(TEST_DB)
GPG_MAILGATE_CONFIG=test/gpg-mailgate-cron-test.conf PYTHONPATH=`pwd` $(PYTHON) webgate-cron.py
$(TEST_DB):
$(PYTHON) test/utils/schema.py $(TEST_DB)
#
# Run an e2e test of Advanced Content Filter.
#
daemontest:
$(PYTHON) test/daemon_test.py
# Before running the crontest goal we need to make sure that the
# database gets regenerated.
clean-db:
rm -f $(TEST_DB)
#
# Run unit tests
#
unittest:
$(PYTHON) -m unittest discover -s test/modules
$(PYTHON) -m unittest discover -s test
pre-clean:
rm -fv test/gpg-mailgate.conf
rm -f test/logs/*.log
restore-keyhome:
git restore test/keyhome
test/tmp:
mkdir test/tmp

View File

@ -1,10 +1,6 @@
# GPG Lacre Project
GPG Lacre is a fork and continuation of original work of gpg-mailgate project:
[gpg-mailgate](https://github.com/TheGreatGooo/gpg-mailgate). It is still
actively developed and should be considered as beta -- with all APIs and
internals being subject to change. Please only use this software if you know
GnuPG well.
Fork and continuation of original work of gpg-mailgate project: https://github.com/fkrone/gpg-mailgate
**GPG Lacre** (wax seal in Portuguese) is a content filter for Postfix that automatically encrypts unencrypted incoming email using PGP or S/MIME for select recipients.
@ -30,13 +26,11 @@ Made possible thanks to:<br>
![](https://nlnet.nl/logo/banner.png)
---
For installation instructions, please refer to the included **INSTALL** file.
---
# Features
- 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)

View File

@ -1,45 +0,0 @@
# Advanced Filter
## Postfix Filters
There are two types of Postfix mail filters: Simple Filters and Advanced
Filters. Simple Filters are executed for each incoming email as a new
process, which may turn out to be expensive in terms of resources. Advanced
Filters work as a mail-processing proxies.
For detailed documentation, see [FILTER README](https://www.postfix.org/FILTER_README.html).
## Installation
Just use the following command to install dependencies:
```
pip install -r requirements.txt
```
## Configuration
Lacre Advanced Filter, also known as daemon, is configured in the `[daemon]`
section of configuration file. Two obligatory parameters to be defined there
are:
* `host` -- IP address or a host name;
* `port` -- TCP port Lacre should listen on.
The other very important section is `[relay]`, which by default uses Simple
Filter destination. It has to be adjusted for Advanced Filter to work,
setting port to `10026`.
Command to spawn a Lacre daemon process is:
```
GPG_MAILGATE_CONFIG=/etc/gpg-mailgate.conf PYTHONPATH=... python -m lacre.daemon
```
Two environment variables used here are:
* `GPG_MAILGATE_CONFIG` (not mandatory) -- path to Lacre configuration,
unless it's kept in default location (`/etc/gpg-maillgate.conf`).
* `PYTHONPATH` (not mandatory) -- location of Lacre modules. You can place
them below your Python's `site-packages` to be reachable by any other
Python software.

View File

@ -1,25 +1,15 @@
# Testing
First tests have been set up to cover GPG Mailgate with at least basic test
that would be easy to run. The tests are called "end-to-end", meaning that we
that would be easy to run. The tests are called "end-to-end", meaning that we
feed some input to GPG Mailgate and inspect the output.
## Running tests
To run tests, use command `make test`.
To run tests, use command `make test` or `make unittest`.
There are 4 types of tests:
* `make e2etest` -- they cover a complete Lacre flow, from feeding it with
an email to accepting its encrypted form;
* `make daemontest` -- similar to the original `e2etest` goal, but tests the
behaviour of the Lacre Daemon, i.e. key part of the Advanced Content
Filter.
* `make unittest` -- just small tests of small units of code;
* `make crontest` -- execute cron job with a SQLite database.
E2E tests (`make e2etest`) should produce some helpful logs, so inspect
contents of `test/logs` directory if something goes wrong.
Tests produce some helpful logs, so inspect contents of `test/logs` directory
if something goes wrong.
If your system's Python binary isn't found in your `$PATH` or you want to use
a specific binary, use make's macro overriding: `make test
@ -27,22 +17,21 @@ PYTHON=/path/to/python`.
## Key building blocks
- *Test Script* (`test/e2e_test.py`) and *Daemon Test Script*
(`test/daemon_test.py`) that orchestrate the other components. They perform
test cases described in the *Test Configuration*. They spawn *Test Mail
Relay* and *GPG Mailgate* in appropriate order.
- *Test Script* (`test/e2e_test.py`) that orchestrates the other components.
It performs test cases described in the *Test Configuration*. It spawns
*Test Mail Relay* and *GPG Mailgate* in appropriate order.
- *Test Mail Relay* (`test/relay.py`), a simplistic mail daemon that only
supports the happy path. It accepts a mail message and prints it to
supports the happy path. It accepts a mail message and prints it to
stdandard output.
- *Test Configuration* (`test/e2e.ini`) specifies test cases: their input,
expected results and helpful documentation. It also specifies the port that
expected results and helpful documentation. It also specifies the port that
the *Test Mail Relay* should listen on.
## Limitations
Currently tests only check if the message has been encrypted, without
verifying that the correct key has been used. That's because we don't know
(yet) how to have a reproducible encrypted message. Option
verifying that the correct key has been used. That's because we don't know
(yet) how to have a reproducible encrypted message. Option
`--faked-system-time` wasn't enough to produce identical output.
## Troubleshooting

View File

@ -1,57 +0,0 @@
# Example configuration for Lacre logging. If you don't intend to change the
# log format, you can just keep this file unchanged.
# HANDLERS:
#
# Two main targets for log entries are defined here: syslog and a plain text
# log file. They are available as "handlers" named "syslog" and "lacrelog"
# respectively.
[loggers]
keys=root
[logger_root]
level=NOTSET
# Comma-separated handler names, see HANDLERS note at the top.
handlers=syslog
[handlers]
# Comma-separated handler names, see HANDLERS note at the top.
keys=syslog
[formatters]
keys=postfixfmt
#
# By default, include messages from all log levels up to DEBUG.
# However, productive systems may use something less verbose, like
# WARN or even ERROR.
#
[handler_lacrelog]
class=FileHandler
level=DEBUG
formatter=postfixfmt
args=('test/logs/lacre.log', 'a+')
# You may want to change the second argument (handlers.SysLogHandler.LOG_MAIL)
# to change the syslog facility used to record messages from Lacre.
#
# Options you can consider are "localX" facilities, available under names from
# handlers.SysLogHandler.LOG_LOCAL0 to handlers.SysLogHandler.LOG_LOCAL7.
#
# Please refer to your syslog configuration for details on how to separate
# records from different facilities.
[handler_syslog]
class=handlers.SysLogHandler
level=INFO
formatter=postfixfmt
args=('/dev/log', handlers.SysLogHandler.LOG_MAIL)
#
# Default Postfix log format.
#
[formatter_postfixfmt]
format=%(asctime)s %(name)s[%(process)d]: %(message)s
datefmt=%b %e %H:%M:%S
style=%
validate=True

70
gpg-mailgate-web/README Normal file
View File

@ -0,0 +1,70 @@
gpg-mailgate-web
----------------
gpg-mailgate-web is a web interface designed to allow any web user
to upload their PGP public key and then have all mail sent via
your mail server be encrypted. (Note: this is not meant for email
authentication, only encryption.)
After submitting their key to a web form, the user will be required
to confirm their email address. A cron script will register the
public key with gpg-mailgate (keyhome_only must be set to no
currently, which is the default) after email confirmation. From
then on, email to the specified address will be encrypted with
the public key.
gpg-mailgate-web is useful for two purposes: for a transparent
PGP encryption layer in front of any web application, or simple as
a web interface for gpg-mailgate so that users on your mail server
can easily upload and change their PGP keys.
Note that all processing relating to the mail server is done via the
cron script. This means that gpg-mailgate and the gpgmw cron can
be installed on a different server from the web server. The MySQL
database must be shared between the two applications though.
1. Installation instructions:
1) Install gpg-mailgate.
2) Create a MySQL database for gpg-mailgate.
a) Schema file is located in schema.sql
b) Database name and account goes in /etc/gpg-mailgate.conf (and set enabled = yes)
3) Copy the contents of public_html to your web directory.
4) Move config.sample.php to config.php and edit the configuration file.
5) Copy cron.py to /usr/local/bin/gpgmw-cron.py and set up a cron job
a) Create /etc/cron.d/gpgmw with the contents:
*/3 * * * * nobody /usr/bin/python /usr/local/bin/gpgmw-cron.py > /dev/null
6) Ensure that cron is working and test your new gpg-mailgate-web installation!
----------------------------------------
2. Adding rudimentary HKP Keyserver functionality for submitting public keys from the GPG client
(so far only implemented and tested with lighttpd - basically you just need to make your http server
listen on port 11371, redirect it to your gpg-mailgate-web directory and add a rewrite rule to catch
'pks/add' in the URI)
1) add the following lines to your lighttp.conf file and change the path to your gpg-mailgate-web directory
server.reject-expect-100-with-417 = "disable"
$SERVER["socket"] == ":11371" {
server.document-root = "/var/www/gpgmw"
setenv.add-response-header = ( "Via" => "1.1 yourserver.tld:11371 (lighttpd)" )
accesslog.filename = "/var/log/lighttpd/hkp-access.log"
url.rewrite-once = ( "^/pks/(.*)" => "/index.php?/pks/$1" )
}
2) reload lighttpd: /etc/init.d/lighttpd restart
3) in the index.php add the following line after the other required_once(...) lines:
require_once("include/phphkp.php");
4) change the constants in the include/phphkp.php file!
5) check if it works with a GPG client of your choice pushing a public key to your server's
domain or IP
(HTTP request to http://yourserver.tld:11371/pks/add with the public key in a POST variable 'keytext')

111
gpg-mailgate-web/cron.py Normal file
View File

@ -0,0 +1,111 @@
#!/usr/bin/python
#
# gpg-mailgate
#
# This file is part of the gpg-mailgate source code.
#
# gpg-mailgate is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# gpg-mailgate source code is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with gpg-mailgate source code. If not, see <http://www.gnu.org/licenses/>.
#
from configparser import RawConfigParser
import GnuPG
import MySQLdb
import smtplib
import markdown
import syslog
from email.MIMEText import MIMEText
from email.mime.multipart import MIMEMultipart
def appendLog(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)
else:
logfile = open(cfg['logging']['file'], 'a')
logfile.write(msg + "\n")
logfile.close()
def send_msg( mailsubject, messagefile, recipients = None ):
mailbody = file( cfg['cron']['mail_templates'] + "/" + messagefile).read()
msg = MIMEMultipart("alternative")
msg["From"] = cfg['cron']['notification_email']
msg["To"] = recipients
msg["Subject"] = mailsubject
msg.attach(MIMEText(mailbody, 'plain'))
msg.attach(MIMEText(markdown.markdown(mailbody), 'html'))
if 'relay' in cfg and 'host' in cfg['relay'] and 'enc_port' in cfg['relay']:
relay = (cfg['relay']['host'], int(cfg['relay']['enc_port']))
smtp = smtplib.SMTP(relay[0], relay[1])
smtp.sendmail( cfg['cron']['notification_email'], recipients, msg.as_string() )
else:
appendLog("Could not send mail due to wrong configuration")
# Read configuration from /etc/gpg-mailgate.conf
_cfg = RawConfigParser()
_cfg.read('/etc/gpg-mailgate.conf')
cfg = dict()
for sect in _cfg.sections():
cfg[sect] = dict()
for (name, value) in _cfg.items(sect):
cfg[sect][name] = value
if 'database' in cfg and 'enabled' in cfg['database'] and cfg['database']['enabled'] == 'yes' and 'name' in cfg['database'] and 'host' in cfg['database'] and 'username' in cfg['database'] and 'password' in cfg['database']:
connection = MySQLdb.connect(host = cfg['database']['host'], user = cfg['database']['username'], passwd = cfg['database']['password'], db = cfg['database']['name'], port = 3306)
cursor = connection.cursor()
# import keys
cursor.execute("SELECT publickey, id, email FROM gpgmw_keys WHERE status = 0 AND confirm = '' LIMIT 100")
result_set = cursor.fetchall()
for row in result_set:
# delete any other public keys associated with this confirmed email address
cursor.execute("DELETE FROM gpgmw_keys WHERE email = %s AND id != %s", (row[2], row[1],))
GnuPG.delete_key(cfg['gpg']['keyhome'], row[2])
appendLog('Deleted key for <' + row[2] + '> via import request')
if row[0].strip(): # we have this so that user can submit blank key to remove any encryption
if GnuPG.confirm_key(row[0], row[2]):
GnuPG.add_key(cfg['gpg']['keyhome'], row[0]) # import the key to gpg
cursor.execute("UPDATE gpgmw_keys SET status = 1 WHERE id = %s", (row[1],)) # mark key as accepted
appendLog('Imported key from <' + row[2] + '>')
if 'send_email' in cfg['cron'] and cfg['cron']['send_email'] == 'yes':
send_msg( "PGP key registration successful", "registrationSuccess.md", row[2] )
else:
cursor.execute("DELETE FROM gpgmw_keys WHERE id = %s", (row[1],)) # delete key
appendLog('Import confirmation failed for <' + row[2] + '>')
if 'send_email' in cfg['cron'] and cfg['cron']['send_email'] == 'yes':
send_msg( "PGP key registration failed", "registrationError.md", row[2] )
else:
# delete key so we don't continue processing it
cursor.execute("DELETE FROM gpgmw_keys WHERE id = %s", (row[1],))
if 'send_email' in cfg['cron'] and cfg['cron']['send_email'] == 'yes':
send_msg( "PGP key deleted", "keyDeleted.md", row[2])
connection.commit()
# delete keys
cursor.execute("SELECT email, id FROM gpgmw_keys WHERE status = 2 LIMIT 100")
result_set = cursor.fetchall()
for row in result_set:
GnuPG.delete_key(cfg['gpg']['keyhome'], row[0])
cursor.execute("DELETE FROM gpgmw_keys WHERE id = %s", (row[1],))
appendLog('Deleted key for <' + row[0] + '>')
connection.commit()
else:
print("Warning: doing nothing since database settings are not configured!")

View File

@ -0,0 +1,54 @@
<?php
/*
gpg-mailgate
This file is part of the gpg-mailgate source code.
gpg-mailgate is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
gpg-mailgate source code is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with gpg-mailgate source code. If not, see <http://www.gnu.org/licenses/>.
*/
/*
DO NOT EDIT THIS FILE!
Instead, copy over "config.sample.php" to "config.php" and edit settings there.
*/
$config = array();
$config['email_web'] = 'admin@example.com';
$config['email_from'] = 'gpg-mailgate-web@example.com';
$config['email_subject_requestpgp'] = 'Confirm your email address';
$config['site_url'] = 'http://example.com/gpgmw';
$config['site_title'] = 'PGP key management';
$config['language'] = 'english';
$config['debug'] = false;
$config['mail_smtp'] = false;
$config['mail_smtp_host'] = 'localhost';
$config['mail_smtp_port'] = 25;
$config['mail_smtp_username'] = 'gpgmw';
$config['mail_smtp_password'] = '';
$config['db_name'] = 'gpgmw';
$config['db_host'] = 'localhost';
$config['db_username'] = 'gpgmw';
$config['db_password'] = '';
$config['pgpverify_enable'] = false;
$config['pgpverify_tmpdir'] = '/tmp';
$config['pgpverify_allowblank'] = true;
$config['lock_time_initial'] = array('requestpgp' => 10);
$config['lock_count_overload'] = array('requestpgp' => 3);
$config['lock_time_overload'] = array('requestpgp' => 900);
$config['lock_time_reset'] = 300;
$config['lock_time_max'] = 3600;
?>

View File

@ -0,0 +1,119 @@
<?php
/*
gpg-mailgate
This file is part of the gpg-mailgate source code.
gpg-mailgate is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
gpg-mailgate source code is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with gpg-mailgate source code. If not, see <http://www.gnu.org/licenses/>.
*/
//
// GENERAL SITE SETTINGS
//
//web team contact
// this email address will be displayed if there is a database error
$config['email_web'] = 'admin@example.com';
//address to send emails from
$config['email_from'] = 'gpg-mailgate-web@example.com';
//this will be used as the subject when a user requests to add a PGP key
$config['email_subject_requestpgp'] = 'Confirm your email address';
//site URL, without trailing slash
$config['site_url'] = 'http://example.com/gpgmw';
//title of the website (displayed on home page)
$config['site_title'] = 'PGP key management';
//language file to use (see language subdirectory)
$config['language'] = 'english';
//whether debug mode should be enabled
$config['debug'] = false;
//
// MAIL SETTINGS
//
//whether to send mail through SMTP (instead of PHP mail function)
$config['mail_smtp'] = false;
//SMTP settings, if mail_smtp is enabled
//this requires Net_SMTP from http://pear.php.net/package/Net_SMTP/ to be installed
$config['mail_smtp_host'] = 'localhost';
$config['mail_smtp_port'] = 25;
$config['mail_smtp_username'] = 'gpgmw';
$config['mail_smtp_password'] = '';
//
// DATABASE SETTINGS
//
//database name (MySQL only); or see include/dbconnect.php
$config['db_name'] = 'gpgmw';
//database host
$config['db_host'] = 'localhost';
//database username
$config['db_username'] = 'gpgmw';
//database password
$config['db_password'] = '';
//
// PGP VERIFICATION SETTINGS
//
//whether to enable immediate verification of PGP keys
// keys will always be verified with the email address in our cron job
// but this will enable verification from the web interface before email confirmation
//for this to work, Crypt_GPG from http://pear.php.net/Crypt_GPG must be installed
// (as well as any of its dependencies), and pgpverify_tmpdir must be set
$config['pgpverify_enable'] = false;
//a temporary directory to use for PGP verification, without trailing slash
// gpgmw will create subdirectories from here to use as temporary gpg home directories
// these directories will (should) be deleted immediately after use
$config['pgpverify_tmpdir'] = '/tmp';
//whether to allow blank "keys"
// this is useful to allow users to delete their key from the keystore
// if they no longer want encryption
$config['pgpverify_allowblank'] = true;
//
// LOCK SETTINGS
//
//the time in seconds a user must wait before trying again; otherwise they get locked out (count not increased)
$config['lock_time_initial'] = array('requestpgp' => 10);
//the number of tries a user has (that passes the lock_time_initial test) before being locked by overload (extended duration)
$config['lock_count_overload'] = array('requestpgp' => 3);
//the time that overloads last
$config['lock_time_overload'] = array('requestpgp' => 900);
//time after which locks no longer apply, assuming the lock isn't active
$config['lock_time_reset'] = 300;
//max time to store locks in the database; this way we can clear old locks with one function
$config['lock_time_max'] = 3600;
?>

View File

@ -0,0 +1,41 @@
<?php
/*
gpg-mailgate
This file is part of the gpg-mailgate source code.
gpg-mailgate is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
gpg-mailgate source code is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with gpg-mailgate source code. If not, see <http://www.gnu.org/licenses/>.
*/
require_once("include/config.php");
require_once("include/language.php");
require_once("include/common.php");
require_once("include/dbconnect.php");
require_once("include/pgp.php");
if(isset($_REQUEST['email']) && isset($_REQUEST['confirm'])) {
$result = confirmPGP($_REQUEST['email'], $_REQUEST['confirm']);
if($result === true) {
get_page("home", array('message' => $lang['confirm_success']));
} else {
get_page("home", array('message' => $lang['confirm_fail_general']));
}
} else {
get_page("home");
}
?>

View File

@ -0,0 +1,273 @@
<?php
/*
gpg-mailgate
This file is part of the gpg-mailgate source code.
gpg-mailgate is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
gpg-mailgate source code is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with gpg-mailgate source code. If not, see <http://www.gnu.org/licenses/>.
*/
function string_begins_with($string, $search)
{
return (strncmp($string, $search, strlen($search)) == 0);
}
function boolToString($bool) {
return $bool ? 'true' : 'false';
}
//returns an absolute path to the include directory
function includePath() {
$self = __FILE__;
$lastSlash = strrpos($self, "/");
return substr($self, 0, $lastSlash + 1);
}
//returns a relative path to the gpg-mailgate-web web root directory, without trailing slash
function basePath() {
$commonPath = __FILE__;
$requestPath = $_SERVER['SCRIPT_FILENAME'];
//count the number of slashes
// number of .. needed for include level is numslashes(request) - numslashes(common)
// then add one more to get to base
$commonSlashes = substr_count($commonPath, '/');
$requestSlashes = substr_count($requestPath, '/');
$numParent = $requestSlashes - $commonSlashes + 1;
$basePath = ".";
for($i = 0; $i < $numParent; $i++) {
$basePath .= "/..";
}
return $basePath;
}
function uid($length) {
$characters = "0123456789abcdefghijklmnopqrstuvwxyz";
$string = "";
for ($p = 0; $p < $length; $p++) {
$string .= $characters[secure_random() % strlen($characters)];
}
return $string;
}
function get_page($page, $args = array()) {
//let pages use some variables
extract($args);
$config = $GLOBALS['config'];
$lang = $GLOBALS['lang'];
$basePath = basePath();
$themePath = $basePath . "/theme";
$themePageInclude = "$themePath/$page.php";
if(file_exists("$themePath/header.php")) {
include("$themePath/header.php");
}
if(file_exists($themePageInclude)) {
include($themePageInclude);
}
if(file_exists("$themePath/footer.php")) {
include("$themePath/footer.php");
}
}
function isAscii($str) {
return 0 == preg_match('/[^\x00-\x7F]/', $str);
}
//returns random number from 0 to 2^24
function secure_random() {
return hexdec(bin2hex(secure_random_bytes(3)));
}
function recursiveDelete($dirPath) {
foreach(
new RecursiveIteratorIterator(
new RecursiveDirectoryIterator(
$dirPath, FilesystemIterator::SKIP_DOTS
),
RecursiveIteratorIterator::CHILD_FIRST
)
as $path) {
$path->isFile() ? unlink($path->getPathname()) : rmdir($path->getPathname());
}
rmdir($dirPath);
}
function gpgmw_mail($subject, $body, $to) { //returns true=ok, false=notok
$config = $GLOBALS['config'];
$from = filter_var($config['email_from'], FILTER_SANITIZE_EMAIL);
$to = filter_var($to, FILTER_SANITIZE_EMAIL);
if($to === false || $from === false) {
return false;
}
if(isset($config['mail_smtp']) && $config['mail_smtp']) {
require_once "Mail.php";
$host = $config['mail_smtp_host'];
$port = $config['mail_smtp_port'];
$username = $config['mail_smtp_username'];
$password = $config['mail_smtp_password'];
$headers = array ('From' => $from,
'To' => $to,
'Subject' => $subject,
'Content-Type' => 'text/plain');
$smtp = Mail::factory('smtp',
array ('host' => $host,
'port' => $port,
'auth' => true,
'username' => $username,
'password' => $password));
$mail = $smtp->send($to, $headers, $body);
if (PEAR::isError($mail)) {
return false;
} else {
return true;
}
} else {
$headers = "From: $from\r\n";
$headers .= "Content-type: text/plain\r\n";
return mail($to, $subject, $body, $headers);
}
}
//secure_random_bytes from https://github.com/GeorgeArgyros/Secure-random-bytes-in-PHP
/*
* The function is providing, at least at the systems tested :),
* $len bytes of entropy under any PHP installation or operating system.
* The execution time should be at most 10-20 ms in any system.
*/
function secure_random_bytes($len = 10) {
/*
* Our primary choice for a cryptographic strong randomness function is
* openssl_random_pseudo_bytes.
*/
$SSLstr = '4'; // http://xkcd.com/221/
if (function_exists('openssl_random_pseudo_bytes') &&
(version_compare(PHP_VERSION, '5.3.4') >= 0 ||
substr(PHP_OS, 0, 3) !== 'WIN'))
{
$SSLstr = openssl_random_pseudo_bytes($len, $strong);
if ($strong)
return $SSLstr;
}
/*
* If mcrypt extension is available then we use it to gather entropy from
* the operating system's PRNG. This is better than reading /dev/urandom
* directly since it avoids reading larger blocks of data than needed.
* Older versions of mcrypt_create_iv may be broken or take too much time
* to finish so we only use this function with PHP 5.3 and above.
*/
if (function_exists('mcrypt_create_iv') &&
(version_compare(PHP_VERSION, '5.3.0') >= 0 ||
substr(PHP_OS, 0, 3) !== 'WIN'))
{
$str = mcrypt_create_iv($len, MCRYPT_DEV_URANDOM);
if ($str !== false)
return $str;
}
/*
* No build-in crypto randomness function found. We collect any entropy
* available in the PHP core PRNGs along with some filesystem info and memory
* stats. To make this data cryptographically strong we add data either from
* /dev/urandom or if its unavailable, we gather entropy by measuring the
* time needed to compute a number of SHA-1 hashes.
*/
$str = '';
$bits_per_round = 2; // bits of entropy collected in each clock drift round
$msec_per_round = 400; // expected running time of each round in microseconds
$hash_len = 20; // SHA-1 Hash length
$total = $len; // total bytes of entropy to collect
$handle = @fopen('/dev/urandom', 'rb');
if ($handle && function_exists('stream_set_read_buffer'))
@stream_set_read_buffer($handle, 0);
do
{
$bytes = ($total > $hash_len)? $hash_len : $total;
$total -= $bytes;
//collect any entropy available from the PHP system and filesystem
$entropy = rand() . uniqid(mt_rand(), true) . $SSLstr;
$entropy .= implode('', @fstat(@fopen( __FILE__, 'r')));
$entropy .= memory_get_usage();
if ($handle)
{
$entropy .= @fread($handle, $bytes);
}
else
{
// Measure the time that the operations will take on average
for ($i = 0; $i < 3; $i ++)
{
$c1 = microtime(true);
$var = sha1(mt_rand());
for ($j = 0; $j < 50; $j++)
{
$var = sha1($var);
}
$c2 = microtime(true);
$entropy .= $c1 . $c2;
}
// Based on the above measurement determine the total rounds
// in order to bound the total running time.
$rounds = (int)($msec_per_round*50 / (int)(($c2-$c1)*1000000));
// Take the additional measurements. On average we can expect
// at least $bits_per_round bits of entropy from each measurement.
$iter = $bytes*(int)(ceil(8 / $bits_per_round));
for ($i = 0; $i < $iter; $i ++)
{
$c1 = microtime();
$var = sha1(mt_rand());
for ($j = 0; $j < $rounds; $j++)
{
$var = sha1($var);
}
$c2 = microtime();
$entropy .= $c1 . $c2;
}
}
// We assume sha1 is a deterministic extractor for the $entropy variable.
$str .= sha1($entropy, true);
} while ($len > strlen($str));
if ($handle)
@fclose($handle);
return substr($str, 0, $len);
}
?>

View File

@ -0,0 +1,31 @@
<?php
/*
gpg-mailgate
This file is part of the gpg-mailgate source code.
gpg-mailgate is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
gpg-mailgate source code is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with gpg-mailgate source code. If not, see <http://www.gnu.org/licenses/>.
*/
require_once(dirname(__FILE__) . '/../config.default.php');
if(file_exists(dirname(__FILE__) . '/../config.php')) {
require_once(dirname(__FILE__) . '/../config.php');
} else {
die("Server configuration error: config.php does not exist.");
}
?>

View File

@ -0,0 +1,76 @@
<?php
/*
gpg-mailgate
This file is part of the gpg-mailgate source code.
gpg-mailgate is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
gpg-mailgate source code is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with gpg-mailgate source code. If not, see <http://www.gnu.org/licenses/>.
*/
function dieDatabaseError($ex = NULL) {
global $config;
if($ex == NULL) {
$pre = "Encountered database error.";
} else {
$pre = "Encountered database error: " . $ex->getMessage() . ".";
}
die($pre . " If this is unexpected, consider <a href=\"mailto:{$config['email_web']}\">reporting it to our web team</a>. Otherwise, <a href=\"/\">click here to return to the home page.</a>");
}
try {
$database = new PDO('mysql:host=' . $config['db_host'] . ';dbname=' . $config['db_name'], $config['db_username'], $config['db_password'], array(PDO::ATTR_EMULATE_PREPARES => false, PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION));
} catch(PDOException $ex) {
dieDatabaseError($ex);
}
function databaseQuery($command, $array = array(), $assoc = false) {
global $database;
if(!is_array($array)) {
dieDatabaseError();
}
try {
$query = $database->prepare($command);
if(!$query) {
print_r($database->errorInfo());
dieDatabaseError();
}
//set fetch mode depending on parameter
if($assoc) {
$query->setFetchMode(PDO::FETCH_ASSOC);
} else {
$query->setFetchMode(PDO::FETCH_NUM);
}
$success = $query->execute($array);
if(!$success) {
print_r($query->errorInfo());
dieDatabaseError();
}
return $query;
} catch(PDOException $ex) {
dieDatabaseError($ex);
}
}
?>

View File

@ -0,0 +1,85 @@
<?php
/*
gpg-mailgate
This file is part of the gpg-mailgate source code.
gpg-mailgate is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
gpg-mailgate source code is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with gpg-mailgate source code. If not, see <http://www.gnu.org/licenses/>.
*/
//uses gpg to verify that a key belongs to a given email address
function verifyPGPKey($content, $email) {
global $config;
//allow blank "keys" if this is set
//this means that encryption for $email will be disabled by the cron if it
// was enabled originally
if($config['pgpverify_allowblank'] && trim($content) == '') {
return true;
}
require_once("Crypt/GPG.php");
//try to create a random subdirectory of $config['pgpverify_tmpdir']
do {
$path = $config['pgpverify_tmpdir'] . '/' . uid(16);
} while(file_exists($path));
$result = @mkdir($path);
if($result === false) {
if($config['debug']) {
die("Failed to create directory [" . $path . "] for PGP verification.");
} else {
return false;
}
}
$gpg = new Crypt_GPG(array('homedir' => $path));
//import the key to our GPG temp directory
try {
$gpg->importKey($content);
} catch(Crypt_GPG_NoDataException $e) {
//user supplied an invalid key!
recursiveDelete($path);
return false;
}
//verify the email address matches
$keys = $gpg->getKeys();
if(count($keys) != 1) {
if($config['debug']) {
die("Error in PGP verification: key count is " . count($keys) . "!");
} else {
recursiveDelete($path);
return false;
}
}
$userIds = $keys[0]->getUserIds();
if(count($userIds) != 1 || strtolower($userIds[0]->getEmail()) != strtolower($email)) {
recursiveDelete($path);
return false;
}
recursiveDelete($path);
return true;
}
?>

View File

@ -0,0 +1,25 @@
<?php
/*
gpg-mailgate
This file is part of the gpg-mailgate source code.
gpg-mailgate is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
gpg-mailgate source code is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with gpg-mailgate source code. If not, see <http://www.gnu.org/licenses/>.
*/
require_once(dirname(__FILE__) . '/../language/' . $config['language'] . '.php');
?>

View File

@ -0,0 +1,124 @@
<?php
/*
gpg-mailgate
This file is part of the gpg-mailgate source code.
gpg-mailgate is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
gpg-mailgate source code is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with gpg-mailgate source code. If not, see <http://www.gnu.org/licenses/>.
*/
//lock.php is basic spam-submit prevention
//lock_time_initial, lock_time_overload, lock_count_overload, lock_time_reset, and lock_time_max should be defined in $config
//returns boolean: true=proceed, false=lock up; the difference between this and lockAction is that this can be used for repeated tasks, like admin
// then, only if action was unsuccessful would lockAction be called
function checkLock($action) {
global $config;
$lock_time_initial = $config['lock_time_initial'];
$lock_time_overload = $config['lock_time_overload'];
$lock_count_overload = $config['lock_count_overload'];
$lock_time_reset = $config['lock_time_reset'];
$lock_time_max = $config['lock_time_max'];
if(!isset($lock_time_initial[$action])) {
return true; //well we can't do anything...
}
$ip = $_SERVER['REMOTE_ADDR'];
$result = databaseQuery("SELECT id, time, num FROM gpgmw_locks WHERE ip = ? AND action = ?", array($ip, $action), true);
if($row = $result->fetch()) {
$id = $row['id'];
$time = $row['time'];
$count = $row['num']; //>=0 count means it's a regular initial lock; -1 count means overload lock
if($count >= 0) {
if(time() <= $time + $lock_time_initial[$action]) {
return false;
}
} else {
if(time() <= $time + $lock_time_overload[$action]) {
return false;
}
}
}
return true;
}
//returns boolean: true=proceed, false=lock up
function lockAction($action) {
global $config;
$lock_time_initial = $config['lock_time_initial'];
$lock_time_overload = $config['lock_time_overload'];
$lock_count_overload = $config['lock_count_overload'];
$lock_time_reset = $config['lock_time_reset'];
$lock_time_max = $config['lock_time_max'];
if(!isset($lock_time_initial[$action])) {
return true; //well we can't do anything...
}
$ip = $_SERVER['REMOTE_ADDR'];
$replace_id = -1;
//first find records with ip/action
$result = databaseQuery("SELECT id, time, num FROM gpgmw_locks WHERE ip = ? AND action = ?", array($ip, $action), true);
if($row = $result->fetch()) {
$id = $row['id'];
$time = $row['time'];
$count = $row['num']; //>=0 count means it's a regular initial lock; -1 count means overload lock
if($count >= 0) {
if(time() <= $time + $lock_time_initial[$action]) {
return false;
} else if(time() > $time + $lock_time_reset) {
//this entry is old, but use it to replace
$replace_id = $id;
} else {
//increase the count; maybe initiate an OVERLOAD
$count = $count + 1;
if($count >= $lock_count_overload[$action]) {
databaseQuery("UPDATE gpgmw_locks SET num = '-1', time = ? WHERE ip = ?", array(time(), $ip));
return false;
} else {
databaseQuery("UPDATE gpgmw_locks SET num = ?, time = ? WHERE ip = ?", array($count, time(), $ip));
}
}
} else {
if(time() <= $time + $lock_time_overload[$action]) {
return false;
} else {
//their overload is over, so this entry is old
$replace_id = $id;
}
}
} else {
databaseQuery("INSERT INTO gpgmw_locks (ip, time, action, num) VALUES (?, ?, ?, '1')", array($ip, time(), $action));
}
if($replace_id != -1) {
databaseQuery("UPDATE gpgmw_locks SET num = '1', time = ? WHERE id = ?", array(time(), $replace_id));
}
//some housekeeping
$delete_time = time() - $lock_time_max;
databaseQuery("DELETE FROM gpgmw_locks WHERE time <= ?", array($delete_time));
return true;
}
?>

View File

@ -0,0 +1,100 @@
<?php
/*
gpg-mailgate
This file is part of the gpg-mailgate source code.
gpg-mailgate is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
gpg-mailgate source code is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with gpg-mailgate source code. If not, see <http://www.gnu.org/licenses/>.
*/
//returns true on success or error message on failure
function requestPGP($email, $key) {
require_once(includePath() . "/lock.php");
global $config, $lang;
if(!checkLock('requestpgp')) {
return $lang['submit_error_trylater'];
}
if(!filter_var($email, FILTER_VALIDATE_EMAIL)) {
return $lang['submit_error_bademail'];
}
if(strlen($email) > 256 || strlen($key) > 1024 * 32) {
return $lang['submit_error_toolong'];
}
if(!isAscii($key)) {
return $lang['submit_error_nonascii'];
}
//housekeeping
databaseQuery("DELETE FROM gpgmw_keys WHERE time < DATE_SUB(NOW(), INTERVAL 48 HOUR) AND confirm != '' AND status = 0");
//if we already have an unaccepted key for this user, only replace if one day has elapsed since the last request
// this may prevent spam
$result = databaseQuery("SELECT HOUR(TIMEDIFF(time, NOW())), id FROM gpgmw_keys WHERE email = ? AND status = 0", array($email));
if($row = $result->fetch()) {
if($row[0] < 24) {
return $lang['submit_error_alreadyqueue'];
} else {
databaseQuery('DELETE FROM gpgmw_keys WHERE id = ?', array($row[1]));
}
}
//if PGP key verification is enabled, do it
if($config['pgpverify_enable']) {
require_once(includePath() . "/gpg.php");
if(!verifyPGPKey($key, $email)) {
return $lang['submit_error_badkey'];
}
}
//well, it looks good, let's submit it
lockAction('requestpgp');
$confirm = uid(32);
$confirm_link = "{$config['site_url']}/confirm.php?email=" . urlencode($email) . "&confirm=$confirm";
$result = gpgmw_mail($config['email_subject_requestpgp'], sprintf($lang['mail_confirm'], $confirm_link), $email);
if(!$result) {
return $lang['submit_error_emailfail'];
}
databaseQuery("INSERT INTO gpgmw_keys (email, publickey, confirm) VALUES (?, ?, ?)", array($email, $key, $confirm));
return true;
}
//returns false on failure or true on success
function confirmPGP($email, $confirm) {
require_once(includePath() . "/lock.php");
if(!lockAction('confirmpgp')) {
return false;
}
$result = databaseQuery("SELECT id FROM gpgmw_keys WHERE confirm = ? AND email = ?", array($confirm, $email));
if($row = $result->fetch()) {
databaseQuery("UPDATE gpgmw_keys SET confirm = '' WHERE id = ?", array($row[0]));
return true;
}
return false;
}
?>

View File

@ -0,0 +1,55 @@
<?php
/*
*
* HKP Keyserver Interface for submitting public keys
* to the gpg-mailgate-web database directly from
* an OpenPGP client
*
* loosely based on ElTramo's phkp code
* http://el-tramo.be/software/phkp
*
* 2014 by Kiritan Flux
*
* Licensed under the GNU General Public License.
*
* check the README for necessary prerequisites
*
*/
//! OpenPGP client command
$PGP_COMMAND="gpg";
//! A dir where the PHP script has write access
$PGP_HOME="/var/gpg/.phkp";
//! The maximum size (in characters) of a submitted key.
//! Set to '0' to disable receiving of keys, and '-1' for no limit.
$MAX_KEYSIZE=102400;
if (preg_match("/pks\/add/",$_SERVER['REQUEST_URI']))
{
if ($MAX_KEYSIZE == -1 || strlen($_POST['keytext']) <= $MAX_KEYSIZE)
{
//write key into temporary file
file_put_contents( "$PGP_HOME/tmp", $_POST['keytext'] );
//run gpg --with-fingerprint to retreive information about the key from the keyfile
$result = shell_exec("$PGP_COMMAND --homedir $PGP_HOME --with-fingerprint $PGP_HOME/tmp");
//extract email addresses from the information
$pattern = '/[a-z0-9_\-\+]+@[a-z0-9\-]+\.([a-z]{2,3})(?:\.[a-z]{2})?/i';
preg_match_all($pattern, $result, $matches);
//for each email address assigned to the key, put intformation into the DB and send confirmation emails
foreach($matches[0] as $match)
{
//echo $match.': '.$_POST['keytext'];
requestPGP($match, $_POST['keytext']);
}
}
else
{
header("HTTP/1.0 403 Forbidden");
}
}
?>

View File

@ -0,0 +1,43 @@
<?php
/*
gpg-mailgate
This file is part of the gpg-mailgate source code.
gpg-mailgate is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
gpg-mailgate source code is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with gpg-mailgate source code. If not, see <http://www.gnu.org/licenses/>.
*/
require_once("include/config.php");
require_once("include/language.php");
require_once("include/common.php");
require_once("include/dbconnect.php");
require_once("include/pgp.php");
require_once("include/phphkp.php");
if(isset($_POST['email']) && isset($_POST['key'])) {
$result = requestPGP($_POST['email'], $_POST['key']);
if($result === true) {
get_page("home", array('message' => $lang['submit_success']));
} else {
get_page("home", array('message' => $result));
}
} else {
get_page("home");
}
?>

View File

@ -0,0 +1,47 @@
<?php
/*
gpg-mailgate
This file is part of the gpg-mailgate source code.
gpg-mailgate is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
gpg-mailgate source code is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with gpg-mailgate source code. If not, see <http://www.gnu.org/licenses/>.
*/
$lang = array();
$lang['home_text'] = 'Use the form below to submit an ASCII-armored PGP public key. After submission, you will receive an email asking you to confirm your email address. Once confirmation is completed, mail sent to your email address via our mail server will be encrypted with your PGP public key.';
$lang['home_footer'] = '<a href="https://github.com/uakfdotb/gpg-mailgate">gpg-mailgate and gpg-mailgate-web</a> are released under the <a href="https://www.gnu.org/licenses/lgpl-3.0.txt">GNU LGPL</a>.';
$lang['home_emaildesc'] = 'Your email address (must match key)';
$lang['home_keydesc'] = 'ASCII-armored PGP public key';
$lang['home_submitkey'] = 'Submit key';
$lang['submit_success'] = 'Key submission successful. Please check your email to confirm your email address.';
$lang['submit_error_trylater'] = 'Error: please wait a bit before trying again.';
$lang['submit_error_bademail'] = 'Error: invalid email address.';
$lang['submit_error_toolong'] = 'Error: email address or key too long.';
$lang['submit_error_nonascii'] = 'Error: only keys encoded with ASCII armor are accepted (gpg --armor).';
$lang['submit_error_alreadyqueue'] = 'Error: there is already a key in the queue for this email address; please wait twenty-four hours between submitting keys, or confirm the previous key and then resubmit.';
$lang['submit_error_badkey'] = 'Error: your key does not appear to be valid (ensure ASCII armor is enabled and that the email address entered matches the email address of the key).';
$lang['submit_error_emailfail'] = 'Error: failed to send email.';
$lang['submit_error_bademail'] = 'Error: invalid email address.';
$lang['submit_error_bademail'] = 'Error: invalid email address.';
$lang['confirm_success'] = 'Your email address has been confirmed successfully. Within a few minutes, emails from our mail server to you should be encrypted with your PGP public key.';
$lang['confirm_fail_general'] = 'Error: failed to confirm any email address. You may have already confirmed the address, or you may have the wrong confirmation key.';
$lang['mail_confirm'] = "Please confirm your email address to complete the submission process. You can do so by clicking the link below\n\n%s\n\nThanks,\ngpg-mailgate-web";
?>

View File

@ -0,0 +1,25 @@
<?php
/*
gpg-mailgate
This file is part of the gpg-mailgate source code.
gpg-mailgate is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
gpg-mailgate source code is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with gpg-mailgate source code. If not, see <http://www.gnu.org/licenses/>.
*/
?>
</body>
</html>

View File

@ -0,0 +1,28 @@
<?php
/*
gpg-mailgate
This file is part of the gpg-mailgate source code.
gpg-mailgate is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
gpg-mailgate source code is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with gpg-mailgate source code. If not, see <http://www.gnu.org/licenses/>.
*/
?>
<html>
<head>
<title>gpg-mailgate-web</title>
</head>
<body>

View File

@ -0,0 +1,46 @@
<?php
/*
gpg-mailgate
This file is part of the gpg-mailgate source code.
gpg-mailgate is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
gpg-mailgate source code is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with gpg-mailgate source code. If not, see <http://www.gnu.org/licenses/>.
*/
?>
<h1><?= $config['site_title'] ?></h1>
<? if(!empty($message)) { ?>
<p><b><i><?= htmlspecialchars($message) ?></i></b></p>
<? } ?>
<p><?= $lang['home_text'] ?></p>
<form method="POST">
<table>
<tr>
<td><?= $lang['home_emaildesc'] ?></td>
<td><input type="text" name="email" /></td>
</tr>
<tr>
<td><?= $lang['home_keydesc'] ?></td>
<td><textarea name="key" rows="10" cols="80"></textarea></td>
</tr>
</table>
<input type="submit" value="<?= $lang['home_submitkey'] ?>" />
</form>
<p><?= $lang['home_footer'] ?></p>

View File

@ -0,0 +1,10 @@
-- confirm is empty once an email address has been confirmed, and otherwise is the confirmation key
-- status
-- initializes to 0
-- is set to 1 after a public key with (confirm='', status=0) has been imported
-- is set to 2 if a key should be deleted (will be deleted based on email address)
-- publickey is the ASCII-armored PGP public key; can be cleared to save space if status > 0
CREATE TABLE gpgmw_keys (id INT NOT NULL PRIMARY KEY AUTO_INCREMENT, email VARCHAR(256), publickey TEXT, confirm VARCHAR(32), status INT NOT NULL DEFAULT 0, time TIMESTAMP DEFAULT CURRENT_TIMESTAMP);
-- see include/lock.php for documentation
CREATE TABLE gpgmw_locks (id INT NOT NULL PRIMARY KEY AUTO_INCREMENT, ip VARCHAR(16), time INT, action VARCHAR(16), num INT);

View File

@ -46,9 +46,6 @@ dec_regex = None
[gpg]
# the directory where gpg-mailgate public keys are stored
# (see INSTALL for details)
#
# Note that this directory should be accessible only for the Lacre user,
# i.e. have mode 700.
keyhome = /var/gpgmailgate/.gnupg
[smime]
@ -59,9 +56,7 @@ cert_path = /var/gpgmailgate/smime
# settings for the register-handler
register_email = register@yourdomain.tld
mail_templates = /var/gpgmailgate/register_templates
# URL to webpanel. Upon receiving an email with a key, register-handler
# uploads it to the web panel.
# URL to webpanel. The server should be able to reach it
webpanel_url = http://yourdomain.tld
[cron]
@ -71,17 +66,9 @@ notification_email = gpg-mailgate@yourdomain.tld
mail_templates = /var/gpgmailgate/cron_templates
[logging]
# path to the logging configuration; see documentation for details:
# https://docs.python.org/3/library/logging.config.html#logging-config-fileformat
config = /etc/gpg-lacre-logging.conf
[daemon]
# Advanced Content Filter section.
#
# Advanced filters differ from Simple ones by providing a daemon that handles
# requests, instead of starting a new process each time a message arrives.
host = 127.0.0.1
port = 10025
# For logging to syslog. 'file = syslog', otherwise use path to the file.
file = syslog
verbose = yes
[relay]
# the relay settings to use for Postfix
@ -96,28 +83,14 @@ enc_port = 25
# Set this option to yes to use TLS for SMTP Servers which require TLS.
starttls = no
[smtp]
# Options when smtp auth is required to send out emails
enabled = false
username = gpg-mailgate
password = changeme
host = yourdomain.tld
port = 587
starttls = true
[database]
# edit the settings below if you want to read keys from a
# gpg-mailgate-web database other than SQLite
# uncomment the settings below if you want
# to read keys from a gpg-mailgate-web database
enabled = yes
url = sqlite:///test.db
# For a MySQL database "gpgmw", user "gpgmw" and password "password",
# use the following URL:
#
#url = mysql://gpgmw:password@localhost/gpgmw
#
# For other RDBMS backends, see:
# https://docs.sqlalchemy.org/en/14/core/engines.html#database-urls
name = gpgmw
host = localhost
username = gpgmw
password = password
[enc_keymap]
# You can find these by running the following command:

View File

@ -1,54 +1,466 @@
#!/usr/bin/python
#
# gpg-mailgate
# gpg-mailgate
#
# This file is part of the gpg-mailgate source code.
# This file is part of the gpg-mailgate source code.
#
# gpg-mailgate is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# gpg-mailgate is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# gpg-mailgate source code is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
# gpg-mailgate source code is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with gpg-mailgate source code. If not, see <http://www.gnu.org/licenses/>.
# You should have received a copy of the GNU General Public License
# along with gpg-mailgate source code. If not, see <http://www.gnu.org/licenses/>.
#
from configparser import RawConfigParser
from email.mime.base import MIMEBase
from email.mime.multipart import MIMEMultipart
import copy
import email
import email.message
import email.utils
import GnuPG
import os
import re
import smtplib
import sys
import time
import logging
import syslog
import traceback
import lacre
import lacre.config as conf
start = time.process_time()
conf.load_config()
lacre.init_logging(conf.get_item('logging', 'config'))
# imports for S/MIME
from M2Crypto import BIO, Rand, SMIME, X509
from email.mime.message import MIMEMessage
# This has to be executed *after* logging initialisation.
import lacre.core as core
# Environment variable name we read to retrieve configuration path. This is to
# enable non-root users to set up and run GPG Mailgate and to make the software
# testable.
CONFIG_PATH_ENV = "GPG_MAILGATE_CONFIG"
LOG = logging.getLogger('gpg-mailgate.py')
# Read configuration from /etc/gpg-mailgate.conf
_cfg = RawConfigParser()
_cfg.read(os.getenv(CONFIG_PATH_ENV, '/etc/gpg-mailgate.conf'))
cfg = dict()
for sect in _cfg.sections():
cfg[sect] = dict()
for (name, value) in _cfg.items(sect):
cfg[sect][name] = value
missing_params = conf.validate_config()
if missing_params:
LOG.error(f"Aborting delivery! Following mandatory config parameters are missing: {missing_params!r}")
sys.exit(lacre.EX_CONFIG)
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)
else:
logfile = open(cfg['logging']['file'], 'a')
logfile.write(msg + "\n")
logfile.close()
# Read e-mail from stdin, parse it
verbose = 'logging' in cfg and 'verbose' in cfg['logging'] and cfg['logging'].get('verbose') == 'yes'
# Read e-mail from stdin
raw = sys.stdin.read()
raw_message = email.message_from_string(raw)
raw_message = email.message_from_string( raw )
from_addr = raw_message['From']
# Read recipients from the command-line
to_addrs = sys.argv[1:]
# Let's start
core.deliver_message(raw_message, from_addr, to_addrs)
process_t = (time.process_time() - start) * 1000
def gpg_encrypt( raw_message, recipients ):
LOG.info("Message delivered in {process:.2f} ms".format(process=process_t))
if not get_bool_from_cfg('gpg', 'keyhome'):
log("No valid entry for gpg keyhome. Encryption aborted.")
return recipients
keys = GnuPG.public_keys( cfg['gpg']['keyhome'] )
for fingerprint in keys:
keys[fingerprint] = sanitize_case_sense(keys[fingerprint])
gpg_to = list()
ungpg_to = list()
for to in recipients:
# Check if recipient is in keymap
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['enc_keymap'][to] in keys:
gpg_to.append( (to, cfg['enc_keymap'][to]) )
continue
else:
log("Key '%s' in encrypt keymap not found in keyring for email address '%s'." % (cfg['enc_keymap'][to], to))
# Check if key in keychain is present
if to in keys.values() and not get_bool_from_cfg('default', 'enc_keymap_only', 'yes'):
gpg_to.append( (to, to) )
continue
# Check if there is a default key for the domain
splitted_to = to.split('@')
if len(splitted_to) > 1:
domain = splitted_to[1]
if get_bool_from_cfg('enc_domain_keymap', domain):
log("Encrypt domain keymap has key '%s'" % cfg['enc_dec_keymap'][domain] )
# Check we've got a matching key!
if cfg['enc_domain_keymap'][domain] in keys:
log("Using default domain key for recipient '%s'" % to)
gpg_to.append( (to, cfg['enc_domain_keymap'][domain]) )
continue
else:
log("Key '%s' in encrypt domain keymap not found in keyring for email address '%s'." % (cfg['enc_domain_keymap'][domain], to))
# At this point no key has been found
if verbose:
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( x[0] for x in gpg_to ))
# Getting PGP style for recipient
gpg_to_smtp_mime = list()
gpg_to_cmdline_mime = list()
gpg_to_smtp_inline = list()
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(','))
elif get_bool_from_cfg('pgp_style', rcpt[0], 'inline'):
gpg_to_smtp_inline.append(rcpt[0])
gpg_to_cmdline_inline.extend(rcpt[1].split(','))
else:
# Log message only if an unknown style is defined
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(','))
else:
gpg_to_smtp_inline.append(rcpt[0])
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'):
raw_message_mime['X-GPG-Mailgate'] = 'Encrypted by GPG Mailgate'
if 'Content-Transfer-Encoding' in raw_message_mime:
raw_message_mime.replace_header('Content-Transfer-Encoding', '8BIT')
else:
raw_message_mime['Content-Transfer-Encoding'] = '8BIT'
encrypted_payloads = encrypt_all_payloads_mime( raw_message_mime, gpg_to_cmdline_mime )
raw_message_mime.set_payload( encrypted_payloads )
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'):
raw_message_inline['X-GPG-Mailgate'] = 'Encrypted by GPG Mailgate'
if 'Content-Transfer-Encoding' in raw_message_inline:
raw_message_inline.replace_header('Content-Transfer-Encoding', '8BIT')
else:
raw_message_inline['Content-Transfer-Encoding'] = '8BIT'
encrypted_payloads = encrypt_all_payloads_inline( raw_message_inline, gpg_to_cmdline_inline )
raw_message_inline.set_payload( encrypted_payloads )
send_msg( raw_message_inline.as_string(), gpg_to_smtp_inline )
return ungpg_to
def encrypt_all_payloads_inline( message, gpg_to_cmdline ):
# This breaks cascaded MIME messages. Blame PGP/INLINE.
encrypted_payloads = list()
if isinstance(message.get_payload(), str):
return encrypt_payload( message, gpg_to_cmdline ).get_payload()
for payload in message.get_payload():
if( isinstance(payload.get_payload(), list) ):
encrypted_payloads.extend( encrypt_all_payloads_inline( payload, gpg_to_cmdline ) )
else:
encrypted_payloads.append( encrypt_payload( payload, gpg_to_cmdline ) )
return encrypted_payloads
def encrypt_all_payloads_mime( message, gpg_to_cmdline ):
# Convert a plain text email into PGP/MIME attachment style. Modeled after enigmail.
submsg1 = email.message.Message()
submsg1.set_payload("Version: 1\n")
submsg1.set_type("application/pgp-encrypted")
submsg1.set_param('PGP/MIME version identification', "", 'Content-Description' )
submsg2 = email.message.Message()
submsg2.set_type("application/octet-stream")
submsg2.set_param('name', "encrypted.asc")
submsg2.set_param('OpenPGP encrypted message', "", 'Content-Description' )
submsg2.set_param('inline', "", 'Content-Disposition' )
submsg2.set_param('filename', "encrypted.asc", 'Content-Disposition' )
if isinstance(message.get_payload(), str):
# WTF! It seems to swallow the first line. Not sure why. Perhaps
# it's skipping an imaginary blank line someplace. (ie skipping a header)
# Workaround it here by prepending a blank line.
# This happens only on text only messages.
additionalSubHeader=""
if 'Content-Type' in message and not message['Content-Type'].startswith('multipart'):
additionalSubHeader="Content-Type: "+message['Content-Type']+"\n"
submsg2.set_payload(additionalSubHeader+"\n" +message.get_payload(decode=True))
check_nested = True
else:
processed_payloads = generate_message_from_payloads(message)
submsg2.set_payload(processed_payloads.as_string())
check_nested = False
message.preamble = "This is an OpenPGP/MIME encrypted message (RFC 2440 and 3156)"
# Use this just to generate a MIME boundary string.
junk_msg = MIMEMultipart()
junk_str = junk_msg.as_string() # WTF! Without this, get_boundary() will return 'None'!
boundary = junk_msg.get_boundary()
# This also modifies the boundary in the body of the message, ie it gets parsed.
if 'Content-Type' in message:
message.replace_header('Content-Type', "multipart/encrypted; protocol=\"application/pgp-encrypted\";\nboundary=\"%s\"\n" % boundary)
else:
message['Content-Type'] = "multipart/encrypted; protocol=\"application/pgp-encrypted\";\nboundary=\"%s\"\n" % boundary
return [ submsg1, encrypt_payload(submsg2, gpg_to_cmdline, check_nested) ]
def encrypt_payload( payload, gpg_to_cmdline, check_nested = True ):
raw_payload = payload.get_payload(decode=True)
if check_nested and b"-----BEGIN PGP MESSAGE-----" in raw_payload and b"-----END PGP MESSAGE-----" in raw_payload:
if verbose:
log("Message is already pgp encrypted. No nested encryption needed.")
return payload
# No check is needed for cfg['gpg']['keyhome'] as this is already done in method gpg_encrypt
gpg = GnuPG.GPGEncryptor( cfg['gpg']['keyhome'], gpg_to_cmdline, payload.get_content_charset() )
gpg.update( raw_payload )
encrypted_data, returncode = gpg.encrypt()
if verbose:
log("Return code from encryption=%d (0 indicates success)." % returncode)
if returncode != 0:
log("Encrytion failed with return code %d. Encryption aborted." % returncode)
return payload
payload.set_payload( encrypted_data )
isAttachment = payload.get_param( 'attachment', None, 'Content-Disposition' ) is not None
if isAttachment:
filename = payload.get_filename()
if filename:
pgpFilename = filename + ".pgp"
if not (payload.get('Content-Disposition') is None):
payload.set_param( 'filename', pgpFilename, 'Content-Disposition' )
if not (payload.get('Content-Type') is None) and not (payload.get_param( 'name' ) is None):
payload.set_param( 'name', pgpFilename )
if not (payload.get('Content-Transfer-Encoding') is None):
payload.replace_header( 'Content-Transfer-Encoding', "7bit" )
return payload
def smime_encrypt( raw_message, recipients ):
if not get_bool_from_cfg('smime', 'cert_path'):
log("No valid path for S/MIME certs found in config file. S/MIME encryption aborted.")
return recipients
cert_path = cfg['smime']['cert_path']+"/"
s = SMIME.SMIME()
sk = X509.X509_Stack()
smime_to = list()
unsmime_to = list()
for addr in recipients:
cert_and_email = get_cert_for_email(addr, cert_path)
if not (cert_and_email is None):
(to_cert, normal_email) = cert_and_email
if verbose:
log("Found cert " + to_cert + " for " + addr + ": " + normal_email)
smime_to.append(addr)
x509 = X509.load_cert(to_cert, format=X509.FORMAT_PEM)
sk.push(x509)
else:
unsmime_to.append(addr)
if smime_to != list():
s.set_x509_stack(sk)
s.set_cipher(SMIME.Cipher('aes_192_cbc'))
p7 = s.encrypt( BIO.MemoryBuffer( raw_message.as_string() ) )
# Output p7 in mail-friendly format.
out = BIO.MemoryBuffer()
out.write('From: ' + from_addr + '\n')
out.write('To: ' + raw_message['To'] + '\n')
if raw_message['Cc']:
out.write('Cc: ' + raw_message['Cc'] + '\n')
if raw_message['Bcc']:
out.write('Bcc: ' + raw_message['Bcc'] + '\n')
if raw_message['Subject']:
out.write('Subject: '+ raw_message['Subject'] + '\n')
if get_bool_from_cfg('default', 'add_header', 'yes'):
out.write('X-GPG-Mailgate: Encrypted by GPG Mailgate\n')
s.write(out, p7)
if verbose:
log("Sending message from " + from_addr + " to " + str(smime_to))
send_msg(out.read(), smime_to)
if unsmime_to != list():
if verbose:
log("Unable to find valid S/MIME certificates for " + str(unsmime_to))
return unsmime_to
def get_cert_for_email( to_addr, cert_path ):
files_in_directory = os.listdir(cert_path)
for filename in files_in_directory:
file_path = os.path.join(cert_path, filename)
if not os.path.isfile(file_path):
continue
if get_bool_from_cfg('default', 'mail_case_insensitive', 'yes'):
if filename.lower() == to_addr:
return (file_path, to_addr)
else:
if filename == to_addr:
return (file_path, to_addr)
# support foo+ignore@bar.com -> foo@bar.com
multi_email = re.match('^([^\+]+)\+([^@]+)@(.*)$', to_addr)
if multi_email:
fixed_up_email = "%s@%s" % (multi_email.group(1), multi_email.group(3))
if verbose:
log("Multi-email %s converted to %s" % (to_addr, fixed_up_email))
return get_cert_for_email(fixed_up_email)
return None
def get_bool_from_cfg( section, key = None, evaluation = None ):
if not (key is None) and not (evaluation is None):
return section in cfg and cfg[section].get(key) == evaluation
elif not (key is None) and (evaluation is None):
return section in cfg and not (cfg[section].get(key) is None)
else:
return section in cfg
def sanitize_case_sense( address ):
if get_bool_from_cfg('default', 'mail_case_insensitive', 'yes'):
address = address.lower()
else:
if isinstance(address, str):
sep = '@'
else:
sep = b'@'
splitted_address = address.split(sep)
if len(splitted_address) > 1:
address = splitted_address[0] + sep + splitted_address[1].lower()
return address
def generate_message_from_payloads( payloads, message = None ):
if message == None:
message = email.mime.multipart.MIMEMultipart(payloads.get_content_subtype())
for payload in payloads.get_payload():
if( isinstance(payload.get_payload(), list) ):
message.attach(generate_message_from_payloads(payload))
else:
message.attach(payload)
return message
def get_first_payload( payloads ):
if payloads.is_multipart():
return get_first_payload(payloads.get_payload(0))
else:
return payloads
def send_msg( message, recipients ):
recipients = [_f for _f in recipients if _f]
if recipients:
if not (get_bool_from_cfg('relay', 'host') and get_bool_from_cfg('relay', 'port')):
log("Missing settings for relay. Sending email aborted.")
return None
log("Sending email to: <%s>" % '> <'.join( recipients ))
relay = (cfg['relay']['host'], int(cfg['relay']['port']))
smtp = smtplib.SMTP(relay[0], relay[1])
if 'relay' in cfg and 'starttls' in cfg['relay'] and cfg['relay']['starttls'] == 'yes':
smtp.starttls()
smtp.sendmail( from_addr, recipients, message )
else:
log("No recipient found")
def sort_recipients( raw_message, from_addr, to_addrs ):
recipients_left = list()
for recipient in to_addrs:
recipients_left.append(sanitize_case_sense(recipient))
# There is no need for nested encryption
first_payload = get_first_payload(raw_message)
if first_payload.get_content_type() == 'application/pkcs7-mime':
if verbose:
log("Message is already encrypted with S/MIME. Encryption aborted.")
send_msg(raw_message.as_string(), recipients_left)
return
first_payload = first_payload.get_payload(decode=True)
if b"-----BEGIN PGP MESSAGE-----" in first_payload and b"-----END PGP MESSAGE-----" in first_payload:
if verbose:
log("Message is already encrypted as PGP/INLINE. Encryption aborted.")
send_msg(raw_message.as_string(), recipients_left)
return
if raw_message.get_content_type() == 'multipart/encrypted':
if verbose:
log("Message is already encrypted. Encryption aborted.")
send_msg(raw_message.as_string(), recipients_left)
return
# Encrypt mails for recipients with known public PGP keys
recipients_left = gpg_encrypt(raw_message, recipients_left)
if recipients_left == list():
return
# Encrypt mails for recipients with known S/MIME certificate
recipients_left = smime_encrypt(raw_message, recipients_left)
if recipients_left == list():
return
# Send out mail to recipients which are left
send_msg(raw_message.as_string(), recipients_left)
# Let's start
sort_recipients(raw_message, from_addr, to_addrs)

View File

@ -1,48 +0,0 @@
"""Lacre --- the Postfix mail filter encrypting incoming email
"""
import logging
import logging.config
# Following structure configures logging iff a file-based configuration cannot
# be performed. It only sets up a syslog handler, so that the admin has at
# least some basic information.
FAIL_OVER_LOGGING_CONFIG = {
'version': 1,
'formatters': {
'sysfmt': {
'format': '%(asctime)s %(module)s %(message)s',
'datefmt': '%Y-%m-%d %H:%M:%S'
},
},
'handlers': {
'syslog': {
'class': 'logging.handlers.SysLogHandler',
'level': 'INFO',
'formatter': 'sysfmt'
},
'lacrelog': {
'class': 'logging.FileHandler',
'level': 'INFO',
'formatter': 'sysfmt',
'filename': 'lacre.log'
}
},
'root': {
'level': 'INFO',
'handlers': ['syslog', 'lacrelog']
}
}
# Exit code taken from <sysexits.h>:
EX_UNAVAILABLE = 69
EX_TEMPFAIL = 75
EX_CONFIG = 78
def init_logging(config_filename):
if config_filename is not None:
logging.config.fileConfig(config_filename)
else:
logging.config.dictConfig(FAIL_OVER_LOGGING_CONFIG)
logging.warning('Lacre logging configuration missing, using syslog as default')

View File

@ -1,122 +0,0 @@
"""Lacre configuration.
Routines defined here are responsible for processing and validating
configuration.
"""
from configparser import RawConfigParser
import os
# Environment variable name we read to retrieve configuration path. This is to
# enable non-root users to set up and run GPG Mailgate and to make the software
# testable.
CONFIG_PATH_ENV = "GPG_MAILGATE_CONFIG"
# List of mandatory configuration parameters. Each item on this list should be
# a pair: a section name and a parameter name.
MANDATORY_CONFIG_ITEMS = [("relay", "host"),
("relay", "port"),
("daemon", "host"),
("daemon", "port"),
("gpg", "keyhome")]
# Global dict to keep configuration parameters. It's hidden behind several
# utility functions to make it easy to replace it with ConfigParser object in
# the future.
cfg = dict()
def load_config() -> dict:
"""Parse configuration file.
If environment variable identified by CONFIG_PATH_ENV
variable is set, its value is taken as a configuration file
path. Otherwise, the default is taken
('/etc/gpg-mailgate.conf').
"""
configFile = os.getenv(CONFIG_PATH_ENV, '/etc/gpg-mailgate.conf')
parser = _read_config(configFile)
# XXX: Global variable. It is a left-over from old GPG-Mailgate code. We
# should drop it and probably use ConfigParser instance where configuration
# parameters are needed.
global cfg
cfg = _copy_to_dict(parser)
return cfg
def _read_config(fileName) -> RawConfigParser:
cp = RawConfigParser()
cp.read(fileName)
return cp
def _copy_to_dict(confParser) -> dict:
config = dict()
for sect in confParser.sections():
config[sect] = dict()
for (name, value) in confParser.items(sect):
config[sect][name] = value
return config
def get_item(section, key, empty_value=None):
global cfg
if config_item_set(section, key):
return cfg[section][key]
else:
return empty_value
def has_section(section) -> bool:
return section in cfg
def config_item_set(section, key) -> bool:
return section in cfg and (key in cfg[section]) and not (cfg[section][key] is None)
def config_item_equals(section, key, value) -> bool:
return section in cfg and key in cfg[section] and cfg[section][key] == value
def flag_enabled(section, key) -> bool:
return config_item_equals(section, key, 'yes')
def validate_config():
"""Check if configuration is complete.
Returns a list of missing parameters, so an empty list means
configuration is complete.
"""
missing = []
for (section, param) in MANDATORY_CONFIG_ITEMS:
if not config_item_set(section, param):
missing.append((section, param))
return missing
#
# High level access to configuration.
#
def relay_params():
"""Return a (HOST, PORT) tuple identifying the mail relay."""
return (cfg["relay"]["host"], int(cfg["relay"]["port"]))
def daemon_params():
"""Return a (HOST, PORT) tuple to setup a server socket for Lacre daemon."""
return (cfg["daemon"]["host"], int(cfg["daemon"]["port"]))
def strict_mode():
"""Check if Lacre is configured to support only a fixed list of keys."""
return ("default" in cfg and cfg["default"]["enc_keymap_only"] == "yes")

View File

@ -1,561 +0,0 @@
#
# gpg-mailgate
#
# This file is part of the gpg-mailgate source code.
#
# gpg-mailgate is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# gpg-mailgate source code is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with gpg-mailgate source code. If not, see <http://www.gnu.org/licenses/>.
#
"""Lacre's actual mail-delivery module.
IMPORTANT: This module has to be loaded _after_ initialisation of the logging
module.
"""
from email.mime.multipart import MIMEMultipart
import copy
import email
import email.message
import email.utils
import GnuPG
import os
import smtplib
import sys
import asyncio
# imports for S/MIME
from M2Crypto import BIO, SMIME, X509
import logging
import lacre.text as text
import lacre.config as conf
import lacre.keyring as kcache
from lacre.mailop import KeepIntact, InlineOpenPGPEncrypt, MimeOpenPGPEncrypt
LOG = logging.getLogger(__name__)
def _gpg_encrypt(raw_message, recipients):
if not conf.config_item_set('gpg', 'keyhome'):
LOG.error("No valid entry for gpg keyhome. Encryption aborted.")
return recipients
gpg_to, ungpg_to = _identify_gpg_recipients(recipients, _load_keys())
LOG.info(f"Got addresses: gpg_to={gpg_to!r}, ungpg_to={ungpg_to!r}")
if gpg_to:
LOG.info("Encrypting email to: %s" % ' '.join(x.email() for x in gpg_to))
gpg_to_smtp_mime, gpg_to_cmdline_mime, \
gpg_to_smtp_inline, gpg_to_cmdline_inline = \
_sort_gpg_recipients(gpg_to)
if gpg_to_smtp_mime:
# Encrypt mail with PGP/MIME
_gpg_encrypt_and_deliver(raw_message,
gpg_to_cmdline_mime, gpg_to_smtp_mime,
_encrypt_all_payloads_mime)
if gpg_to_smtp_inline:
# Encrypt mail with PGP/INLINE
_gpg_encrypt_and_deliver(raw_message,
gpg_to_cmdline_inline, gpg_to_smtp_inline,
_encrypt_all_payloads_inline)
LOG.info(f"Not processed emails: {ungpg_to}")
return ungpg_to
def _sort_gpg_recipients(gpg_to):
gpg_to_smtp_mime = list()
gpg_to_cmdline_mime = list()
gpg_to_smtp_inline = list()
gpg_to_cmdline_inline = list()
default_to_pgp_mime = conf.config_item_equals('default', 'mime_conversion', 'yes')
for rcpt in gpg_to:
# Checking pre defined styles in settings first
if conf.config_item_equals('pgp_style', rcpt.email(), 'mime'):
gpg_to_smtp_mime.append(rcpt.email())
gpg_to_cmdline_mime.extend(rcpt.key().split(','))
elif conf.config_item_equals('pgp_style', rcpt.email(), 'inline'):
gpg_to_smtp_inline.append(rcpt.email())
gpg_to_cmdline_inline.extend(rcpt.key().split(','))
else:
# Log message only if an unknown style is defined
if conf.config_item_set('pgp_style', rcpt.email()):
LOG.debug("Style %s for recipient %s is not known. Use default as fallback."
% (conf.get_item("pgp_style", rcpt.email()), rcpt.email()))
# If no style is in settings defined for recipient, use default from settings
if default_to_pgp_mime:
gpg_to_smtp_mime.append(rcpt.email())
gpg_to_cmdline_mime.extend(rcpt.key().split(','))
else:
gpg_to_smtp_inline.append(rcpt.email())
gpg_to_cmdline_inline.extend(rcpt.key().split(','))
return gpg_to_smtp_mime, gpg_to_cmdline_mime, gpg_to_smtp_inline, gpg_to_cmdline_inline
def _gpg_encrypt_and_return(message, cmdline, to, encrypt_f) -> str:
msg_copy = copy.deepcopy(message)
_customise_headers(msg_copy)
encrypted_payloads = encrypt_f(msg_copy, cmdline)
msg_copy.set_payload(encrypted_payloads)
return msg_copy.as_string()
def _gpg_encrypt_and_deliver(message, cmdline, to, encrypt_f):
out = _gpg_encrypt_and_return(message, cmdline, to, encrypt_f)
send_msg(out, to)
def _customise_headers(msg_copy):
if conf.config_item_equals('default', 'add_header', 'yes'):
msg_copy['X-GPG-Mailgate'] = 'Encrypted by GPG Mailgate'
if 'Content-Transfer-Encoding' in msg_copy:
msg_copy.replace_header('Content-Transfer-Encoding', '8BIT')
else:
msg_copy['Content-Transfer-Encoding'] = '8BIT'
def _load_keys():
"""Return a map from a key's fingerprint to email address."""
keyring = kcache.KeyRing(conf.get_item('gpg', 'keyhome'))
return asyncio.run(keyring.freeze_identities())
class GpgRecipient:
"""A tuple-like object that contains GPG recipient data."""
def __init__(self, left, right):
"""Initialise a tuple-like object that contains GPG recipient data."""
self._left = left
self._right = right
def __getitem__(self, index):
"""Pretend this object is a tuple by returning an indexed tuple element."""
if index == 0:
return self._left
elif index == 1:
return self._right
else:
raise IndexError()
def __repr__(self):
"""Return textual representation of this GPG Recipient."""
return f"GpgRecipient({self._left!r}, {self._right!r})"
def email(self):
"""Return this recipient's email address."""
return self._left
def key(self):
"""Return this recipient's key ID."""
return self._right
def _identify_gpg_recipients(recipients, keys: kcache.KeyCache):
# This list will be filled with pairs (M, N), where M is the destination
# address we're going to deliver the message to and N is the identity we're
# going to encrypt it for.
gpg_to = list()
# This will be the list of recipients that haven't provided us with their
# public keys.
ungpg_to = list()
# In "strict mode", only keys included in configuration are used to encrypt
# email.
strict_mode = conf.strict_mode()
# GnuPG keys found in our keyring.
for to in recipients:
own_key = _try_configured_key(to, keys)
if own_key is not None:
gpg_to.append(GpgRecipient(own_key[0], own_key[1]))
continue
direct_key = _try_direct_key_lookup(to, keys, strict_mode)
if direct_key is not None:
gpg_to.append(GpgRecipient(direct_key[0], direct_key[1]))
continue
domain_key = _try_configured_domain_key(to, keys)
if domain_key is not None:
gpg_to.append(GpgRecipient(domain_key[0], domain_key[1]))
continue
ungpg_to.append(to)
LOG.debug(f'Collected recipients; GPG: {gpg_to}; UnGPG: {ungpg_to}')
return gpg_to, ungpg_to
def _find_key(recipient, keys, strict_mode):
own_key = _try_configured_key(recipient, keys)
if own_key is not None:
return own_key
direct_key = _try_direct_key_lookup(recipient, keys, strict_mode)
if direct_key is not None:
return direct_key
domain_key = _try_configured_domain_key(recipient, keys)
if domain_key is not None:
return domain_key
return None
def _try_configured_key(recipient, keys):
if conf.config_item_set('enc_keymap', recipient):
key = conf.get_item('enc_keymap', recipient)
if key in keys:
LOG.debug(f"Found key {key} configured for {recipient}")
return (recipient, key)
LOG.debug(f"No configured key found for {recipient}")
return None
def _try_direct_key_lookup(recipient, keys, strict_mode):
if strict_mode:
return None
if keys.has_email(recipient):
LOG.info(f"Found key for {recipient}")
return recipient, recipient
(newto, topic) = text.parse_delimiter(recipient)
if keys.has_email(newto):
LOG.info(f"Found key for {newto}, stripped {recipient}")
return recipient, newto
return None
def _try_configured_domain_key(recipient, keys):
parts = recipient.split('@')
if len(parts) != 2:
return None
domain = parts[1]
if conf.config_item_set('enc_domain_keymap', domain):
domain_key = conf.get_item('enc_domain_keymap', domain)
if domain_key in keys:
LOG.debug(f"Found domain key {domain_key} for {recipient}")
return recipient, domain_key
LOG.debug(f"No domain key for {recipient}")
return None
def _encrypt_all_payloads_inline(message, gpg_to_cmdline):
# This breaks cascaded MIME messages. Blame PGP/INLINE.
encrypted_payloads = list()
if isinstance(message.get_payload(), str):
return _encrypt_payload(message, gpg_to_cmdline).get_payload()
for payload in message.get_payload():
if(isinstance(payload.get_payload(), list)):
encrypted_payloads.extend(_encrypt_all_payloads_inline(payload, gpg_to_cmdline))
else:
encrypted_payloads.append(_encrypt_payload(payload, gpg_to_cmdline))
return encrypted_payloads
def _encrypt_all_payloads_mime(message: email.message.Message, gpg_to_cmdline):
# Convert a plain text email into PGP/MIME attachment style. Modeled after enigmail.
pgp_ver_part = email.message.Message()
pgp_ver_part.set_payload("Version: 1"+text.EOL)
pgp_ver_part.set_type("application/pgp-encrypted")
pgp_ver_part.set_param('PGP/MIME version identification', "", 'Content-Description')
encrypted_part = email.message.Message()
encrypted_part.set_type("application/octet-stream")
encrypted_part.set_param('name', "encrypted.asc")
encrypted_part.set_param('OpenPGP encrypted message', "", 'Content-Description')
encrypted_part.set_param('inline', "", 'Content-Disposition')
encrypted_part.set_param('filename', "encrypted.asc", 'Content-Disposition')
if isinstance(message.get_payload(), str):
# WTF! It seems to swallow the first line. Not sure why. Perhaps
# it's skipping an imaginary blank line someplace. (ie skipping a header)
# Workaround it here by prepending a blank line.
# This happens only on text only messages.
additionalSubHeader = ""
encoding = sys.getdefaultencoding()
if 'Content-Type' in message and not message['Content-Type'].startswith('multipart'):
additionalSubHeader = "Content-Type: " + message['Content-Type'] + text.EOL
encoding = message.get_content_charset(sys.getdefaultencoding())
LOG.debug(f"Identified encoding as {encoding}")
encrypted_part.set_payload(additionalSubHeader+text.EOL + message.get_payload(decode=True).decode(encoding))
check_nested = True
else:
processed_payloads = _generate_message_from_payloads(message)
encrypted_part.set_payload(processed_payloads.as_string())
check_nested = False
message.preamble = "This is an OpenPGP/MIME encrypted message (RFC 2440 and 3156)"
# Use this just to generate a MIME boundary string.
junk_msg = MIMEMultipart()
junk_str = junk_msg.as_string() # WTF! Without this, get_boundary() will return 'None'!
boundary = junk_msg.get_boundary()
# This also modifies the boundary in the body of the message, ie it gets parsed.
if 'Content-Type' in message:
message.replace_header('Content-Type', f"multipart/encrypted; protocol=\"application/pgp-encrypted\"; boundary=\"{boundary}\""+text.EOL)
else:
message['Content-Type'] = f"multipart/encrypted; protocol=\"application/pgp-encrypted\"; boundary=\"{boundary}\""+text.EOL
return [pgp_ver_part, _encrypt_payload(encrypted_part, gpg_to_cmdline, check_nested)]
def _encrypt_payload(payload, gpg_to_cmdline, check_nested=True):
raw_payload = payload.get_payload(decode=True)
if check_nested and text.is_payload_pgp_inline(raw_payload):
LOG.debug("Message is already pgp encrypted. No nested encryption needed.")
return payload
# No check is needed for conf.get_item('gpg', 'keyhome') as this is already
# done in method gpg_encrypt
gpg = GnuPG.GPGEncryptor(conf.get_item('gpg', 'keyhome'), gpg_to_cmdline,
payload.get_content_charset())
gpg.update(raw_payload)
encrypted_data, returncode = gpg.encrypt()
LOG.debug("Return code from encryption=%d (0 indicates success)." % returncode)
if returncode != 0:
LOG.info("Encrytion failed with return code %d. Encryption aborted." % returncode)
return payload
payload.set_payload(encrypted_data)
isAttachment = payload.get_param('attachment', None, 'Content-Disposition') is not None
if isAttachment:
filename = payload.get_filename()
if filename:
pgpFilename = filename + ".pgp"
if not (payload.get('Content-Disposition') is None):
payload.set_param('filename', pgpFilename, 'Content-Disposition')
if not (payload.get('Content-Type') is None) and not (payload.get_param('name') is None):
payload.set_param('name', pgpFilename)
if not (payload.get('Content-Transfer-Encoding') is None):
payload.replace_header('Content-Transfer-Encoding', "7bit")
return payload
def _smime_encrypt(raw_message, recipients):
global LOG
global from_addr
if not conf.config_item_set('smime', 'cert_path'):
LOG.info("No valid path for S/MIME certs found in config file. S/MIME encryption aborted.")
return recipients
cert_path = conf.get_item('smime', 'cert_path')+"/"
s = SMIME.SMIME()
sk = X509.X509_Stack()
smime_to = list()
unsmime_to = list()
for addr in recipients:
cert_and_email = _get_cert_for_email(addr, cert_path)
if not (cert_and_email is None):
(to_cert, normal_email) = cert_and_email
LOG.debug("Found cert " + to_cert + " for " + addr + ": " + normal_email)
smime_to.append(addr)
x509 = X509.load_cert(to_cert, format=X509.FORMAT_PEM)
sk.push(x509)
else:
unsmime_to.append(addr)
if smime_to:
s.set_x509_stack(sk)
s.set_cipher(SMIME.Cipher('aes_192_cbc'))
p7 = s.encrypt(BIO.MemoryBuffer(raw_message.as_string()))
# Output p7 in mail-friendly format.
out = BIO.MemoryBuffer()
out.write('From: ' + from_addr + text.EOL)
out.write('To: ' + raw_message['To'] + text.EOL)
if raw_message['Cc']:
out.write('Cc: ' + raw_message['Cc'] + text.EOL)
if raw_message['Bcc']:
out.write('Bcc: ' + raw_message['Bcc'] + text.EOL)
if raw_message['Subject']:
out.write('Subject: ' + raw_message['Subject'] + text.EOL)
if conf.config_item_equals('default', 'add_header', 'yes'):
out.write('X-GPG-Mailgate: Encrypted by GPG Mailgate' + text.EOL)
s.write(out, p7)
LOG.debug(f"Sending message from {from_addr} to {smime_to}")
send_msg(out.read(), smime_to)
if unsmime_to:
LOG.debug(f"Unable to find valid S/MIME certificates for {unsmime_to}")
return unsmime_to
def _get_cert_for_email(to_addr, cert_path):
insensitive = conf.config_item_equals('default', 'mail_case_insensitive', 'yes')
LOG.info(f'Retrieving certificate for {to_addr!r} from {cert_path!r}, sensitivity={insensitive!r}')
files_in_directory = os.listdir(cert_path)
for filename in files_in_directory:
file_path = os.path.join(cert_path, filename)
if not os.path.isfile(file_path):
continue
if insensitive:
if filename.casefold() == to_addr:
return (file_path, to_addr)
else:
if filename == to_addr:
return (file_path, to_addr)
# support foo+ignore@bar.com -> foo@bar.com
LOG.info(f"An email with topic? {to_addr}")
(fixed_up_email, topic) = text.parse_delimiter(to_addr)
LOG.info(f'Got {fixed_up_email!r} and {topic!r}')
if topic is None:
# delimiter not used
LOG.info('Topic not found')
return None
else:
LOG.info(f"Looking up certificate for {fixed_up_email} after parsing {to_addr}")
return _get_cert_for_email(fixed_up_email, cert_path)
def _generate_message_from_payloads(payloads, message=None):
if message is None:
message = email.mime.multipart.MIMEMultipart(payloads.get_content_subtype())
for payload in payloads.get_payload():
if(isinstance(payload.get_payload(), list)):
message.attach(_generate_message_from_payloads(payload))
else:
message.attach(payload)
return message
def _get_first_payload(payloads):
if payloads.is_multipart():
return _get_first_payload(payloads.get_payload(0))
else:
return payloads
def send_msg(message: str, recipients, fromaddr=None):
"""Send MESSAGE to RECIPIENTS to the mail relay."""
global from_addr
if fromaddr is not None:
from_addr = fromaddr
recipients = [_f for _f in recipients if _f]
if recipients:
LOG.info(f"Sending email to: {recipients!r}")
relay = conf.relay_params()
smtp = smtplib.SMTP(relay[0], relay[1])
if conf.flag_enabled('relay', 'starttls'):
smtp.starttls()
smtp.sendmail(from_addr, recipients, message)
else:
LOG.info("No recipient found")
def _is_encrypted(raw_message: email.message.Message):
if raw_message.get_content_type() == 'multipart/encrypted':
return True
first_part = _get_first_payload(raw_message)
if first_part.get_content_type() == 'application/pkcs7-mime':
return True
return text.is_message_pgp_inline(first_part)
def delivery_plan(recipients, message: email.message.Message, key_cache: kcache.KeyCache):
"""Generate a sequence of delivery strategies."""
if _is_encrypted(message):
LOG.debug(f'Message is already encrypted: {message!r}')
return [KeepIntact(recipients)]
gpg_to, ungpg_to = _identify_gpg_recipients(recipients, key_cache)
gpg_mime_to, gpg_mime_cmd, gpg_inline_to, gpg_inline_cmd = \
_sort_gpg_recipients(gpg_to)
keyhome = conf.get_item('gpg', 'keyhome')
plan = []
if gpg_mime_to:
plan.append(MimeOpenPGPEncrypt(gpg_mime_to, gpg_mime_cmd, keyhome))
if gpg_inline_to:
plan.append(InlineOpenPGPEncrypt(gpg_inline_to, gpg_inline_cmd, keyhome))
if ungpg_to:
plan.append(KeepIntact(ungpg_to))
return plan
def deliver_message(raw_message: email.message.Message, from_address, to_addrs):
"""Send RAW_MESSAGE to all TO_ADDRS using the best encryption method available."""
global from_addr
# Ugly workaround to keep the code working without too many changes.
from_addr = from_address
sanitize = text.choose_sanitizer(conf.get_item('default', 'mail_case_insensitive'))
recipients_left = [sanitize(recipient) for recipient in to_addrs]
# There is no need for nested encryption
LOG.debug("Seeing if it's already encrypted")
if _is_encrypted(raw_message):
LOG.debug("Message is already encrypted. Encryption aborted.")
send_msg(raw_message.as_string(), recipients_left)
return
# Encrypt mails for recipients with known public PGP keys
LOG.debug("Encrypting with OpenPGP")
recipients_left = _gpg_encrypt(raw_message, recipients_left)
if not recipients_left:
return
# Encrypt mails for recipients with known S/MIME certificate
LOG.debug("Encrypting with S/MIME")
recipients_left = _smime_encrypt(raw_message, recipients_left)
if not recipients_left:
return
# Send out mail to recipients which are left
LOG.debug("Sending the rest as text/plain")
send_msg(raw_message.as_string(), recipients_left)

View File

@ -1,117 +0,0 @@
"""Lacre Daemon, the Advanced Mail Filter message dispatcher."""
import logging
import lacre
import lacre.config as conf
import sys
from aiosmtpd.controller import Controller
from aiosmtpd.smtp import Envelope
import asyncio
import email
import time
from watchdog.observers import Observer
# Mail status constants.
#
# These are the only values that our mail handler is allowed to return.
RESULT_OK = '250 OK'
RESULT_ERROR = '500 Could not process your message'
# Load configuration and init logging, in this order. Only then can we load
# the last Lacre module, i.e. lacre.mailgate.
conf.load_config()
lacre.init_logging(conf.get_item("logging", "config"))
LOG = logging.getLogger('lacre.daemon')
import lacre.core as gate
import lacre.keyring as kcache
class MailEncryptionProxy:
"""A mail handler dispatching to appropriate mail operation."""
def __init__(self, keyring: kcache.KeyRing):
"""Initialise the mail proxy with a reference to the key cache."""
self._keyring = keyring
async def handle_DATA(self, server, session, envelope: Envelope):
"""Accept a message and either encrypt it or forward as-is."""
start = time.process_time()
try:
keys = await self._keyring.freeze_identities()
message = email.message_from_bytes(envelope.content)
for operation in gate.delivery_plan(envelope.rcpt_tos, message, keys):
LOG.debug(f"Sending mail via {operation!r}")
new_message = operation.perform(message)
gate.send_msg(new_message, operation.recipients(), envelope.mail_from)
except TypeError as te:
LOG.exception("Got exception while processing", exc_info=te)
return RESULT_ERROR
ellapsed = (time.process_time() - start) * 1000
LOG.info(f'Message delivered in {ellapsed:.2f} ms')
return RESULT_OK
def _init_controller(keys: kcache.KeyRing, tout: float = 5):
proxy = MailEncryptionProxy(keys)
host, port = conf.daemon_params()
LOG.info(f"Initialising a mail Controller at {host}:{port}")
return Controller(proxy, hostname=host, port=port, ready_timeout=tout)
def _init_reloader(keyring_dir: str, reloader) -> kcache.KeyringModificationListener:
listener = kcache.KeyringModificationListener(reloader)
observer = Observer()
observer.schedule(listener, keyring_dir, recursive=False)
return observer
def _validate_config():
missing = conf.validate_config()
if missing:
params = ", ".join([_full_param_name(tup) for tup in missing])
LOG.error(f"Following mandatory parameters are missing: {params}")
sys.exit(lacre.EX_CONFIG)
def _full_param_name(tup):
return f"[{tup[0]}]{tup[1]}"
async def _sleep():
while True:
await asyncio.sleep(360)
def _main():
_validate_config()
keyring_path = conf.get_item('gpg', 'keyhome')
keyring = kcache.KeyRing(keyring_path)
controller = _init_controller(keyring)
reloader = _init_reloader(keyring_path, keyring)
LOG.info(f'Watching keyring directory {keyring_path}...')
reloader.start()
LOG.info('Starting the daemon...')
controller.start()
try:
asyncio.run(_sleep())
except KeyboardInterrupt:
LOG.info("Finishing...")
except:
LOG.exception('Unexpected exception caught, your system may be unstable')
finally:
LOG.info('Shutting down keyring watcher and the daemon...')
reloader.stop()
reloader.join()
controller.stop()
LOG.info("Done")
if __name__ == '__main__':
_main()

View File

@ -1,153 +0,0 @@
"""Data structures and utilities to make keyring access easier.
IMPORTANT: This module has to be loaded _after_ initialisation of the logging
module.
"""
import lacre.text as text
import lacre.config as conf
import logging
from os import stat
from watchdog.events import FileSystemEventHandler, FileSystemEvent
from asyncio import Semaphore, run
import copy
import GnuPG
LOG = logging.getLogger(__name__)
def _sanitize(keys):
sanitize = text.choose_sanitizer(conf.get_item('default', 'mail_case_insensitive'))
return {fingerprint: sanitize(keys[fingerprint]) for fingerprint in keys}
class KeyCacheMisconfiguration(Exception):
"""Exception used to signal that KeyCache is misconfigured."""
class KeyCache:
"""A store for OpenPGP keys.
Key case is sanitised while loading from GnuPG if so
configured. See mail_case_insensitive parameter in section
[default].
"""
def __init__(self, keys: dict = None):
"""Initialise an empty cache.
With keyring_dir given, set location of the directory from which keys should be loaded.
"""
self._keys = keys
def __getitem__(self, fingerpring):
"""Look up email assigned to the given fingerprint."""
return self._keys[fingerpring]
def __setitem__(self, fingerprint, email):
"""Assign an email to a fingerpring, overwriting it if it was already present."""
self._keys[fingerprint] = email
def __contains__(self, fingerprint):
"""Check if the given fingerprint is assigned to an email."""
# This method has to be present for KeyCache to be a dict substitute.
# See mailgate, function _identify_gpg_recipients.
return fingerprint in self._keys
def has_email(self, email):
"""Check if cache contains a key assigned to the given email."""
return email in self._keys.values()
def __repr__(self):
"""Return text representation of this object."""
details = ' '.join(self._keys.keys())
return f'<KeyCache {details}>'
class KeyRing:
"""A high-level adapter for GnuPG-maintained keyring directory.
Its role is to keep a cache of keys present in the keyring,
reload it when necessary and produce static copies of
fingerprint=>email maps.
"""
def __init__(self, path: str):
"""Initialise the adapter."""
self._path = path
self._keys = self._load_and_sanitize()
self._sema = Semaphore()
self._last_mod = None
def _load_and_sanitize(self):
keys = self._load_keyring_from(self._path)
return _sanitize(keys)
def _load_keyring_from(self, keyring_dir):
return GnuPG.public_keys(keyring_dir)
async def freeze_identities(self) -> KeyCache:
"""Return a static, async-safe copy of the identity map."""
async with self._sema:
keys = copy.deepcopy(self._keys)
return KeyCache(keys)
def load(self):
"""Load keyring, replacing any previous contents of the cache."""
LOG.debug('Reloading keys...')
run(self._load())
async def _load(self):
last_mod = self._read_mod_time()
if self._is_modified(last_mod):
async with self._sema:
self.replace_keyring(self._load_keyring_from(self._path))
self._last_mod = self._read_mod_time()
reload = load
def replace_keyring(self, keys: dict):
"""Overwrite previously stored key cache with KEYS."""
keys = _sanitize(keys)
LOG.info(f'Storing {len(keys)} keys')
self._keys = keys
def _read_mod_time(self):
# (mode, ino, dev, nlink, uid, gid, size, atime, mtime, ctime)
# 0 1 2 3 4 5 6 7 8 9
MTIME = 8
st = stat(self._path)
return st[MTIME]
def _is_modified(self, last_mod):
if self._last_mod is None:
LOG.debug('Keyring not loaded before')
return True
elif self._last_mod != last_mod:
LOG.debug('Keyring directory mtime changed')
return True
else:
LOG.debug('Keyring not modified ')
return False
class KeyringModificationListener(FileSystemEventHandler):
"""A filesystem event listener that triggers key cache reload."""
def __init__(self, keyring: KeyRing):
"""Initialise a listener with a callback to be executed upon each change."""
self._keyring = keyring
def handle(self, event: FileSystemEvent):
"""Reload keys upon FS event."""
if 'pubring.kbx' in event.src_path:
LOG.debug(f'Reloading on event {event!r}')
self._keyring.reload()
# All methods should do the same: reload the key cache.
# on_created = handle
# on_deleted = handle
on_modified = handle

View File

@ -1,130 +0,0 @@
"""Mail operations for a given recipient.
There are 3 operations available:
- OpenPGPEncrypt: to deliver the message to a recipient with an OpenPGP public
key available.
- SMimeEncrypt: to deliver the message to a recipient with an S/MIME
certificate.
- KeepIntact: a no-operation (implementation of the Null Object pattern), used
for messages already encrypted or those who haven't provided their keys or
certificates.
"""
import logging
import lacre.core as core
from email.message import Message
LOG = logging.getLogger(__name__)
class MailOperation:
"""Contract for an operation to be performed on a message."""
def __init__(self, recipients=[]):
"""Initialise the operation with a recipient."""
self._recipients = recipients
def perform(self, message: Message):
"""Perform this operation on MESSAGE.
Return target message.
"""
raise NotImplementedError(self.__class__())
def recipients(self):
"""Return list of recipients of the message."""
return self._recipients
def add_recipient(self, recipient):
"""Register another message recipient."""
self._recipients.append(recipient)
class OpenPGPEncrypt(MailOperation):
"""OpenPGP-encrypt the message."""
def __init__(self, recipients, keys, keyhome):
"""Initialise encryption operation."""
super().__init__(recipients)
self._keys = keys
self._keyhome = keyhome
def extend_keys(self, keys):
"""Register GPG keys to encrypt this message for."""
self._keys.extend(keys)
def __repr__(self):
"""Generate a representation with just method and key."""
return f"<{type(self).__name__} {self._recipients} {self._keys}>"
class InlineOpenPGPEncrypt(OpenPGPEncrypt):
"""Inline encryption strategy."""
def __init__(self, recipients, keys, keyhome):
"""Initialise strategy object."""
super().__init__(recipients, keys, keyhome)
def perform(self, msg: Message):
"""Encrypt with PGP Inline."""
LOG.debug('Sending PGP/Inline...')
return core._gpg_encrypt_and_return(msg,
self._keys, self._recipients,
core._encrypt_all_payloads_inline)
class MimeOpenPGPEncrypt(OpenPGPEncrypt):
"""MIME encryption strategy."""
def __init__(self, recipients, keys, keyhome):
"""Initialise strategy object."""
super().__init__(recipients, keys, keyhome)
def perform(self, msg: Message):
"""Encrypt with PGP MIME."""
LOG.debug('Sending PGP/MIME...')
return core._gpg_encrypt_and_return(msg,
self._keys, self._recipients,
core._encrypt_all_payloads_mime)
class SMimeEncrypt(MailOperation):
"""S/MIME encryption operation."""
def __init__(self, recipient, email, certificate):
"""Initialise S/MIME encryption for a given EMAIL and CERTIFICATE."""
super().__init__(recipient)
self._email = email
self._cert = certificate
def perform(self, message: Message):
"""Encrypt with a certificate."""
LOG.warning(f"Delivering clear-text to {self._recipients}")
return message
def __repr__(self):
"""Generate a representation with just method and key."""
return f"<S/MIME {self._recipients}, {self._cert}>"
class KeepIntact(MailOperation):
"""A do-nothing operation (Null Object implementation).
This operation should be used for mail that's already encrypted.
"""
def __init__(self, recipients):
"""Initialise pass-through operation for a given recipient."""
super().__init__(recipients)
def perform(self, message: Message):
"""Return MESSAGE unmodified."""
return message.as_string()
def __repr__(self):
"""Return representation with just method and email."""
return f"<KeepIntact {self._recipients}>"

View File

@ -1,87 +0,0 @@
"""Basic payload-processing routines."""
import sys
import re
import logging
from email.message import Message
# The standard way to encode line-ending in email:
EOL = "\r\n"
EOL_BYTES = b"\r\n"
PGP_INLINE_BEGIN = EOL_BYTES + b"-----BEGIN PGP MESSAGE-----" + EOL_BYTES
PGP_INLINE_END = EOL_BYTES + b"-----END PGP MESSAGE-----" + EOL_BYTES
LOG = logging.getLogger(__name__)
def parse_content_type(content_type: str):
"""Analyse Content-Type email header.
Return a pair: type and sub-type.
"""
parts = [p.strip() for p in content_type.split(';')]
if len(parts) == 1:
# No additional attributes provided. Use default encoding.
return (content_type, sys.getdefaultencoding())
# At least one attribute provided. Find out if any of them is named
# 'charset' and if so, use it.
ctype = parts[0]
encoding = [p for p in parts[1:] if p.startswith('charset=')]
if encoding:
eq_idx = encoding[0].index('=')
return (ctype, encoding[0][eq_idx+1:])
else:
return (ctype, sys.getdefaultencoding())
def parse_delimiter(address: str):
"""Parse an email with delimiter and topic.
Return destination emaili and topic as a tuple.
"""
withdelim = re.match('^([^\\+]+)\\+([^@]+)@(.*)$', address)
LOG.debug(f'Parsed email: {withdelim!r}')
if withdelim:
return (withdelim.group(1) + '@' + withdelim.group(3), withdelim.group(2))
else:
return (address, None)
def _lowercase_whole_address(address: str):
return address.lower()
def _lowercase_domain_only(address: str):
parts = address.split('@', maxsplit=2)
if len(parts) > 1:
return parts[0] + '@' + parts[1].lower()
else:
return address
def choose_sanitizer(mail_case_insensitive: bool):
"""Return a function to sanitize email case sense."""
if mail_case_insensitive:
return _lowercase_whole_address
else:
return _lowercase_domain_only
def is_payload_pgp_inline(payload: bytes) -> bool:
"""Find out if the payload (bytes) contains PGP/inline markers."""
return PGP_INLINE_BEGIN in payload and PGP_INLINE_END in payload
def is_message_pgp_inline(message: Message) -> bool:
"""Find out if a message is already PGP-Inline encrypted."""
if message.is_multipart() or isinstance(message.get_payload(), list):
# more than one payload, check each one of them
return any(is_message_pgp_inline(m.payload()) for m in message.iter_parts())
else:
# one payload, check it
return is_payload_pgp_inline(message.get_payload(decode=True))

View File

@ -7,28 +7,37 @@ from M2Crypto import BIO, Rand, SMIME, X509
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
import logging
# Read configuration from /etc/gpg-mailgate.conf
_cfg = RawConfigParser()
_cfg.read('/etc/gpg-mailgate.conf')
cfg = dict()
for sect in _cfg.sections():
cfg[sect] = dict()
for (name, value) in _cfg.items(sect):
cfg[sect][name] = value
import lacre
import lacre.config as conf
def log(msg):
if 'logging' in cfg and 'file' in cfg['logging']:
if cfg['logging']['file'] == "syslog":
syslog.syslog(syslog.LOG_INFO | syslog.LOG_MAIL, msg)
else:
logfile = open(cfg['logging']['file'], 'a')
logfile.write(msg + "\n")
logfile.close()
CERT_PATH = cfg['smime']['cert_path']+"/"
def send_msg( message, from_addr, recipients = None ):
if conf.config_item_set('relay', 'host') and conf.config_item_set('relay', 'enc_port'):
relay = (conf.get_item('relay', 'host'), int(conf.get_item('relay', 'enc_port')))
if 'relay' in cfg and 'host' in cfg['relay'] and 'enc_port' in cfg['relay']:
relay = (cfg['relay']['host'], int(cfg['relay']['enc_port']))
smtp = smtplib.SMTP(relay[0], relay[1])
smtp.sendmail( from_addr, recipients, message.as_string() )
else:
LOG.info("Could not send mail due to wrong configuration")
log("Could not send mail due to wrong configuration")
if __name__ == "__main__":
# try:
conf.load_config()
lacre.init_logging(conf.get_item('logging', 'config'))
LOG = logging.getLogger(__name__)
CERT_PATH = conf.get_item('smime', 'cert_path') + '/'
# Read e-mail from stdin
raw = sys.stdin.read()
register_msg = email.message_from_string( raw )
@ -54,18 +63,18 @@ if __name__ == "__main__":
break
if sign_part == None:
LOG.info("Unable to find PKCS7 signature or public PGP key in registration email")
log("Unable to find PKCS7 signature or public PGP key in registration email")
failure_msg = file( conf.get_item('mailregister', 'mail_templates') + "/registrationError.md").read()
failure_msg = file( cfg['mailregister']['mail_templates'] + "/registrationError.md").read()
msg = MIMEMultipart("alternative")
msg["From"] = conf.get_item('mailregister', 'register_email')
msg["From"] = cfg['mailregister']['register_email']
msg["To"] = from_addr
msg["Subject"] = "S/MIME / OpenPGP registration failed"
msg.attach(MIMEText(failure_msg, 'plain'))
msg.attach(MIMEText(markdown.markdown(failure_msg), 'html'))
send_msg(msg, conf.get_item('mailregister', 'register_email'), [from_addr])
send_msg(msg, cfg['mailregister']['register_email'], [from_addr])
sys.exit(0)
if sign_type == 'smime':
@ -96,42 +105,42 @@ if __name__ == "__main__":
# format in user-specific data
# sending success mail only for S/MIME as GPGMW handles this on its own
success_msg = file(conf.get_item('mailregister', 'mail_templates')+"/registrationSuccess.md").read()
success_msg = file(cfg['mailregister']['mail_templates']+"/registrationSuccess.md").read()
success_msg = success_msg.replace("[:FROMADDRESS:]", from_addr)
msg = MIMEMultipart("alternative")
msg["From"] = conf.get_item('mailregister', 'register_email')
msg["From"] = cfg['mailregister']['register_email']
msg["To"] = from_addr
msg["Subject"] = "S/MIME certificate registration succeeded"
msg.attach(MIMEText(success_msg, 'plain'))
msg.attach(MIMEText(markdown.markdown(success_msg), 'html'))
send_msg(msg, conf.get_item('mailregister', 'register_email'), [from_addr])
send_msg(msg, cfg['mailregister']['register_email'], [from_addr])
LOG.info("S/MIME Registration succeeded")
log("S/MIME Registration succeeded")
elif sign_type == 'pgp':
# send POST to gpg-mailgate webpanel
sig = sign_part
payload = {'email': from_addr, 'key': sig}
r = requests.post(conf.get_item('mailregister', 'webpanel_url'), data=payload)
r = requests.post(cfg['mailregister']['webpanel_url'], data=payload)
if r.status_code != 200:
LOG.info("Could not hand registration over to GPGMW. Error: %s" % r.status_code)
error_msg = open(conf.get_item('mailregister', 'mail_templates')+"/gpgmwFailed.md").read()
log("Could not hand registration over to GPGMW. Error: %s" % r.status_code)
error_msg = file(cfg['mailregister']['mail_templates']+"/gpgmwFailed.md").read()
error_msg = error_msg.replace("[:FROMADDRESS:]", from_addr)
msg = MIMEMultipart("alternative")
msg["From"] = conf.get_item('mailregister', 'register_email')
msg["From"] = cfg['mailregister']['register_email']
msg["To"] = from_addr
msg["Subject"] = "PGP key registration failed"
msg.attach(MIMEText(error_msg, 'plain'))
msg.attach(MIMEText(markdown.markdown(error_msg), 'html'))
send_msg(msg, conf.get_item('mailregister', 'register_email'), [from_addr])
send_msg(msg, cfg['mailregister']['register_email'], [from_addr])
else:
LOG.info("PGP registration is handed over to GPGMW")
log("PGP registration is handed over to GPGMW")
# except:
# LOG.info("Registration exception")
# log("Registration exception")
# sys.exit(0)

View File

@ -1,6 +0,0 @@
aiosmtpd==1.4.2
SQLAlchemy==1.4.32
Markdown==3.4.1
M2Crypto==0.38.0
requests==2.27.1
watchdog==2.1.9

View File

@ -1,130 +0,0 @@
#
# gpg-mailgate
#
# This file is part of the gpg-mailgate source code.
#
# gpg-mailgate is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# gpg-mailgate source code is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with gpg-mailgate source code. If not, see <http://www.gnu.org/licenses/>.
#
import configparser
import logging
import subprocess
import os
import time
def _spawn(cmd):
env_dict = {
"PATH": os.getenv("PATH"),
"PYTHONPATH": os.getcwd(),
"GPG_MAILGATE_CONFIG": "test/gpg-mailgate-daemon-test.conf"
}
logging.debug(f"Spawning command: {cmd} with environment: {env_dict!r}")
return subprocess.Popen(cmd,
stdin=None,
stdout=subprocess.PIPE,
env=env_dict)
def _interrupt(proc):
# proc.send_signal(signal.SIGINT)
proc.terminate()
def _load(name):
logging.debug(f"Loading file {name}")
f = open(name, "r")
contents = f.read()
f.close()
return contents
def _send(host, port, mail_from, mail_to, message):
logging.debug(f"Sending message to {host}:{port}")
_spawn([os.getenv("PYTHON") or "python",
"test/utils/sendmail.py",
"-f", mail_from,
"-t", mail_to,
"-m", message])
def _load_test_config():
cp = configparser.ConfigParser()
cp.read("test/e2e.ini")
return cp
def _identity(x):
return x
def _inversion(x):
return not(x)
def _report_result(message_file, expected, test_output, boolean_func=_identity):
status = None
expected_line = "\r\n" + expected # + "\r\n"
cond_met = boolean_func(expected_line in test_output)
if cond_met:
status = "Success"
else:
status = "Failure"
print(message_file.ljust(35), status)
def _execute_case(config, case_name):
logging.info(f"Executing case {case_name}")
python = os.getenv("PYTHON", "python")
relay_mock = _spawn([python, "test/utils/relay.py", "2500"])
time.sleep(1) # Wait for the relay to start up.
_send("localhost", 10025, "dave@disposlab",
config.get(case_name, 'to'), config.get(case_name, 'in'))
relay_mock.wait()
(test_out, _) = relay_mock.communicate()
test_out = test_out.decode('utf-8')
logging.debug(f"Read {len(test_out)} characters of output: '{test_out}'")
if 'out' in config[case_name]:
_report_result(config.get(case_name, "in"), config.get(case_name, "out"), test_out)
else:
_report_result(config.get(case_name, "in"), config.get(case_name, "out-not"), test_out, boolean_func=_inversion)
def _main():
conf = _load_test_config()
logging.basicConfig(filename="test/logs/daemon-test.log",
format="%(asctime)s %(pathname)s:%(lineno)d %(levelname)s [%(funcName)s] %(message)s",
datefmt="%Y-%m-%d %H:%M:%S",
level=logging.DEBUG)
logging.info("Starting Lacre Daemon tests...")
python = os.getenv("PYTHON", "python")
server = _spawn([python, "-m", "lacre.daemon"])
for case_no in range(1, conf.getint("tests", "cases") + 1):
_execute_case(conf, case_name=f"case-{case_no}")
_interrupt(server)
if __name__ == '__main__':
_main()

View File

@ -22,7 +22,7 @@
[relay]
port: 2500
script: test/utils/relay.py
script: test/relay.py
[dirs]
keys: test/keyhome
@ -30,12 +30,11 @@ certs: test/certs
[tests]
# Number of "test-*" sections in this file, describing test cases.
cases: 9
cases: 6
e2e_log: test/logs/e2e.log
e2e_log_format: %(asctime)s %(pathname)s:%(lineno)d %(levelname)s [%(funcName)s] %(message)s
e2e_log_datefmt: %Y-%m-%d %H:%M:%S
lacre_log: test/logs/gpg-mailgate.log
log_config: test/gpg-lacre-log.ini
[case-1]
descr: Clear text message to a user without a key
@ -72,21 +71,3 @@ descr: Multipart encrypted message to a user with an Ed25519 key.
to: bob@disposlab
in: test/msgin/multipart2rsa.msg
out: -----BEGIN PGP MESSAGE-----
[case-7]
descr: Clear text message to a user with an RSA key and PGP/MIME enabled in configuration
to: evan@disposlab
in: test/msgin/clear2rsa2.msg
out: -----BEGIN PGP MESSAGE-----
[case-8]
descr: Clear text message to address with delimiter and a user with an Ed25519 key.
to: bob@disposlab
in: test/msgin/clear2ed-delim.msg
out: -----BEGIN PGP MESSAGE-----
[case-9]
descr: Clear text message with inline PGP markers to recipient without a key.
to: carlos@disposlab
in: test/msgin/with-markers2clear.msg
out-not: This message includes inline PGP markers.

View File

@ -1,38 +1,43 @@
#
# gpg-mailgate
# gpg-mailgate
#
# This file is part of the gpg-mailgate source code.
# This file is part of the gpg-mailgate source code.
#
# gpg-mailgate is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# gpg-mailgate is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# gpg-mailgate source code is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
# gpg-mailgate source code is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with gpg-mailgate source code. If not, see <http://www.gnu.org/licenses/>.
# You should have received a copy of the GNU General Public License
# along with gpg-mailgate source code. If not, see <http://www.gnu.org/licenses/>.
#
import os
import sys
import subprocess
import difflib
import configparser
import logging
from time import sleep
RELAY_SCRIPT = "test/utils/relay.py"
RELAY_SCRIPT = "test/relay.py"
CONFIG_FILE = "test/gpg-mailgate.conf"
def _build_config(config):
cp = configparser.RawConfigParser()
def build_config(config):
cp = configparser.ConfigParser()
cp.add_section("logging")
cp.set("logging", "config", config["log_config"])
cp.set("logging", "file", config["log_file"])
cp.set("logging", "verbose", "yes")
cp.add_section("gpg")
cp.set("gpg", "keyhome", config["gpg_keyhome"])
@ -44,64 +49,40 @@ def _build_config(config):
cp.set("relay", "host", "localhost")
cp.set("relay", "port", config["port"])
cp.add_section("daemon")
cp.set("daemon", "host", "localhost")
cp.set("daemon", "port", "10025")
cp.add_section("enc_keymap")
cp.set("enc_keymap", "alice@disposlab", "1CD245308F0963D038E88357973CF4D9387C44D7")
cp.set("enc_keymap", "bob@disposlab", "19CF4B47ECC9C47AFA84D4BD96F39FDA0E31BB67")
cp.set("enc_keymap", "evan@disposlab", "530B1BB2D0CC7971648198BBA4774E507D3AF5BC")
cp.add_section("pgp_style")
# Default style is PGP/Inline, so to cover more branches, one test identity
# uses PGP/MIME.
cp.set("pgp_style", "evan@disposlab", "mime")
logging.debug(f"Created config with keyhome={config['gpg_keyhome']}, cert_path={config['smime_certpath']} and relay at port {config['port']}")
return cp
def _write_test_config(outfile, **config):
def write_test_config(outfile, **config):
logging.debug(f"Generating configuration with {config!r}")
out = open(outfile, "w+")
cp = _build_config(config)
cp = build_config(config)
cp.write(out)
out.close()
logging.debug(f"Wrote configuration to {outfile}")
def load_file(name):
f = open(name, 'r')
contents = f.read()
f.close()
def _load_file(name):
f = open(name, 'r')
contents = f.read()
f.close()
return bytes(contents, 'utf-8')
return bytes(contents, 'utf-8')
def _identity(x):
return x
def _inversion(x):
return not(x)
def _report_result(message_file, expected, test_output, boolean_func=_identity):
def report_result(message_file, expected, test_output):
status = None
expected_line = "\r\n" + expected # + "\r\n"
cond_met = boolean_func(expected_line in test_output)
if cond_met:
if expected in test_output:
status = "Success"
else:
status = "Failure"
print(message_file.ljust(35), status)
print(message_file.ljust(30), status)
def _execute_e2e_test(case_name, config, config_path):
def execute_e2e_test(case_name, config, config_path):
"""Read test case configuration from config and run that test case.
Parameter case_name should refer to a section in test
@ -121,17 +102,17 @@ def _execute_e2e_test(case_name, config, config_path):
logging.debug(f"Spawning relay: {relay_cmd}")
relay_proc = subprocess.Popen(relay_cmd,
stdin=None,
stdout=subprocess.PIPE)
stdin = None,
stdout = subprocess.PIPE)
logging.debug(f"Spawning GPG-Lacre: {gpglacre_cmd}, stdin = {config.get(case_name, 'in')}")
# pass PATH because otherwise it would be dropped
gpglacre_proc = subprocess.run(gpglacre_cmd,
input=_load_file(config.get(case_name, "in")),
capture_output=True,
env={"GPG_MAILGATE_CONFIG": config_path,
"PATH": os.getenv("PATH")})
input = load_file(config.get(case_name, "in")),
capture_output = True,
env = {"GPG_MAILGATE_CONFIG": config_path,
"PATH": os.getenv("PATH")})
# Let the relay process the data.
relay_proc.wait()
@ -141,20 +122,16 @@ def _execute_e2e_test(case_name, config, config_path):
logging.debug(f"Read {len(testout)} characters of test output: '{testout}'")
if 'out' in config[case_name]:
_report_result(config.get(case_name, "in"), config.get(case_name, "out"), testout)
else:
_report_result(config.get(case_name, "in"), config.get(case_name, "out-not"), testout, boolean_func=_inversion)
report_result(config.get(case_name, "in"), config.get(case_name, "out"), testout)
def _load_test_config():
def load_test_config():
cp = configparser.ConfigParser()
cp.read("test/e2e.ini")
return cp
config = _load_test_config()
config = load_test_config()
logging.basicConfig(filename = config.get("tests", "e2e_log"),
# Get raw values of log and date formats because they
@ -166,16 +143,16 @@ logging.basicConfig(filename = config.get("tests", "e2e_log"),
config_path = os.getcwd() + "/" + CONFIG_FILE
_write_test_config(config_path,
port = config.get("relay", "port"),
gpg_keyhome = config.get("dirs", "keys"),
smime_certpath = config.get("dirs", "certs"),
log_config = config.get("tests", "log_config"))
write_test_config(config_path,
port = config.get("relay", "port"),
gpg_keyhome = config.get("dirs", "keys"),
smime_certpath = config.get("dirs", "certs"),
log_file = config.get("tests", "lacre_log"))
for case_no in range(1, config.getint("tests", "cases") + 1):
for case_no in range(1, config.getint("tests", "cases")+1):
case_name = f"case-{case_no}"
logging.info(f"Executing {case_name}: {config.get(case_name, 'descr')}")
_execute_e2e_test(case_name, config, config_path)
execute_e2e_test(case_name, config, config_path)
print("See diagnostic output for details. Tests: '%s', Lacre: '%s'" % (config.get("tests", "e2e_log"), config.get("tests", "lacre_log")))

View File

@ -1,24 +0,0 @@
[loggers]
keys=root
[logger_root]
level=NOTSET
handlers=lacrelog
[handlers]
keys=lacrelog
[formatters]
keys=postfixfmt
[handler_lacrelog]
class=FileHandler
level=DEBUG
formatter=postfixfmt
args=('test/logs/lacre.log', 'a+')
[formatter_postfixfmt]
format=%(asctime)s %(name)s[%(process)d]: %(message)s
datefmt=%b %e %H:%M:%S
style=%
validate=True

View File

@ -1,27 +0,0 @@
[logging]
config = test/gpg-lacre-log.ini
file = test/logs/gpg-mailgate.log
format = %(asctime)s %(module)s[%(process)d]: %(message)s
date_format = ISO
[gpg]
keyhome = test/keyhome
[smime]
cert_path = test/certs
[database]
enabled = yes
url = sqlite:///test/lacre.db
[relay]
host = localhost
port = 2500
[cron]
send_email = no
[enc_keymap]
alice@disposlab = 1CD245308F0963D038E88357973CF4D9387C44D7
bob@disposlab = 19CF4B47ECC9C47AFA84D4BD96F39FDA0E31BB67

View File

@ -1,32 +0,0 @@
[logging]
config = test/gpg-lacre-log.ini
file = test/logs/gpg-mailgate.log
format = %(asctime)s %(module)s[%(process)d]: %(message)s
date_format = ISO
[gpg]
keyhome = test/keyhome
cache_refresh_minutes = 1
[smime]
cert_path = test/certs
[database]
enabled = yes
url = sqlite:///test/lacre.db
[relay]
host = localhost
port = 2500
[daemon]
host = localhost
port = 10025
[cron]
send_email = no
[enc_keymap]
alice@disposlab = 1CD245308F0963D038E88357973CF4D9387C44D7
bob@disposlab = 19CF4B47ECC9C47AFA84D4BD96F39FDA0E31BB67

Binary file not shown.

View File

@ -1,13 +0,0 @@
-----BEGIN PGP PUBLIC KEY BLOCK-----
mDMEYdTFkRYJKwYBBAHaRw8BAQdA2tgdP1pMt3cv3XAW7ov5AFn74mMZvyTksp9Q
eO1PkpK0GkJvYiBGb29iYXIgPGJvYkBkaXNwb3NsYWI+iJYEExYIAD4WIQQZz0tH
7MnEevqE1L2W85/aDjG7ZwUCYdTFkQIbAwUJA8JnAAULCQgHAgYVCgkICwIEFgID
AQIeAQIXgAAKCRCW85/aDjG7ZxVnAP49t7BU2H+/WCpa3fCAlMEcik82sU4p+U9D
pMsbjawwYgEA1SbA5CF835cMjoEufy1h+2M4T9gI/0X2lk8OAtwwggm4OARh1MXg
EgorBgEEAZdVAQUBAQdAUVNKx2OsGtNdRsnl3J/uv6obkUC0KcO4ikdRs+iejlMD
AQgHiHgEGBYIACAWIQQZz0tH7MnEevqE1L2W85/aDjG7ZwUCYdTF4AIbDAAKCRCW
85/aDjG7Z039APwLGP5ibqCC9yIr4YVbdWff1Ch+2C91MR2ObF93Up9+ogD8D2zd
OjjB6xRD0Q2FN+alsNGCtdutAs18AZ5l33RMzws=
=wWoq
-----END PGP PUBLIC KEY BLOCK-----

View File

@ -1,92 +0,0 @@
#
# gpg-mailgate
#
# This file is part of the gpg-mailgate source code.
#
# gpg-mailgate is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# gpg-mailgate source code is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with gpg-mailgate source code. If not, see <http://www.gnu.org/licenses/>.
#
"""Unit-tests as contracts for external dependencies.
Unit tests defined here are our contracts for the dependencies used by Lacre.
Since not all software is documented thoroughly, they are also a form of
documentation.
"""
import email
import unittest
from configparser import RawConfigParser
class EmailParsingTest(unittest.TestCase):
"""This test serves as a package contract and documentation of its behaviour."""
def test_message_from_bytes_produces_message_with_str_headers(self):
rawmsg = b"From: alice@lacre.io\r\n" \
+ b"To: bob@lacre.io\r\n" \
+ b"Subject: Test message\r\n" \
+ b"\r\n" \
+ b"Test message from Alice to Bob.\r\n"
parsed = email.message_from_bytes(rawmsg)
self.assertEqual(parsed["From"], "alice@lacre.io")
self.assertEqual(parsed["To"], "bob@lacre.io")
self.assertEqual(parsed["Subject"], "Test message")
def test_bytes_message_payload_decoded_produces_bytes(self):
rawmsg = b"From: alice@lacre.io\r\n" \
+ b"To: bob@lacre.io\r\n" \
+ b"Subject: Test message\r\n" \
+ b"\r\n" \
+ b"Test message from Alice to Bob.\r\n"
parsed = email.message_from_bytes(rawmsg)
self.assertEqual(parsed.get_payload(), "Test message from Alice to Bob.\r\n")
self.assertEqual(parsed.get_payload(decode=True), b"Test message from Alice to Bob.\r\n")
def test_message_from_string_produces_message_with_str_headers(self):
rawmsg = "From: alice@lacre.io\r\n" \
+ "To: bob@lacre.io\r\n" \
+ "Subject: Test message\r\n" \
+ "\r\n" \
+ "Test message from Alice to Bob.\r\n"
parsed = email.message_from_string(rawmsg)
self.assertEqual(parsed["From"], "alice@lacre.io")
self.assertEqual(parsed["To"], "bob@lacre.io")
self.assertEqual(parsed["Subject"], "Test message")
def test_str_message_payload_decoded_produces_bytes(self):
rawmsg = "From: alice@lacre.io\r\n" \
+ "To: bob@lacre.io\r\n" \
+ "Subject: Test message\r\n" \
+ "\r\n" \
+ "Test message from Alice to Bob.\r\n"
parsed = email.message_from_string(rawmsg)
self.assertEqual(parsed.get_payload(), "Test message from Alice to Bob.\r\n")
self.assertEqual(parsed.get_payload(decode=True), b"Test message from Alice to Bob.\r\n")
class RawConfigParserTest(unittest.TestCase):
def test_config_parser_returns_str(self):
cp = RawConfigParser()
cp.read("test/sample.ini")
self.assertEqual(cp.get("foo", "bar"), "quux")
self.assertEqual(cp.get("foo", "baz"), "14")
if __name__ == '__main__':
unittest.main()

View File

@ -1,46 +0,0 @@
import GnuPG
import unittest
class GnuPGUtilitiesTest(unittest.TestCase):
def test_build_default_command(self):
cmd = GnuPG._build_command("test/keyhome")
self.assertEqual(cmd, ["gpg", "--homedir", "test/keyhome"])
def test_build_command_extended_with_args(self):
cmd = GnuPG._build_command("test/keyhome", "--foo", "--bar")
self.assertEqual(cmd, ["gpg", "--homedir", "test/keyhome", "--foo", "--bar"])
def test_key_confirmation_with_matching_email(self):
armored_key = self._load('test/keys/bob@disposlab')
matching_email = 'bob@disposlab'
is_confirmed = GnuPG.confirm_key(armored_key, matching_email)
self.assertTrue(is_confirmed)
def test_key_confirmation_email_mismatch(self):
armored_key = self._load('test/keys/bob@disposlab')
not_matching_email = 'lucy@disposlab'
is_confirmed = GnuPG.confirm_key(armored_key, not_matching_email)
self.assertFalse(is_confirmed)
def test_key_listing(self):
keys = GnuPG.public_keys('test/keyhome')
known_identities = {
'1CD245308F0963D038E88357973CF4D9387C44D7': 'alice@disposlab',
'19CF4B47ECC9C47AFA84D4BD96F39FDA0E31BB67': 'bob@disposlab',
'530B1BB2D0CC7971648198BBA4774E507D3AF5BC': 'evan@disposlab'
}
self.assertDictEqual(keys, known_identities)
def _load(self, filename):
with open(filename) as f:
return f.read()
if __name__ == '__main__':
unittest.main()

View File

@ -1,20 +0,0 @@
from lacre.keyring import KeyCache
import unittest
class LacreKeyCacheTest(unittest.TestCase):
def test_extend_keyring(self):
kc = KeyCache({'FINGERPRINT': 'john.doe@example.com'})
self.assertTrue('FINGERPRINT' in kc)
def test_membership_methods(self):
kc = KeyCache({
'FINGERPRINT': 'alice@example.com',
'OTHERPRINT': 'bob@example.com'
})
self.assertTrue('FINGERPRINT' in kc)
self.assertFalse('FOOTPRINT' in kc)
self.assertTrue(kc.has_email('bob@example.com'))
self.assertFalse(kc.has_email('dave@example.com'))

View File

@ -1,37 +0,0 @@
import lacre.text
import sys
import unittest
class LacreTextTest(unittest.TestCase):
def test_parse_content_type_without_charset(self):
(mtype, mcharset) = lacre.text.parse_content_type('text/plain')
self.assertEqual(mtype, 'text/plain')
self.assertEqual(mcharset, sys.getdefaultencoding())
def test_parse_content_type_with_charset(self):
(mtype, mcharset) = lacre.text.parse_content_type('text/plain; charset="UTF-8"')
self.assertEqual(mtype, 'text/plain')
self.assertEqual(mcharset, '"UTF-8"')
def test_parse_content_type_with_other_attributes(self):
(mtype, mcharset) = lacre.text.parse_content_type('text/plain; some-param="Some Value"')
self.assertEqual(mtype, 'text/plain')
self.assertEqual(mcharset, sys.getdefaultencoding())
def test_parse_content_type_with_several_attributes(self):
(mtype, mcharset) = lacre.text.parse_content_type('text/plain; charset="UTF-8"; some-param="Some Value"')
self.assertEqual(mtype, 'text/plain')
self.assertEqual(mcharset, '"UTF-8"')
def test_parse_email_without_delimiter(self):
addr = "Some.Name@example.com"
(addr2, topic) = lacre.text.parse_delimiter(addr)
self.assertEqual(addr2, "Some.Name@example.com")
self.assertEqual(topic, None)
def test_parse_email_with_delimiter(self):
addr = "Some.Name+some-topic@example.com"
(addr2, topic) = lacre.text.parse_delimiter(addr)
self.assertEqual(addr2, "Some.Name@example.com")
self.assertEqual(topic, "some-topic")

View File

@ -1,15 +0,0 @@
from M2Crypto import BIO
import unittest
class M2CryptoBioMemoryBufferTest(unittest.TestCase):
def test_memory_buffer_write_str(self):
mb = BIO.MemoryBuffer()
mb.write("Foo")
mb.close()
self.assertEqual(len(mb), 3)
def test_memory_buffer_write_bytes(self):
mb = BIO.MemoryBuffer()
mb.write(b"Foo")
mb.close()
self.assertEqual(len(mb), 3)

View File

@ -1,5 +1,5 @@
From: Dave <dave@localhost>
To: Carlos <carlos@localhost>
Subject: Test
Body of the message.
From: Dave <dave@localhost>
To: Carlos <carlos@localhost>
Subject: Test
Body of the message.

View File

@ -1,5 +0,0 @@
From: Dave <dave@localhost>
To: Bob <bob+foobar@localhost>
Subject: Test
Body of the message.

View File

@ -1,5 +1,5 @@
From: Dave <dave@localhost>
To: Bob <bob@localhost>
Subject: Test
Body of the message.
From: Dave <dave@localhost>
To: Bob <bob@localhost>
Subject: Test
Body of the message.

View File

@ -1,5 +1,5 @@
From: Dave <dave@localhost>
To: Alice <alice@localhost>
Subject: Test
Body of the message.
From: Dave <dave@localhost>
To: Alice <alice@localhost>
Subject: Test
Body of the message.

View File

@ -1,6 +0,0 @@
From: Dave <dave@disposlab
To: Evan <evan@disposlab>
Subject: Test
Content-Type: text/plain; charset="utf-8"
Body of the message.

View File

@ -1,13 +1,13 @@
From: Dave <dave@localhost>
To: Bob <bob@localhost>
Subject: Test
Content-Transfer-Encoding: 7bit
-----BEGIN PGP MESSAGE-----
hF4DujWCoRS24dYSAQdAyGDF9Us11JDr8+XPmvlJHsMS7A4UBIcCiresJyZpSxYw
Cqcugy5AX5fgSAiL1Cd2b1zpQ/rYdTWkFYMVbH4jBEoPC3z/aSd+hTnneJFDUdXl
0koBDIw7NQylu6SrW+Y/DmXgalIHtwACuKivJTq/z9jdwFScV7adRR/VO53Inah3
L1+Ho7Zta95AYW3UPu71Gw3rrkfjY4uGDiFAFg==
=yTzD
-----END PGP MESSAGE-----
From: Dave <dave@localhost>
To: Bob <bob@localhost>
Subject: Test
Content-Transfer-Encoding: 7bit
-----BEGIN PGP MESSAGE-----
hF4DujWCoRS24dYSAQdAyGDF9Us11JDr8+XPmvlJHsMS7A4UBIcCiresJyZpSxYw
Cqcugy5AX5fgSAiL1Cd2b1zpQ/rYdTWkFYMVbH4jBEoPC3z/aSd+hTnneJFDUdXl
0koBDIw7NQylu6SrW+Y/DmXgalIHtwACuKivJTq/z9jdwFScV7adRR/VO53Inah3
L1+Ho7Zta95AYW3UPu71Gw3rrkfjY4uGDiFAFg==
=yTzD
-----END PGP MESSAGE-----

View File

@ -1,43 +1,43 @@
Date: Sun, 18 Jul 2021 16:53:45 +0200
From: User Alice <alice@disposlab>
To: User Bob <bob@disposlab>
Subject: encrypted
Message-ID: <YPRAeEEc3z2M9BCy@disposlab>
MIME-Version: 1.0
Content-Type: multipart/encrypted; protocol="application/pgp-encrypted";
boundary="95hZs/zeBetwhuEy"
Content-Disposition: inline
Status: RO
Content-Length: 1140
Lines: 30
--95hZs/zeBetwhuEy
Content-Type: application/pgp-encrypted
Content-Disposition: attachment
Version: 1
--95hZs/zeBetwhuEy
Content-Type: application/octet-stream
Content-Disposition: attachment; filename="msg.asc"
-----BEGIN PGP MESSAGE-----
hQGMA/vsqjpkmZurAQwAnb+2kDPgVFWVLkafuzVJGqFWKNtdVsvk7I1zhzFw5Hsr
h4irSHcH0X0QjaHprNiMBDfIZaCx5VVsvGYLiu/iQkdVPXItugTpln8aAvDt8/Bp
Hse69tgG5S9o4fPK4K2bMjNdomclDdz51cu9NXYjk/6OtzVwcSypyEmxgw24Oo1+
Q8KfZN9n6VTXGNlrV9KnAZYs/5aaSABTeC+cDvOcjDbPAmwDHYS3qsbITYoGHnEz
QfPIakYWPtPWkajhm4Z/iyEUSTeqew1/gAJ8sZnJpV0eg1Cr/44XgklZKFr8aJgk
SG8PkQxsyzAZklpwMSWdbb+t9a5nEKvky3zMpdmS1GE7ubTO7nQ1geUdBiv1UUNh
BY9d4nlGirqxX1MZUTGZidJgCy0365xbJSKkU0yFFW2uWtCKzJTEQBk3YZkNmnGH
h8BiVvMhQ8SxKBRPeH6Zb6HHlbcgkPvJAAI4VLqkZPCBvp9irmcdFGmrgCWLxzgk
sIjYGLA+ZuSXOKuAssXE0sAbASPAkUJRTIjzXFrCnr/MB3ZonESH01fsbsX+E/Qi
+2oLrgjjPHcPq76fvdO6fJP6c1pM8TlOoZKn/RkPm1llULtOn4n5JZJjeUA0F2ID
Te/U9i4YtcFZbuvw2bjeu8sAf77U6O3iTTBWkPWQT3H4YMskQc7lS1Mug6A9HL/n
TQvAwh2MIveYyEy/y/dKeFUbpSKxyOInhTg1XtYFiT8bzEF7OEJLU9GyF5oMs67d
o12uYlEnPhWz9oZp11aSdnyeADpVu6BQsPbwfTifcpajQSarH5sG8+rDSPju
=7CnH
-----END PGP MESSAGE-----
--95hZs/zeBetwhuEy--
Date: Sun, 18 Jul 2021 16:53:45 +0200
From: User Alice <alice@disposlab>
To: User Bob <bob@disposlab>
Subject: encrypted
Message-ID: <YPRAeEEc3z2M9BCy@disposlab>
MIME-Version: 1.0
Content-Type: multipart/encrypted; protocol="application/pgp-encrypted";
boundary="95hZs/zeBetwhuEy"
Content-Disposition: inline
Status: RO
Content-Length: 1140
Lines: 30
--95hZs/zeBetwhuEy
Content-Type: application/pgp-encrypted
Content-Disposition: attachment
Version: 1
--95hZs/zeBetwhuEy
Content-Type: application/octet-stream
Content-Disposition: attachment; filename="msg.asc"
-----BEGIN PGP MESSAGE-----
hQGMA/vsqjpkmZurAQwAnb+2kDPgVFWVLkafuzVJGqFWKNtdVsvk7I1zhzFw5Hsr
h4irSHcH0X0QjaHprNiMBDfIZaCx5VVsvGYLiu/iQkdVPXItugTpln8aAvDt8/Bp
Hse69tgG5S9o4fPK4K2bMjNdomclDdz51cu9NXYjk/6OtzVwcSypyEmxgw24Oo1+
Q8KfZN9n6VTXGNlrV9KnAZYs/5aaSABTeC+cDvOcjDbPAmwDHYS3qsbITYoGHnEz
QfPIakYWPtPWkajhm4Z/iyEUSTeqew1/gAJ8sZnJpV0eg1Cr/44XgklZKFr8aJgk
SG8PkQxsyzAZklpwMSWdbb+t9a5nEKvky3zMpdmS1GE7ubTO7nQ1geUdBiv1UUNh
BY9d4nlGirqxX1MZUTGZidJgCy0365xbJSKkU0yFFW2uWtCKzJTEQBk3YZkNmnGH
h8BiVvMhQ8SxKBRPeH6Zb6HHlbcgkPvJAAI4VLqkZPCBvp9irmcdFGmrgCWLxzgk
sIjYGLA+ZuSXOKuAssXE0sAbASPAkUJRTIjzXFrCnr/MB3ZonESH01fsbsX+E/Qi
+2oLrgjjPHcPq76fvdO6fJP6c1pM8TlOoZKn/RkPm1llULtOn4n5JZJjeUA0F2ID
Te/U9i4YtcFZbuvw2bjeu8sAf77U6O3iTTBWkPWQT3H4YMskQc7lS1Mug6A9HL/n
TQvAwh2MIveYyEy/y/dKeFUbpSKxyOInhTg1XtYFiT8bzEF7OEJLU9GyF5oMs67d
o12uYlEnPhWz9oZp11aSdnyeADpVu6BQsPbwfTifcpajQSarH5sG8+rDSPju
=7CnH
-----END PGP MESSAGE-----
--95hZs/zeBetwhuEy--

View File

@ -1,39 +1,39 @@
Date: Sun, 18 Jul 2021 12:08:41 +0200
From: User Alice <alice@disposlab>
To: User Bob <bob@disposlab>
Subject: signed
Message-ID: <YPP9qer9j2u4qXsq@disposlab>
MIME-Version: 1.0
Content-Type: multipart/signed; micalg=pgp-sha256;
protocol="application/pgp-signature"; boundary="U/XjR71RAixRcb28"
Content-Disposition: inline
Status: RO
Content-Length: 870
Lines: 26
--U/XjR71RAixRcb28
Content-Type: text/plain; charset=us-ascii
Content-Disposition: inline
A signed msg.
--U/XjR71RAixRcb28
Content-Type: application/pgp-signature; name="signature.asc"
-----BEGIN PGP SIGNATURE-----
iQGzBAEBCAAdFiEEHNJFMI8JY9A46INXlzz02Th8RNcFAmDz/aUACgkQlzz02Th8
RNdtOQv/ca8c51KoVq7CyPJUr54n4DEk/LlYniR0W51tL2a4rQxyF2AxqjdI8T4u
bT1+bqPNYgegesyCLokeZKqhLVtCH+UVOTdtUq5bB1J7ALuuVTOIdR5woMBBsazV
ETYEMzL6y2sGPW92ynriEw6B9pPnFKFPhOOZLrnMzM8CpkTfNmGoej+EdV74s0z4
RayKu/WaZ1Dtx2Vy2YDtG36p/Y3n62bnzQJCRyPYfrmCxH5X5i5oibQwxLROCFNE
4X3iVZLPHFg/DS9m4L7mBe0MJewGa1oPFr7t3ZfJ+24aJ/AvUv5uQIO+s6a7AcjD
Pgw/IjeM/uZdPrzniZI2zsWEgsjRCL1fj49XWVNkTHrWCqLvkBg+suucNO2SR0/d
ps+RP5mkJJHaSZyPpxwo9/PHKX67Mkpn/uEXlE8nV6IqKoXRzr1N0qwyhvbZQZLD
FMumxx/eOSiOpaiRhGhoZiUpf+VdnV/1ClpAcdbthy/psx/CMYVblAM8xg74NR9+
Q/WlFbRl
=uMdE
-----END PGP SIGNATURE-----
--U/XjR71RAixRcb28--
Date: Sun, 18 Jul 2021 12:08:41 +0200
From: User Alice <alice@disposlab>
To: User Bob <bob@disposlab>
Subject: signed
Message-ID: <YPP9qer9j2u4qXsq@disposlab>
MIME-Version: 1.0
Content-Type: multipart/signed; micalg=pgp-sha256;
protocol="application/pgp-signature"; boundary="U/XjR71RAixRcb28"
Content-Disposition: inline
Status: RO
Content-Length: 870
Lines: 26
--U/XjR71RAixRcb28
Content-Type: text/plain; charset=us-ascii
Content-Disposition: inline
A signed msg.
--U/XjR71RAixRcb28
Content-Type: application/pgp-signature; name="signature.asc"
-----BEGIN PGP SIGNATURE-----
iQGzBAEBCAAdFiEEHNJFMI8JY9A46INXlzz02Th8RNcFAmDz/aUACgkQlzz02Th8
RNdtOQv/ca8c51KoVq7CyPJUr54n4DEk/LlYniR0W51tL2a4rQxyF2AxqjdI8T4u
bT1+bqPNYgegesyCLokeZKqhLVtCH+UVOTdtUq5bB1J7ALuuVTOIdR5woMBBsazV
ETYEMzL6y2sGPW92ynriEw6B9pPnFKFPhOOZLrnMzM8CpkTfNmGoej+EdV74s0z4
RayKu/WaZ1Dtx2Vy2YDtG36p/Y3n62bnzQJCRyPYfrmCxH5X5i5oibQwxLROCFNE
4X3iVZLPHFg/DS9m4L7mBe0MJewGa1oPFr7t3ZfJ+24aJ/AvUv5uQIO+s6a7AcjD
Pgw/IjeM/uZdPrzniZI2zsWEgsjRCL1fj49XWVNkTHrWCqLvkBg+suucNO2SR0/d
ps+RP5mkJJHaSZyPpxwo9/PHKX67Mkpn/uEXlE8nV6IqKoXRzr1N0qwyhvbZQZLD
FMumxx/eOSiOpaiRhGhoZiUpf+VdnV/1ClpAcdbthy/psx/CMYVblAM8xg74NR9+
Q/WlFbRl
=uMdE
-----END PGP SIGNATURE-----
--U/XjR71RAixRcb28--

View File

@ -1,11 +0,0 @@
From: Dave <dave@localhost>
To: Carlos <carlos@localhost>
Subject: Test
This message includes inline PGP markers.
It's enough to include these two lines:
-----BEGIN PGP MESSAGE-----
-----END PGP MESSAGE-----
Test logs will give a hint which path this message takes.

87
test/relay.py Normal file
View File

@ -0,0 +1,87 @@
#!/usr/local/bin/python2
#
# This quick-and-dirty script supports only the happy case of SMTP session,
# i.e. what gpg-mailgate/gpg-lacre needs to deliver encrypted email.
#
# It listens on the port given as the only command-line argument and consumes a
# message, then prints it to standard output. The goal is to be able to
# compare that output with expected clear-text or encrypted message body.
#
import sys
import socket
EXIT_UNAVAILABLE = 1
ENCODING = 'utf-8'
BUFFER_SIZE = 4096
EOM = "\r\n.\r\n"
LAST_LINE = -3
def welcome(msg):
return b"220 %b\r\n" % (msg)
def ok(msg = b"OK"):
return b"250 %b\r\n" % (msg)
def bye():
return b"251 Bye"
def provide_message():
return b"354 Enter a message, ending it with a '.' on a line by itself\r\n"
def receive_and_confirm(session):
session.recv(BUFFER_SIZE)
session.sendall(ok())
def localhost_at(port):
return ('127.0.0.1', port)
def serve(port):
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
try:
s.bind(localhost_at(port))
s.listen(1)
except socket.error as e:
print("Cannot connect", e)
sys.exit(EXIT_UNAVAILABLE)
(conn, addr) = s.accept()
conn.sendall(welcome(b"TEST SERVER"))
receive_and_confirm(conn) # Ignore HELO/EHLO
receive_and_confirm(conn) # Ignore sender address
receive_and_confirm(conn) # Ignore recipient address
data = conn.recv(BUFFER_SIZE)
conn.sendall(provide_message())
# Consume until we get <CR><LF>.<CR><LF>, the end-of-message marker.
message = ''
while not message.endswith(EOM):
message += conn.recv(BUFFER_SIZE).decode(ENCODING)
conn.sendall(ok(b"OK, id=test"))
conn.recv(BUFFER_SIZE)
conn.sendall(bye())
conn.close()
# Trim EOM marker as we're only interested in the message body.
return message[:-len(EOM)]
def error(msg, exit_code):
print("ERROR: %s" % (msg))
sys.exit(exit_code)
if len(sys.argv) < 2:
error("Usage: relay.py PORT_NUMBER", EXIT_UNAVAILABLE)
port = int(sys.argv[1])
body = serve(port)
print(body)

View File

@ -1,3 +0,0 @@
[foo]
bar: quux
baz: 14

16
test/test_gnupg.py Normal file
View File

@ -0,0 +1,16 @@
import GnuPG
import unittest
class GnuPGUtilitiesTest(unittest.TestCase):
def test_build_default_command(self):
cmd = GnuPG.build_command("test/keyhome")
self.assertEqual(cmd, ["gpg", "--homedir", "test/keyhome"])
def test_build_command_extended_with_args(self):
cmd = GnuPG.build_command("test/keyhome", "--foo", "--bar")
self.assertEqual(cmd, ["gpg", "--homedir", "test/keyhome", "--foo", "--bar"])
if __name__ == '__main__':
unittest.main()

View File

@ -1,104 +0,0 @@
#!/usr/local/bin/python2
#
# This quick-and-dirty script supports only the happy case of SMTP session,
# i.e. what gpg-mailgate/gpg-lacre needs to deliver encrypted email.
#
# It listens on the port given as the only command-line argument and consumes a
# message, then prints it to standard output. The goal is to be able to
# compare that output with expected clear-text or encrypted message body.
#
import sys
import socket
import logging
EXIT_UNAVAILABLE = 1
ENCODING = 'utf-8'
BUFFER_SIZE = 4096
EOM = "\r\n.\r\n"
LAST_LINE = -3
def welcome(msg):
return b"220 %b\r\n" % (msg)
def ok(msg = b"OK"):
return b"250 %b\r\n" % (msg)
def bye():
return b"251 Bye"
def provide_message():
return b"354 Enter a message, ending it with a '.' on a line by itself\r\n"
def receive_and_confirm(session):
session.recv(BUFFER_SIZE)
session.sendall(ok())
def localhost_at(port):
return ('127.0.0.1', port)
def serve(port):
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
try:
s.bind(localhost_at(port))
logging.info(f"Listening on localhost, port {port}")
s.listen(1)
logging.info("Listening...")
except socket.error as e:
print("Cannot connect", e)
logging.error(f"Cannot connect {e}")
sys.exit(EXIT_UNAVAILABLE)
logging.debug("About to accept a connection...")
(conn, addr) = s.accept()
logging.debug(f"Accepting connection from {conn}")
conn.sendall(welcome(b"TEST SERVER"))
receive_and_confirm(conn) # Ignore HELO/EHLO
receive_and_confirm(conn) # Ignore sender address
receive_and_confirm(conn) # Ignore recipient address
conn.recv(BUFFER_SIZE)
conn.sendall(provide_message())
# Consume until we get <CR><LF>.<CR><LF>, the end-of-message marker.
message = ''
while not message.endswith(EOM):
message += conn.recv(BUFFER_SIZE).decode(ENCODING)
conn.sendall(ok(b"OK, id=test"))
conn.recv(BUFFER_SIZE)
conn.sendall(bye())
conn.close()
logging.debug(f"Received {len(message)} characters of data")
# Trim EOM marker as we're only interested in the message body.
return message[:-len(EOM)]
def error(msg, exit_code):
logging.error(msg)
print("ERROR: %s" % (msg))
sys.exit(exit_code)
# filename is relative to where we run the tests from, i.e. the project root
# directory
logging.basicConfig(filename='test/logs/relay.log',
format='%(asctime)s %(pathname)s:%(lineno)d %(levelname)s [%(funcName)s] %(message)s',
datefmt='%Y-%m-%d %H:%M:%S',
level=logging.DEBUG)
if len(sys.argv) < 2:
error("Usage: relay.py PORT_NUMBER", EXIT_UNAVAILABLE)
port = int(sys.argv[1])
body = serve(port)
print(body)

View File

@ -1,91 +0,0 @@
import sys
import sqlalchemy
from sqlalchemy.sql import insert
def define_db_schema():
meta = sqlalchemy.MetaData()
gpgmw_keys = sqlalchemy.Table('gpgmw_keys', meta,
sqlalchemy.Column('id', sqlalchemy.Integer, primary_key=True),
sqlalchemy.Column('email', sqlalchemy.String(256)),
sqlalchemy.Column('publickey', sqlalchemy.Text),
sqlalchemy.Column('confirm', sqlalchemy.String(32)),
sqlalchemy.Column('status', sqlalchemy.Integer),
sqlalchemy.Column('time', sqlalchemy.DateTime))
return (meta, gpgmw_keys)
if len(sys.argv) != 2:
print("ERROR: output database missing")
sys.exit(1)
(meta, gpgmw_keys) = define_db_schema()
dbname = sys.argv[1]
test_db = sqlalchemy.create_engine(f"sqlite:///{dbname}")
# Initialise the schema
meta.create_all(test_db)
conn = test_db.connect()
# Populate the database with dummy data
conn.execute(gpgmw_keys.insert(), [
{"id": 1, "email": "alice@disposlab", "publickey": "-----BEGIN PGP PUBLIC KEY BLOCK-----\n\
\n\
mQGNBGDYY5oBDAC+HAVjA05jsIpHfQ2KQ9m2olo1Qnlk+dkjD+Gagxj1ACezyiGL\n\
cfZfoE/MJYLCH9yPcX1fUIAPwdAyfJKlvkVcz+MhEpgl3aP3NM2L2unSx3v9ZFwT\n\
/qyMo9Zst5VSD04TVx2ySQB1vucd2ppgp66X7hlCxs+P8d0FV7VcdrNYol2oOtYP\n\
yEFXkdyXLI/INI6jrqNkBF87ej+dlTQZAm3zoj61Xwq4gW0YesAZoJyXs8X+a4Am\n\
8KF7YYcTcIy89yXflotmExpE+i77datSBLM/FpIPiUfkfK6q/TNyno8Z3PBC0QD5\n\
21leqfp/QHRkwmqFbIVuoeonCvrAccjM0ITLjW+P0xXJa3q0lQQCgcGOgqTuNWPT\n\
6FhlmvkXt6fBZ11C2I1b033HTePvjIwxOrEY8pSqYwerVX9EU7FXT+S98HNW/1nF\n\
cNk3SoofzUOcKZOwc5n0NEESrW7sWpmD6Qmf52+GURuO+15DSUt13xqmnte19Xqd\n\
n98y0wrYAUgyUY8AEQEAAbQPYWxpY2VAZGlzcG9zbGFiiQHUBBMBCAA+FiEEHNJF\n\
MI8JY9A46INXlzz02Th8RNcFAmDYY5oCGwMFCQPCZwAFCwkIBwIGFQoJCAsCBBYC\n\
AwECHgECF4AACgkQlzz02Th8RNdZeAv+IVVK49f0tY5QOSERu5RqdyFNpsVlUws9\n\
swvSvXXK/ZQxZ3YD3o0WEJG5G8jRO+Zjrljx6zzH39ofEKn8QMQUuw+SVPrzbqQb\n\
Yp/idn1E9RZCyyhtwcYnIwUObq2NNsCk8UmnjYvpwoh/QcHic13/RSUj7vejujtB\n\
SRTjNUE/RK5ROY8r+xZW9ZV/Q0NEzKl2wQtmbt8vTRX9yNEB171XZHG7dg4bTzm+\n\
zs0jPGNT0ygcx+uE7DZ3RkyPLRk3fB+GPiYrL2lfPF1KkrHGY4PGhClKdR1kjfBA\n\
Kweb6ExZg0fBYlB8ia8z3RZQF29pztoVfk8KIimg9RoYNOKw3Jp5SnHsbz9JygmZ\n\
mp3M3Lrs7357oSn9x25/nrFGeUBWbbKoXSdoXZr0Ix4xxkOJPAK966w0pQq+sP+o\n\
Ozg3F2rFRc6SoQw1pNLQ57hhWTblQlz8ETY7GnVJ+0xiqkAq2hrLt0jhQ5taWjV6\n\
Fgy8fKUPd5OAMvB9bfmAErclWcqKarMcuQGNBGDYY5oBDAC6yOtgUwtKUsI3jTu2\n\
VdjNDEnt/VLdRseT4JosSMglZ963nlA4mltCjxj59DeM0Ft8eyF7Bu4EFw5Kid+O\n\
vKGA5rGZBE0IVROOvSJQNbcELkY9XYtZjOJ7elfG37rDQKfDk82xqod9iTd48nm7\n\
vrllvylQhKfXa+m99KxWabtKqCyXVjaZP9vfD3nVauu16oHW6rQavlLXo5MetFan\n\
Iwv1sTqnpzCt+cuG/7vUt89rOiJRalRP3/e1K5MSM6aWC/SHZs6HcrT+WT5nuPA+\n\
5VQ4gFCSb8UlscF4sI++hhB/k821vyl9hIjnR3aRiFWdrkykQOfZNhovvsnmJmk9\n\
+Zcq0M3pZBnBuLgxVwJNVa4gi63cYwtExpcAZcG28wSVmcXcPN2wxEpYg5n/nvvG\n\
8Dsk0AA5WU5WW8aLLLQNBmVg2y4Oa1Fy0M7yfSylLWBAdj7y8+UzspN6JCbYhOpP\n\
lLRCJv5+JOgR0MrA+lxfFZwfcSO12x+gkfQ9oyUBdXNuydMAEQEAAYkBtgQYAQgA\n\
IBYhBBzSRTCPCWPQOOiDV5c89Nk4fETXBQJg2GOaAhsMAAoJEJc89Nk4fETXpooL\n\
/iJKgNF80neUamewma1aZJjwKWoHysSWWSlPeU6pGctuJv15fbAfI/NM1iXnSEGt\n\
odsn0oHtuAASlVB0ckSFdE0a2DwLgO6s6oEJof/yrE5hIAAlwzjHsi1G/dtHcfIo\n\
SjHzE22qUZwwm5ketuvKvEDKKp3b1ccu37AZC1caRFh3q8xB5ByLh1gPiDJ+ehwU\n\
puXkXPdFQhQTZib4LYuMxzh6A+S9U0AM7WMKjX7PhJ68maOeQ+yOIBSWtBKyWwZu\n\
Sx01w+Y/USPz02AxUn102se52FCISc/NijlX1JvFQdzf/WaZu28nTmW9OXSW3WeK\n\
ql7zNQqj494JD8gJuRGCU9AaiCmOaBokRdLiGbin/wxiG1CkXGRDN5/r0m/1IoNz\n\
I4m2SLsB/a89WACQ//CKJyNn4xPOEQoix35tXjdjTLAVyTrX502vHGieZ3HJU2tb\n\
nmmMf/H0kReMtNYFwHxoTpBJ8vk+xcZ+6ETzH8nk6av+zZ/5T5Y0aD5zO89PcQk6\n\
pw==\n\
=Tbwz\n\
-----END PGP PUBLIC KEY BLOCK-----\
", "status": 0, "confirm": "", "time": None},
{"id": 2, "email": "bob@disposlab", "publickey": "-----BEGIN PGP PUBLIC KEY BLOCK-----\n\
\n\
mDMEYdTFkRYJKwYBBAHaRw8BAQdA2tgdP1pMt3cv3XAW7ov5AFn74mMZvyTksp9Q\n\
eO1PkpK0GkJvYiBGb29iYXIgPGJvYkBkaXNwb3NsYWI+iJYEExYIAD4WIQQZz0tH\n\
7MnEevqE1L2W85/aDjG7ZwUCYdTFkQIbAwUJA8JnAAULCQgHAgYVCgkICwIEFgID\n\
AQIeAQIXgAAKCRCW85/aDjG7ZxVnAP49t7BU2H+/WCpa3fCAlMEcik82sU4p+U9D\n\
pMsbjawwYgEA1SbA5CF835cMjoEufy1h+2M4T9gI/0X2lk8OAtwwggm4OARh1MXg\n\
EgorBgEEAZdVAQUBAQdAUVNKx2OsGtNdRsnl3J/uv6obkUC0KcO4ikdRs+iejlMD\n\
AQgHiHgEGBYIACAWIQQZz0tH7MnEevqE1L2W85/aDjG7ZwUCYdTF4AIbDAAKCRCW\n\
85/aDjG7Z039APwLGP5ibqCC9yIr4YVbdWff1Ch+2C91MR2ObF93Up9+ogD8D2zd\n\
OjjB6xRD0Q2FN+alsNGCtdutAs18AZ5l33RMzws=\n\
=wWoq\n\
-----END PGP PUBLIC KEY BLOCK-----\
", "status": 0, "confirm": "", "time": None},
{"id": 3, "email": "cecil@lacre.io", "publickey": "RUBBISH", "status": 0, "confirm": "", "time": None}
])

View File

@ -1,56 +0,0 @@
import logging
import smtplib
import sys
import getopt
def _load_file(name):
f = open(name, 'r')
contents = f.read()
f.close()
return contents
def _send(host, port, from_addr, recipients, message):
logging.info(f"From {from_addr} to {recipients} at {host}:{port}")
try:
smtp = smtplib.SMTP(host, port)
# smtp.starttls()
return smtp.sendmail(from_addr, recipients, message)
except smtplib.SMTPDataError as e:
logging.error(f"Couldn't deliver message. Got error: {e}")
return None
except ConnectionRefusedError as e:
logging.exception(f"Connection refused: {e}")
return None
logging.basicConfig(filename="test/logs/sendmail.log",
format="%(asctime)s %(pathname)s:%(lineno)d %(levelname)s [%(funcName)s] %(message)s",
datefmt="%Y-%m-%d %H:%M:%S",
level=logging.DEBUG)
sender = recipient = message = None
opts, _ = getopt.getopt(sys.argv[1:], "f:t:m:")
for opt, value in opts:
if opt == "-f":
sender = value
logging.debug(f"Sender is {sender}")
if opt == "-t":
recipient = value
logging.debug(f"Recipient is {recipient}")
if opt == "-m":
message = _load_file(value)
logging.debug(f"Message is {message}")
if message is None:
message = """\
From: dave@disposlab
To: alice@disposlab
Subject: Test message
Lorem ipsum dolor sit amet.
"""
_send('localhost', 10025, sender, [recipient], message)

View File

@ -1,154 +0,0 @@
#!/usr/bin/python
#
# gpg-mailgate
#
# This file is part of the gpg-mailgate source code.
#
# gpg-mailgate is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# gpg-mailgate source code is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with gpg-mailgate source code. If not, see <http://www.gnu.org/licenses/>.
#
import GnuPG
import sqlalchemy
from sqlalchemy.sql import select, delete, and_
import smtplib
import markdown
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
import logging
import lacre
import lacre.config as conf
def _load_file(name):
f = open(name)
data = f.read()
f.close()
return data
def _authenticate_maybe(smtp):
if conf.config_item_equals('smtp', 'enabled', 'true'):
LOG.debug(f"Connecting to {conf.get_item('smtp', 'host')}:{conf.get_item('smtp', 'port')}")
smtp.connect(conf.get_item('smtp', 'host'), conf.get_item('smtp', 'port'))
smtp.ehlo()
if conf.config_item_equals('smtp', 'starttls', 'true'):
LOG.debug("StartTLS enabled")
smtp.starttls()
smtp.ehlo()
smtp.login(conf.get_item('smtp', 'username'), conf.get_item('smtp', 'password'))
def _send_msg(mailsubject, messagefile, recipients = None):
mailbody = _load_file(conf.get_item('cron', 'mail_templates') + "/" + messagefile)
msg = MIMEMultipart("alternative")
msg["From"] = conf.get_item('cron', 'notification_email')
msg["To"] = recipients
msg["Subject"] = mailsubject
msg.attach(MIMEText(mailbody, 'plain'))
msg.attach(MIMEText(markdown.markdown(mailbody), 'html'))
if conf.config_item_set('relay', 'host') and conf.config_item_set('relay', 'enc_port'):
relay = (conf.get_item('relay', 'host'), int(conf.get_item('relay', 'enc_port')))
smtp = smtplib.SMTP(relay[0], relay[1])
_authenticate_maybe(smtp)
smtp.sendmail(conf.get_item('cron', 'notification_email'), recipients, msg.as_string())
else:
LOG.info("Could not send mail due to wrong configuration")
def _setup_db_connection(url):
engine = sqlalchemy.create_engine(url)
return (engine, engine.connect())
def _define_db_schema():
meta = sqlalchemy.MetaData()
gpgmw_keys = sqlalchemy.Table('gpgmw_keys', meta,
sqlalchemy.Column('id', sqlalchemy.Integer, primary_key=True),
sqlalchemy.Column('email', sqlalchemy.String(256)),
sqlalchemy.Column('publickey', sqlalchemy.Text),
sqlalchemy.Column('confirm', sqlalchemy.String(32)),
sqlalchemy.Column('status', sqlalchemy.Integer),
sqlalchemy.Column('time', sqlalchemy.DateTime))
return (gpgmw_keys)
# Read configuration from /etc/gpg-mailgate.conf
conf.load_config()
lacre.init_logging(conf.get_item('logging', 'config'))
LOG = logging.getLogger('webgate-cron.py')
if conf.config_item_equals('database', 'enabled', 'yes') and conf.config_item_set('database', 'url'):
(engine, conn) = _setup_db_connection(conf.get_item("database", "url"))
(gpgmw_keys) = _define_db_schema()
selq = select(gpgmw_keys.c.publickey, gpgmw_keys.c.id, gpgmw_keys.c.email)\
.where(and_(gpgmw_keys.c.status == 0, gpgmw_keys.c.confirm == ""))\
.limit(100)
LOG.debug(f"Retrieving keys to be processed: {selq}")
result_set = conn.execute(selq)
for key_id, row_id, email in result_set:
# delete any other public keys associated with this confirmed email address
delq = delete(gpgmw_keys).where(and_(gpgmw_keys.c.email == email, gpgmw_keys.c.id != row_id))
LOG.debug(f"Deleting public keys associated with confirmed email: {delq}")
conn.execute(delq)
GnuPG.delete_key(conf.get_item('gpg', 'keyhome'), email)
LOG.info('Deleted key for <' + email + '> via import request')
if key_id.strip(): # we have this so that user can submit blank key to remove any encryption
if GnuPG.confirm_key(key_id, email):
GnuPG.add_key(conf.get_item('gpg', 'keyhome'), key_id) # import the key to gpg
modq = gpgmw_keys.update().where(gpgmw_keys.c.id == row_id).values(status=1)
LOG.debug(f"Key imported, updating key: {modq}")
conn.execute(modq) # mark key as accepted
LOG.warning('Imported key from <' + email + '>')
if conf.config_item_equals('cron', 'send_email', 'yes'):
_send_msg("PGP key registration successful", "registrationSuccess.md", email)
else:
delq = delete(gpgmw_keys).where(gpgmw_keys.c.id == row_id)
LOG.debug(f"Cannot confirm key, deleting it: {delq}")
conn.execute(delq) # delete key
LOG.warning('Import confirmation failed for <' + email + '>')
if conf.config_item_equals('cron', 'send_email', 'yes'):
_send_msg("PGP key registration failed", "registrationError.md", email)
else:
# delete key so we don't continue processing it
delq = delete(gpgmw_keys).where(gpgmw_keys.c.id == row_id)
LOG.debug(f"Deleting key: {delq}")
conn.execute(delq)
if conf.config_item_equals('cron', 'send_email', 'yes'):
_send_msg("PGP key deleted", "keyDeleted.md", email)
# delete keys
stat2q = select(gpgmw_keys.c.email, gpgmw_keys.c.id).where(gpgmw_keys.c.status == 2).limit(100)
stat2_result_set = conn.execute(stat2q)
for email, row_id in stat2_result_set:
GnuPG.delete_key(conf.get_item('gpg', 'keyhome'), email)
delq = delete(gpgmw_keys).where(gpgmw_keys.c.id == row_id)
LOG.debug(f"Deleting keys that have already been processed: {delq}")
conn.execute(delq)
LOG.info('Deleted key for <' + email + '>')
else:
print("Warning: doing nothing since database settings are not configured!")
LOG.error("Warning: doing nothing since database settings are not configured!")