#! /usr/bin/env python3
# -*- coding: utf-8 -*-
"""*********************************************************************
* Edward is free software: you can redistribute it and/or modify *
* it under the terms of the GNU Affero Public License as published by *
* the Free Software Foundation, either version 3 of the License, or *
* (at your option) any later version. *
* *
* Edward 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 Affero Public License for more details. *
* *
* You should have received a copy of the GNU Affero Public License *
* along with Edward. If not, see . *
* *
* Copyright (C) 2014-2015 Andrew Engelbrecht (AGPLv3+) *
* Copyright (C) 2014 Josh Drake (AGPLv3+) *
* Copyright (C) 2014 Lisa Marie Maginnis (AGPLv3+) *
* Copyright (C) 2009-2015 Tails developers ( GPLv3+) *
* Copyright (C) 2009 W. Trevor King ( GPLv2+) *
* *
* Special thanks to Josh Drake for writing the original edward bot! :) *
* *
************************************************************************
Code sourced from these projects:
* http://agpl.fsf.org/emailselfdefense.fsf.org/edward/CURRENT/edward.tar.gz
* https://git-tails.immerda.ch/whisperback/tree/whisperBack/encryption.py?h=feature/python3
* http://www.physics.drexel.edu/~wking/code/python/send_pgp_mime
"""
import sys
import gpgme
import re
import io
import os
import email.parser
import email.message
import email.encoders
from email.mime.multipart import MIMEMultipart
from email.mime.application import MIMEApplication
from email.mime.nonmultipart import MIMENonMultipart
import edward_config
def main ():
handle_args()
gpgme_ctx = get_gpg_context(edward_config.gnupghome,
edward_config.sign_with_key)
email_text = sys.stdin.read()
email_from, email_subject = email_from_subject(email_text)
plaintext, keys = email_decode_flatten(email_text, gpgme_ctx, False)
encrypt_to_key = choose_reply_encryption_key(keys)
reply_message = generate_reply(plaintext, email_from, \
email_subject, encrypt_to_key,
gpgme_ctx)
print(reply_message)
def get_gpg_context (gnupghome, sign_with_key_fp):
os.environ['GNUPGHOME'] = gnupghome
gpgme_ctx = gpgme.Context()
gpgme_ctx.armor = True
try:
sign_with_key = gpgme_ctx.get_key(sign_with_key_fp)
except:
error("unable to load signing key. is the gnupghome "
+ "and signing key properly set in the edward_config.py?")
exit(1)
gpgme_ctx.signers = [sign_with_key]
return gpgme_ctx
def email_decode_flatten (email_text, gpgme_ctx, from_decryption):
body = ""
keys = []
email_struct = email.parser.Parser().parsestr(email_text)
for subpart in email_struct.walk():
payload, description, filename, content_type \
= get_email_subpart_info(subpart)
if payload == "":
continue
if content_type == "multipart":
continue
if content_type == "application/pgp-encrypted":
if ((description == "PGP/MIME version identification")
and (payload.strip() != "Version: 1")):
debug("Warning: unknown " + description
+ ": " + payload.strip())
# ignore the version number
continue
if (filename == "encrypted.asc") or (content_type == "pgp/mime"):
plaintext, more_keys = decrypt_text(payload, gpgme_ctx)
body += plaintext
keys += more_keys
elif content_type == "application/pgp-keys":
keys += add_gpg_keys(payload, gpgme_ctx)
elif content_type == "text/plain":
if from_decryption == True:
body += payload + "\n"
keys += add_gpg_keys(payload, gpgme_ctx)
else:
plaintext, more_keys = decrypt_text(payload, gpgme_ctx)
body += plaintext
keys += more_keys
keys += add_gpg_keys(payload, gpgme_ctx)
return body, keys
def email_from_subject (email_text):
email_struct = email.parser.Parser().parsestr(email_text)
email_from = email_struct['From']
email_subject = email_struct['Subject']
return email_from, email_subject
def get_email_subpart_info (part):
charset = part.get_content_charset()
payload_bytes = part.get_payload(decode=True)
filename = part.get_filename()
content_type = part.get_content_type()
description_list = part.get_params(header='content-description')
if charset == None:
charset = 'utf-8'
if payload_bytes != None:
payload = payload_bytes.decode(charset)
else:
payload = ""
if description_list != None:
description = description_list[0][0]
else:
description = ""
return payload, description, filename, content_type
def add_gpg_keys (text, gpgme_ctx):
key_blocks = scan_and_grab(text,
'-----BEGIN PGP PUBLIC KEY BLOCK-----',
'-----END PGP PUBLIC KEY BLOCK-----')
keys = []
for key_block in key_blocks:
fp = io.BytesIO(key_block.encode('ascii'))
result = gpgme_ctx.import_(fp)
imports = result.imports
if imports != []:
fingerprint = imports[0][0]
keys += [gpgme_ctx.get_key(fingerprint)]
debug("added gpg key: " + fingerprint)
return keys
def decrypt_text (gpg_text, gpgme_ctx):
body = ""
keys = []
msg_blocks = scan_and_grab(gpg_text,
'-----BEGIN PGP MESSAGE-----',
'-----END PGP MESSAGE-----')
plaintexts_and_sigs = decrypt_blocks(msg_blocks, gpgme_ctx)
for pair in plaintexts_and_sigs:
plaintext = pair[0]
sigs = pair[1]
for sig in sigs:
keys += [gpgme_ctx.get_key(sig.fpr)]
# recursive for nested layers of mime and/or gpg
plaintext, more_keys = email_decode_flatten(plaintext, gpgme_ctx, True)
body += plaintext
keys += more_keys
return body, keys
def scan_and_grab (text, start_text, end_text):
matches = re.search('(' + start_text + '.*' + end_text + ')',
text, flags=re.DOTALL)
if matches != None:
match_tuple = matches.groups()
else:
match_tuple = ()
return match_tuple
def decrypt_blocks (msg_blocks, gpgme_ctx):
return [decrypt_block(block, gpgme_ctx) for block in msg_blocks]
def decrypt_block (msg_block, gpgme_ctx):
block_b = io.BytesIO(msg_block.encode('ascii'))
plain_b = io.BytesIO()
try:
sigs = gpgme_ctx.decrypt_verify(block_b, plain_b)
except:
return ("",[])
plaintext = plain_b.getvalue().decode('utf-8')
return (plaintext, sigs)
def choose_reply_encryption_key (keys):
reply_key = None
for key in keys:
if (key.can_encrypt == True):
reply_key = key
break
return reply_key
def generate_reply (plaintext, email_from, email_subject, encrypt_to_key,
gpgme_ctx):
reply = "To: " + email_from + "\n"
reply += "Subject: " + email_subject + "\n"
if (encrypt_to_key != None):
plaintext_reply = "thanks for the message!\n\n\n"
plaintext_reply += email_quote_text(plaintext)
# quoted printable encoding lets most ascii characters look normal
# before the decrypted mime message is decoded.
char_set = email.charset.Charset("utf-8")
char_set.body_encoding = email.charset.QP
# MIMEText doesn't allow setting the text encoding
# so we use MIMENonMultipart.
plaintext_mime = MIMENonMultipart('text', 'plain')
plaintext_mime.set_payload(plaintext_reply, charset=char_set)
encrypted_text = encrypt_sign_message(plaintext_mime.as_string(),
encrypt_to_key,
gpgme_ctx)
control_mime = MIMEApplication("Version: 1",
_subtype='pgp-encrypted',
_encoder=email.encoders.encode_7or8bit)
control_mime['Content-Description'] = 'PGP/MIME version identification'
control_mime.set_charset('us-ascii')
encoded_mime = MIMEApplication(encrypted_text,
_subtype='octet-stream; name="encrypted.asc"',
_encoder=email.encoders.encode_7or8bit)
encoded_mime['Content-Description'] = 'OpenPGP encrypted message'
encoded_mime['Content-Disposition'] = 'inline; filename="encrypted.asc"'
encoded_mime.set_charset('us-ascii')
message_mime = MIMEMultipart(_subtype="encrypted", protocol="application/pgp-encrypted")
message_mime.attach(control_mime)
message_mime.attach(encoded_mime)
message_mime['Content-Disposition'] = 'inline'
reply += message_mime.as_string()
else:
reply += "\n"
reply += "Sorry, i couldn't find your key.\n"
reply += "I'll need that to encrypt a message to you."
return reply
def email_quote_text (text):
quoted_message = re.sub(r'^', r'> ', text, flags=re.MULTILINE)
return quoted_message
def encrypt_sign_message (plaintext, encrypt_to_key, gpgme_ctx):
plaintext_bytes = io.BytesIO(plaintext.encode('ascii'))
encrypted_bytes = io.BytesIO()
gpgme_ctx.encrypt_sign([encrypt_to_key], gpgme.ENCRYPT_ALWAYS_TRUST,
plaintext_bytes, encrypted_bytes)
encrypted_txt = encrypted_bytes.getvalue().decode('ascii')
return encrypted_txt
def error (error_msg):
sys.stderr.write(progname + ": " + error_msg + "\n")
def debug (debug_msg):
if edward_config.debug == True:
error(debug_msg)
def handle_args ():
if __name__ == "__main__":
global progname
progname = sys.argv[0]
if len(sys.argv) > 1:
print(progname + ": error, this program doesn't " \
"need any arguments.", file=sys.stderr)
exit(1)
main()