Compare commits

...

312 commits

Author SHA1 Message Date
59df4bb2e7 errors corrected 2024-11-05 15:18:27 +01:00
a5e90f40c9 Merge pull request 'Update' (#1) from Disroot/gpg-lacre:main into main
Reviewed-on: #1
2024-11-05 13:25:09 +01:00
e5e653483f revert ec6c43afcb
revert improved formatting

Just some small Markdown syntax corrections.
2024-11-05 13:23:57 +01:00
pfm
ea3eb7c09b Release candidate: 0.3.1
Improves logging and some failure handling.

Reviewed-on: Disroot/gpg-lacre#163
2024-09-21 14:31:45 +02:00
pfm
c8ed650053 Merge pull request 'Return Permanent Failure when dealing with undeliverable mail' (#162) from perm-fail-on-unencrypted into develop
Reviewed-on: Disroot/gpg-lacre#162
2024-09-21 14:26:54 +02:00
1a7c83d565
Return Permanent Failure when dealing with undeliverable mail 2024-09-21 14:26:09 +02:00
pfm
c1d9210c5e Merge pull request 'Log encryption failure tracebacks' (#160) from log-encryption-failures into develop
Reviewed-on: Disroot/gpg-lacre#160
2024-09-13 12:23:56 +02:00
273ebc7510
Update exception message format 2024-09-13 12:23:44 +02:00
93002c4910
Log encryption failure tracebacks
When we fail to encrypt a message, we log a message with a complete traceback.
The goal is to ensure that when diagnosing it later, we have complete
information.
2024-09-13 12:22:17 +02:00
pfm
c1c4bd1573 Merge pull request 'Log key cache expiry dates' (#156) from bugfix/key-queue-ttl-logs into develop
Reviewed-on: Disroot/gpg-lacre#156
2024-09-13 12:01:00 +02:00
pfm
9a35d47a6d Release 0.3 (#157)
Reviewed-on: Disroot/gpg-lacre#157
2024-09-13 11:15:02 +02:00
e01d5f6feb
Log key cache expiry dates 2024-08-28 22:07:19 +02:00
pfm
64b401fb66 Merge pull request 'Parse [database]max_queue_hours as integer, handle invalid format' (#155) from bugfix/key-queue-ttl into develop
Reviewed-on: Disroot/gpg-lacre#155
Reviewed-by: muppeth <muppeth@no-reply@disroot.org>
2024-08-27 21:13:10 +02:00
1bec99afaf
Parse [database]max_queue_hours as integer, handle invalid format 2024-08-27 18:01:20 +02:00
pfm
58a7117bb9 Merge pull request 'Improve error handling' (#152) from error-handling into main
Reviewed-on: Disroot/gpg-lacre#152
2024-08-23 14:30:00 +02:00
ba23eeb8b3
Update expected cleartext output
Lacre no longer transforms cleartext payloads.
2024-08-23 14:16:28 +02:00
e643ce7722
Do not touch original payload in KeepIntact mode 2024-08-23 14:16:28 +02:00
8b5d924321
Implement LazyMessage, a wrapper for original contents
We want to avoid deserialising message contents, because Python's email module
might produce different representation than the MUA sending original message.
The result would be a transformed message, which could mean broken message in
certain conditions.
2024-08-23 14:16:28 +02:00
b6155ade96
Improve tests
- Always default to 'python' if PYTHON env. var. unset.
- Enable SQLAlchemy warnings in daemon tests.
- Commit changes in schema initialisation script.
2024-08-23 14:16:28 +02:00
4977185ba1
Add header-only parser and minor test improvements 2024-08-23 14:16:27 +02:00
474d20f32b
Add contract tests for headers-only parsing of email messages 2024-08-23 14:16:27 +02:00
95b1396ee0
Upgrade SQLAlchemy, enable warning capturing 2024-08-23 14:16:27 +02:00
c92b9aed80
Only expire KeyConfirmationQueue items that haven't been confirmed 2024-08-23 14:16:27 +02:00
abd3f923fb
Extract expiry calculation from KeyConfirmationQueue 2024-08-23 14:16:27 +02:00
e6619a660f
When cleaning up after cron-job, remove expired queue items
- Implement KeyConfirmationQueue.delete_expired_queue_items to delete items
older than a given number of hours.

- Introduce configuration parameter to specify maximum number of hours.  It
defaults to 1 hour.

- Update documentation to explain that we never assign ST_TO_BE_DELETED.
2024-08-23 14:16:27 +02:00
ce595971e1
Document key lifecycle 2024-08-23 14:16:27 +02:00
09d7a498df
Improve delivery error-handling
- Introduce exceptions to be raised upon transient and permanent delivery
failures, as specified by SMTP RFC.  Depending on type of failure, return
either 451 or 554 reply code.

- When serialising a message, treat ValueError as a serialisation issue (and
try again to deliver in cleartext).
2024-08-23 14:16:27 +02:00
8955cf6c9d
Improve error-handling and error-reporting
1. Log the full traceback only once for each error (when we bounce the
message).

2. Use 451 response code on processing failure.

3. Disable decoding message contents as we operate on raw data anyway.
2024-08-23 14:16:27 +02:00
0ef3012e33
Fix a typo in bouncing, wrap UnicodeEncodeError in custom exception 2024-08-23 14:16:27 +02:00
pfm
d3f1aa3a02 Merge pull request 'Improve error handling' (#146) from error-handling into main
Reviewed-on: Disroot/gpg-lacre#146
2024-03-03 08:35:03 +00:00
e28864074c
Log exception and traceback when we fail-over to cleartext 2024-03-03 09:26:50 +01:00
0ec1bc3056
Set default values for non-nullable lacre_keys columns 2024-03-03 09:25:14 +01:00
110ea885f2
Deliver cleartext if Unicode encoding or message serialisation fail 2024-03-02 18:36:41 +01:00
3138864d32
Include exception in ExecutionTimeLogger log record
Also: cover ExecutionTimeLogger with a unit test.
2024-03-02 18:06:51 +01:00
pfm
1d8acc8eb8 Merge pull request '142_lacreadm' (#145) from 142_lacreadm into main
Reviewed-on: Disroot/gpg-lacre#145
2024-03-01 19:50:10 +00:00
8006b96df2 Rename lacreadm wrapper, mention it in documentation 2024-03-01 19:47:10 +00:00
f80e4ecb9e Implement a very thin wrapper around 'python -m lacre.admin' 2024-03-01 19:47:10 +00:00
676ff47933 Don't ignore 'bin' directory 2024-03-01 19:47:10 +00:00
f1c135850c lacre.admin: Report misconfiguration
Also: log more info when the daemon starts.
2024-03-01 19:47:10 +00:00
pfm
ccfaa39501 Merge pull request 'Fix unencrypted delivery' (#144) from v0.2_unencrypted-delivery-fix into main
Reviewed-on: Disroot/gpg-lacre#144
2024-03-01 19:34:19 +00:00
7806d8c32a
Log message headers on a hard error
When we know we need to bounce a message and [daemon]log_headers is enabled,
we record up to 2.5kB of message headers at ERROR level.  This could help
diagnosing issues later.

Also: no longer record MIME Type, Charset and Content-Transfer-Encoding, as
the issues related to these properties no longer occur.
2024-03-01 20:28:51 +01:00
04ca103494
Fix unencrypted delivery in case of message generation failure
When we fail to produce byte representation of the email message being
processed, we may end up bouncing a message.  An example of such case would be
a message with a Message-Id header that Python's email parser library cannot
process.

In such cases, just take whatever original content we have received and pass
it to the destination without touching it to minimise any chances of breaking
the overall flow.
2024-03-01 20:14:09 +01:00
pfm
d75ded751e Merge pull request 'Rename GPG-Mailgate to Lacre' (#138) from 81_rename-to-lacre into main
Reviewed-on: Disroot/gpg-lacre#138
2024-02-23 07:35:21 +00:00
f601080e87 lacre.admin: Add more 'queue' documentation, clean up 2024-02-23 08:33:47 +01:00
80c25f6d2e lacre.admin: Document new sub-command 'database' 2024-02-23 08:25:52 +01:00
aa2eb604d4 lacre.admin: Add a sub-command to manipulate database schema
- It supports option '-i' to initialise the schema.
- It logs a warning-level record of the schema manipulation.
2024-02-21 21:10:49 +01:00
f7e6708949 Adjust lacre.dbschema to reflect original schema.sql
- Set nullability of columns.
- Set up primary keys and auto-increment where necessary.
- Add missing 'lacre_locks' table.
- Implement a function to create tables.
2024-02-21 21:10:49 +01:00
be615df6e4 Split webgate-cron.py into small functions 2024-01-21 11:28:46 +01:00
bfd3541b18 Retrieve data from db result before returning from Context Manager
SQLAlchemy's connection is a Context Manager and if we return a result from
code wrapped in a Context Manager, its cursor might already be closed.
2024-01-20 18:52:47 +01:00
8d2bf403a7 Add lacre.admin queue --list option, log query parameters 2024-01-16 20:33:23 +01:00
55a369df83 Add debug sqlalchemy logs (disabled by default) 2024-01-08 22:45:59 +01:00
cd67b0934e Unify configuration requirements 2024-01-08 22:19:10 +01:00
276e0d0cd4 Use one config for cron and daemon tests 2024-01-07 21:52:52 +01:00
bc2fc53416 Update gitignore: generated config, project name 2024-01-06 15:46:33 +01:00
260a3f3e9c Configure pooling for cron tests 2024-01-06 15:26:18 +01:00
a943b50adb Update test configuration after renaming 2024-01-06 15:06:36 +01:00
a98ff611ee Continue renaming: config files, tests, docs 2024-01-06 14:45:09 +01:00
ad3a54fcd7 Rename GPG-Mailgate to Lacre
Update naming in documentation and the source code.
2024-01-06 14:34:54 +01:00
7208f66527 Improve simple filter structure 2024-01-05 22:21:20 +01:00
a09fd67a59 Make keys unexpirable 2024-01-05 22:11:14 +01:00
pfm
748fd00957 Merge pull request 'lacre.repositories: Configure SQLAlchemy connection pooling' (#136) from connection-pooling into main
Reviewed-on: Disroot/gpg-lacre#136
2024-01-04 18:52:03 +00:00
8f8f081d28 Fix key-removal condition, improve logging 2024-01-04 19:45:25 +01:00
07539a97d3 Improve logging
- Don't re-configure lacre.notify logger.
- Issue more DEBUG logs when deleting keys.
2023-12-20 23:03:04 +01:00
5c327b166a webgate-cron: Log more information, including exceptions 2023-12-19 18:21:00 +01:00
41b7535412 Add more logging, add --delete option to admin queue sub-command 2023-12-19 09:02:42 +01:00
9b5d578985 lacre.config: Make both enums case-insensitive
Also: use PGPStyle in lacre.core.
2023-12-17 20:42:57 +01:00
ff429c93e6 Convert pooling parameters to integers 2023-12-17 14:12:52 +01:00
90da933bf9 Make disconnect handling configuration explicit
- Provide a new reuqired parameter: [database]pooling_mode and use it during
  SQLAlchemy engine initialisation.

- Update tests and configuration (including sample configuration).

- Adjust repository unit test to load config during setup.

- Pass an engine instance to repository constructors instead of connections.
  Engine keeps a connection pool and we rely on it.
2023-12-17 14:03:20 +01:00
86cc27e918 lacre.repositories: Configure SQLAlchemy connection pooling
Provide 3 new configuration parameters in database section:

- max_connection_age --- number of seconds before an idle connection is
  "recycled", i.e. replaced with a new one;

- pool_size --- number of simultaneous connections kept in the pool;

- max_overflow --- maximum number of simultaneous connections we could make to
  the database.

Update sample config, including links to documentation.
2023-12-16 23:32:27 +01:00
pfm
18a64bcd72 Merge pull request 'Add ability to deliver cleartext when keys can't be loaded' (#135) from fix/keys-not-loaded into main
Reviewed-on: Disroot/gpg-lacre#135
2023-12-10 20:41:48 +00:00
e8d0d248b3 lacre.repositories: Add missing import 2023-12-10 21:39:59 +01:00
23a05c11ac Remove EncryptionException formatting test 2023-12-10 21:35:35 +01:00
8cc1136a90 lacre.daemon: When keys can't be loaded, fail gracefully
- Introduce '[daemon]bounce_on_keys_missing' option to let the admin decide if
  they want Lacre to deliver cleartext message when identity database is
  unreachable or throws exceptions.  It defaults to 'no'.

- In IdentityRepository, use option mentioned above to decide what to do when
  an exception is caught.
2023-12-10 21:27:05 +01:00
pfm
628de8a28d Merge pull request 'Fix cron script and more' (#134) from fix/cron-script into main
Reviewed-on: Disroot/gpg-lacre#134
2023-12-09 20:26:37 +00:00
c0b98649d4 lacre.admin: Document import command 2023-12-09 21:14:35 +01:00
fe2c0cbf76 Fix unprintable exception issue 2023-12-09 20:57:09 +01:00
75c48282b0 Rework encryption exception handling
Also: remove misleading comment about message.defects.
2023-12-09 20:38:46 +01:00
fc08813bdc Improve unencryptable message logs 2023-12-09 19:48:20 +01:00
d51c675881 lacre.admin: Make import -r option a flag (Boolean) 2023-12-05 21:51:35 +01:00
abaf8820d7 lacre.admin: Add -r / --reload option to import command
With -r option, import command will first remove all identities and then load
them again from pubring.kbx.
2023-12-05 21:49:23 +01:00
94d0a62766 Identity removal: execute prepared DELETE 2023-12-05 21:33:19 +01:00
cc1bacbe3d Move some imports to lacre.notify 2023-12-05 21:13:02 +01:00
pfm
4c603839b5 Merge pull request 'lacre.repositories: Fix IdentityRepository existence predicate' (#133) from 132-fix-repo-upsert into main
Reviewed-on: Disroot/gpg-lacre#133
2023-12-04 21:59:00 +00:00
0d852bc279 lacre.repositories: Fix IdentityRepository existence predicate 2023-12-04 22:57:43 +01:00
pfm
b7713207ab Merge pull request 'Fix unencrypted delivery and key removal' (#130) from 129-key-removal into main
Reviewed-on: Disroot/gpg-lacre#130
2023-12-02 20:59:13 +00:00
ac5dddfa98 Remove configuration options no longer used 2023-12-02 21:48:17 +01:00
052551072e Change table prefix from 'gpgmw' to 'lacre' 2023-12-02 20:02:59 +01:00
0975ce3a69 lacre.admin: Handle database exceptions 2023-11-26 19:52:58 +01:00
b44bd7b150 lacre.admin: Implement identity import, fix identity list
- Let the user specify a directory, using the one from configuration by
  default.

- If user requested identity list without a specific email, list all.  Drop
  support for '-a' option.
2023-11-26 18:30:25 +01:00
0fe5e6b3dc Make GnuPG.public_keys docstring more complete 2023-11-26 18:29:43 +01:00
aa8c353a05 Replace NBSP in doc/admin.md with a regular space 2023-11-26 18:29:09 +01:00
97c4f9f14a lacre.repositories: Fix naming after refactoring 2023-11-25 16:09:23 +01:00
626fce5f2c lacre.admin: Implement 'identities' sub-command 2023-11-25 16:08:54 +01:00
95c5802c38 Add test/lacre.db to gitignore 2023-11-25 15:09:00 +01:00
9b5c43b769 Fix crontest config, polish Makefile 2023-11-25 15:05:27 +01:00
7fe52ae8b5 Don't pass table definition to KeyConfirmationQueue 2023-11-25 15:02:48 +01:00
1ad0d2df0e Implement lacre.admin CLI tool 2023-11-25 14:07:10 +01:00
becb39f139 Clean up database access
- Don't pass table definitions to repository constructors.

- Keep an internal reference to Engine in lacre.repository.

- Implement KeyConfirmationQueue.count_keys.
2023-11-25 14:04:32 +01:00
4950e0b9c3 Keep secondary keyring for test purposes 2023-11-25 01:13:43 +01:00
acd33fec1e Fix inheritance issues
- Use accessor methods.
- Avoid data duplication.
2023-11-25 01:11:44 +01:00
72217e38ea GnuPG module: make key-listing more thorough
- Flush key-collecting structures each time a new public key entry is found.
  This will avoid adding sub-keys and overwriting main keys with them.

- Use parseaddr from email.utils to parse emails (and drop realname part).

- Record logs produced during unit tests.

- Fix a small bug in test code.

Also: add basic information about available test identities to testing
documentation.
2023-11-25 01:08:15 +01:00
7c2d32bf3c Make IdentityRepository a KeyRing
- Keep only one class to provide access to identities stored in the database.

- Remove old code and its tests.

- Align KeyRing and IdentityRepository APIs.

- Implement a (very) simple unit test for IdentityRepository.
2023-11-24 22:59:21 +01:00
5efef3c9cb Fix table name, unify metadata handling 2023-11-20 22:27:35 +01:00
89affde0d5 Add tests for GnuPG parsing routines 2023-11-20 22:11:37 +01:00
bfa2643dc7 Implement identity repository
Also: rename key_id to fingerprint.
2023-11-20 22:11:25 +01:00
56da7e0cb4 Refactor calculating execution time
- Implement a context manager logging execution time.
- Use that context manager in daemon's handle_DATA method.
2023-11-20 22:03:59 +01:00
4fbae908d6 Don't require less-than and greater-than around the email
Keys don't have to be surrounded with less-than and greater-than characters,
so this code could mishandle valid keys.
2023-11-19 22:45:08 +01:00
c6b2dbf618 Add docs, improve logging 2023-11-17 22:55:37 +01:00
7ac928af76 Handle gpg-mailgate.py missing params better 2023-11-17 22:51:09 +01:00
a3eb892df9 Remove duplicate logger initialisation 2023-11-15 20:25:42 +01:00
2edd842f90 Use lacre.dbschema definition of identities table 2023-11-12 20:20:38 +01:00
6ca5db2db3 Issue an INFO log entry after configuring logging
Also: reformat with spaces instead of tabs.
2023-11-12 19:57:12 +01:00
9bbc86bc53 Extract parts of cron script to modules
Introduce new Python modules:

- lacre.notify -- to send notifications from the cron script;

- lacre.dbschema -- to keep database schema definition as code (SQLAlchemy);

- lacre.repositories -- to define key and identity repositories with high
  level APIs that we can then use elsewhere.

Also:

- rework GnuPG.add_key to return fingerprint so we can use it in the cron
  script;

- rename cron-job's logger name, replacing dash with an underscore as logging
  module doesn't like dashes.
2023-11-12 19:56:45 +01:00
bf677585be Don't require watchdog anymore 2023-11-01 21:26:42 +01:00
5e108c189a Replace file-based identity store with a dedicated db table 2023-10-29 19:39:08 +01:00
02edb4cc96 Validate keyring type config parameter on daemon startup 2023-10-27 23:53:17 +02:00
3dd6913599 Initialise db connection lazily, use isolated asyncio test case 2023-10-23 22:44:53 +02:00
e5339d264c Improve asyncio usage 2023-10-23 22:35:27 +02:00
43f43a4137 Fix DatabaseKeyring tests 2023-10-23 20:26:23 +02:00
41442e5b59 Add basic support for RDBMS-based keyring 2023-09-30 22:38:33 +02:00
274bfbaf3b Always use 'python' binary during tests 2023-09-30 22:33:49 +02:00
c570bcd383 Update Alice's key expiry date 2023-09-25 19:44:37 +02:00
624a335a41 GnuPG: clean up and collect more diagnostic info
- Use regular expressions instead of finding particular characters in gnupg
  output to decide whether confirmation line was found.

- Use tempfile.mkdtemp to create secure temporary directories.

- Record information about the key considered by GnuPG. When missing in
  exception, it means no key was found.
2023-09-21 20:21:01 +02:00
6c114b6dcd Ensure correct logging initialisation in webgate-cron 2023-09-21 20:21:01 +02:00
fccabc083c Fix unencrypted delivery arguments
When falling back to unencrypted mail delivery, do not pass sender information
to SendFrom.call method.
2023-09-21 20:21:01 +02:00
pfm
401f67844a Merge pull request 'Refresh docs' (#128) from 126-refresh-docs into main
Reviewed-on: Disroot/gpg-lacre#128
2023-07-08 13:40:42 +00:00
cfbb413e7e Explicitly mention requirements.txt file 2023-07-08 02:03:20 +02:00
adcafb30c3 Reorder and simplify first secions of README 2023-07-08 02:02:47 +02:00
f0d4447f4a Move requirements to INSTALL, improve language 2023-06-25 22:41:13 +02:00
addb119b3e Update INSTALL.md
- Explain requirements files are used now.
- Mention recommended Python version.
- Refresh instructions after splitting Lacre into components.
2023-06-25 22:31:34 +02:00
bcd0284eac Update README
- Simplify where possible.
- Remove outdated parts.
- Explain that only Python 3.9 is tested and supported.
- Link to Lacre Webgate repository.
2023-06-18 21:31:47 +02:00
pfm
c8f6743768 Merge pull request 'Handle missing Content-Type properly' (#125) from 124-missing-ct into main
Reviewed-on: Disroot/gpg-lacre#125
2023-05-19 18:38:17 +00:00
a30b5e7577 Handle missing Content-Type properly
- ContentManager sets default Content-Type even if it was missing in the
  original message.

- Make sure that when Content-Type is missing, copying parameters doesn't
  raise an error.

- Add a unit-test to check that.
2023-05-19 20:30:00 +02:00
pfm
07fb8d6ae8 Merge pull request 'Fix encoding issues' (#123) from post-test-fixes into main
Reviewed-on: Disroot/gpg-lacre#123
2023-05-11 20:22:23 +00:00
5a5b6c27a4 Fix tests after rebase 2023-05-08 22:32:19 +02:00
3297bbfcca Clean up and document Advanced Mail Filter E2E test
- Remove unused code.
- Add docstrings.
2023-05-08 22:17:02 +02:00
b3c0235486 Make case-discovery code cleaner and more flexible 2023-05-08 22:17:02 +02:00
0cdbf5ba7d Add emoji test 2023-05-08 22:17:02 +02:00
c08d66ac80 Migrate daemon E2E tests to unittest framework 2023-05-08 22:17:02 +02:00
518b823b5c Fix simple filter: pass policy to as_bytes()
Also: adjust expected test output because it's now Base64-encoded.
2023-05-08 22:17:02 +02:00
459779bea6 Add more test cases 2023-05-08 22:17:02 +02:00
61cf50effe Fix MIME content sub-type handling for non-plain text messages 2023-05-08 22:17:02 +02:00
bc92d7a31c [e2e_test] Identify test cases by iteracting config sections 2023-05-08 22:17:02 +02:00
da0ffb4a51 Add a test message with Emoji 2023-05-08 22:17:02 +02:00
34e8b6a4eb Move the last key-related function to keyring module 2023-05-08 22:17:02 +02:00
285f5dbf18 Don't overwrite CTE
We rely on Content Manager to select the right Content-Transfer-Encoding.
2023-05-08 22:17:02 +02:00
1acb330c02 Rewrite e2e_test to use unittest framework 2023-05-08 22:17:02 +02:00
fdd11dba14 Log more information about FS events while reloading keys 2023-05-08 22:17:02 +02:00
682de14630 Split the code into smaller modules
Introduce modules:
- lacre.transport - for actual delivery via SMTP
- lacre.smime - to take care of S/MIME stuff

Implement lacre.transport.SendFrom class that does a almost exactly the same
thing as the original send_msg function, but without using global variable to
store original message sender.
2023-05-08 22:17:02 +02:00
ff6e0bfbdd Move recipient-processing code to a dedicated module 2023-05-08 22:17:02 +02:00
5f5b374f84 Unify send_msg, add more type hints 2023-05-08 22:17:02 +02:00
67e6df17fb Move success flag before 'try' 2023-05-08 22:17:02 +02:00
0da169ae61 Log exceptions from failover delivery 2023-05-08 22:17:02 +02:00
f4e21217c2 [simple-filter] Retry delivery recoding text parts 2023-05-08 22:17:02 +02:00
1f1fe1dadb Add lots of log messages 2023-05-08 22:17:02 +02:00
8a42f3fea1 Improve error-handling for simple filter and test relay 2023-05-08 22:17:02 +02:00
8def4b40dc Correct sample message in contract tests 2023-05-08 22:17:02 +02:00
e595e8baf4 Fix contract tests 2023-05-08 22:17:02 +02:00
a5f79c1ae7 Wrap recipient lists
Instead of passing pairs of lists (emails and keys) separately, implement a
class RecipientList to wrap such pair of lists.
2023-05-08 22:17:02 +02:00
c5e788b2a0 Add more contract tests for email module 2023-05-08 22:17:02 +02:00
12b7c3394d Make test relay slightly cleaner 2023-05-08 22:17:02 +02:00
b2bd6a9926 Encapsulate recipient lists
Implement RecipientList class with:
- recipient (email) list,
- key (identity) list.

Cover with basic unit test.
2023-05-08 22:17:02 +02:00
86a3e0031b Adjust gpg-mailgate.py script to use SMTPUTF8 policy 2023-05-08 22:17:02 +02:00
54ee9d9875 Add minor E2E test improvements 2023-05-08 22:17:02 +02:00
603a88489e Polish the code 2023-05-08 22:17:02 +02:00
ffd5f08ad9 Make PGP message recognition more thorough 2023-05-08 22:17:02 +02:00
d342f206de Handle messages as EmailMessage
In the daemon, specify policy as SMTPUTF8.  That sets the deafult message type
to EmailMessage.

EmailMessage class is richer, including support for Content Managers, giving
it the capability to properly handle textual data and its encodings.

Also: add another contract test.
2023-05-08 22:17:02 +02:00
ace2ce6b06 Add more email-parsing contract tests 2023-05-08 22:17:01 +02:00
509aac6de3 Improve command-generation, logging and readability 2023-05-08 22:17:01 +02:00
ea8b246538 Clean up PGP/MIME flow
- Use MIMEPart instead of Message when encrypting in PGP/MIME mode.

- Wrap text/plain messages in MIMEPart, instead of manipulating payloads
  manually.

- Add a test for wrapping.
2023-05-08 22:17:01 +02:00
765637fd3a Fix logging initialisation by fixing import order 2023-05-08 22:17:01 +02:00
1a3ce89ce5 Always encrypt decoded payload 2023-05-08 22:17:01 +02:00
3c8b792203 Fix line-discarding bug 2023-05-08 22:17:01 +02:00
56101b86c0 Clean up the code after refactor
- Clean up PGP/MIME flow by using API instead of explicit/manual generation of
  headers.
- Fix E2E test configuration for PGP/MIME case.
- Add first lacre.core unit tests.
- Add another Contract Test.
2023-05-08 22:17:01 +02:00
27b07e672d Rework PGP/MIME flow 2023-05-08 22:17:01 +02:00
5e408259c0 Start using Content Manager
Also:
- Pass text to Popen in GnuPG (used to be bytes).
- Make is_payload_pgp_inline type-agnostic (str / bytes).
2023-05-08 22:17:01 +02:00
d2ed4a9cee Make test reports more readable 2023-05-08 22:17:01 +02:00
94e22caf8e When encryption fails, revert to cleartext delivery
When GnuPG refuses to encrypt a message (e.g. when key has expired), record
information about the failure and send to logs, then deliver cleartext.  This
way we won't bounce email that could be delivered without encryption.

Also: add more E2E tests.
2023-05-08 22:16:41 +02:00
1cdca1d06d Log up to 2,5kB of message headers 2023-05-08 22:14:24 +02:00
3c1544e423 [daemon] Parse Envelope.original_content 2023-05-08 22:14:24 +02:00
ffffa6a364 Correct sample message file name 2023-05-08 22:14:24 +02:00
b242edf098 Add a more sophisticated UTF-8 test case
Add a new test message to verify Lacre's behaviour when processing
UTF-8 messages with text in two different scripts (latin-based and cyrillic).

Also: log Content-Transfer-Encoding when logging headers is enabled.
2023-05-08 22:14:24 +02:00
b94123e83e Use SMTPUTF8 policy, add more debug logging 2023-05-08 22:14:24 +02:00
4da4019321 Log message defects and optionally some non-PII headers 2023-05-08 22:14:24 +02:00
b6bd36a460 Use bytes instead of str to hold message bodies
smtplib.SMTP expects ASCII-only message bodies when message body is provided
as a 'str'.  If we pass a 'bytes', we need to choose encoding earlier and we
do this by calling 'as_bytes' on messages with SMTP policy, which takes care
of formatting the body properly.

As a result, ISO-8859-x messages are converted to Quoted Printable and UTF-8
messages are Base64-encoded.

Testing this behaviour is tricky, because we use the same SMTP client to send
test data.  For this reason, test code has become a bit ugly, but it does
exactly what we need.
2023-05-08 22:14:24 +02:00
0fac54a29a Add UTF-8 and ISO-8859-2 test cases, make tests more reliable 2023-05-08 22:14:24 +02:00
037a527c44 Add a test case with non-ASCII message content 2023-05-08 22:14:24 +02:00
5eb687f0cd Add config parameter to set DATA size limit
Expose a new parameter: [daemon]max_data_bytes, to limit Lacre's memory
usage and allow processing of messages larger than 32MB (which is the
default limit).
2023-05-08 22:14:24 +02:00
f6bf86c533 Re-use the same Event Loop for each coroutine
Function asyncio.run creates a new event loop each time it's called and
executes coroutine in that new loop.  However, we want all our coroutines to
be executed from the same event loop, so we acquire a loop when lacre.daemon
starts and then use it to execute them later.

See: Disroot/gpg-lacre#109
2023-05-08 22:14:24 +02:00
pfm
609a81adcd Merge pull request 'Remove documentation and sample config for decryption' (#121) from doc-updates into main
Reviewed-on: Disroot/gpg-lacre#121
2023-03-11 11:33:44 +00:00
37335de329 Remove documentation and sample config for decryption
We no longer support decrypting emails, so:

- Remove parameters from exmaple configuration file.
- Remove relevant section from installation instructions (INSTALL.md).
2023-03-11 12:30:24 +01:00
pfm
7389dc8fde Merge pull request 'Add missing test case for add_key and delete_key' (#116) from wiktor/gpg-lacre:add-test-for-add-delete-key into main
Reviewed-on: Disroot/gpg-lacre#116
Reviewed-by: pfm <pfm@no-reply@disroot.org>
2023-02-16 18:04:17 +00:00
46bb125684
Add missing test case for add_key and delete_key 2022-12-23 13:49:43 +01:00
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
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
c7c49977c9
Change 'Indented code blocks' to 'Fenced code blocks' in adv-filt.md 2022-11-01 23:53:37 +01:00
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
86b0cd335e Replace UTF-8 non-breaking space with a plain space 2022-10-26 19:20:47 +02:00
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
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
ec6c43afcb improved formatting
Just some small Markdown syntax corrections.
2022-10-24 23:59:01 +00:00
18c790f986 Only reload on pubring.kbx file modifications 2022-10-23 13:51:42 +02:00
d39cadb9aa Use proper name in webgate-cron.py logs 2022-10-23 13:51:10 +02:00
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
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
b91501d3dd Convert all test input messages to CRLF line endings 2022-10-22 20:52:14 +02:00
fc85cdb841 Rework PGP-Inline verification/recognition 2022-10-22 19:58:16 +02:00
ba7978b4a6 Make webgate-cron code more readable 2022-10-22 14:33:20 +02:00
2ac26c09ce Simplify code, improve log entries, add comments 2022-10-22 11:23:17 +02:00
00289759a3 Add aiosmtpd dependency 2022-10-22 11:23:11 +02:00
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
99e939bb4e Remove too verbose debug logs, implement repr() for KeyCache 2022-10-20 22:27:34 +02:00
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
a82d9f96b3 Fix indentation and some warnings 2022-10-19 21:04:25 +02:00
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
5f8c94673d Add basic daemon documentation 2022-10-19 18:36:23 +00:00
641253b3ec Make key-loading async, remove unused parameter 2022-10-19 18:36:23 +00:00
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
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
acbb2ab776 Document dependencies
Add requirements.txt file with versions of dependencies known to work well.
2022-10-19 18:36:23 +00:00
2da97a5a9a Reformat code, add doc comments 2022-10-19 18:36:23 +00:00
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
386c23f9f8 Document dependencies on the logging module 2022-10-19 18:36:23 +00:00
1db0a09fa5 Log processing time for successful deliveries 2022-10-19 18:36:23 +00:00
a85b7b7a43 Reload key cache only if keyring dir was modified 2022-10-19 18:36:23 +00:00
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
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
5f601fa50c Implement a basic KeyCache 2022-10-19 18:36:23 +00:00
07263d5afa Reformat tests 2022-10-19 18:36:23 +00:00
c41df63e42 Reorder tests to avoid interferences 2022-10-19 18:36:23 +00:00
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
ce6a0c5466 Continue refactoring
- Add more encryption strategies.
- Replace tuples (email + key) with dedicated objects.
2022-10-19 18:36:23 +00:00
8963eee47f Reformat GnuPG module 2022-10-19 18:36:23 +00:00
a5bcf2d9b2 Make daemon E2E tests use configured parameters 2022-10-19 18:36:23 +00:00
d01865d21c Refactor into smaller functions and objects 2022-10-19 18:36:23 +00:00
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
ce2e55e90c Change indentation from tabs to 4 spaces 2022-10-19 18:36:23 +00:00
0cb656f89d Add more debug logging to _try_direct_key_lookup 2022-10-19 18:36:23 +00:00
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
68e4a452d2 Split _gpg_encrypt into smaller functions 2022-10-19 18:36:23 +00:00
1edef79787 Update documentation to cover daemon tests 2022-10-19 18:36:23 +00:00
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
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
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
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
7849c55d9f Extend the daemon skeleton 2022-10-19 18:36:23 +00:00
29b5b50901 Mailgate: replace tabs with spaces 2022-10-19 18:36:23 +00:00
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
8f191cae72 Restore keyring before running E2E tests 2022-10-19 18:36:23 +00:00
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
9e998b54e9 Extract a predicate to classify messages as already encrypted
Also: perform minor cleanup.
2022-10-19 18:36:23 +00:00
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
156b09fd4e
added execute permission to webgate-cron.py 2022-10-19 09:35:52 +02:00
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
5ffbbec5f0 Rename sort_recipients to deliver_message, remove unused imports 2022-06-11 21:05:27 +02:00
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
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
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
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
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
65d2a77486 Don't break Content-Type header with LF - part 2 2022-06-02 19:43:27 +02:00
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
46be24670c Fix charset resolution in Content-Type parser 2022-06-01 23:44:41 +02:00
55b58d25bc Use literal separator '@' in sanitize_case_sense 2022-06-01 23:23:51 +02:00
d3b1717290 Extract PGP/INLINE checks, remove unnecessary byte-check 2022-06-01 23:00:05 +02:00
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
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
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
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
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
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
d8bef9cdb0 Explain syslog logging better in sample logging config 2022-05-13 21:57:29 +02:00
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
acdb2dd5c8 Log cron-job inner workings 2022-05-11 19:57:26 +02:00
7a8720c142 Update installation instructions, add sample logging config 2022-05-11 19:15:59 +02:00
8f0d8f4933 Add newlines to key material 2022-05-06 20:40:16 +02:00
617a208fe9 Fix indentation and names after rebase 2022-05-06 20:25:22 +02:00
9dfc447169 Use Lacre logging and configuration in register-handler 2022-05-06 20:13:23 +02:00
af5a5b4176 Use Lacre logging and configuration in cron.py 2022-05-06 20:13:19 +02:00
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
baf7954270 Use list booleanness instead of comparing with empty list 2022-05-06 19:39:56 +02:00
031c7234f6 Reorder top-level expressions 2022-05-06 19:39:56 +02:00
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
55fa0d0601 [GnuPG.confirm_key] Convert email to bytes() before comparison 2022-05-06 19:14:52 +02:00
71afd6ed05 [GnuPG.confirm_key] Set 700 mode for temp. key home 2022-04-26 18:42:48 +02:00
a82ff9f3f1 Use actual keys (RSA and Ed25519) in cron-test 2022-04-24 10:42:51 +02:00
52b31028c5 Fix config key typo 2022-04-24 10:07:52 +02:00
b19a76e297 Update testing documentation 2022-04-23 13:13:57 +02:00
ffc53b935a Finish migration to SQLAlchemy and automate testing cron.py 2022-04-23 13:08:40 +02:00
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
86b725349f
added smtp auth to cron.py 2022-04-20 13:01:58 +02:00
ea0e012c04 Use 'open' to open a file 2022-04-19 21:35:22 +02:00
454b519c70 Use bytes in confirm_key's line operations 2022-04-19 21:22:34 +02:00
f472f4ac22 Use isinstance() instead of type() 2022-04-19 21:16:40 +02:00
fdadc89c31 Use bytes in GnuPG.confirm_key too 2022-04-19 20:49:49 +02:00
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
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
100 changed files with 5216 additions and 2585 deletions

8
.gitignore vendored
View file

@ -1,3 +1,6 @@
# Generated project files:
test/lacre.db
*.py[cod]
# C extensions
@ -10,7 +13,6 @@ dist
build
eggs
parts
bin
var
sdist
develop-eggs
@ -26,10 +28,10 @@ pip-log.txt
.tox
nosetests.xml
# GPG-Mailgate test files
# Lacre test files
test/logs
test/tmp
test/gpg-mailgate.conf
test/lacre.conf
test/keyhome/random_seed
# Emacs files

View file

@ -1,22 +1,24 @@
#
# gpg-mailgate
# lacre
#
# This file is part of the gpg-mailgate source code.
# This file is part of the lacre 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.
# lacre 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.
# lacre 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 lacre source code. If not, see <http://www.gnu.org/licenses/>.
#
"""GnuPG wrapper module."""
import os
import os.path
import subprocess
@ -24,144 +26,331 @@ import shutil
import random
import string
import sys
import logging
import re
import tempfile
from email.utils import parseaddr
LINE_FINGERPRINT = 'fpr'
LINE_USER_ID = 'uid'
LINE_PUBLIC_KEY = 'pub'
POS_FINGERPRINT = 9
POS_UID = 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
RX_CONFIRM = re.compile(br'key "([^"]+)" imported')
def public_keys( keyhome ):
cmd = build_command(keyhome, '--list-keys', '--with-colons')
p = subprocess.Popen( cmd, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE )
p.wait()
keys = dict()
fingerprint = None
email = None
for line in p.stdout.readlines():
line = line.decode('utf-8')
if line[0:3] == LINE_FINGERPRINT:
fingerprint = line.split(':')[POS_FINGERPRINT]
if line[0:3] == LINE_USER_ID:
if ('<' not in line or '>' not in line):
continue
email = line.split('<')[1].split('>')[0]
if not (fingerprint is None or email is None):
keys[fingerprint] = email
fingerprint = None
email = None
return keys
class EncryptionException(Exception):
"""Represents a failure to encrypt a payload.
# confirms a key has a given email address
def confirm_key( content, email ):
tmpkeyhome = ''
Arguments passed to exception constructor:
- issue: human-readable explanation of the issue;
- recipient: owner of the key;
- cause: any additional information, if present;
- key: fingerprint of the key.
"""
pass
while True:
tmpkeyhome = '/tmp/' + ''.join(random.choice(string.ascii_uppercase + string.digits) for x in range(12))
if not os.path.exists(tmpkeyhome):
break
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
def _build_command(key_home, *args, **kwargs):
cmd = ["gpg", '--homedir', key_home]
cmd.extend(args)
return cmd
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 public_keys(keyhome, *, key_id=None):
"""List public keys from keyring KEYHOME.
Returns a dict with fingerprints as keys and email as values."""
cmd = _build_command(keyhome, '--list-keys', '--with-colons')
if key_id is not None:
cmd.append(key_id)
p = subprocess.Popen(cmd, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
p.wait()
keys = dict()
collected = set()
fingerprint = None
email = None
for line in p.stdout.readlines():
line = line.decode(sys.getdefaultencoding())
if line[0:3] == LINE_PUBLIC_KEY:
# New identity has started, reset state.
fingerprint = None
email = None
if line[0:3] == LINE_FINGERPRINT and not fingerprint:
fingerprint = _extract_fingerprint(line)
if line[0:3] == LINE_USER_ID:
email = _parse_uid_line(line)
if fingerprint and email and not email in collected:
keys[fingerprint] = email
collected.add(email)
fingerprint = None
email = None
p.stdout.close()
p.stderr.close()
return keys
def _extract_fingerprint(line):
fpr_line = line.split(':')
if len(fpr_line) <= POS_FINGERPRINT:
return None
else:
return fpr_line[POS_FINGERPRINT]
def _parse_uid_line(line: str):
userid_line = line.split(':')
if len(userid_line) <= POS_UID:
return None
else:
(_, email) = parseaddr(userid_line[POS_UID])
return email
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: str):
"""Verify that the key CONTENT is assigned to identity EMAIL."""
content = _to_bytes(content)
expected_email = email.lower()
tmpkeyhome = tempfile.mkdtemp()
LOG.debug('Importing into temporary directory: %s', tmpkeyhome)
result = _import_key(tmpkeyhome, content)
confirmed = False
for line in result.splitlines():
LOG.debug('Line from GnuPG: %s', line)
found = RX_CONFIRM.search(line)
if found:
(_, extracted_email) = parseaddr(found.group(1).decode())
confirmed = (extracted_email == expected_email)
LOG.debug('Confirmed email %s: %s', extracted_email, confirmed)
# cleanup
shutil.rmtree(tmpkeyhome)
return confirmed
def _import_key(keyhome, content):
content = _to_bytes(content)
# Ensure we get expected output regardless of the system locale.
localized_env = os.environ.copy()
localized_env["LANG"] = "C"
p = subprocess.Popen(_build_command(keyhome, '--import', '--batch'), stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=localized_env)
output = p.communicate(input=content)[1]
p.wait()
return output
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."""
output = _import_key(keyhome, content)
def delete_key( keyhome, email ):
from email.utils import parseaddr
result = parseaddr(email)
email = None
for line in output.splitlines():
found = RX_CONFIRM.search(line)
if found:
(_, extracted_email) = parseaddr(found.group(1).decode())
email = extracted_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
# Find imported key to get its fingerprint
imported = public_keys(keyhome, key_id=email)
if len(imported.keys()) == 1:
fingerprint = list(imported.keys())[0]
return fingerprint, imported[fingerprint]
else:
return None, None
def delete_key(keyhome, email):
"""Remove key assigned to identity EMAIL from keyring KEYHOME."""
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.communicate()
p.wait()
return True
LOG.warn('Failed to parse email before deleting key: %s', email)
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 = None
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."""
if self._message is None:
self._message = message
else:
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 = self._popen()
encdata, err = p.communicate(input=self._message)
if p.returncode != 0:
LOG.debug('Errors: %s', err)
details = parse_status(err)
raise EncryptionException(details['issue'], details['recipient'], details['cause'], details['key'])
return (encdata, p.returncode)
# add recipients
for recipient in self._recipients:
cmd.append("-r")
cmd.append(recipient)
def _popen(self):
if self._charset:
return subprocess.Popen(self._command(), stdin=subprocess.PIPE, stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
encoding=self._charset)
else:
return subprocess.Popen(self._command(), stdin=subprocess.PIPE, stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
# add on the charset, if set
if self._charset:
cmd.append("--comment")
cmd.append('Charset: ' + self._charset)
def _command(self):
cmd = _build_command(self._keyhome,
"--trust-model", "always",
"--status-fd", "2",
"--batch",
"--yes",
"--pgp7",
"--no-secmem-warning",
"-a", "-e")
# 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('Built command: %s', cmd)
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")
STATUS_FD_PREFIX = b'[GNUPG:] '
STATUS_FD_PREFIX_LEN = len(STATUS_FD_PREFIX)
KEY_EXPIRED = b'KEYEXPIRED'
KEY_REVOKED = b'KEYREVOKED'
NO_RECIPIENTS = b'NO_RECP'
INVALID_RECIPIENT = b'INV_RECP'
KEY_CONSIDERED = b'KEY_CONSIDERED'
NOAVAIL = b'n/a'
# INV_RECP reason code descriptions.
INVALID_RECIPIENT_CAUSES = [
'No specific reason given',
'Not Found',
'Ambiguous specification',
'Wrong key usage',
'Key revoked',
'Key expired',
'No CRL known',
'CRL too old',
'Policy mismatch',
'Not a secret key',
'Key not trusted',
'Missing certificate',
'Missing issuer certificate',
'Key disabled',
'Syntax error in specification'
]
def parse_status(status_buffer: str) -> dict:
"""Parse --status-fd output and return important information."""
return parse_status_lines(status_buffer.splitlines())
def parse_status_lines(lines: list) -> dict:
"""Parse --status-fd output and return important information."""
result = {'issue': NOAVAIL, 'recipient': NOAVAIL, 'cause': 'Unknown', 'key': NOAVAIL}
LOG.debug('Processing stderr lines %s', lines)
for line in lines:
LOG.debug('At gnupg stderr line %s', line)
if not line.startswith(STATUS_FD_PREFIX):
continue
if line.startswith(KEY_EXPIRED, STATUS_FD_PREFIX_LEN):
result['issue'] = 'key expired'
elif line.startswith(KEY_REVOKED, STATUS_FD_PREFIX_LEN):
result['issue'] = 'key revoked'
elif line.startswith(NO_RECIPIENTS, STATUS_FD_PREFIX_LEN):
result['issue'] = 'no recipients'
elif line.startswith(KEY_CONSIDERED, STATUS_FD_PREFIX_LEN):
result['key'] = line.split(b' ')[2]
elif line.startswith(INVALID_RECIPIENT, STATUS_FD_PREFIX_LEN):
words = line.split(b' ')
reason_code = int(words[2])
result['recipient'] = words[3]
if reason_code:
result['cause'] = INVALID_RECIPIENT_CAUSES[reason_code]
return result

View file

@ -1,183 +1,214 @@
# Installation instructions
## Content
- General information
- Install GPG-Mailgate
- Install GPG-Mailgate-Web
- Install Lacre
- Install [Lacre-Webgate](https://git.disroot.org/Lacre/lacre-webgate)
- 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.
Lacre is divided in 3 main parts: Lacre itself, Lacre-Webgate and Register-handler. Some parts of the Lacre 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 Lacre 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.
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
## Install Lacre
### Requirements
- Python 2.X is already installed (GPG-Mailgate is not Python 3 compatible)
- Postfix is already installed and configured. It is recommended that you have already tested your configuration so we can exclude this as a main cause of problems
- GnuPG is already installed and configured
- Python 3.9.
- Dependencies listed in [requirements file](https://packaging.python.org/en/latest/tutorials/installing-packages/#requirements-files), `requirements.txt`.
- Postfix: installed, configured and tested.
- GnuPG: installed, configured and tested (e.g. via command-line).
### Installation
1. Install the Python-M2Crypto module:
1. Install the dependencies:
```
python -m pip install -r requirements.txt
```
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/lacre nobody
```
3. Create dedicated directories for storing PGP keys and S/MIME certificates and make the user `nobody` owner of these:
mkdir -p /var/gpgmailgate/.gnupg
mkdir -p /var/gpgmailgate/smime
chown -R nobody:nogroup /var/gpgmailgate/
4. Place the `gpg-mailgate.py` in `/usr/local/bin/`, make the user `nobody` owner of the file and make it executable:
```
install --owner=nobody --group=nogroup -d /var/lacre/ /var/lacre/.gnupg /var/lacre/smime
```
chown nobody:nogroup /usr/local/bin/gpg-mailgate.py
chmod u+x /usr/local/bin/gpg-mailgate.py
4. Place the `lacre.py` in `/usr/local/bin/`, make the user `nobody` owner of the file and make it executable:
5. Place the `GnuPG` directory in `/usr/local/lib/python2.7/dist-packages` (replace 2.7 with your Python 2 version)
```
install --owner=nobody --group=nogroup --mode=u=rx lacre.py /usr/local/bin/
```
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.
5. Place `GnuPG` and `lacre` directories in `/usr/local/lib/python3.x/` (replace 3.x with your Python version). Make sure they're available for Python `import`s by executing `python -m lacre.admin -h` command.
7. Add the following to the end of `/etc/postfix/master.cf`
```
cp -r lacre /usr/local/lib/python3.9/
cp -r GnuPG/ /usr/local/lib/python3.9/
```
gpg-mailgate unix - n n - - pipe
flags= user=nobody argv=/usr/local/bin/gpg-mailgate.py ${recipient}
6. Configure `/etc/lacre.conf` based on the provided `lacre.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.
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
7. Configure logging by copying `lacre-logging.conf.sample` to `/etc/lacre-logging.conf` and editing it according to your needs. The path to this file is included in `[logging]` section of `lacre.conf` file, so if you place it somewhere else, make sure to update the path too. See also: Python logging package's [Configuration file format](https://docs.python.org/3/library/logging.config.html#configuration-file-format).
If you use Postfix versions from 2.5 onwards, it is recommended to change `${recipient}` to `${original_recipient}` in line two of the lines above.
8. Add the following to the end of `/etc/postfix/master.cf`
8. Add the following line to `/etc/postfix/main.cf`
```
lacre unix - n n - - pipe
flags= user=nobody argv=/usr/local/bin/lacre.py ${recipient}
content_filter = gpg-mailgate
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
```
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:
If you use Postfix versions from 2.5 onwards, it is recommended to change `${recipient}` to `${original_recipient}` in second line of the snippet above.
keyserver-options auto-key-retrieve
9. Add the following line to `/etc/postfix/main.cf`
10. Restart Postfix
```
content_filter = lacre
```
10. Optional: GPG can automatically download new public keys for automatic signature verification. To enable automatic create the file `/var/gpgmailgate/.gnupg/gpg.conf`. Add the following line to the file:
```
keyserver-options auto-key-retrieve
```
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
- Replace `/some/public.key` with the location of a public key
- `/some/public.key` can be deleted after importation
- Confirm that it's working:
`sudo -u nobody /usr/bin/gpg --list-keys --homedir=/var/gpgmailgate/.gnupg`
```
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 the import).
- Confirm that it's working: `sudo -u nobody /usr/bin/gpg --list-keys --homedir=/var/gpgmailgate/.gnupg`
If you already have a keyring you would like to import into Lacre, you can use `lacre.admin` command-line utility. Read more in [Lacre administration](doc/admin.md).
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).
Lacre 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 Lacre they might use a piece of software that does not support PGP/MIME encrypted mails. You can tell Lacre 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!**
## Install Lacre-Webgate
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:
`sudo -u nobody /usr/bin/gpg --homedir=/var/gpgmailgate/.gnupg --import /some/private.key`
From now on PGP encrypted mails will be decrypted for the recipients for whom the keys are imported.
You also can remove a private key by using the following command. Replace `user@example.com` with the user's address for whom you want to remove the key:
`sudo -u nobody /usr/bin/gpg --homedir=/var/gpgmailgate/.gnupg --delete-secret-keys user@example.com`
## Install GPG-Mailgate-Web
### Requirements
- A webserver is installed and reachable
- The webserver is able to handle PHP scripts
- MySQL is installed
- Python 2.X is already installed
- 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
[Lacre / lacre-webgate](https://git.disroot.org/Lacre/lacre-webgate/)
repository.
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.
2. Create a new database for Lacre-Webgate.
3. Import the schema file `schema.sql` into the newly created database.
4. Edit the config file located at `/etc/gpg-mailgate.conf`. Set `enabled = yes` in `[database]` and fill in the necessary settings for the database connection.
4. Edit the config file located at `/etc/lacre.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](https://git.disroot.org/Lacre/lacre-webgate/src/branch/main/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
```
9. Copy `cron.py` to `/usr/local/bin/gpgmw-cron.py`. Make it executable and and transfer ownership to `nobody`:
8. Copy the templates found in the [cron_templates](cron_templates/) directory into the newly created directory and transfer ownership:
chown nobody:nogroup /usr/local/bin/gpgmw-cron.py
chmod u+x /usr/local/bin/gpgmw-cron.py
```
chown -R nobody:nogroup /var/gpgmailgate/cron_templates
```
10. Create `/etc/cron.d/gpgmw` with contents:
`*/3 * * * * nobody /usr/bin/python /usr/local/bin/gpgmw-cron.py > /dev/null`
9. Copy `cron.py` to `/usr/local/bin/cron.py`. Make it executable and and transfer ownership to `nobody`:
```
install -u nobody -g nobody -m u+x cron.py /usr/local/bin/lacre-cron.py
```
10. Create `/etc/cron.d/lacre-cron` with contents:
`*/3 * * * * nobody /usr/bin/python /usr/local/bin/lacre-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).
### Lacre-Webgate as keyserver
Lacre-Webgate can also be used as a keyserver. For more information have a look at Lacre-Webgate's [README](https://git.disroot.org/Lacre/lacre-webgate/src/branch/main/README.md).
## Install Register-handler
### Requirements
- Already set up and working GPG-Mailgate-Web. It should be reachable from the machine that will run register-handler
- Already set up and working Lacre-Webgate. It should be reachable from the machine that will run register-handler
- Postfix is already installed and configured. It is recommended that you have already tested your configuration so we can exclude this as a main cause of problems. Your Postfix configuration should also support aliases
### Installation
1. Install the Python-requests module:
apt-get install python-requests
2. Create directories for storing email templates:
mkdir -p /var/gpgmailgate/register_templates
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
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.
```
install -u nobody -g nogroup -m a+x register-handler.py /usr/local/bin/
```
5. Edit the config file located at `/etc/lacre.conf`. Set the parameter `webpanel_url` in `[mailregister]` to the url of your Lacre-Webgate 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`

View file

@ -1,5 +1,6 @@
.POSIX:
.PHONY: test unittest pre-clean clean
.PHONY: test e2etest unittest crontest daemontest pre-clean clean restore-keyhome
.SUFFIXES: .gv .png
#
# On systems where Python 3.x binary has a different name, just
@ -7,36 +8,94 @@
#
# make test PYTHON=/usr/local/bin/python3.8
#
# This marco is passed via environment to test/e2e_test.py, where it's
# This macro is passed via environment to test/e2e_test.py, where it's
# used to compute further commands.
#
PYTHON = python3
PYTHON = python
GRAPHVIZ = dot
#
# SQLite database used during tests
#
# This database stores key queue and identity repository for e2etest,
# daemontest, and crontest.
#
TEST_DB = test/lacre.db
#
# List of graph files
#
GRAPHS = doc/key-lifecycle.png
#
# Main goal to run all tests.
#
test: e2etest daemontest unittest crontest
#
# Build graphviz diagrams.
#
doc: ${GRAPHS}
#
# Run a set of end-to-end tests.
#
# Test scenarios are described and configured by the test/e2e.ini
# file. Basically this is just a script that feeds GPG Mailgate with
# known input and checks whether output meets expectations.
# Test scenarios are described and configured by the test/e2e.ini file.
# Basically this is just a script that feeds Lacre 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 LACRE_CONFIG env. variable to make sure
# it slurps the right config.
#
crontest: clean-db $(TEST_DB)
LACRE_CONFIG=test/lacre-daemon.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: restore-keyhome
$(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
LACRE_CONFIG=test/lacre.conf $(PYTHON) -m unittest discover -s test/modules
pre-clean:
rm -fv test/gpg-mailgate.conf
rm -fv test/lacre.conf
rm -f test/logs/*.log
restore-keyhome:
git restore test/keyhome
git restore test/keyhome.other
test/tmp:
mkdir test/tmp
test/logs:
mkdir test/logs
clean: pre-clean
clean: pre-clean clean-db
rm -rfv test/tmp test/logs
# Convert dot source to PNG image.
.gv.png:
$(GRAPHVIZ) -Tpng $< > ${<:S/.gv/.png/}

View file

@ -1,51 +1,68 @@
# GPG Lacre Project
# Lacre Project
Fork and continuation of original work of gpg-mailgate project: https://github.com/fkrone/gpg-mailgate
**Lacre** (wax seal in Portuguese) is an add-on for Postfix that automatically
encrypts incoming email before delivering it to recipients' inbox for
recipients that have provided their public keys.
Lacre is a fork and continuation of the original work on
[gpg-mailgate](https://github.com/TheGreatGooo/gpg-mailgate) project. 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 and accept occasional failures.
**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.
This project is the continuation of the work of "gpg-mailgate" on providing open source, GnuPG based email encryption for emails at rest. All incoming emails are automatically encrypted with user's public key before they are saved on the server. It is a server side encryption solution while the control of the encryption keys are fully at the hands of the end-user and private keys are never stored on the server.
# How it works
The scope of the project is to improve on the already existing code, provide easy to use key upload system (standalone as well as Roundcube plugin) and key discoverability. Beside providing a solution that is easy to use we will also provide easy to digest material about encryption, how it works and how to make use of it in situations other the just mailbox encryption. Understanding how encryption works is the key to self-determination and is therefore an important part of the project.
Lacre is a [content filter](https://www.postfix.org/FILTER_README.html). This
means, that when Postfix receives a message, it "forwards" that message to
Lacre and if Lacre delivers it to a given destination, the message arrives to
recipient's inbox.
GPG Lacre will be battle tested on the email infrastructure of https://disroot.org (an ethical non-profit service provider).
After receiving the message, Lacre does the following:
1. If message already is encrypted, it just delivers the message immediately.
2. Checks the list of recipients, finds their public keys if any were
provided.
3. Encrypts message if possible.
4. Delivers the message.
---
The work on this project in 2021 is funded by https://nlnet.nl/thema/NGIZeroPET.html for which we are very thankful.
The scope of the work for 2021 is:
- Rewrite code to python3
- Improve standalone key upload website
- Provide Roundcube plugin for key management
- Improve key server features
- Provide webiste with information and tutorials on how to use GPG in general and also **Lacre**
- (Optional) provide Autocrypt support
Work on this project in 2021 was funded by
[NGI Zero PET](https://nlnet.nl/thema/NGIZeroPET.html)
for which we are very thankful.
Made possible thanks to:<br>
![](https://nlnet.nl/logo/banner.png)
---
For installation instructions, please refer to the included **INSTALL** file.
# Installation
For installation instructions, please refer to the included [INSTALL](INSTALL.md) file.
---
# Features
# Planned 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)
- Decrypts PGP encrypted mails for present private keys (but no signature check and it does not always work with PGP/INLINE encrypted mails)
- Easy installation
- gpg-mailgate-web extension is a web interface allowing any user to upload PGP keys so that emails sent to them from your mail server will be encrypted (see gpg-mailgate-web directory for details)
- people can submit their public key like to any keyserver to gpg-mailgate with the gpg-mailgate-web extension
- people can send an S/MIME signed email to register@yourdomain.tld to register their public key
- people can send their public OpenPGP key as attachment or inline to register@yourdomain.tld to register it
- People can submit their public key like to any keyserver to gpg-mailgate with the gpg-mailgate-web extension
- People can send an S/MIME signed email to register@yourdomain.tld to register their public key
- People can send their public OpenPGP key as attachment or inline to register@yourdomain.tld to register it
See also: [lacre-webgate](https://git.disroot.org/Lacre/lacre-webgate/) -- a
web interface allowing any user to upload PGP keys so that emails sent to them
from your mail server will be encrypted
This is forked from the original project at http://code.google.com/p/gpg-mailgate/
# Authors
This is a combined work of many developers and contributors. We would like to pay honours to original gpg mailbox developers for making this project happen, and providing solid solution for encryption emails at rest:
This is a combined work of many developers and contributors. We would like to
pay honours to original gpg mailbox developers for making this project happen,
and providing solid solution for encryption emails at rest:
* mcmaster <mcmaster@aphrodite.hurricanelabs.rsoc>
* Igor Rzegocki <ajgon@irgon.com> - [GitHub](https://github.com/ajgon/gpg-mailgate)
@ -57,4 +74,4 @@ This is a combined work of many developers and contributors. We would like to pa
* Bruce Markey - [GitHub](https://github.com/TheEd1tor)
* Remko Tronçon - [GitHub](https://github.com/remko/phkp/)
* Kiritan Flux [GitHub](https://github.com/kflux)
* Fabian Krone [GitHub] (https://github.com/fkrone/gpg-mailgate)
* Fabian Krone [GitHub](https://github.com/fkrone/gpg-mailgate)

21
bin/lacreadm Executable file
View file

@ -0,0 +1,21 @@
#!/bin/sh
#
# lacre
#
# This file is part of the lacre source code.
#
# lacre 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.
#
# lacre 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 lacre source code. If not, see <http://www.gnu.org/licenses/>.
#
python -m lacre.admin $*

77
doc/admin.md Normal file
View file

@ -0,0 +1,77 @@
# Lacre administration
## Command-line tool
There's a little tool for administrators. As long as Lacre Python packages
are available via `PYTHONPATH`, you can use it like this:
```sh
python -m lacre.admin -h
```
Of course `-h` displays some help.
**Note:** Help output includes information about the configuration file being
in use, which may be useful at times.
**Note:** You can also use a tiny shell wrapper around this tool, see
`bin/lacreadm`.
## Initialising database schema
If you want to initialise Lacre's database (which is also used by the
frontend), run:
```sh
python -m lacre.admin database -i
```
## Inspecting key confirmation queue
To find out how many keys are waiting to be confirmed, run:
```sh
python -m lacre.admin queue
```
To see identities (emails) waiting for confirmation, use `--list` (or `-l`)
option:
```sh
python -m lacre.admin queue -l
```
To delete one of these emails, use `--delete` (or `-d`) option:
```sh
python -m lacre.admin queue -d malory@example.org
```
## Inspecting identities registered
To list all identities, run:
```sh
python -m lacre.admin identities -a
```
To preview a particular identity, run:
```sh
python -m lacre.admin identities -e alice@example.com
```
## Importing identities from existing GnuPG keyring
If you already have a GnuPG keyring with your users' public keys or for some
reason Lacre's identity database needs to be re-populated with identities,
there's a command to do that:
```sh
python -m lacre.admin import -d /path/to/gnupg/directory
```
If you want to just re-populate the database, Lacre can remove all identities
prior to importing keys -- just add `-r` flag.

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:
```
LACRE_CONFIG=/etc/lacre.conf PYTHONPATH=... python -m lacre.daemon
```
Two environment variables used here are:
* `LACRE_CONFIG` (not mandatory) -- path to Lacre configuration,
unless it's kept in default location (`/etc/lacre.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.

76
doc/key-lifecycle.gv Normal file
View file

@ -0,0 +1,76 @@
digraph key_lifecycle {
node [fontname="Helvetica,Arial,sans-serif" fontsize=12 shape=Mrecord]
edge [fontname="Helvetica,Arial,sans-serif" fontsize=10]
start [label="" shape=circle]
end [label="" shape=circle]
// An ASCII-armoured key is stored in lacre_keys table with:
//
// lacre_keys.confirm = <random string>
// lacre_keys.status = 0 (default value)
submitted [label="Submitted"]
// User has confirmed their email.
//
// lacre_keys.confirm = ''
confirmed [label="Email confirmed" color=green4]
// The key has been imported into GnuPG keyring and an identity has been
// created in lacre_identities table.
//
// lacre_keys.status = 1
imported [label="Imported" color=green4]
// Any old key for this email has been deleted.
deleted [label="Previous key\ndeleted"]
// When a key expires, we only fail to encrypt at the moment.
//
// See https://git.disroot.org/Disroot/gpg-lacre/issues/148
expired [label="Expired" color=red]
// A key may end up being non-usable in several different ways and this is
// a catch-all node to represent them.
//
// - User hasn't confirmed their email.
// - Provided key's email didn't match the one provided in submission form.
rejected [label="Key not used,\nremoved from database" color=brown]
// User submits ASCII-armoured OpenPGP key.
start -> submitted [label="user action:\nkey submission" color=green4]
// The user has clicked the confirmation link.
//
// - lacre_keys.confirm = ''
submitted -> confirmed [label="user action:\nemail confirmation" color=green4]
// Enough time has passed since submission that we decide to drop the key
// from the queue.
submitted -> rejected [label="confirmation timed out\nno user action" color=brown]
// A confirmed key is imported:
// - import into GnuPG keyring;
// - mark key as accepted (lacre_keys.status = 1);
// - update identity database;
// - send notification.
confirmed -> imported [label="import\n[non-empty key]" color=green4]
// Empty key is imported.
//
// Effectively this means key removal and disabling encryption.
confirmed -> deleted [label="import\n[empty key]" color=green4]
deleted -> end
// XXX: Import of revokation keys isn't implemented yet.
confirmed -> deleted [label="import\n[revokation key]\n(not implemented)" color=gray fontcolor=gray]
// Key validation fails, the key is not imported.
confirmed -> rejected [label="invalid key" color=brown]
// We don't explicitly make keys expired, but when they expire GnuPG
// refuses to encrypt payloads.
imported -> expired [label="expiry" color=red fontcolor=red]
rejected -> end
}

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,24 +27,33 @@ 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
When things go wrong, be sure to study `test/logs/e2e.log` and
`test/logs/gpg-mailgate.log` files -- they contain some useful information.
## Test identities
There are several identities in test/keyhome and in the test database:
* alice@disposlab: 1CD245308F0963D038E88357973CF4D9387C44D7
* bob@disposlab: 19CF4B47ECC9C47AFA84D4BD96F39FDA0E31BB67
* evan@disposlab: 530B1BB2D0CC7971648198BBA4774E507D3AF5BC

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

@ -1,136 +0,0 @@
[default]
# Whether gpg-mailgate should add a header after it has processed an email
# This may be useful for debugging purposes
add_header = yes
# Whether we should only encrypt emails if they are explicitly defined in
# the key mappings below ([enc_keymap] section)
# This means gpg-mailgate won't automatically detect PGP recipients for encrypting
enc_keymap_only = no
# Whether we should only decrypt emails if they are explicitly defined in
# the key mappings below ([dec_keymap] section)
# This means gpg-mailgate won't automatically detect PGP recipients for decrypting
dec_keymap_only = no
# If dec_keymap_only is set to yes and recipients have private keys present for decrypting
# but are not on in the keymap, this can cause that mails for them will be
# encrypted. Set this to no if you want this behaviour.
failsave_dec = yes
# Convert encrypted text/plain email to MIME-attached encrypt style.
# (Default is to use older inline-style PGP encoding.)
mime_conversion = yes
# RFC 2821 defines that the user part (User@domain.tld) of a mail address should be treated case sensitive.
# However, in the real world this is ignored very often. This option disables the RFC 2821
# compatibility so both the user part and the domain part are treated case insensitive.
# Disabling the compatibility is more convenient to users. So if you know that your
# recipients all ignore the RFC you could this to yes.
mail_case_insensitive = no
# This setting disables PGP/INLINE decryption completely. However,
# PGP/MIME encrypted mails will still be decrypted if possible. PGP/INLINE
# decryption has to be seen as experimental and could have some negative
# side effects. So if you want to take the risk set this to no.
no_inline_dec = yes
# Here you can define a regex for which the gateway should try to decrypt mails.
# It could be used to define that decryption should be used for a wider range of
# mail addresses e.g. a whole domain. No key is needed here. It is even active if
# dec_keymap is set to yes. If this feature should be disabled, don't leave it blank.
# Set it to None. For further regex information please have a look at
# https://docs.python.org/2/library/re.html
dec_regex = None
[gpg]
# the directory where gpg-mailgate public keys are stored
# (see INSTALL for details)
keyhome = /var/gpgmailgate/.gnupg
[smime]
# the directory for the S/MIME certificate files
cert_path = /var/gpgmailgate/smime
[mailregister]
# 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
webpanel_url = http://yourdomain.tld
[cron]
# settings for the gpgmw cron job
send_email = yes
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
[relay]
# the relay settings to use for Postfix
# gpg-mailgate will submit email to this relay after it is done processing
# unless you alter the default Postfix configuration, you won't have to modify this
host = 127.0.0.1
port = 10028
# This is the default port of postfix. It is used to send some
# mails through the GPG-Mailgate so they are encrypted
enc_port = 25
# Set this option to yes to use TLS for SMTP Servers which require TLS.
starttls = no
[database]
# uncomment the settings below if you want
# to read keys from a gpg-mailgate-web database
enabled = yes
name = gpgmw
host = localhost
username = gpgmw
password = password
[enc_keymap]
# You can find these by running the following command:
# gpg --list-keys --keyid-format long user@example.com
# Which will return output similar to:
# pub 1024D/AAAAAAAAAAAAAAAA 2007-10-22
# uid Joe User <user@example.com>
# sub 2048g/BBBBBBBBBBBBBBBB 2007-10-22
# You want the AAAAAAAAAAAAAAAA not BBBBBBBBBBBBBBBB.
#you@domain.tld = 12345678
[enc_domain_keymap]
# This seems to be similar to the [enc_keymap] section. However, you
# can define default keys for a domain here. Entries in the enc_keymap
# and individual keys stored on the system have a higher priority than
# the default keys specified here.
#
#
# You can find these by running the following command:
# gpg --list-keys --keyid-format long user@example.com
# Which will return output similar to:
# pub 1024D/AAAAAAAAAAAAAAAA 2007-10-22
# uid Joe User <user@example.com>
# sub 2048g/BBBBBBBBBBBBBBBB 2007-10-22
# You want the AAAAAAAAAAAAAAAA not BBBBBBBBBBBBBBBB.
#domain.tld = 12345678
[dec_keymap]
# You can find these by running the following command:
# gpg --list-secret-keys --keyid-format long user@example.com
# Which will return output similar to:
# sec 1024D/AAAAAAAAAAAAAAAA 2007-10-22
# uid Joe User <user@example.com>
# ssb 2048g/BBBBBBBBBBBBBBBB 2007-10-22
# You want the AAAAAAAAAAAAAAAA not BBBBBBBBBBBBBBBB.
#you@domain.tld = 12345678
[pgp_style]
# Here a PGP style (inline or PGP/MIME) could be defined for recipients.
# This overwrites the setting mime_conversion for the defined recipients.
# Valid entries are inline and mime
# If an entry is not valid, the setting mime_conversion is used as fallback.
#you@domian.tld = mime

View file

@ -1,466 +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
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
# imports for S/MIME
from M2Crypto import BIO, Rand, SMIME, X509
from email.mime.message import MIMEMessage
# 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"
# 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
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()
verbose = 'logging' in cfg and 'verbose' in cfg['logging'] and cfg['logging'].get('verbose') == 'yes'
# Read e-mail from stdin
raw = sys.stdin.read()
raw_message = email.message_from_string( raw )
from_addr = raw_message['From']
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)

57
lacre-logging.conf.sample Normal file
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

181
lacre.conf.sample Normal file
View file

@ -0,0 +1,181 @@
[default]
# Whether lacre should add a header after it has processed an email
# This may be useful for debugging purposes
add_header = yes
# Whether we should only encrypt emails if they are explicitly defined in
# the key mappings below ([enc_keymap] section)
# This means lacre won't automatically detect PGP recipients for encrypting
enc_keymap_only = no
# Convert encrypted text/plain email to MIME-attached encrypt style.
# (Default is to use older inline-style PGP encoding.)
mime_conversion = yes
# RFC 2821 defines that the user part (User@domain.tld) of a mail address should be treated case sensitive.
# However, in the real world this is ignored very often. This option disables the RFC 2821
# compatibility so both the user part and the domain part are treated case insensitive.
# Disabling the compatibility is more convenient to users. So if you know that your
# recipients all ignore the RFC you could this to yes.
mail_case_insensitive = no
[gpg]
# the directory where lacre 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/lacre/.gnupg
[smime]
# the directory for the S/MIME certificate files
cert_path = /var/lacre/smime
[mailregister]
# settings for the register-handler
register_email = register@yourdomain.tld
mail_templates = /var/lacre/register_templates
# URL to webpanel. Upon receiving an email with a key, register-handler
# uploads it to the web panel.
webpanel_url = http://yourdomain.tld
[cron]
# settings for the cron job
send_email = yes
notification_email = lacre@yourdomain.tld
mail_templates = /var/lacre/cron_templates
[logging]
# path to the logging configuration; see documentation for details:
# https://docs.python.org/3/library/logging.config.html#logging-config-fileformat
config = /etc/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
# Maximum size (in bytes) of message body, i.e. data provided after DATA
# message. Following value comes from aiosmtpd module's default for this
# setting.
max_data_bytes = 33554432
# Sometimes it may make sense to log additional information from mail headers.
# This should never be PII, but information like encoding, content types, etc.
log_headers = no
# Sometimes we might fail to load keys and need to choose between delivering
# in cleartext or not delivering. The default is to deliver cleartext, but
# administrators can make this decision on their own.
bounce_on_keys_missing = no
[relay]
# the relay settings to use for Postfix
# lacre will submit email to this relay after it is done processing
# unless you alter the default Postfix configuration, you won't have to modify this
host = 127.0.0.1
port = 10028
# This is the default port of postfix. It is used to send some
# mails through the Lacre so they are encrypted
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 = lacre
password = changeme
host = yourdomain.tld
port = 587
starttls = true
[database]
# edit the settings below if you want to read keys from a
# lacre-webgate database other than SQLite
enabled = yes
url = sqlite:///test.db
# Pooling mode: pessimistic or optimistic (required parameter).
#
# - Pessimistic disconnect-handling: pre_ping. Connection pool will try using
# connection before it executes a SQL query to find out if the connection is
# still alive. If not, it'll just establish a new connection.
#
# - Optimistic distonnect-handling: just avoid using connections after some
# time.
#
pooling_mode = optimistic
# For a MySQL database "lacre", user "lacre" and password "password",
# use the following URL:
#
#url = mysql://lacre:password@localhost/lacre
#
# For other RDBMS backends, see:
# https://docs.sqlalchemy.org/en/14/core/engines.html#database-urls
# Number of seconds after which an idle connection is recycled. This is
# useful with MySQL servers. This is only used with pooling_mode=optimistic.
# For more information, see:
# https://docs.sqlalchemy.org/en/14/core/engines.html#sqlalchemy.create_engine.params.pool_recycle
#max_connection_age = 3600
# Number of connections stored in the pool.
#pool_size = 5
# If the pool size is not enough for current traffic, some connections can be
# made and closed after use, to avoid pool growth and connection rejections.
#max_overflow = 10
# Number of hours we will wait for the user to confirm their email. Cron-job
# will delete items older than this number of hours. Default: 1h.
#max_queue_hours = 1
[enc_keymap]
# You can find these by running the following command:
# gpg --list-keys --keyid-format long user@example.com
# Which will return output similar to:
# pub 1024D/AAAAAAAAAAAAAAAA 2007-10-22
# uid Joe User <user@example.com>
# sub 2048g/BBBBBBBBBBBBBBBB 2007-10-22
# You want the AAAAAAAAAAAAAAAA not BBBBBBBBBBBBBBBB.
#you@domain.tld = 12345678
[enc_domain_keymap]
# This seems to be similar to the [enc_keymap] section. However, you
# can define default keys for a domain here. Entries in the enc_keymap
# and individual keys stored on the system have a higher priority than
# the default keys specified here.
#
#
# You can find these by running the following command:
# gpg --list-keys --keyid-format long user@example.com
# Which will return output similar to:
# pub 1024D/AAAAAAAAAAAAAAAA 2007-10-22
# uid Joe User <user@example.com>
# sub 2048g/BBBBBBBBBBBBBBBB 2007-10-22
# You want the AAAAAAAAAAAAAAAA not BBBBBBBBBBBBBBBB.
#domain.tld = 12345678
[dec_keymap]
# You can find these by running the following command:
# gpg --list-secret-keys --keyid-format long user@example.com
# Which will return output similar to:
# sec 1024D/AAAAAAAAAAAAAAAA 2007-10-22
# uid Joe User <user@example.com>
# ssb 2048g/BBBBBBBBBBBBBBBB 2007-10-22
# You want the AAAAAAAAAAAAAAAA not BBBBBBBBBBBBBBBB.
#you@domain.tld = 12345678
[pgp_style]
# Here a PGP style (inline or PGP/MIME) could be defined for recipients.
# This overwrites the setting mime_conversion for the defined recipients.
# Valid entries are inline and mime
# If an entry is not valid, the setting mime_conversion is used as fallback.
#you@domian.tld = mime

80
lacre.py Executable file
View file

@ -0,0 +1,80 @@
#!/usr/bin/python
#
# lacre
#
# This file is part of the lacre source code.
#
# lacre 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.
#
# lacre 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 lacre source code. If not, see <http://www.gnu.org/licenses/>.
#
import email
from email.policy import SMTPUTF8
import sys
import logging
import lacre
import lacre.config as conf
from lacre.stats import time_logger
conf.load_config()
lacre.init_logging(conf.get_item('logging', 'config'))
# This has to be executed *after* logging initialisation.
import lacre.core as core
from lacre.lazymessage import LazyMessage
LOG = logging.getLogger('lacre.py')
def main():
with time_logger('Message delivery', LOG):
missing_params = conf.validate_config()
config_file = conf.config_source()
if missing_params:
LOG.error(f"Aborting delivery! Following mandatory config parameters are missing in {config_file!r}: {missing_params}")
sys.exit(lacre.EX_CONFIG)
delivered = False
raw_message = None
# Read recipients from the command-line
to_addrs = sys.argv[1:]
# Read e-mail from stdin, parse it
raw = sys.stdin.read()
raw_message = email.message_from_string(raw, policy=SMTPUTF8)
from_addr = raw_message['From']
lmessage = LazyMessage(to_addrs, lambda: raw_message)
try:
# Let's start
core.deliver_message(raw_message, from_addr, to_addrs)
delivered = True
except:
LOG.exception('Could not handle message')
if not delivered:
# It seems we weren't able to deliver the message. In case it was
# some silly message-encoding issue that shouldn't bounce the
# message, we just try recoding the message body and delivering it.
try:
from_addr = raw_message['From']
core.failover_delivery(raw_message, to_addrs, from_addr)
except:
LOG.exception('Failover delivery failed too')
if __name__ == '__main__':
main()

51
lacre/__init__.py Normal file
View file

@ -0,0 +1,51 @@
"""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)
logging.captureWarnings(True)
logging.info('Configured from %s', config_filename)
else:
logging.config.dictConfig(FAIL_OVER_LOGGING_CONFIG)
logging.captureWarnings(True)
logging.warning('Lacre logging configuration missing, using syslog as default')

63
lacre/_keyringcommon.py Normal file
View file

@ -0,0 +1,63 @@
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 '<KeyCache %s>' % (details)
def __iter__(self):
return iter(self._keys.keys())
def emails(self):
return { email: fingerprint for (fingerprint, email) in self._keys.items() }
class KeyRing:
"""Contract to be implemented by a key-store (a.k.a. keyring)."""
def freeze_identities(self) -> KeyCache:
"""Return a static, async-safe copy of the identity map."""
raise NotImplementedError('KeyRing.load not implemented')
def register_or_update(self, email: str, key_id: str):
"""Add a new (email,key) pair to the keystore."""
raise NotImplementedError('KeyRing.register_or_update not implemented')
def post_init_hook(self):
"""Lets the keyring perform additional operations following its initialisation."""
pass
def shutdown(self):
"""Lets the keyring perform operations prior to shutting down."""
pass

168
lacre/admin.py Normal file
View file

@ -0,0 +1,168 @@
"""Lacre administrative tool.
This is a command-line tool expected to be run by a person who knows what they
are doing. Also, please read the docs first.
"""
import sys
import argparse
import logging
import GnuPG
import lacre
import lacre.config as conf
conf.load_config()
lacre.init_logging(conf.get_item('logging', 'config'))
import lacre.repositories as repo
import lacre.dbschema as db
if __name__ == '__main__':
LOG = logging.getLogger('lacre.admin')
else:
LOG = logging.getLogger(__name__)
def _no_database():
print('Database unavailable or not configured properly')
sys.exit(lacre.EX_CONFIG)
def sub_db(args):
"""Sub-command to manipulate database."""
LOG.debug('Database operations ahead')
if args.init:
eng = repo.init_engine(conf.get_item('database', 'url'))
LOG.warning('Initialising database schema with engine: %s', eng)
print('Creating database tables')
db.create_tables(eng)
def sub_queue(args):
"""Sub-command to inspect queue contents."""
LOG.debug('Inspecting queue...')
eng = repo.init_engine(conf.get_item('database', 'url'))
queue = repo.KeyConfirmationQueue(engine=eng)
if args.delete:
queue.delete_key_by_email(args.delete)
elif args.list:
for k in queue.fetch_keys():
print(f'- {k.id}: {k.email}')
elif args.to_delete:
for k in queue.fetch_keys_to_delete():
print(f'- {k.id}: {k.email}')
else:
cnt = queue.count_keys()
if cnt is None:
_no_database()
print(f'Keys in the queue: {cnt}')
def sub_identities(args):
"""Sub-command to inspect identity database."""
LOG.debug('Inspecting identities...')
eng = repo.init_engine(conf.get_item('database', 'url'))
identities = repo.IdentityRepository(engine=eng)
all_identities = identities.freeze_identities()
if all_identities is None:
_no_database()
if args.email:
all_rev = all_identities.emails()
print('-', args.email, all_rev[args.email])
else:
for id_ in all_identities:
print('-', all_identities[id_], id_)
def sub_import(args):
"""Sub-command to import all identities known to GnuPG into Lacre database."""
LOG.debug('Importing identities...')
source_dir = args.homedir or conf.get_item('gpg', 'keyhome')
public = GnuPG.public_keys(source_dir)
eng = repo.init_engine(conf.get_item('database', 'url'))
identities = repo.IdentityRepository(engine=eng)
if args.reload:
identities.delete_all()
total = 0
for (fingerprint, email) in public.items():
LOG.debug('Importing %s - %s', email, fingerprint)
identities.register_or_update(email, fingerprint)
total += 1
LOG.debug('Imported %d identities', total)
print(f'Imported {total} identities')
def main():
missing = conf.validate_config()
if missing:
LOG.error('Missing configuration parameters: %s', missing)
print('Insufficient configuration, aborting.')
sys.exit(lacre.EX_CONFIG)
general_conf = conf.config_source()
log_conf = conf.get_item('logging', 'config')
parser = argparse.ArgumentParser(
prog='lacre.admin',
description='Lacre Admin\'s best friend',
epilog=f'Config read from {general_conf}. For diagnostic info, see {log_conf}'
)
sub_commands = parser.add_subparsers(help='Sub-commands', required=True)
cmd_db = sub_commands.add_parser('database',
help='',
aliases=['db']
)
cmd_db.add_argument('-i', '--init', action='store_true',
help='Initialise database schema')
cmd_db.set_defaults(operation=sub_db)
cmd_import = sub_commands.add_parser('import',
help='Load identities from GnuPG directory to Lacre database'
)
cmd_import.add_argument('-d', '--homedir', default=False,
help='specify GnuPG directory (default: use configured dir.)')
cmd_import.add_argument('-r', '--reload', action='store_true',
help='delete all keys from database before importing')
cmd_import.set_defaults(operation=sub_import)
cmd_queue = sub_commands.add_parser('queue',
help='Inspect key queue',
aliases=['q']
)
cmd_queue.add_argument('-D', '--delete',
help='delete specified email from the queue')
cmd_queue.add_argument('-l', '--list', action='store_true',
help='list keys in the queue')
cmd_queue.add_argument('-d', '--to-delete', action='store_true',
help='list keys to be deleted')
cmd_queue.set_defaults(operation=sub_queue)
cmd_identities = sub_commands.add_parser('identities',
help='Inspect identity database',
aliases=['id']
)
cmd_identities.add_argument('-e', '--email', help='look up a single email')
cmd_identities.set_defaults(operation=sub_identities)
user_request = parser.parse_args()
user_request.operation(user_request)
if __name__ == '__main__':
main()

191
lacre/config.py Normal file
View file

@ -0,0 +1,191 @@
"""Lacre configuration.
Routines defined here are responsible for processing and validating
configuration.
"""
from enum import Enum, auto
from configparser import RawConfigParser
from collections import namedtuple
import os
# Environment variable name we read to retrieve configuration path. This is to
# enable non-root users to set up and run Lacre and to make the software
# testable.
CONFIG_PATH_ENV = "LACRE_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"),
('database', 'enabled'),
('database', 'url'),
('database', 'pooling_mode')]
CRON_REQUIRED = [('cron', 'mail_templates')]
# 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/lacre.conf').
"""
config_file = config_source()
parser = _read_config(config_file)
# 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 config_source() -> str:
"""Return path of configuration file.
Taken from LACRE_CONFIG environment variable, and if it's not
set, defaults to /etc/lacre.conf."""
return os.getenv(CONFIG_PATH_ENV, '/etc/lacre.conf')
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(*, additional=None):
"""Check if configuration is complete.
Returns a list of missing parameters, so an empty list means
configuration is complete.
If 'additional' parameter is specified, it should be a list of
tuples (section, param).
"""
missing = []
for (section, param) in MANDATORY_CONFIG_ITEMS:
if not config_item_set(section, param):
missing.append((section, param))
if additional:
for (section, param) in additional:
if not config_item_set(section, param):
missing.append((section, param))
return missing
#
# High level access to configuration.
#
Host = namedtuple('Host', ['name', 'port'])
def relay_params() -> Host:
"""Return a Host named tuple identifying the mail relay."""
return Host(name = cfg["relay"]["host"], port = 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")
def should_log_headers() -> bool:
"""Check if Lacre should log message headers."""
return flag_enabled('daemon', 'log_headers')
class FromStrMixin:
"""Additional operations for configuration enums."""
@classmethod
def from_str(cls, name, *, required=False):
if name is None:
return None
name = name.upper()
if name in cls.__members__:
return cls.__members__[name]
if required:
raise NameError('Unsupported or missing value')
else:
return None
@classmethod
def from_config(cls, section, key, *, required=False):
param = get_item(section, key)
return cls.from_str(param, required=required)
class PGPStyle(FromStrMixin, Enum):
"""PGP message structure: PGP/Inline or PGP/MIME."""
MIME = auto()
INLINE = auto()
class PoolingMode(FromStrMixin, Enum):
"""Database connection pool behaviour.
- Optimistic - recycles connections.
- Pessimistic - checks connection before usage.
"""
OPTIMISTIC = auto()
PESSIMISTIC = auto()

417
lacre/core.py Normal file
View file

@ -0,0 +1,417 @@
#
# lacre
#
# This file is part of the lacre source code.
#
# lacre 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.
#
# lacre 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 lacre 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
from email.message import EmailMessage, MIMEPart
import email.utils
from email.policy import SMTPUTF8
import GnuPG
import asyncio
from typing import Tuple
import logging
import lacre.text as text
import lacre.config as conf
import lacre.keyring as kcache
import lacre.recipients as recpt
import lacre.smime as smime
from lacre.transport import send_msg, register_sender, SendFrom
from lacre.mailop import KeepIntact, InlineOpenPGPEncrypt, MimeOpenPGPEncrypt, MailSerialisationException
from lacre.lazymessage import LazyMessage
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_recipients, cleartext_recipients = \
recpt.identify_gpg_recipients(recipients, kcache.freeze_and_load_keys())
LOG.info(f"Got addresses: gpg_to={gpg_recipients!r}, ungpg_to={cleartext_recipients!r}")
if gpg_recipients:
LOG.info("Encrypting email to: %s", gpg_recipients)
mime, inline = _sort_gpg_recipients(gpg_recipients)
if mime:
# Encrypt mail with PGP/MIME
_gpg_encrypt_and_deliver(raw_message,
mime.keys(), mime.emails(),
_encrypt_all_payloads_mime)
if inline:
# Encrypt mail with PGP/INLINE
_gpg_encrypt_and_deliver(raw_message,
inline.keys(), inline.emails(),
_encrypt_all_payloads_inline)
LOG.info('Not processed emails: %s', cleartext_recipients)
return cleartext_recipients
def _sort_gpg_recipients(gpg_to) -> Tuple[recpt.RecipientList, recpt.RecipientList]:
recipients_mime = list()
keys_mime = list()
recipients_inline = list()
keys_inline = list()
default_to_pgp_mime = conf.flag_enabled('default', 'mime_conversion')
for rcpt in gpg_to:
# Checking pre defined styles in settings first
style = conf.PGPStyle.from_config('pgp_style', rcpt.email())
if style is conf.PGPStyle.MIME:
recipients_mime.append(rcpt.email())
keys_mime.extend(rcpt.key().split(','))
elif style is conf.PGPStyle.INLINE:
recipients_inline.append(rcpt.email())
keys_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:
recipients_mime.append(rcpt.email())
keys_mime.extend(rcpt.key().split(','))
else:
recipients_inline.append(rcpt.email())
keys_inline.extend(rcpt.key().split(','))
mime = recpt.RecipientList(recipients_mime, keys_mime)
inline = recpt.RecipientList(recipients_inline, keys_inline)
LOG.debug('Loaded recipients: MIME %s; Inline %s', repr(mime), repr(inline))
return mime, inline
def _gpg_encrypt_copy(message: EmailMessage, keys, recipients, encrypt_f, lmessage: LazyMessage = None) -> EmailMessage:
if lmessage:
message = lmessage.get_message()
msg_copy = copy.deepcopy(message)
_customise_headers(msg_copy)
encrypted_payloads = encrypt_f(msg_copy, keys)
msg_copy.set_payload(encrypted_payloads)
return msg_copy
def _gpg_encrypt_to_bytes(message: EmailMessage, keys, recipients, encrypt_f, lmessage) -> bytes:
msg_copy = _gpg_encrypt_copy(message, keys, recipients, encrypt_f, lmessage)
try:
return msg_copy.as_bytes(policy=SMTPUTF8)
except IndexError as ie:
raise MailSerialisationException(ie)
def _gpg_encrypt_to_str(message: EmailMessage, keys, recipients, encrypt_f) -> str:
msg_copy = _gpg_encrypt_copy(message, keys, recipients, encrypt_f)
return msg_copy.as_string(policy=SMTPUTF8)
def _gpg_encrypt_and_deliver(message: EmailMessage, keys, recipients, encrypt_f):
out = _gpg_encrypt_to_str(message, keys, recipients, encrypt_f)
send_msg(out, recipients)
def _customise_headers(message: EmailMessage):
if conf.flag_enabled('default', 'add_header'):
message['X-Lacre'] = 'Encrypted by Lacre'
def _encrypt_all_payloads_inline(message: EmailMessage, gpg_to_cmdline, lmessage: LazyMessage = None):
if lmessage:
message = lmessage.get_message()
# 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: EmailMessage, gpg_to_cmdline, lmessage: LazyMessage = None):
# Convert a plain text email into PGP/MIME attachment style. Modeled after enigmail.
pgp_ver_part = MIMEPart()
pgp_ver_part.set_content('Version: 1' + text.EOL_S)
pgp_ver_part.set_type("application/pgp-encrypted")
pgp_ver_part.set_param('PGP/MIME version identification', "", 'Content-Description')
encrypted_part = MIMEPart()
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 lmessage:
message = lmessage.get_message()
message.preamble = "This is an OpenPGP/MIME encrypted message (RFC 2440 and 3156)"
boundary = _make_boundary()
if isinstance(message.get_payload(), str):
LOG.debug('Rewrapping a flat, text-only message')
wrapped_payload = _rewrap_payload(message)
encrypted_part.set_payload(wrapped_payload.as_string())
_set_type_and_boundary(message, boundary)
check_nested = True
else:
processed_payloads = _generate_message_from_payloads(message)
encrypted_part.set_payload(processed_payloads.as_string())
_set_type_and_boundary(message, boundary)
check_nested = False
return [pgp_ver_part, _encrypt_payload(encrypted_part, gpg_to_cmdline, check_nested)]
def _rewrap_payload(message: EmailMessage, lmessage: LazyMessage = None) -> MIMEPart:
# In PGP/MIME (RFC 3156), the payload has to be a valid MIME entity. In
# other words, we need to wrap text/* message's payload in a new MIME
# entity.
wrapper = MIMEPart(policy=SMTPUTF8)
if lmessage:
message = lmessage.get_message()
content = message.get_content()
wrapper.set_content(content)
wrapper.set_type(message.get_content_type())
# Copy all Content-Type parameters.
for (pname, pvalue) in message.get_params(failobj=list()):
# Skip MIME type that's also returned by get_params().
if not '/' in pname:
wrapper.set_param(pname, pvalue)
return wrapper
def _make_boundary():
junk_msg = MIMEMultipart()
# XXX See EmailTest.test_boundary_generated_after_as_string_call.
_ = junk_msg.as_string()
return junk_msg.get_boundary()
def _set_type_and_boundary(message: EmailMessage, boundary):
message.set_type('multipart/encrypted')
message.set_param('protocol', 'application/pgp-encrypted')
message.set_param('boundary', boundary)
def _encrypt_payload(payload: EmailMessage, recipients, check_nested=True, lmessage: LazyMessage = None, **kwargs):
if lmessage:
payload = lmessage.get_message()
raw_payload = payload.get_payload(decode=True)
LOG.debug('About to encrypt raw payload: %s', raw_payload)
LOG.debug('Original message: %s', payload)
if check_nested and text.is_payload_pgp_inline(raw_payload):
LOG.debug("Message is already pgp encrypted. No nested encryption needed.")
return payload
gpg = _make_encryptor(raw_payload, recipients)
gpg.update(raw_payload)
encrypted_data, exit_code = gpg.encrypt()
payload.set_payload(encrypted_data)
isAttachment = payload.get_param('attachment', None, 'Content-Disposition') is not None
if isAttachment:
_append_gpg_extension(payload)
return payload
def _make_encryptor(raw_data, recipients):
# No check is needed for conf.get_item('gpg', 'keyhome') as this is already
# done in method gpg_encrypt
keyhome = conf.get_item('gpg', 'keyhome')
if isinstance(raw_data, str):
return GnuPG.GPGEncryptor(keyhome, recipients, 'utf-8')
else:
return GnuPG.GPGEncryptor(keyhome, recipients)
def _append_gpg_extension(attachment):
filename = attachment.get_filename()
if not filename:
return
pgpFilename = filename + ".pgp"
# Attachment name can come from one of two places: Content-Disposition or
# Content-Type header, hence the two cases below.
if not (attachment.get('Content-Disposition') is None):
attachment.set_param('filename', pgpFilename, 'Content-Disposition')
if not (attachment.get('Content-Type') is None) and not (attachment.get_param('name') is None):
attachment.set_param('name', pgpFilename)
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 _recode(m: EmailMessage):
payload = m.get_payload()
m.set_content(payload)
def failover_delivery(message: EmailMessage, recipients, from_address):
"""Try delivering message just one last time."""
LOG.debug('Failover delivery')
send = SendFrom(from_address)
if message.get_content_maintype() == 'text':
LOG.debug('Flat text message, adjusting coding')
_recode(message)
b = message.as_bytes(policy=SMTPUTF8)
send(b, recipients)
elif message.get_content_maintype() == 'multipart':
LOG.debug('Multipart message, adjusting coding of text entities')
for part in message.iter_parts():
if part.get_content_maintype() == 'text':
_recode(part)
b = message.as_bytes(policy=SMTPUTF8)
send(b, recipients)
else:
LOG.warning('No failover strategy, giving up')
def _is_encrypted(raw_message: EmailMessage, lmessage: LazyMessage = None):
if lmessage:
raw_message = lmessage.get_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: EmailMessage, key_cache: kcache.KeyCache, lmessage: LazyMessage = None):
"""Generate a sequence of delivery strategies."""
if lmessage:
message = lmessage.get_message()
if _is_encrypted(message):
LOG.debug('Message is already encrypted: %s', message)
return [KeepIntact(recipients)]
gpg_recipients, cleartext_recipients = recpt.identify_gpg_recipients(recipients, key_cache)
mime, inline = _sort_gpg_recipients(gpg_recipients)
keyhome = conf.get_item('gpg', 'keyhome')
plan = []
if mime:
plan.append(MimeOpenPGPEncrypt(mime.emails(), mime.keys(), keyhome))
if inline:
plan.append(InlineOpenPGPEncrypt(inline.emails(), inline.keys(), keyhome))
if cleartext_recipients:
plan.append(KeepIntact(cleartext_recipients))
return plan
def deliver_message(raw_message: EmailMessage, from_address, to_addrs):
"""Send RAW_MESSAGE to all TO_ADDRS using the best encryption method available."""
# Ugly workaround to keep the code working without too many changes.
register_sender(from_address)
sanitize = text.choose_sanitizer(conf.get_item('default', 'mail_case_insensitive'))
recipients_left = [sanitize(recipient) for recipient in to_addrs]
send = SendFrom(from_address)
# 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(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, from_address)
if not recipients_left:
return
# Send out mail to recipients which are left
LOG.debug("Sending the rest as text/plain")
send(raw_message.as_bytes(policy=SMTPUTF8), recipients_left)

159
lacre/daemon.py Normal file
View file

@ -0,0 +1,159 @@
"""Lacre Daemon, the Advanced Mail Filter message dispatcher."""
import logging
import lacre
from lacre.text import DOUBLE_EOL_BYTES
from lacre.stats import time_logger
import lacre.config as conf
import sys
from aiosmtpd.controller import Controller
from aiosmtpd.smtp import Envelope
import asyncio
import email
from email.policy import SMTPUTF8
# Load configuration and init logging, in this order. Only then can we load
# the last Lacre module, i.e. lacre.core.
conf.load_config()
lacre.init_logging(conf.get_item("logging", "config"))
LOG = logging.getLogger('lacre.daemon')
from GnuPG import EncryptionException
import lacre.core as gate
import lacre.keyring as kcache
import lacre.transport as xport
from lacre.mailop import KeepIntact, MailSerialisationException
from lacre.lazymessage import LazyMessage
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."""
with time_logger('Message delivery', LOG):
try:
keys = self._keyring.freeze_identities()
lmessage = LazyMessage(envelope.rcpt_tos, lambda: envelope.original_content)
message = email.message_from_bytes(envelope.original_content, policy=SMTPUTF8)
if message.defects:
LOG.warning("Issues found: %s", repr(message.defects))
send = xport.SendFrom(envelope.mail_from)
for operation in gate.delivery_plan(envelope.rcpt_tos, message, keys, lmessage):
LOG.debug(f"Sending mail via {operation!r}")
try:
new_message = operation.perform(message, lmessage)
send(new_message, operation.recipients())
except (EncryptionException, MailSerialisationException) as e:
# If the message can't be encrypted or serialised to a
# stream of bytes, deliver original payload in
# cleartext.
LOG.exception('Unable to encrypt message, delivering in cleartext')
self._send_unencrypted(operation, envelope, send)
except xport.TransientFailure:
LOG.info('Bouncing message')
return xport.RESULT_TRANS_FAIL
except xport.PermanentFailure:
LOG.exception('Permanent failure')
return xport.RESULT_PERM_FAIL
except:
if conf.should_log_headers():
LOG.exception('Unexpected exception caught, bouncing message. Erroneous message headers: %s', self._beginning(envelope))
else:
LOG.exception('Unexpected exception caught, bouncing message')
return xport.RESULT_PERM_FAIL
return xport.RESULT_OK
def _send_unencrypted(self, operation, envelope: Envelope, send: xport.SendFrom):
# Do not parse and re-generate the message, just send it as it is.
try:
send(envelope.original_content, operation.recipients())
except:
LOG.exception('Unencrypted delivery failed, returning PERMANENT FAILURE to sender')
raise xport.PermanentFailure()
def _beginning(self, e: Envelope) -> bytes:
double_eol_pos = e.original_content.find(DOUBLE_EOL_BYTES)
if double_eol_pos < 0:
limit = len(e.original_content)
else:
limit = double_eol_pos
end = min(limit, 2560)
return e.original_content[0:end]
def _init_controller(keys: kcache.KeyRing, max_body_bytes=None, 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,
data_size_limit=max_body_bytes,
# Do not decode data into str as we only operate on raw
# data available via Envelope.original_content.
decode_data=False)
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)
async def _main():
_validate_config()
keyring_path = conf.get_item('gpg', 'keyhome')
max_data_bytes = int(conf.get_item('daemon', 'max_data_bytes', 2**25))
loop = asyncio.get_event_loop()
try:
keyring = kcache.init_keyring()
controller = _init_controller(keyring, max_data_bytes)
keyring.post_init_hook()
LOG.info('Starting the daemon with GnuPG=%s, socket=%s, database=%s',
keyring_path,
conf.daemon_params(),
conf.get_item('database', 'url'))
controller.start()
await _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...')
keyring.shutdown()
controller.stop()
LOG.info("Done")
if __name__ == '__main__':
asyncio.run(_main())

65
lacre/dbschema.py Normal file
View file

@ -0,0 +1,65 @@
"""Database schema for Lacre.
This definition includes:
- 'lacre_keys' -- temporary key storage, used by the frontend to submit keys and
by webgate-cron script to import submitted keys.
- 'lacre_identities' -- identity catalogue, used by encryption logic to match
emails with corresponding keys.
- 'lacre_locks' -- used only by the frontend.
"""
import sqlalchemy
# Values for lacre_keys.status column:
# - ST_DEFAULT: initial state;
# - ST_IMPORTED: key has been successfully processed by cron job;
# - ST_TO_BE_DELETED: key can be deleted. We only have checks for this value
# but never assign it, so this is a candidate for removal.
ST_DEFAULT, ST_IMPORTED, ST_TO_BE_DELETED = range(3)
# lacre_keys.confirmed is set to an empty string when a key is confirmed by the user.
CO_CONFIRMED = ''
_meta = sqlalchemy.MetaData()
LACRE_KEYS = sqlalchemy.Table('lacre_keys', _meta,
sqlalchemy.Column('id', sqlalchemy.Integer, primary_key=True, nullable=False, autoincrement='auto'),
sqlalchemy.Column('email', sqlalchemy.String(256), index=True),
# ASCII-armored key
sqlalchemy.Column('publickey', sqlalchemy.Text),
# Empty string means this key has been confirmed.
sqlalchemy.Column('confirm', sqlalchemy.String(32)),
# Status: see ST_* constants at the top of the file.
sqlalchemy.Column('status', sqlalchemy.Integer, nullable=False, default=0),
sqlalchemy.Column('time', sqlalchemy.DateTime))
LACRE_LOCKS = sqlalchemy.Table('lacre_locks', _meta,
sqlalchemy.Column('id', sqlalchemy.Integer, primary_key=True, nullable=False, autoincrement='auto'),
sqlalchemy.Column('ip', sqlalchemy.String(16)),
sqlalchemy.Column('time', sqlalchemy.Integer),
sqlalchemy.Column('action', sqlalchemy.String(16)),
sqlalchemy.Column('num', sqlalchemy.Integer),
)
LACRE_IDENTITIES = sqlalchemy.Table('lacre_identities', _meta,
sqlalchemy.Column('email', sqlalchemy.String(256), index=True, nullable=False),
# Key fingerprint
sqlalchemy.Column('fingerprint', sqlalchemy.String(64), index=True, nullable=False))
def init_identities_table() -> sqlalchemy.Table:
return LACRE_IDENTITIES
def init_locks_table() -> sqlalchemy.Table:
return LACRE_LOCKS
def init_keys_table() -> sqlalchemy.Table:
return LACRE_KEYS
def create_tables(engine):
_meta.create_all(engine)
def table_metadata():
return _meta

36
lacre/keymgmt.py Normal file
View file

@ -0,0 +1,36 @@
"""Key management utilities."""
from datetime import datetime, timedelta
import logging
from lacre.config import get_item
# By default, we let keys stay in confirmation queue for 1 hour.
_DEFAULT_TTL = 1
LOG = logging.getLogger(__name__)
def calculate_expiry_date(now: datetime) -> datetime:
"""Calculate date-time of key queue item expiry.
Given current timestamp and configuration item
[database]max_queue_hours, return a date-time object that should be
older than any key in our confirmation queue. If a key is older
than this threshold, we should remove it."""
max_hours = _get_ttl()
return now - timedelta(hours=max_hours)
def _get_ttl():
max_hours = get_item('database', 'max_queue_hours', _DEFAULT_TTL)
try:
ttl = int(max_hours)
LOG.debug('Key configmration queue max item age: %d hours', ttl)
return ttl
except ValueError:
# Not a valid integer, so we return the default.
LOG.exception('Invalid max_queue_hours format: %s, using default (%d)', max_hours, _DEFAULT_TTL)
return _DEFAULT_TTL

28
lacre/keyring.py Normal file
View file

@ -0,0 +1,28 @@
"""Data structures and utilities to make keyring access easier.
IMPORTANT: This module has to be loaded _after_ initialisation of the logging
module.
"""
import lacre.config as conf
from lacre._keyringcommon import KeyRing, KeyCache
from lacre.repositories import IdentityRepository, init_engine
import logging
LOG = logging.getLogger(__name__)
def init_keyring() -> KeyRing:
"""Initialise appropriate type of keyring."""
url = conf.get_item('database', 'url')
db_engine = init_engine(url)
return IdentityRepository(engine=db_engine)
def freeze_and_load_keys() -> KeyCache:
"""Load and return keys.
Doesn't refresh the keys when they change on disk.
"""
keyring = init_keyring()
return keyring.freeze_identities()

33
lacre/lazymessage.py Normal file
View file

@ -0,0 +1,33 @@
from aiosmtpd.smtp import Envelope
from email import message_from_bytes
from email.message import EmailMessage
from email.parser import BytesHeaderParser
from email.policy import SMTPUTF8
class LazyMessage:
def __init__(self, recipients, content_provider):
self._content_provider = content_provider
self._recipients = recipients
self._headers = None
self._message = None
def get_original_content(self) -> bytes:
return self._content_provider()
def get_recipients(self):
return self._recipients
def get_headers(self) -> EmailMessage:
if self._message:
return self._message
if not self._headers:
self._headers = BytesHeaderParser(policy=SMTPUTF8).parsebytes(self.get_original_content())
return self._headers
def get_message(self) -> EmailMessage:
if not self._message:
self._message = message_from_bytes(self.get_original_content(), policy=SMTPUTF8)
return self._message

143
lacre/mailop.py Normal file
View file

@ -0,0 +1,143 @@
"""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 lacre.lazymessage import LazyMessage
from email.message import Message, EmailMessage
from email.parser import BytesHeaderParser
from email.policy import SMTP, SMTPUTF8
LOG = logging.getLogger(__name__)
class MailSerialisationException(BaseException):
"""We can't turn an EmailMessage into sequence of bytes."""
pass
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, lmessage: LazyMessage) -> bytes:
"""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, lmessage: LazyMessage) -> bytes:
"""Encrypt with PGP Inline."""
LOG.debug('Sending PGP/Inline...')
return core._gpg_encrypt_to_bytes(msg,
self._keys, self._recipients,
core._encrypt_all_payloads_inline,
lmessage)
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, lmessage: LazyMessage) -> bytes:
"""Encrypt with PGP MIME."""
LOG.debug('Sending PGP/MIME...')
return core._gpg_encrypt_to_bytes(msg,
self._keys, self._recipients,
core._encrypt_all_payloads_mime,
lmessage)
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, lmessage: LazyMessage) -> bytes:
"""Encrypt with a certificate."""
LOG.warning(f"Delivering clear-text to {self._recipients}")
return message.as_bytes(policy=SMTP)
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, lmessage: LazyMessage) -> bytes:
"""Return MESSAGE unmodified."""
try:
return lmessage.get_original_content()
except (IndexError, UnicodeEncodeError, ValueError) as e:
raise MailSerialisationException(e)
def __repr__(self):
"""Return representation with just method and email."""
return f"<KeepIntact {self._recipients}>"

54
lacre/notify.py Normal file
View file

@ -0,0 +1,54 @@
"""Lacre notification sender"""
import logging
import lacre
import lacre.config as conf
import smtplib
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
import markdown
LOG = logging.getLogger(__name__)
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 notify(mailsubject, messagefile, recipients = None):
"""Send notification email."""
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'):
host = conf.relay_params()
smtp = smtplib.SMTP(host.name, host.port)
_authenticate_maybe(smtp)
LOG.info('Delivering notification: %s', recipients)
smtp.sendmail(conf.get_item('cron', 'notification_email'), recipients, msg.as_string())
else:
LOG.warning("Could not send mail due to wrong configuration")

202
lacre/recipients.py Normal file
View file

@ -0,0 +1,202 @@
#
# lacre
#
# This file is part of the lacre source code.
#
# lacre 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.
#
# lacre 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 lacre source code. If not, see <http://www.gnu.org/licenses/>.
#
"""Recipient processing package.
Defines:
- GpgRecipient, wrapper for user's email and identity.
- RecipientList, a wrapper for lists of GpgRecipient objects.
"""
import logging
import lacre.config as conf
import lacre.keyring as kcache
import lacre.text as text
LOG = logging.getLogger(__name__)
class Recipient:
"""Wraps recipient's email."""
def __init__(self, email):
"""Initialise the recipient."""
self._email = email
def email(self) -> str:
"""Return email address of this recipient."""
return self._email
def __str__(self):
"""Return string representation of this recipient: the email address."""
return self._email
class GpgRecipient(Recipient):
"""A tuple-like object that contains GPG recipient data."""
def __init__(self, left, right):
"""Initialise a tuple-like object that contains GPG recipient data."""
super().__init__(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.email()
elif index == 1:
return self._right
else:
raise IndexError()
def __repr__(self):
"""Return textual representation of this GPG Recipient."""
return f"GpgRecipient({self.email()!r}, {self._right!r})"
__str__ = __repr__
def key(self):
"""Return this recipient's key ID."""
return self._right
class RecipientList:
"""Encalsulates two lists of recipients.
First list contains addresses, the second - GPG identities.
"""
def __init__(self, recipients=[], keys=[]):
"""Initialise lists of recipients and identities."""
self._recipients = [GpgRecipient(email, key) for (email, key) in zip(recipients, keys)]
def emails(self):
"""Return list of recipients."""
return [r.email() for r in self._recipients]
def keys(self):
"""Return list of GPG identities."""
return [r.key() for r in self._recipients]
def __iadd__(self, recipient: GpgRecipient):
"""Append a recipient."""
LOG.debug('Adding %s to %s', recipient, self._recipients)
self._recipients.append(recipient)
LOG.debug('Added; got: %s', self._recipients)
return self
def __len__(self):
"""Provide len().
With this method, it is possible to write code like:
rl = RecipientList()
if rl:
# do something
"""
return len(self._recipients)
def __repr__(self):
"""Returns textual object representation."""
return '<RecipientList %d %s>' % (len(self._recipients), ','.join(self.emails()))
def identify_gpg_recipients(recipients, keys: kcache.KeyCache):
"""Split recipient list into GPG and non-GPG ones."""
# 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_recipients = list()
# This will be the list of recipients that haven't provided us with their
# public keys.
cleartext_recipients = list()
# In "strict mode", only keys included in configuration are used to encrypt
# email.
strict_mode = conf.strict_mode()
for recipient in recipients:
gpg_recipient = _find_key(recipient, keys, strict_mode)
if gpg_recipient is not None:
gpg_recipients.append(gpg_recipient)
else:
cleartext_recipients.append(recipient)
LOG.debug('Collected recipients; GPG: %s; cleartext: %s', gpg_recipients, cleartext_recipients)
return gpg_recipients, cleartext_recipients
def _find_key(recipient, keys: kcache.KeyCache, strict_mode):
own_key = _try_configured_key(recipient, keys)
if own_key is not None:
return GpgRecipient(own_key[0], own_key[1])
direct_key = _try_direct_key_lookup(recipient, keys, strict_mode)
if direct_key is not None:
return GpgRecipient(direct_key[0], direct_key[1])
domain_key = _try_configured_domain_key(recipient, keys)
if domain_key is not None:
return GpgRecipient(domain_key[0], domain_key[1])
return None
def _try_configured_key(recipient, keys: kcache.KeyCache):
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: kcache.KeyCache, 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: kcache.KeyCache):
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

225
lacre/repositories.py Normal file
View file

@ -0,0 +1,225 @@
"""Lacre identity and key repositories."""
from datetime import datetime, timedelta
from sqlalchemy import create_engine, select, delete, and_, func
from sqlalchemy.exc import OperationalError
import logging
from lacre.config import flag_enabled, config_item_set, get_item, PoolingMode
from lacre._keyringcommon import KeyRing, KeyCache
import lacre.dbschema as db
LOG = logging.getLogger(__name__)
_HOUR_IN_SECONDS = 3600
# Internal state
_engine = None
def init_engine(url, db_debug=False):
global _engine
if not _engine:
config = _conn_config(db_debug)
_engine = create_engine(url, **config)
return _engine
def _conn_config(db_debug):
config = dict()
mode = PoolingMode.from_config('database', 'pooling_mode', required=True)
if mode is PoolingMode.OPTIMISTIC:
# Optimistic distonnect-handling: recycle connections.
config['pool_recycle'] = int(get_item('database', 'max_connection_age', _HOUR_IN_SECONDS))
elif mode is PoolingMode.PESSIMISTIC:
# Pessimistic disconnect-handling: pre_ping.
config['pool_pre_ping'] = True
# Additional pool settings
if config_item_set('database', 'pool_size'):
config['pool_size'] = int(get_item('database', 'pool_size'))
if config_item_set('database', 'max_overflow'):
config['max_overflow'] = int(get_item('database', 'max_overflow'))
if db_debug:
config['echo'] = 'debug'
config['echo_pool'] = 'debug'
LOG.debug('Database engine configuration: %s', config)
return config
class IdentityRepository(KeyRing):
def __init__(self, /, connection=None, *, engine):
self._identities = db.LACRE_IDENTITIES
self._engine = engine
def register_or_update(self, email, fprint):
assert email, "email is mandatory"
assert fprint, "fprint is mandatory"
if self._exists(email):
self._update(email, fprint)
else:
self._insert(email, fprint)
def _exists(self, email: str) -> bool:
selq = select(self._identities.c.email).where(self._identities.c.email == email)
with self._engine.connect() as conn:
return [e for e in conn.execute(selq)]
def _insert(self, email, fprint):
insq = self._identities.insert().values(email=email, fingerprint=fprint)
LOG.debug('Registering identity: %s -- %s', insq, insq.compile().params)
with self._engine.connect() as conn:
conn.execute(insq)
conn.commit()
def _update(self, email, fprint):
upq = self._identities.update() \
.values(fingerprint=fprint) \
.where(self._identities.c.email == email)
LOG.debug('Updating identity: %s -- %s', upq, upq.compile().params)
with self._engine.connect() as conn:
conn.execute(upq)
conn.commit()
def delete(self, email):
delq = delete(self._identities).where(self._identities.c.email == email)
LOG.debug('Deleting assigned keys: %s -- %s', delq, delq.compile().params)
with self._engine.connect() as conn:
conn.execute(delq)
conn.commit()
def delete_all(self):
LOG.warn('Deleting all identities from the database')
delq = delete(self._identities)
with self._engine.connect() as conn:
conn.execute(delq)
conn.commit()
def freeze_identities(self) -> KeyCache:
"""Return a static, async-safe copy of the identity map.
Depending on the value of [daemon]bounce_on_keys_missing value,
if we get a database exception, this method will either return
empty collection or let the exception be propagated.
"""
try:
return self._load_identities()
except OperationalError:
if flag_enabled('daemon', 'bounce_on_keys_missing'):
raise
else:
LOG.exception('Failed to load keys, returning empty collection')
return KeyCache({})
def _load_identities(self) -> KeyCache:
all_identities = select(self._identities.c.fingerprint, self._identities.c.email)
with self._engine.connect() as conn:
result = conn.execute(all_identities)
LOG.debug('Retrieving all keys: %s', all_identities)
return KeyCache({key_id: email for key_id, email in result})
class KeyConfirmationQueue:
"""Encapsulates access to lacre_keys table."""
# Default number of items retrieved from the database.
keys_read_max = 100
def __init__(self, /, engine):
self._keys = db.LACRE_KEYS
self._engine = engine
def fetch_keys(self, /, max_keys=None):
"""Runs a query to retrieve at most `keys_read_max` keys and returns db result."""
max_keys = max_keys or self.keys_read_max
LOG.debug('Row limit: %d', max_keys)
selq = select(self._keys.c.publickey, self._keys.c.id, self._keys.c.email) \
.where(and_(self._keys.c.status == db.ST_DEFAULT, self._keys.c.confirm == db.CO_CONFIRMED)) \
.limit(max_keys)
LOG.debug('Retrieving keys to be processed: %s -- %s', selq, selq.compile().params)
with self._engine.connect() as conn:
return [e for e in conn.execute(selq)]
def count_keys(self):
selq = select(func.count(self._keys.c.id)) \
.where(and_(self._keys.c.status == db.ST_DEFAULT, self._keys.c.confirm == db.CO_CONFIRMED))
LOG.debug('Counting all keys: %s -- %s', selq, selq.compile().params)
try:
with self._engine.connect() as conn:
res = conn.execute(selq)
# This is a 1-element tuple.
return res.one_or_none()[0]
except OperationalError:
LOG.exception('Cannot count keys')
return None
def fetch_keys_to_delete(self):
seldel = select(self._keys.c.email, self._keys.c.id) \
.where(self._keys.c.status == db.ST_TO_BE_DELETED) \
.limit(self.keys_read_max)
with self._engine.connect() as conn:
return [e for e in conn.execute(seldel)]
def delete_expired_queue_items(self, older_than: datetime):
"""Remove keys that have been in queue before `older_than`."""
delq = delete(self._keys) \
.where(
and_(
self._keys.c.time < older_than,
# We only want to delete keys that haven't been confirmed.
self._keys.c.confirm != db.CO_CONFIRMED
)
)
LOG.debug('Deleting queue items older than %s: %s', repr(older_than), delq)
with self._engine.connect() as conn:
conn.execute(delq)
conn.commit()
def delete_keys(self, row_id, /, email=None):
"""Remove key from the database."""
if email is not None:
LOG.debug('Deleting key: id=%s, email=%s', row_id, email)
delq = delete(self._keys).where(and_(self._keys.c.email == email, self._keys.c.id == row_id))
else:
LOG.debug('Deleting key: id=%s', row_id)
delq = delete(self._keys).where(self._keys.c.id == row_id)
with self._engine.connect() as conn:
LOG.debug('Deleting public keys associated with confirmed email: %s', delq)
conn.execute(delq)
conn.commit()
def delete_key_by_email(self, email):
"""Remove keys linked to the given email from the database."""
delq = delete(self._keys).where(self._keys.c.email == email)
LOG.debug('Deleting email for: %s', email)
with self._engine.connect() as conn:
conn.execute(delq)
conn.commit()
def mark_accepted(self, row_id):
modq = self._keys.update().where(self._keys.c.id == row_id).values(status=db.ST_IMPORTED)
LOG.debug("Key imported, updating key: %s", modq)
with self._engine.connect() as conn:
conn.execute(modq)
conn.commit()

126
lacre/smime.py Normal file
View file

@ -0,0 +1,126 @@
#
# lacre
#
# This file is part of the lacre source code.
#
# lacre 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.
#
# lacre 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 lacre source code. If not, see <http://www.gnu.org/licenses/>.
#
"""S/MIME handling module."""
import os
from M2Crypto import BIO, SMIME, X509
import logging
import lacre.text as text
import lacre.config as conf
import lacre.transport as xport
LOG = logging.getLogger(__name__)
#
# WARNING: This file is not covered with E2E tests.
#
def encrypt(raw_message, recipients, from_addr):
"""Encrypt with S/MIME."""
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()
cleartext_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:
cleartext_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_S)
out.write('To: ' + raw_message['To'] + text.EOL_S)
if raw_message['Cc']:
out.write('Cc: ' + raw_message['Cc'] + text.EOL_S)
if raw_message['Bcc']:
out.write('Bcc: ' + raw_message['Bcc'] + text.EOL_S)
if raw_message['Subject']:
out.write('Subject: ' + raw_message['Subject'] + text.EOL_S)
if conf.config_item_equals('default', 'add_header', 'yes'):
out.write('X-Lacre: Encrypted by Lacre' + text.EOL_S)
s.write(out, p7)
LOG.debug(f"Sending message from {from_addr} to {smime_to}")
send_msg = xport.SendFrom(from_addr)
send_msg(out.read(), smime_to)
if cleartext_to:
LOG.debug(f"Unable to find valid S/MIME certificates for {cleartext_to}")
return cleartext_to
def _path_comparator(insensitive: bool):
if insensitive:
return lambda filename, recipient: filename.casefold() == recipient
else:
return lambda filename, recipient: filename == recipient
def _get_cert_for_email(to_addr, cert_path):
insensitive = conf.config_item_equals('default', 'mail_case_insensitive', 'yes')
paths_equal = _path_comparator(insensitive)
LOG.info('Retrieving certificate for %s from %s, insensitive=%s',
to_addr, cert_path, insensitive)
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 paths_equal(file_path, 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)

29
lacre/stats.py Normal file
View file

@ -0,0 +1,29 @@
"""Insights into Lacre's inner workings."""
import time
import logging
class ExecutionTimeLogger:
"""Context-manager that measures how much time some operation took and logs it."""
def __init__(self, message: str, logger: logging.Logger):
self._message = message
self._log = logger
self._start = None
def __enter__(self):
self._start = time.process_time()
self._log.info('Start: %s', self._message)
def __exit__(self, exc_type=None, exc_value=None, traceback=None):
end = time.process_time()
ellapsed = (end - self._start) * 1000
if exc_type:
exception = (exc_type, exc_value, traceback)
self._log.error('%s took %d ms, raised exception', self._message, ellapsed, exc_info=exception)
else:
self._log.info('%s took %d ms', self._message, ellapsed)
def time_logger(msg: str, logger: logging.Logger):
return ExecutionTimeLogger(msg, logger)

104
lacre/text.py Normal file
View file

@ -0,0 +1,104 @@
"""Basic payload-processing routines."""
import sys
import re
import logging
from email.message import EmailMessage
# The standard way to encode line-ending in email:
EOL = b"\r\n"
EOL_S = EOL.decode()
DOUBLE_EOL_BYTES = EOL*2
PGP_BEGIN = b"-----BEGIN PGP MESSAGE-----"
PGP_END = b"-----END PGP MESSAGE-----"
PGP_BEGIN_S = PGP_BEGIN.decode()
PGP_END_S = PGP_END.decode()
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) -> bool:
"""Find out if the payload (bytes) contains PGP/inline markers."""
if isinstance(payload, bytes):
return payload.startswith(PGP_BEGIN) and _ends_with(payload, PGP_END)
elif isinstance(payload, str):
return payload.startswith(PGP_BEGIN_S) and _ends_with(payload, PGP_END_S)
else:
raise TypeError('Expected str or bytes')
def _ends_with(payload, marker) -> bool:
# Length of the span at the end of the payload we want to inspect should
# include CRLF, CR or LF, so make it slightly larger than the marker
# itself.
span = len(marker) + 2
return marker in payload[-span:]
def is_message_pgp_inline(message: EmailMessage) -> 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))

110
lacre/transport.py Normal file
View file

@ -0,0 +1,110 @@
"""SMTP transport module."""
import smtplib
import logging
from typing import AnyStr, List
import lacre.config as conf
from lacre.mailop import MailSerialisationException
# Mail status constants.
#
# These are the only values that our mail handler is allowed to return.
RESULT_OK = '250 OK'
RESULT_TRANS_FAIL = '451 Aborted: error in processing'
RESULT_PERM_FAIL = '554 Transaction failed'
# See RFC 5321, section 4.2.1 "Reply Code Severities and Theory" for more
# information on SMTP reply codes.
RESP_TRANSIENT_NEG = 4
RESP_PERMANENT_NEG = 5
LOG = logging.getLogger(__name__)
# This is a left-over from old architecture.
from_addr = None
def register_sender(fromaddr):
"""Set module state: message sender address."""
global from_addr
LOG.warning('Setting global recipient: %s', fromaddr)
from_addr = fromaddr
def send_msg(message: AnyStr, recipients: List[str]):
"""Send MESSAGE to RECIPIENTS to the mail relay."""
global from_addr
LOG.debug('Delivery from %s to %s', from_addr, recipients)
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.name, relay.port)
if conf.flag_enabled('relay', 'starttls'):
smtp.starttls()
smtp.sendmail(from_addr, recipients, message)
else:
LOG.info("No recipient found")
class TransientFailure(BaseException):
"""Signals a transient delivery failure (4xx SMTP reply).
Message should be bounced and re-sent later.
"""
pass
class PermanentFailure(BaseException):
"""Signals a permanent delivery failure (5xx SMTP reply)."""
pass
class SendFrom:
"""A class wrapping the transport process."""
def __init__(self, from_addr):
"""Initialise the transport."""
self._from_addr = from_addr
def __call__(self, message: AnyStr, recipients: List[str]):
"""Send the given message to all recipients from the list.
- Message is the email object serialised to str or bytes.
- Empty recipients are filtered out before communication.
"""
recipients = [_f for _f in recipients if _f]
if not recipients:
LOG.warning("No recipient found")
return
LOG.info("Sending email to: %s", recipients)
relay = conf.relay_params()
smtp = smtplib.SMTP(relay.name, relay.port)
if conf.flag_enabled('relay', 'starttls'):
smtp.starttls()
try:
smtp.sendmail(self._from_addr, recipients, message)
except smtplib.SMTPResponseException as re:
resp_class = self._get_class(re.smtp_code)
if resp_class == RESP_TRANSIENT_NEG:
LOG.warning('Transient delivery failure: %s', re)
raise TransientFailure()
elif resp_class == RESP_PERMANENT_NEG:
LOG.error('Permanent delivery failure: %s', re)
raise PermanentFailure()
except smtplib.SMTPException as err:
LOG.error('Failed to deliver message: %s', err)
raise PermanentFailure()
except UnicodeEncodeError as uee:
LOG.error('Failed to deliver for non-SMTP reason', uee)
raise MailSerialisationException(uee)
def _get_class(self, resp_code):
return int(resp_code / 100)

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)

5
requirements.txt Normal file
View file

@ -0,0 +1,5 @@
aiosmtpd==1.4.2
SQLAlchemy==2.0.29
Markdown==3.4.1
M2Crypto==0.38.0
requests==2.27.1

141
test/daemon_test.py Normal file
View file

@ -0,0 +1,141 @@
#
# lacre
#
# This file is part of the lacre source code.
#
# lacre 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.
#
# lacre source code is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of