Compare commits

...

118 Commits

Author SHA1 Message Date
pfm 2215886850 Merge pull request 'Add basic unit tests for GnuPG module' (#114) from gnupg-tests into main
Reviewed-on: Disroot/gpg-lacre#114
2022-12-14 20:29:01 +00:00
Piotr F. Mieszkowski 588b447e69 Add basic unit tests for GnuPG module
- Cover basic GnuPG functionality with tests (confirm_key, public_keys).
- Add a test public key file.
- Fix resource leak by closing streams opened by Popen.
2022-12-14 21:09:46 +01:00
pfm 5c3fecfb85 Merge pull request 'Formatting corrections and whitespace character fixes' (#112) from Onnayaku/gpg-lacre:doc-fixes into main
Reviewed-on: Disroot/gpg-lacre#112
Reviewed-by: pfm <pfm@no-reply@disroot.org>
2022-12-14 19:32:40 +00:00
Onnayaku c7c49977c9
Change 'Indented code blocks' to 'Fenced code blocks' in adv-filt.md 2022-11-01 23:53:37 +01:00
Onnayaku 2c6adfe335
Minor formatting corrections and whitespace character fixes in adv-filt.md testing.md INSTALL.md and README.md 2022-11-01 21:35:48 +01:00
pfm 9f1c4db49d Merge pull request 'doc-updates' (#108) from doc-updates into main
Reviewed-on: Disroot/gpg-lacre#108
2022-10-26 17:21:41 +00:00
Piotr F. Mieszkowski 86b0cd335e Replace UTF-8 non-breaking space with a plain space 2022-10-26 19:20:47 +02:00
Piotr F. Mieszkowski 6f379709f3 Update README to explicitly state that Lacre is in beta state 2022-10-26 19:19:24 +02:00
pfm dab882550e Merge pull request 'improved formatting' (#107) from EmanuelLoos/gpg-lacre:main into main
Reviewed-on: Disroot/gpg-lacre#107
2022-10-26 16:57:35 +00:00
EmanuelLoos 68c09c0eb1 improved formatting
Just some small Markdown syntax corrections.
2022-10-26 16:57:35 +00:00
pfm c3cc37bf56 Merge pull request 'Improve code quality' (#103) from rc2-improvements into main
Reviewed-on: Disroot/gpg-lacre#103
Reviewed-by: muppeth <muppeth@no-reply@disroot.org>
2022-10-26 16:49:04 +00:00
Piotr F. Mieszkowski 18c790f986 Only reload on pubring.kbx file modifications 2022-10-23 13:51:42 +02:00
Piotr F. Mieszkowski d39cadb9aa Use proper name in webgate-cron.py logs 2022-10-23 13:51:10 +02:00
Piotr F. Mieszkowski 53378b516e Add a test for message with PGP inline markers that's not encrypted
If a user mentions PGP markers inside their message, we should not classify it
as already encrypted.
2022-10-22 21:29:59 +02:00
Piotr F. Mieszkowski b4f30d7e8f Add test clear text input message with PGP markers
It's possible to trick Lacre by sending PGP markers in message body, causing
it to classify that message being already encrypted.  This test case is used
to reproduce this scenario.
2022-10-22 20:54:08 +02:00
Piotr F. Mieszkowski b91501d3dd Convert all test input messages to CRLF line endings 2022-10-22 20:52:14 +02:00
Piotr F. Mieszkowski fc85cdb841 Rework PGP-Inline verification/recognition 2022-10-22 19:58:16 +02:00
Piotr F. Mieszkowski ba7978b4a6 Make webgate-cron code more readable 2022-10-22 14:33:20 +02:00
Piotr F. Mieszkowski 2ac26c09ce Simplify code, improve log entries, add comments 2022-10-22 11:23:17 +02:00
Piotr F. Mieszkowski 00289759a3 Add aiosmtpd dependency 2022-10-22 11:23:11 +02:00
Piotr F. Mieszkowski 8f8d9dc1b6 Rename mailgate.py to core.py 2022-10-22 11:23:04 +02:00
pfm 4bdbd0febb Merge pull request 'Improve logging' (#101) from 100-logging-improvements into main
Reviewed-on: Disroot/gpg-lacre#101
Reviewed-by: muppeth <muppeth@no-reply@disroot.org>
2022-10-21 06:30:23 +00:00
Piotr F. Mieszkowski 99e939bb4e Remove too verbose debug logs, implement repr() for KeyCache 2022-10-20 22:27:34 +02:00
Piotr F. Mieszkowski 540ca2adf3 Improve logging
- Report processing time in milliseconds.
- Use module names in log messages instead of file-names without extensions.
2022-10-20 21:56:01 +02:00
pfm 9aa1c3732c Merge pull request 'Adjust cron tests after recent FE decoupling' (#99) from cron-tests into main
Reviewed-on: Disroot/gpg-lacre#99
Reviewed-by: muppeth <muppeth@no-reply@disroot.org>
2022-10-19 19:09:38 +00:00
Piotr F. Mieszkowski a82d9f96b3 Fix indentation and some warnings 2022-10-19 21:04:25 +02:00
Piotr F. Mieszkowski fe49d985ec Adjust cron tests after recent FE decoupling 2022-10-19 20:54:40 +02:00
pfm 13636bfddd Merge pull request 'Implement Advanced Content Filter' (#97) from daemon into main
Reviewed-on: Disroot/gpg-lacre#97
Reviewed-by: muppeth <muppeth@no-reply@disroot.org>
2022-10-19 18:48:39 +00:00
Piotr F. Mieszkowski 5f8c94673d Add basic daemon documentation 2022-10-19 18:36:23 +00:00
Piotr F. Mieszkowski 641253b3ec Make key-loading async, remove unused parameter 2022-10-19 18:36:23 +00:00
Piotr F. Mieszkowski 9696b7e997 Separate key-cache and key-loader
Extract key-loading code to a dedicated class KeyRing in lacre.keyring module.
KeyCache only keeps a static map of identities, making it safe to use in
asynchronous context (and race condition resistant).
2022-10-19 18:36:23 +00:00
Piotr F. Mieszkowski 9f3ad49f14 Rename lacre.keycache to lacre.keyring
This will better reflect the fact we're doing more than just caching.
2022-10-19 18:36:23 +00:00
Piotr F. Mieszkowski acbb2ab776 Document dependencies
Add requirements.txt file with versions of dependencies known to work well.
2022-10-19 18:36:23 +00:00
Piotr F. Mieszkowski 2da97a5a9a Reformat code, add doc comments 2022-10-19 18:36:23 +00:00
Piotr F. Mieszkowski eb0d5a1326 Reload keyring on filesystem events
Subscribe to FS events from keyring directory using Python Watchdog and when a
modification is observed, reload the key cache.

Since we may receive more than one event about a single modification, keep
directory's last modification to recognise 'false positives'.
2022-10-19 18:36:23 +00:00
Piotr F. Mieszkowski 386c23f9f8 Document dependencies on the logging module 2022-10-19 18:36:23 +00:00
Piotr F. Mieszkowski 1db0a09fa5 Log processing time for successful deliveries 2022-10-19 18:36:23 +00:00
Piotr F. Mieszkowski a85b7b7a43 Reload key cache only if keyring dir was modified 2022-10-19 18:36:23 +00:00
Piotr F. Mieszkowski f5cff3292a Reload key cache periodically
Use [default]cache_refresh_minutes configuration parameter to define periods
between cache reloads.  After this number of minutes cache will be reloaded.
2022-10-19 18:36:23 +00:00
Piotr F. Mieszkowski d7e4947afd Add cache validity configuration parameter
Also, log basic information in KeyCache and provide load() and reload()
operations to make daemon's code cleaner.
2022-10-19 18:36:23 +00:00
Piotr F. Mieszkowski 5f601fa50c Implement a basic KeyCache 2022-10-19 18:36:23 +00:00
Piotr F. Mieszkowski 07263d5afa Reformat tests 2022-10-19 18:36:23 +00:00
Piotr F. Mieszkowski c41df63e42 Reorder tests to avoid interferences 2022-10-19 18:36:23 +00:00
Piotr F. Mieszkowski a2eeaeee9d Implement Advanced Filter flow for cleartext and OpenPGP
- Polish implementation of mail operations (lacre/mailop.py).  Add two
strategies: InlineOpenPGPEncrypt and MimeOpenPGPEncrypt, to support two modes
of OpenPGP encryption.

- In delivery_plan, only use those strategies that actually make sense with
the recipients we'd got.

- Add flag_enabled predicate (lacre/config.py) to make configuration checks
easier / simpler.

- Handle TypeError errors in Advanced Filter, indicating a delivery failure
when they appear.

- Add type hints to some of the functions.
2022-10-19 18:36:23 +00:00
Piotr F. Mieszkowski ce6a0c5466 Continue refactoring
- Add more encryption strategies.
- Replace tuples (email + key) with dedicated objects.
2022-10-19 18:36:23 +00:00
Piotr F. Mieszkowski 8963eee47f Reformat GnuPG module 2022-10-19 18:36:23 +00:00
Piotr F. Mieszkowski a5bcf2d9b2 Make daemon E2E tests use configured parameters 2022-10-19 18:36:23 +00:00
Piotr F. Mieszkowski d01865d21c Refactor into smaller functions and objects 2022-10-19 18:36:23 +00:00
Piotr F. Mieszkowski ddcef93abb Fix a bug introduced by refactoring, clean up code
- Fix certificate retrieval.

- Store recipients within MailOperation objects.

- Log more information.

- Fix some warnings.
2022-10-19 18:36:23 +00:00
Piotr F. Mieszkowski ce2e55e90c Change indentation from tabs to 4 spaces 2022-10-19 18:36:23 +00:00
Piotr F. Mieszkowski 0cb656f89d Add more debug logging to _try_direct_key_lookup 2022-10-19 18:36:23 +00:00
Piotr F. Mieszkowski 603710c41e Continue splitting _sort_gpg_recipients
Extract new functions to match keys using enc_keymap and enc_domain_keymap
configuration sections, another one to look them up directly in GnuPG keyring,
optionally stripping delimiters ("+" followed by a topic).

Add some comments and docstrings.
2022-10-19 18:36:23 +00:00
Piotr F. Mieszkowski 68e4a452d2 Split _gpg_encrypt into smaller functions 2022-10-19 18:36:23 +00:00
Piotr F. Mieszkowski 1edef79787 Update documentation to cover daemon tests 2022-10-19 18:36:23 +00:00
Piotr F. Mieszkowski 414f1d5921 Implement E2E tests for lacre.daemon
- Add a dedicated configuration file for lacre.daemon.

- Implement test/daemon_test.py like test/e2e_test.py, to automate the
following procedure:

    1. Start up lacre.daemon.
    2. For each test case, send test message to the daemon and verify that the
       output received by test/utils/relay.py contains expected pattern.

- Simplify Makefile.

- Fix indentation here and there.
2022-10-19 18:36:23 +00:00
Piotr F. Mieszkowski a131cd66d3 Move different parts of Lacre tests to subdirectories
In particular, move:

- test utilities to test/utils;
- unit tests to test/modules.

Also: start implementing the Lacre daemon test (just a stub for now).
2022-10-19 18:36:23 +00:00
Piotr F. Mieszkowski 4c844384e3 Implement a bare minimum of advanced filtering
- Forward messages without encryption.

- Include a simple test setup in the Makefile.

- Add a test to send a test message to the daemon.
2022-10-19 18:36:23 +00:00
Piotr F. Mieszkowski 6455c1a280 [daemon] Add configuration, implement no-op filter
- Add a "mailop" module to define mail operations.  Each should inherit from
MailOperation class (which just defines the contract).

- Make lacre.mailgate.delivery_plan always return KeepIntact strategy to have
a daemon that just forwards messages without modifying them.

- Add sample configuration.

- Include daemon configuration in mandatory parameter check.
2022-10-19 18:36:23 +00:00
Piotr F. Mieszkowski 7849c55d9f Extend the daemon skeleton 2022-10-19 18:36:23 +00:00
Piotr F. Mieszkowski 29b5b50901 Mailgate: replace tabs with spaces 2022-10-19 18:36:23 +00:00
Piotr F. Mieszkowski 3f2760ba2d Create skeleton of the Lacre daemon
Also:
- Expose a function to read mail relay configuration.
- Replace tabs with 4 spaces in lacre.config.
2022-10-19 18:36:23 +00:00
Piotr F. Mieszkowski 8f191cae72 Restore keyring before running E2E tests 2022-10-19 18:36:23 +00:00
Piotr F. Mieszkowski b198f0c4f4 Fix logging
First initialise logging, then import lacre.mailgate module.  Otherwise,
module's logging quitely initialises its own root logger that doesn't use
configuration provided by the user.

Also: remove unnecessary "global" keywords.
2022-10-19 18:36:23 +00:00
Piotr F. Mieszkowski 9e998b54e9 Extract a predicate to classify messages as already encrypted
Also: perform minor cleanup.
2022-10-19 18:36:23 +00:00
muppeth 4d22ef9406 Merge pull request 'Removing webfrontend as it was migrated to standalone repository' (#98) from uncoupleFE into main
Reviewed-on: Disroot/gpg-lacre#98
2022-10-19 18:31:05 +00:00
muppeth 156b09fd4e
added execute permission to webgate-cron.py 2022-10-19 09:35:52 +02:00
muppeth 2ebd090d6c
uncoupled web frontend (lacre-webgate) from gpg-lacre; moved cron.py from frontend to backend dir and changed name (webgate-cron.py); 2022-10-12 16:29:09 +02:00
pfm 002e150805 Merge pull request 'Move core logic from gpg-mailgate.py to lacre.mailgate module' (#91) from 90-core-module into master
Reviewed-on: Disroot/gpg-lacre#91
2022-06-11 19:10:36 +00:00
Piotr F. Mieszkowski 5ffbbec5f0 Rename sort_recipients to deliver_message, remove unused imports 2022-06-11 21:05:27 +02:00
Piotr F. Mieszkowski b627fde510 Move gpg-mailgate.py logic to lacre.mailgate module
gpg-mailgate.py script keeps its role, but only needs to call code defined in
lacre.mailgate.
2022-06-11 21:00:42 +02:00
pfm 8a366f2f17 Merge pull request 'Handle addresses with delimiters' (#89) from 87-handle-delimiters into master
Reviewed-on: Disroot/gpg-lacre#89
2022-06-08 19:32:27 +00:00
Piotr F. Mieszkowski 881a8d1756 Add GnuPG encryption support for addresses with delimiters
If a user registers their key for address alice@example.com but receives a
message sent to alice+something@example.com, this message should be encrypted
as well.

- Implement delimiter support for GnuPG encryption.

- Add E2E test case for a clear text message delivered to an address with
delimiter.

- Fix minor bug: wrong configuration parameter was retrieved when logging
information about enc_domain_keymap being active.
2022-06-08 21:20:58 +02:00
Piotr F. Mieszkowski c86c620668 Extract delimiter support, add unit tests
Also: fix recursive call to get_cert_for_email.
2022-06-07 22:14:32 +02:00
pfm c4781f2ac8 Merge pull request 'Record execution time and log it' (#86) from 84-measure-time into master
Reviewed-on: Disroot/gpg-lacre#86
2022-06-02 21:44:56 +00:00
Piotr F. Mieszkowski 251e6d1270 Record execution time and log it
After each execution, log an entry with information about total seconds from
the start to the end of execution and the value returned by
time.process_time() function, which returns:

	sum of the kernel and user-space CPU time

according to the documentation.

This feature can be used to collect stats about Lacre performance.
2022-06-02 23:41:14 +02:00
pfm cb219799d2 Merge pull request 'Use CRLF for line-endings' (#83) from 78-fix-encryption into master
Reviewed-on: Disroot/gpg-lacre#83
2022-06-02 18:00:17 +00:00
Piotr F. Mieszkowski 937046eb17 Use CRLF for line-endings
Mail RFCs use CRLF for line endings and it turns out things may break in
strange ways if only LF is used.
2022-06-02 19:56:32 +02:00
Piotr F. Mieszkowski 65d2a77486 Don't break Content-Type header with LF - part 2 2022-06-02 19:43:27 +02:00
Piotr F. Mieszkowski 06abbc0edd Don't break Content-Type header with LF 2022-06-02 19:37:06 +02:00
pfm 9820e42457 Merge pull request 'Handle text data carefully' (#82) from 75-unify-types into master
Reviewed-on: Disroot/gpg-lacre#82
2022-06-01 21:59:52 +00:00
Piotr F. Mieszkowski 46be24670c Fix charset resolution in Content-Type parser 2022-06-01 23:44:41 +02:00
Piotr F. Mieszkowski 55b58d25bc Use literal separator '@' in sanitize_case_sense 2022-06-01 23:23:51 +02:00
Piotr F. Mieszkowski d3b1717290 Extract PGP/INLINE checks, remove unnecessary byte-check 2022-06-01 23:00:05 +02:00
Piotr F. Mieszkowski 4c6fdc52ec Check mandatory config early, add tests
Also: extend failover logging configuration with file-based handler to make
sure that the user gets _some_ logs even if they do not configure Lacre at
all.
2022-05-31 22:09:10 +02:00
Piotr F. Mieszkowski 3bcc1151e5 Add E2E case: a user with a key and PGP/MIME configured
- Add a new test input message for a new test identity, test scenario
  configuration and a test key.

- While retrieving message payload, determine charset based on the
  Content-Type header.  When missing, default to UTF-8.

- Use more comprehensible variables names.

- Adjust logging levels.
2022-05-30 00:49:40 +02:00
Piotr F. Mieszkowski 707fc96234
Add more contract tests
- Verify that Message.get_payload() returns str, unless passed decode=True,
when it returns bytes.

- Verify that RawConfigParser returns str.
2022-05-25 22:13:40 +02:00
Piotr F. Mieszkowski 558872d9d0 Start documenting dependency contracts with unit tests
Implement some unit tests for 'email' package so we know precisely how this
package behaves.
2022-05-16 20:57:12 +02:00
pfm 35987a6654 Merge pull request 'Clean-up' (#70) from 69-cleanup into master
Reviewed-on: Disroot/gpg-lacre#70
2022-05-14 09:49:14 +00:00
Piotr F. Mieszkowski d27eef911a Update sample config
- Include helpful information about webpanel_url.
- Remove unused database settings.
- Add a hint about MySQL url to the database section.
2022-05-14 11:40:20 +02:00
Piotr F. Mieszkowski 92f3cedc51 Remove leftover .read() call 2022-05-14 11:26:49 +02:00
pfm 5639d8e5b6 Merge pull request 'Improve logging and configuration' (#65) from logging-and-config into master
- Replace custom logging code with "logging" package.
- Unify access to configuration and extract to "lacre.config" package.
- Introduce a new configuration file (with a sample included) to control how and where Lacre writes diagnostic output.
- Update sample configuration.

Reviewed-on: Disroot/gpg-lacre#65
2022-05-13 20:01:10 +00:00
Piotr F. Mieszkowski d8bef9cdb0 Explain syslog logging better in sample logging config 2022-05-13 21:57:29 +02:00
Piotr F. Mieszkowski 11b78ce0fb Adjust log entry levels for severe conditions
When Lacre is misconfigured or can't proceed, report WARNING or even ERROR
level messages.
2022-05-13 21:27:50 +02:00
Piotr F. Mieszkowski acdb2dd5c8 Log cron-job inner workings 2022-05-11 19:57:26 +02:00
Piotr F. Mieszkowski 7a8720c142 Update installation instructions, add sample logging config 2022-05-11 19:15:59 +02:00
Piotr F. Mieszkowski 8f0d8f4933 Add newlines to key material 2022-05-06 20:40:16 +02:00
Piotr F. Mieszkowski 617a208fe9 Fix indentation and names after rebase 2022-05-06 20:25:22 +02:00
Piotr F. Mieszkowski 9dfc447169 Use Lacre logging and configuration in register-handler 2022-05-06 20:13:23 +02:00
Piotr F. Mieszkowski af5a5b4176 Use Lacre logging and configuration in cron.py 2022-05-06 20:13:19 +02:00
Piotr F. Mieszkowski 75ccfb0850 Use logging module
- Replace custom logging code with calls to logging module.
- Use logging.config to provide configuration parameters.

To make Lacre's logging more flexible, use fileConfig from logging.config to
set up all parameters.  If the configuration file is missing, use dictConfig
with hardcoded reasonable defaults.
2022-05-06 19:39:56 +02:00
Piotr F. Mieszkowski baf7954270 Use list booleanness instead of comparing with empty list 2022-05-06 19:39:56 +02:00
Piotr F. Mieszkowski 031c7234f6 Reorder top-level expressions 2022-05-06 19:39:56 +02:00
Piotr F. Mieszkowski d90b50f7e7 Extract config, separate logging, split into smaller functions
- Move configuration-processing code to a separate module (lacre.config) and
  provide a simple API to access configuration parameters.
- Prepare to use builtin logging module to log diagnostic data.
- Rework the configuration-processing file to make it cleaner.
- Log additional information while processing configuration.
- Reorder functions.
2022-05-06 19:39:56 +02:00
pfm 7767dfaff9 Merge pull request 'Fix minor cron.py issues' (#64) from fix-cron-script into master
Reviewed-on: Disroot/gpg-lacre#64
2022-05-06 17:25:11 +00:00
Piotr F. Mieszkowski 55fa0d0601 [GnuPG.confirm_key] Convert email to bytes() before comparison 2022-05-06 19:14:52 +02:00
Piotr F. Mieszkowski 71afd6ed05 [GnuPG.confirm_key] Set 700 mode for temp. key home 2022-04-26 18:42:48 +02:00
Piotr F. Mieszkowski a82ff9f3f1 Use actual keys (RSA and Ed25519) in cron-test 2022-04-24 10:42:51 +02:00
Piotr F. Mieszkowski 52b31028c5 Fix config key typo 2022-04-24 10:07:52 +02:00
Piotr F. Mieszkowski b19a76e297 Update testing documentation 2022-04-23 13:13:57 +02:00
Piotr F. Mieszkowski ffc53b935a Finish migration to SQLAlchemy and automate testing cron.py 2022-04-23 13:08:40 +02:00
Piotr F. Mieszkowski 7aff414fb7 Use SQLAlchemy to access database
- Replace hardcoded MySQLdb package with sqlalchemy to support other RDBMS
backends.

- Provide a script that could eventually replace schema.sql (schema.py).

- Update sample configuration.
2022-04-23 09:39:20 +02:00
muppeth 86b725349f
added smtp auth to cron.py 2022-04-20 13:01:58 +02:00
Piotr F. Mieszkowski ea0e012c04 Use 'open' to open a file 2022-04-19 21:35:22 +02:00
Piotr F. Mieszkowski 454b519c70 Use bytes in confirm_key's line operations 2022-04-19 21:22:34 +02:00
Piotr F. Mieszkowski f472f4ac22 Use isinstance() instead of type() 2022-04-19 21:16:40 +02:00
Piotr F. Mieszkowski fdadc89c31 Use bytes in GnuPG.confirm_key too 2022-04-19 20:49:49 +02:00
Piotr F. Mieszkowski 5a34249090 Fix minor cron.py issues
Related to #63: TypeError: memoryview: a bytes-like object is required, not
'str'.

- Allow GnuPG.add_key accept either 'str' or 'bytes'.
- Import MIMEText from email.mime.text.
2022-04-10 19:03:18 +02:00
muppeth c4e9e3e840 compatibility update for php7/8 (#61)
Co-authored-by: muppeth <muppeth@disroot.org>
Reviewed-on: Disroot/gpg-lacre#61
2022-03-23 10:05:11 +00:00
69 changed files with 2824 additions and 2318 deletions

View File

@ -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")

View File

@ -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.

View File

@ -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

View File

@ -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)

45
doc/adv-filt.md Normal file
View File

@ -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.

View File

@ -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

View File

@ -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

View File

@ -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')

View File

@ -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!")

View File

@ -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;
?>

View File

@ -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;
?>

View File

@ -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");
}
?>

View File

@ -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);
}
?>

View File

@ -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.");
}
?>

View File

@ -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);
}
}
?>

View File

@ -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;
}
?>

View File

@ -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');
?>

View File

@ -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;
}
?>

View File

@ -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;
}
?>

View File

@ -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");
}
}
?>

View File

@ -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");
}
?>

View File

@ -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";
?>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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);

View File

@ -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:

View File

@ -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))

48
lacre/__init__.py Normal file
View File

@ -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')

122
lacre/config.py Normal file
View File

@ -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")

561
lacre/core.py Normal file
View File

@ -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)

117
lacre/daemon.py Normal file
View File

@ -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()

153
lacre/keyring.py Normal file
View File

@ -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

130
lacre/mailop.py Normal file
View File

@ -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}>"

87
lacre/text.py Normal file
View File

@ -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))

View File

@ -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)

6
requirements.txt Normal file
View File

@ -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

130
test/daemon_test.py Normal file
View File

@ -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()

View File

@ -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.

View File

@ -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")))

24
test/gpg-lacre-log.ini Normal file
View File

@ -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

View File

@ -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

View File

@ -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.

View File

@ -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-----

View File

@ -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()

View File

@ -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()

View File

@ -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'))

View File

@ -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")

View File

@ -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)

View File

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

View File

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

View File

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

View File

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

View File

@ -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.

View File

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

View File

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

View File

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

View File

@ -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.

View File

@ -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)

3
test/sample.ini Normal file
View File

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

View File

@ -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()

104
test/utils/relay.py Normal file
View File

@ -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)

91
test/utils/schema.py Normal file
View File

@ -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}
])

56
test/utils/sendmail.py Normal file
View File

@ -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)

154
webgate-cron.py Executable file
View File

@ -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!")