forked from Disroot/gpg-lacre
Compare commits
118 Commits
php_update
...
main
Author | SHA1 | Date |
---|---|---|
pfm | 2215886850 | |
Piotr F. Mieszkowski | 588b447e69 | |
pfm | 5c3fecfb85 | |
Onnayaku | c7c49977c9 | |
Onnayaku | 2c6adfe335 | |
pfm | 9f1c4db49d | |
Piotr F. Mieszkowski | 86b0cd335e | |
Piotr F. Mieszkowski | 6f379709f3 | |
pfm | dab882550e | |
EmanuelLoos | 68c09c0eb1 | |
pfm | c3cc37bf56 | |
Piotr F. Mieszkowski | 18c790f986 | |
Piotr F. Mieszkowski | d39cadb9aa | |
Piotr F. Mieszkowski | 53378b516e | |
Piotr F. Mieszkowski | b4f30d7e8f | |
Piotr F. Mieszkowski | b91501d3dd | |
Piotr F. Mieszkowski | fc85cdb841 | |
Piotr F. Mieszkowski | ba7978b4a6 | |
Piotr F. Mieszkowski | 2ac26c09ce | |
Piotr F. Mieszkowski | 00289759a3 | |
Piotr F. Mieszkowski | 8f8d9dc1b6 | |
pfm | 4bdbd0febb | |
Piotr F. Mieszkowski | 99e939bb4e | |
Piotr F. Mieszkowski | 540ca2adf3 | |
pfm | 9aa1c3732c | |
Piotr F. Mieszkowski | a82d9f96b3 | |
Piotr F. Mieszkowski | fe49d985ec | |
pfm | 13636bfddd | |
Piotr F. Mieszkowski | 5f8c94673d | |
Piotr F. Mieszkowski | 641253b3ec | |
Piotr F. Mieszkowski | 9696b7e997 | |
Piotr F. Mieszkowski | 9f3ad49f14 | |
Piotr F. Mieszkowski | acbb2ab776 | |
Piotr F. Mieszkowski | 2da97a5a9a | |
Piotr F. Mieszkowski | eb0d5a1326 | |
Piotr F. Mieszkowski | 386c23f9f8 | |
Piotr F. Mieszkowski | 1db0a09fa5 | |
Piotr F. Mieszkowski | a85b7b7a43 | |
Piotr F. Mieszkowski | f5cff3292a | |
Piotr F. Mieszkowski | d7e4947afd | |
Piotr F. Mieszkowski | 5f601fa50c | |
Piotr F. Mieszkowski | 07263d5afa | |
Piotr F. Mieszkowski | c41df63e42 | |
Piotr F. Mieszkowski | a2eeaeee9d | |
Piotr F. Mieszkowski | ce6a0c5466 | |
Piotr F. Mieszkowski | 8963eee47f | |
Piotr F. Mieszkowski | a5bcf2d9b2 | |
Piotr F. Mieszkowski | d01865d21c | |
Piotr F. Mieszkowski | ddcef93abb | |
Piotr F. Mieszkowski | ce2e55e90c | |
Piotr F. Mieszkowski | 0cb656f89d | |
Piotr F. Mieszkowski | 603710c41e | |
Piotr F. Mieszkowski | 68e4a452d2 | |
Piotr F. Mieszkowski | 1edef79787 | |
Piotr F. Mieszkowski | 414f1d5921 | |
Piotr F. Mieszkowski | a131cd66d3 | |
Piotr F. Mieszkowski | 4c844384e3 | |
Piotr F. Mieszkowski | 6455c1a280 | |
Piotr F. Mieszkowski | 7849c55d9f | |
Piotr F. Mieszkowski | 29b5b50901 | |
Piotr F. Mieszkowski | 3f2760ba2d | |
Piotr F. Mieszkowski | 8f191cae72 | |
Piotr F. Mieszkowski | b198f0c4f4 | |
Piotr F. Mieszkowski | 9e998b54e9 | |
muppeth | 4d22ef9406 | |
muppeth | 156b09fd4e | |
muppeth | 2ebd090d6c | |
pfm | 002e150805 | |
Piotr F. Mieszkowski | 5ffbbec5f0 | |
Piotr F. Mieszkowski | b627fde510 | |
pfm | 8a366f2f17 | |
Piotr F. Mieszkowski | 881a8d1756 | |
Piotr F. Mieszkowski | c86c620668 | |
pfm | c4781f2ac8 | |
Piotr F. Mieszkowski | 251e6d1270 | |
pfm | cb219799d2 | |
Piotr F. Mieszkowski | 937046eb17 | |
Piotr F. Mieszkowski | 65d2a77486 | |
Piotr F. Mieszkowski | 06abbc0edd | |
pfm | 9820e42457 | |
Piotr F. Mieszkowski | 46be24670c | |
Piotr F. Mieszkowski | 55b58d25bc | |
Piotr F. Mieszkowski | d3b1717290 | |
Piotr F. Mieszkowski | 4c6fdc52ec | |
Piotr F. Mieszkowski | 3bcc1151e5 | |
Piotr F. Mieszkowski | 707fc96234 | |
Piotr F. Mieszkowski | 558872d9d0 | |
pfm | 35987a6654 | |
Piotr F. Mieszkowski | d27eef911a | |
Piotr F. Mieszkowski | 92f3cedc51 | |
pfm | 5639d8e5b6 | |
Piotr F. Mieszkowski | d8bef9cdb0 | |
Piotr F. Mieszkowski | 11b78ce0fb | |
Piotr F. Mieszkowski | acdb2dd5c8 | |
Piotr F. Mieszkowski | 7a8720c142 | |
Piotr F. Mieszkowski | 8f0d8f4933 | |
Piotr F. Mieszkowski | 617a208fe9 | |
Piotr F. Mieszkowski | 9dfc447169 | |
Piotr F. Mieszkowski | af5a5b4176 | |
Piotr F. Mieszkowski | 75ccfb0850 | |
Piotr F. Mieszkowski | baf7954270 | |
Piotr F. Mieszkowski | 031c7234f6 | |
Piotr F. Mieszkowski | d90b50f7e7 | |
pfm | 7767dfaff9 | |
Piotr F. Mieszkowski | 55fa0d0601 | |
Piotr F. Mieszkowski | 71afd6ed05 | |
Piotr F. Mieszkowski | a82ff9f3f1 | |
Piotr F. Mieszkowski | 52b31028c5 | |
Piotr F. Mieszkowski | b19a76e297 | |
Piotr F. Mieszkowski | ffc53b935a | |
Piotr F. Mieszkowski | 7aff414fb7 | |
muppeth | 86b725349f | |
Piotr F. Mieszkowski | ea0e012c04 | |
Piotr F. Mieszkowski | 454b519c70 | |
Piotr F. Mieszkowski | f472f4ac22 | |
Piotr F. Mieszkowski | fdadc89c31 | |
Piotr F. Mieszkowski | 5a34249090 | |
muppeth | c4e9e3e840 |
|
@ -1,22 +1,24 @@
|
|||
#
|
||||
# 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
|
||||
|
@ -24,6 +26,7 @@ import shutil
|
|||
import random
|
||||
import string
|
||||
import sys
|
||||
import logging
|
||||
|
||||
|
||||
LINE_FINGERPRINT = 'fpr'
|
||||
|
@ -31,137 +34,164 @@ LINE_USER_ID = 'uid'
|
|||
|
||||
POS_FINGERPRINT = 9
|
||||
|
||||
def build_command(key_home, *args, **kwargs):
|
||||
cmd = ["gpg", '--homedir', key_home] + list(args)
|
||||
return cmd
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
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 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()
|
||||
def _build_command(key_home, *args, **kwargs):
|
||||
cmd = ["gpg", '--homedir', key_home] + list(args)
|
||||
return cmd
|
||||
|
||||
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
|
||||
|
||||
# confirms a key has a given email address
|
||||
def confirm_key( content, email ):
|
||||
tmpkeyhome = ''
|
||||
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()
|
||||
|
||||
while True:
|
||||
tmpkeyhome = '/tmp/' + ''.join(random.choice(string.ascii_uppercase + string.digits) for x in range(12))
|
||||
if not os.path.exists(tmpkeyhome):
|
||||
break
|
||||
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
|
||||
|
||||
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
|
||||
p.stdout.close()
|
||||
p.stderr.close()
|
||||
return keys
|
||||
|
||||
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
|
||||
|
||||
# cleanup
|
||||
shutil.rmtree(tmpkeyhome)
|
||||
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
|
||||
|
||||
return confirmed
|
||||
|
||||
# adds a key and ensures it has the given email address
|
||||
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 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 delete_key( keyhome, email ):
|
||||
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
|
||||
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
|
||||
|
||||
return False
|
||||
|
||||
class GPGEncryptor:
|
||||
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)
|
||||
"""A wrapper for 'gpg -e' command."""
|
||||
|
||||
def update(self, message):
|
||||
self._message += message
|
||||
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 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 update(self, message):
|
||||
"""Append MESSAGE to buffer about to be encrypted."""
|
||||
self._message += message
|
||||
|
||||
def _command(self):
|
||||
cmd = build_command(self._keyhome, "--trust-model", "always", "--batch", "--yes", "--pgp7", "--no-secmem-warning", "-a", "-e")
|
||||
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)
|
||||
|
||||
# add recipients
|
||||
for recipient in self._recipients:
|
||||
cmd.append("-r")
|
||||
cmd.append(recipient)
|
||||
def _command(self):
|
||||
cmd = _build_command(self._keyhome, "--trust-model", "always", "--batch", "--yes", "--pgp7", "--no-secmem-warning", "-a", "-e")
|
||||
|
||||
# add on the charset, if set
|
||||
if self._charset:
|
||||
cmd.append("--comment")
|
||||
cmd.append('Charset: ' + self._charset)
|
||||
# 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
|
||||
|
||||
return cmd
|
||||
|
||||
class GPGDecryptor:
|
||||
def __init__(self, keyhome):
|
||||
self._keyhome = keyhome
|
||||
self._message = ''
|
||||
"""A wrapper for 'gpg -d' command."""
|
||||
|
||||
def update(self, message):
|
||||
self._message += message
|
||||
def __init__(self, keyhome):
|
||||
"""Initialise the wrapper."""
|
||||
self._keyhome = keyhome
|
||||
self._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 update(self, message):
|
||||
"""Append encrypted content to be decrypted."""
|
||||
self._message += message
|
||||
|
||||
def _command(self):
|
||||
return build_command(self._keyhome, "--trust-model", "always", "--batch", "--yes", "--no-secmem-warning", "-a", "-d")
|
||||
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")
|
||||
|
|
181
INSTALL.md
181
INSTALL.md
|
@ -1,11 +1,14 @@
|
|||
# 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.
|
||||
|
@ -13,8 +16,10 @@ 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 2.X is already installed (GPG-Mailgate is not Python 3 compatible)
|
||||
|
||||
- Python 3.x is already installed
|
||||
- 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
|
||||
|
||||
|
@ -22,104 +27,133 @@ 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/python2.7/dist-packages` (replace 2.7 with your Python 2 version)
|
||||
5. Place the `GnuPG` directory in `/usr/local/lib/python3.x/dist-packages` (replace 3.x with your Python 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. Add the following to the end of `/etc/postfix/master.cf`
|
||||
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).
|
||||
|
||||
gpg-mailgate unix - n n - - pipe
|
||||
flags= user=nobody argv=/usr/local/bin/gpg-mailgate.py ${recipient}
|
||||
8. Add the following to the end of `/etc/postfix/master.cf`
|
||||
|
||||
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
|
||||
```
|
||||
gpg-mailgate unix - n n - - pipe
|
||||
flags= user=nobody argv=/usr/local/bin/gpg-mailgate.py ${recipient}
|
||||
|
||||
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.
|
||||
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
|
||||
```
|
||||
|
||||
8. Add the following line to `/etc/postfix/main.cf`
|
||||
If you use Postfix versions from 2.5 onwards, it is recommended to change `${recipient}` to `${original_recipient}` in line two of the lines above.
|
||||
|
||||
content_filter = gpg-mailgate
|
||||
9. Add the following line to `/etc/postfix/main.cf`
|
||||
|
||||
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:
|
||||
```
|
||||
content_filter = gpg-mailgate
|
||||
```
|
||||
|
||||
keyserver-options auto-key-retrieve
|
||||
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:
|
||||
|
||||
10. Restart Postfix
|
||||
```
|
||||
keyserver-options auto-key-retrieve
|
||||
```
|
||||
|
||||
11. 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 2.X is already installed
|
||||
- Python 3.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.
|
||||
|
||||
|
@ -127,34 +161,43 @@ All files you need can be found in the [gpg-mailgate-web] (gpg-mailgate-web/) di
|
|||
|
||||
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:
|
||||
|
||||
chown -R nobody:nogroup /var/gpgmailgate/cron_templates
|
||||
```
|
||||
mkdir -p /var/gpgmailgate/cron_templates
|
||||
```
|
||||
|
||||
8. Copy the templates found in the [cron_templates](cron_templates/) directory into the newly created directory and transfer ownership:
|
||||
|
||||
```
|
||||
chown -R nobody:nogroup /var/gpgmailgate/cron_templates
|
||||
```
|
||||
|
||||
9. Copy `cron.py` to `/usr/local/bin/gpgmw-cron.py`. Make it executable and and transfer ownership to `nobody`:
|
||||
|
||||
chown nobody:nogroup /usr/local/bin/gpgmw-cron.py
|
||||
chmod u+x /usr/local/bin/gpgmw-cron.py
|
||||
```
|
||||
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
|
||||
|
||||
|
@ -162,21 +205,29 @@ 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
|
||||
|
||||
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:
|
||||
```
|
||||
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:
|
||||
|
||||
```
|
||||
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`
|
||||
|
@ -185,4 +236,4 @@ GPG-Mailgate-Web can also be used as a keyserver. For more information have a lo
|
|||
|
||||
8. Restart postfix.
|
||||
|
||||
9. Test your installation.
|
||||
9. Test your installation.
|
40
Makefile
40
Makefile
|
@ -1,5 +1,5 @@
|
|||
.POSIX:
|
||||
.PHONY: test unittest pre-clean clean
|
||||
.PHONY: test e2etest unittest crontest daemontest pre-clean clean restore-keyhome
|
||||
|
||||
#
|
||||
# On systems where Python 3.x binary has a different name, just
|
||||
|
@ -12,6 +12,13 @@
|
|||
#
|
||||
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.
|
||||
#
|
||||
|
@ -19,19 +26,46 @@ PYTHON = python3
|
|||
# file. Basically this is just a script that feeds GPG Mailgate with
|
||||
# known input and checks whether output meets expectations.
|
||||
#
|
||||
test: test/tmp test/logs pre-clean
|
||||
e2etest: test/tmp test/logs pre-clean restore-keyhome
|
||||
$(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
|
||||
$(PYTHON) -m unittest discover -s test/modules
|
||||
|
||||
pre-clean:
|
||||
rm -fv test/gpg-mailgate.conf
|
||||
rm -f test/logs/*.log
|
||||
|
||||
restore-keyhome:
|
||||
git restore test/keyhome
|
||||
|
||||
test/tmp:
|
||||
mkdir test/tmp
|
||||
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
# GPG Lacre Project
|
||||
|
||||
Fork and continuation of original work of gpg-mailgate project: https://github.com/fkrone/gpg-mailgate
|
||||
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.
|
||||
|
||||
|
||||
**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.
|
||||
|
@ -26,11 +30,13 @@ 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)
|
||||
|
|
|
@ -0,0 +1,45 @@
|
|||
# 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.
|
|
@ -1,15 +1,25 @@
|
|||
# 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` or `make unittest`.
|
||||
To run tests, use command `make test`.
|
||||
|
||||
Tests produce some helpful logs, so inspect contents of `test/logs` directory
|
||||
if something goes wrong.
|
||||
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.
|
||||
|
||||
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
|
||||
|
@ -17,21 +27,22 @@ PYTHON=/path/to/python`.
|
|||
|
||||
## Key building blocks
|
||||
|
||||
- *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 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 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
|
||||
|
|
|
@ -0,0 +1,57 @@
|
|||
# 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
|
|
@ -1,70 +0,0 @@
|
|||
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')
|
|
@ -1,111 +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/>.
|
||||
#
|
||||
|
||||
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!")
|
|
@ -1,54 +0,0 @@
|
|||
<?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;
|
||||
|
||||
?>
|
|
@ -1,119 +0,0 @@
|
|||
<?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;
|
||||
|
||||
?>
|
|
@ -1,41 +0,0 @@
|
|||
<?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");
|
||||
}
|
||||
|
||||
?>
|
|
@ -1,273 +0,0 @@
|
|||
<?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);
|
||||
}
|
||||
|
||||
?>
|
|
@ -1,31 +0,0 @@
|
|||
<?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.");
|
||||
}
|
||||
|
||||
?>
|
|
@ -1,76 +0,0 @@
|
|||
<?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);
|
||||
}
|
||||
}
|
||||
|
||||
?>
|
|
@ -1,85 +0,0 @@
|
|||
<?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;
|
||||
}
|
||||
|
||||
?>
|
|
@ -1,25 +0,0 @@
|
|||
<?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');
|
||||
|
||||
?>
|
|
@ -1,124 +0,0 @@
|
|||
<?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;
|
||||
}
|
||||
|
||||
?>
|
|
@ -1,100 +0,0 @@
|
|||
<?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;
|
||||
}
|
||||
|
||||
?>
|
|
@ -1,55 +0,0 @@
|
|||
<?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 (ereg("/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");
|
||||
}
|
||||
}
|
||||
|
||||
?>
|
|
@ -1,43 +0,0 @@
|
|||
<?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");
|
||||
}
|
||||
|
||||
?>
|
|
@ -1,47 +0,0 @@
|
|||
<?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";
|
||||
|
||||
?>
|
|
@ -1,25 +0,0 @@
|
|||
<?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>
|
|
@ -1,28 +0,0 @@
|
|||
<?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>
|
|
@ -1,46 +0,0 @@
|
|||
<?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>
|
|
@ -1,10 +0,0 @@
|
|||
-- 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);
|
|
@ -46,6 +46,9 @@ 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]
|
||||
|
@ -56,7 +59,9 @@ cert_path = /var/gpgmailgate/smime
|
|||
# settings for the register-handler
|
||||
register_email = register@yourdomain.tld
|
||||
mail_templates = /var/gpgmailgate/register_templates
|
||||
# URL to webpanel. The server should be able to reach it
|
||||
|
||||
# URL to webpanel. Upon receiving an email with a key, register-handler
|
||||
# uploads it to the web panel.
|
||||
webpanel_url = http://yourdomain.tld
|
||||
|
||||
[cron]
|
||||
|
@ -66,9 +71,17 @@ notification_email = gpg-mailgate@yourdomain.tld
|
|||
mail_templates = /var/gpgmailgate/cron_templates
|
||||
|
||||
[logging]
|
||||
# For logging to syslog. 'file = syslog', otherwise use path to the file.
|
||||
file = syslog
|
||||
verbose = yes
|
||||
# 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
|
||||
|
||||
[relay]
|
||||
# the relay settings to use for Postfix
|
||||
|
@ -83,14 +96,28 @@ 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]
|
||||
# uncomment the settings below if you want
|
||||
# to read keys from a gpg-mailgate-web database
|
||||
# edit the settings below if you want to read keys from a
|
||||
# gpg-mailgate-web database other than SQLite
|
||||
enabled = yes
|
||||
name = gpgmw
|
||||
host = localhost
|
||||
username = gpgmw
|
||||
password = password
|
||||
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
|
||||
|
||||
[enc_keymap]
|
||||
# You can find these by running the following command:
|
||||
|
|
478
gpg-mailgate.py
478
gpg-mailgate.py
|
@ -1,466 +1,54 @@
|
|||
#!/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 syslog
|
||||
import traceback
|
||||
import time
|
||||
import logging
|
||||
|
||||
import lacre
|
||||
import lacre.config as conf
|
||||
|
||||
# imports for S/MIME
|
||||
from M2Crypto import BIO, Rand, SMIME, X509
|
||||
from email.mime.message import MIMEMessage
|
||||
start = time.process_time()
|
||||
conf.load_config()
|
||||
lacre.init_logging(conf.get_item('logging', 'config'))
|
||||
|
||||
# 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"
|
||||
# This has to be executed *after* logging initialisation.
|
||||
import lacre.core as core
|
||||
|
||||
# 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
|
||||
LOG = logging.getLogger('gpg-mailgate.py')
|
||||
|
||||
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()
|
||||
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)
|
||||
|
||||
verbose = 'logging' in cfg and 'verbose' in cfg['logging'] and cfg['logging'].get('verbose') == 'yes'
|
||||
|
||||
# Read e-mail from stdin
|
||||
# Read e-mail from stdin, parse it
|
||||
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:]
|
||||
|
||||
def gpg_encrypt( raw_message, recipients ):
|
||||
|
||||
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)
|
||||
core.deliver_message(raw_message, from_addr, to_addrs)
|
||||
process_t = (time.process_time() - start) * 1000
|
||||
|
||||
LOG.info("Message delivered in {process:.2f} ms".format(process=process_t))
|
||||
|
|
|
@ -0,0 +1,48 @@
|
|||
"""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')
|
|
@ -0,0 +1,122 @@
|
|||
"""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")
|
|
@ -0,0 +1,561 @@
|
|||
#
|
||||
# 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)
|
|
@ -0,0 +1,117 @@
|
|||
"""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()
|
|
@ -0,0 +1,153 @@
|
|||
"""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
|
|
@ -0,0 +1,130 @@
|
|||
"""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}>"
|
|
@ -0,0 +1,87 @@
|
|||
"""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))
|
|
@ -7,37 +7,28 @@ from M2Crypto import BIO, Rand, SMIME, X509
|
|||
from email.mime.text import MIMEText
|
||||
from email.mime.multipart import MIMEMultipart
|
||||
|
||||
# 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 logging
|
||||
|
||||
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']+"/"
|
||||
import lacre
|
||||
import lacre.config as conf
|
||||
|
||||
def send_msg( message, from_addr, recipients = None ):
|
||||
|
||||
if 'relay' in cfg and 'host' in cfg['relay'] and 'enc_port' in cfg['relay']:
|
||||
relay = (cfg['relay']['host'], int(cfg['relay']['enc_port']))
|
||||
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])
|
||||
smtp.sendmail( from_addr, recipients, message.as_string() )
|
||||
else:
|
||||
log("Could not send mail due to wrong configuration")
|
||||
LOG.info("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 )
|
||||
|
@ -63,18 +54,18 @@ if __name__ == "__main__":
|
|||
break
|
||||
|
||||
if sign_part == None:
|
||||
log("Unable to find PKCS7 signature or public PGP key in registration email")
|
||||
LOG.info("Unable to find PKCS7 signature or public PGP key in registration email")
|
||||
|
||||
failure_msg = file( cfg['mailregister']['mail_templates'] + "/registrationError.md").read()
|
||||
failure_msg = file( conf.get_item('mailregister', 'mail_templates') + "/registrationError.md").read()
|
||||
msg = MIMEMultipart("alternative")
|
||||
msg["From"] = cfg['mailregister']['register_email']
|
||||
msg["From"] = conf.get_item('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, cfg['mailregister']['register_email'], [from_addr])
|
||||
send_msg(msg, conf.get_item('mailregister', 'register_email'), [from_addr])
|
||||
sys.exit(0)
|
||||
|
||||
if sign_type == 'smime':
|
||||
|
@ -105,42 +96,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(cfg['mailregister']['mail_templates']+"/registrationSuccess.md").read()
|
||||
success_msg = file(conf.get_item('mailregister', 'mail_templates')+"/registrationSuccess.md").read()
|
||||
success_msg = success_msg.replace("[:FROMADDRESS:]", from_addr)
|
||||
|
||||
msg = MIMEMultipart("alternative")
|
||||
msg["From"] = cfg['mailregister']['register_email']
|
||||
msg["From"] = conf.get_item('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, cfg['mailregister']['register_email'], [from_addr])
|
||||
send_msg(msg, conf.get_item('mailregister', 'register_email'), [from_addr])
|
||||
|
||||
log("S/MIME Registration succeeded")
|
||||
LOG.info("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(cfg['mailregister']['webpanel_url'], data=payload)
|
||||
r = requests.post(conf.get_item('mailregister', 'webpanel_url'), data=payload)
|
||||
|
||||
if r.status_code != 200:
|
||||
log("Could not hand registration over to GPGMW. Error: %s" % r.status_code)
|
||||
error_msg = file(cfg['mailregister']['mail_templates']+"/gpgmwFailed.md").read()
|
||||
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()
|
||||
error_msg = error_msg.replace("[:FROMADDRESS:]", from_addr)
|
||||
|
||||
msg = MIMEMultipart("alternative")
|
||||
msg["From"] = cfg['mailregister']['register_email']
|
||||
msg["From"] = conf.get_item('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, cfg['mailregister']['register_email'], [from_addr])
|
||||
send_msg(msg, conf.get_item('mailregister', 'register_email'), [from_addr])
|
||||
else:
|
||||
log("PGP registration is handed over to GPGMW")
|
||||
LOG.info("PGP registration is handed over to GPGMW")
|
||||
# except:
|
||||
# log("Registration exception")
|
||||
# LOG.info("Registration exception")
|
||||
# sys.exit(0)
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
aiosmtpd==1.4.2
|
||||
SQLAlchemy==1.4.32
|
||||
Markdown==3.4.1
|
||||
M2Crypto==0.38.0
|
||||
requests==2.27.1
|
||||
watchdog==2.1.9
|
|
@ -0,0 +1,130 @@
|
|||
#
|
||||
# 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()
|
23
test/e2e.ini
23
test/e2e.ini
|
@ -22,7 +22,7 @@
|
|||
|
||||
[relay]
|
||||
port: 2500
|
||||
script: test/relay.py
|
||||
script: test/utils/relay.py
|
||||
|
||||
[dirs]
|
||||
keys: test/keyhome
|
||||
|
@ -30,11 +30,12 @@ certs: test/certs
|
|||
|
||||
[tests]
|
||||
# Number of "test-*" sections in this file, describing test cases.
|
||||
cases: 6
|
||||
cases: 9
|
||||
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
|
||||
|
@ -71,3 +72,21 @@ 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.
|
||||
|
|
121
test/e2e_test.py
121
test/e2e_test.py
|
@ -1,43 +1,38 @@
|
|||
#
|
||||
# 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/relay.py"
|
||||
RELAY_SCRIPT = "test/utils/relay.py"
|
||||
CONFIG_FILE = "test/gpg-mailgate.conf"
|
||||
|
||||
def build_config(config):
|
||||
cp = configparser.ConfigParser()
|
||||
|
||||
def _build_config(config):
|
||||
cp = configparser.RawConfigParser()
|
||||
|
||||
cp.add_section("logging")
|
||||
cp.set("logging", "file", config["log_file"])
|
||||
cp.set("logging", "verbose", "yes")
|
||||
cp.set("logging", "config", config["log_config"])
|
||||
|
||||
cp.add_section("gpg")
|
||||
cp.set("gpg", "keyhome", config["gpg_keyhome"])
|
||||
|
@ -49,40 +44,64 @@ 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()
|
||||
|
||||
return bytes(contents, 'utf-8')
|
||||
def _load_file(name):
|
||||
f = open(name, 'r')
|
||||
contents = f.read()
|
||||
f.close()
|
||||
|
||||
def report_result(message_file, expected, test_output):
|
||||
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):
|
||||
status = None
|
||||
if expected in test_output:
|
||||
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(30), status)
|
||||
print(message_file.ljust(35), 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
|
||||
|
@ -102,17 +121,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()
|
||||
|
@ -122,16 +141,20 @@ def execute_e2e_test(case_name, config, config_path):
|
|||
|
||||
logging.debug(f"Read {len(testout)} characters of test output: '{testout}'")
|
||||
|
||||
report_result(config.get(case_name, "in"), config.get(case_name, "out"), 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)
|
||||
|
||||
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
|
||||
|
@ -143,16 +166,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_file = config.get("tests", "lacre_log"))
|
||||
_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"))
|
||||
|
||||
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")))
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
[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
|
|
@ -0,0 +1,27 @@
|
|||
[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
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
[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.
|
@ -0,0 +1,13 @@
|
|||
-----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-----
|
|
@ -0,0 +1,92 @@
|
|||
#
|
||||
# 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()
|
|
@ -0,0 +1,46 @@
|
|||
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()
|
|
@ -0,0 +1,20 @@
|
|||
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'))
|
|
@ -0,0 +1,37 @@
|
|||
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")
|
|
@ -0,0 +1,15 @@
|
|||
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)
|
|
@ -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.
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
From: Dave <dave@localhost>
|
||||
To: Bob <bob+foobar@localhost>
|
||||
Subject: Test
|
||||
|
||||
Body of the message.
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
From: Dave <dave@disposlab
|
||||
To: Evan <evan@disposlab>
|
||||
Subject: Test
|
||||
Content-Type: text/plain; charset="utf-8"
|
||||
|
||||
Body of the message.
|
|
@ -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-----
|
||||
|
|
|
@ -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--
|
||||
|
|
|
@ -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--
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
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.
|
|
@ -1,87 +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
|
||||
|
||||
|
||||
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)
|
|
@ -0,0 +1,3 @@
|
|||
[foo]
|
||||
bar: quux
|
||||
baz: 14
|
|
@ -1,16 +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"])
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
|
@ -0,0 +1,104 @@
|
|||
#!/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)
|
|
@ -0,0 +1,91 @@
|
|||
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}
|
||||
])
|
|
@ -0,0 +1,56 @@
|
|||
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)
|
|
@ -0,0 +1,154 @@
|
|||
#!/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!")
|
Loading…
Reference in New Issue