[Mailman-Users] AES encryption and Resent-Message-ID
Lindsay Haisley
fmouse-mailman at fmp.com
Tue Jun 19 07:38:04 CEST 2012
Here are a few tidbits pursuant to putting an encrypted copy of a list
post recipient in a "Resent-Message-ID" header, as Stephen Turnbull
suggested. There are four parts:
1. A patch to SMTPDirect.py
2. A secret key entry in mm_cfg.py
3. A utility, ~mailman/bin/aes_genkey, to manage key generation
4. A handler module to do encryption, decryption and key generation -
AEScrypt.py
Here's the patch to SMTPDirect.py (mm 2.1.15):
--- SMTPDirect.py.orig 2012-06-17 17:16:25.000000000 -0500
+++ SMTPDirect.py 2012-06-18 23:29:58.000000000 -0500
@@ -43,6 +43,7 @@
from email.Utils import formataddr
from email.Header import Header
from email.Charset import Charset
+import AEScrypt
DOT = '.'
@@ -307,6 +308,11 @@
'host' : DOT.join(rdomain),
}
envsender = '%s@%s' % ((mm_cfg.VERP_FORMAT % d), DOT.join(bdomain))
+ try:
+ skey = AEScrypt.encrypt(recip)
+ msgcopy["Resent-Message-ID"] = skey + "@" + DOT.join(bdomain)
+ except:
+ pass
if mlist.personalize == 2:
# When fully personalizing, we want the To address to point to the
# recipient, not to the mailing list
mm_cfg.py requires an AES key in AES_SECRET_KEY. Without this, the
Resent-Message-ID header isn't inserted in outgoing posts and everything
works as it does without this stuff.
The AES key can be generated with aes_genkey which lives in ~mailman/bin
and works like other scripts in this directory. Running it with -a
appends AES_SECRET_KEY to mm_cfg.py with an appropriate comment.
~mailman/bin/aes_genkey
-----------------------
#! /usr/bin/python
"""Generate an AES secret key on stdout for inclusion in mm_cfg.py as
AES_SECRET_KEY.
Usage: %(PROGRAM)s [options]
Where:
-a append AES secret key to mm_cfg.py
-h / --help
Print help and exit.
"""
import sys
import getopt
import os
import paths
from Mailman import mm_cfg
from Mailman.Handlers import AEScrypt
from Mailman.i18n import _
def usage(code, msg=''):
if code:
fd = sys.stderr
else:
fd = sys.stdout
print >> fd, _(__doc__)
if msg:
print >> fd, msg
sys.exit(code)
def main():
try:
opts, args = getopt.getopt(sys.argv[1:], 'ha', ['help'])
except getopt.error, msg:
usage(1, msg)
for opt, arg in opts:
if opt in ('-h', '--help'):
usage(0)
if opt in ('-a',):
try:
f = mm_cfg.AES_SECRET_KEY
print "AES secret key already in mm_cfg.py"
return(0)
except:
mm = open(os.getenv("HOME") + "/Mailman/mm_cfg.py", "a")
ktxt = """
# Experimental address encryption key. To renew this key,
# delete AES_SECRET_KEY and run 'aes_keygen -a' and restart
# Mailman.
AES_SECRET_KEY = '%s'
""" % (AEScrypt.genkey(),)
mm.write(ktxt)
mm.close()
print "AES secret key added to mm_cfg.py"
return(0)
print AEScrypt.genkey()
if __name__ == '__main__':
sys.exit(main())
The final part is the encryption/decryption module, AEScrypt.py For
this to work the python-crypto ("Crypto") package must be installed.
~mailman/Mailman/Handlers/AEScrypt.py
-------------------------------------
from Crypto.Cipher import AES
from Crypto.Util import randpool
from Mailman import mm_cfg
import base64
block_size = 16
key_size = 32
mode = AES.MODE_CBC
try:
key_string = mm_cfg.AES_SECRET_KEY
except:
pass
def genkey():
key_bytes = randpool.RandomPool(512).get_bytes(key_size)
key_string = base64.urlsafe_b64encode(str(key_bytes))
return key_string
def encrypt(plain_text):
pad = block_size - len(plain_text) % block_size
data = plain_text + pad * chr(pad)
iv_bytes = randpool.RandomPool(512).get_bytes(block_size)
encrypted_bytes = iv_bytes + AES.new(base64.urlsafe_b64decode(key_string), mode, iv_bytes).encrypt(data)
return base64.urlsafe_b64encode(str(encrypted_bytes))
def decrypt(cypher_text):
key_bytes = base64.urlsafe_b64decode(key_string)
encrypted_bytes = base64.urlsafe_b64decode(cypher_text)
iv_bytes = encrypted_bytes[:block_size]
encrypted_bytes = encrypted_bytes[block_size:]
plain_text = AES.new(key_bytes, mode, iv_bytes).decrypt(encrypted_bytes)
pad = ord(plain_text[-1])
return plain_text[:-pad]
The Resent-Message-ID header has the domain name of the server host
appended to it and this will need to be stripped before decrypting the
address string. Something like 'crypt, dn = full_header.split("@")'
will pull the encrypted address from the header. A withlist script can
easily extract the plain text content of the encrypted string.
I hope this helps someone.
--
Lindsay Haisley | "Real programmers use butterflies"
FMP Computer Services |
512-259-1190 | - xkcd
http://www.fmp.com |
More information about the Mailman-Users
mailing list