[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