#! /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()