Let a Server Send Encrypted and Signed E-Mails

Here’s a small Python program that generates code for an encrypted and signed e-mail in PGP/MIME format.

#!/usr/bin/env python
#
# BlackBox multipart e-mail generator
# Frank Abelbeck, Mai 2016
#
# Usage: genmail.py Messagetext Subject [File1 [File2 [... FileN]]]
#
# prints the message text to stdout
import email.encoders
import email.mime.multipart
import email.mime.audio
import email.mime.image
import email.mime.base
import email.mime.text
import mimetypes
import sys
import os.path
import subprocess

TO = "To.Address@To.Domain.TLD"
FROM = "From.Address@From.Domain.TLD"
PATH = "/path/to/a/directory/containing/a/keyring/"

#
# construct core message, either as text or as multipart (in case of attachments)
#
try:
        messagetext,subjecttext = sys.argv[1:3]
except ValueError:
        sys.exit(0) # wrong usage, nevermind for this should not be used interactively

if len(sys.argv) == 3:
        # create simple text message
        msgshell = email.mime.text.MIMEText(messagetext)
        msgshell["Subject"] = subjecttext
        msgshell["To"]      = TO
        msgshell["From"]    = FROM
else:
        # with attachments: create multipart message
        msgshell = email.mime.multipart.MIMEMultipart()
        msgshell["Subject"] = subjecttext
        msgshell["To"]      = TO
        msgshell["From"]    = FROM

        msg = email.mime.text.MIMEText(messagetext)
        msgshell.attach(msg)

        for filename in sys.argv[3:]:
                if not os.path.isfile(filename): continue
                ctype,encoding = mimetypes.guess_type(filename)
                if ctype is None or encoding is not None:
                        ctype = "application/octet-stream"
                mtype,stype = ctype.split("/",1)

                if mtype == 'text':
                        with open(filename) as f:
                                msg = email.mime.text.MIMEText(f.read(), _subtype=stype)
                elif mtype == 'image':
                        with open(filename, 'rb') as f:
                                msg = email.mime.image.MIMEImage(f.read(), _subtype=stype)
                elif mtype == 'audio':
                        with open(filename, 'rb') as f:
                                msg = email.mime.audio.MIMEAudio(f.read(), _subtype=stype)
                else:
                        with open(filename, 'rb') as f:
                                msg = email.mime.base.MIMEBase(mtype, stype)
                                msg.set_payload(f.read())
                        email.encoders.encode_base64(msg)
                msg.add_header("Content-Disposition","attachment",filename=filename)
                msgshell.attach(msg)

#
# create PGP/MIME message using subprocess and the gpg program:
#   msgshell.as_string() ---bytes---> /usr/bin/gpg ---string---> ciphertext
# gnupg parameters:
#   --armor               create ASCII output
#   --homedir PATH        use keyring stored in given directory
#   --batch               do it non-interactively
#   --recipient TO        use TO user's public key for encryption
#   --local-user FROM     use FROM user's secret key to sign the message
#   --encrypt --sign      encrypt and sign the message
try:
        ciphertext = subprocess.check_output(
                [
                        "/usr/bin/gpg",
                        "--armor",
                        "--homedir",PATH,
                        "--batch",
                        "--recipient",TO,
                        "--local-user",FROM,
                        "--encrypt", "--sign"
                ],input=bytes(msgshell.as_string(),"utf-8"),stderr=subprocess.DEVNULL
        ).decode()
except subprocess.CalledProcessError:
        # something went wrong with gpg: exit, gracefully
        sys.exit(0)

#
# construct encrypted multipart message
#
gpgmsg = email.mime.multipart.MIMEMultipart(
        _subtype="encrypted",
        _data="This is an OpenPGP/MIME encrypted message (RFC 4880 and 3156)",
        protocol="application/pgp-encrypted")
gpgmsg["To"] = TO
gpgmsg["From"] = FROM
gpgmsg["Subject"] = subjecttext

gpgcon = email.mime.base.MIMEBase(_maintype="application",_subtype="pgp-encrypted")
gpgcon["Content-Description"] = "OpenPGP/MIME version identification"
gpgcon.set_payload("Version: 1\n")

gpgenc = email.mime.base.MIMEBase(_maintype="application",_subtype="octet-stream")
gpgenc["Content-Description"] = "OpenPGP encrypted and signed message"
gpgenc.set_payload(ciphertext)

gpgmsg.attach(gpgcon)
gpgmsg.attach(gpgenc)

#
# write email content to stdout
#
print(gpgmsg)

My storage server uses this script to notify me in a secure way, even when sending files. By using PGP/MIME, all attachments are wrapped in a multipart message which in turn is encrypted and signed as a whole.

In order to sign its messages, the server needs access to its own private key, in a non-interactive way. Thus the private key is not protected by any passphrase. This is a calculated risk; if the machine gets compromised, I’ll have to untrust the key.

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s