AES encryption and Resent-Message-ID

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 |
participants (1)
-
Lindsay Haisley