| 1 | #! /usr/bin/env python3 |
| 2 | # -*- coding: utf-8 -*- |
| 3 | """********************************************************************* |
| 4 | * Edward is free software: you can redistribute it and/or modify * |
| 5 | * it under the terms of the GNU Affero Public License as published by * |
| 6 | * the Free Software Foundation, either version 3 of the License, or * |
| 7 | * (at your option) any later version. * |
| 8 | * * |
| 9 | * Edward is distributed in the hope that it will be useful, * |
| 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of * |
| 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * |
| 12 | * GNU Affero Public License for more details. * |
| 13 | * * |
| 14 | * You should have received a copy of the GNU Affero Public License * |
| 15 | * along with Edward. If not, see <http://www.gnu.org/licenses/>. * |
| 16 | * * |
| 17 | * Copyright (C) 2014-2015 Andrew Engelbrecht (AGPLv3+) * |
| 18 | * Copyright (C) 2014 Josh Drake (AGPLv3+) * |
| 19 | * Copyright (C) 2014 Lisa Marie Maginnis (AGPLv3+) * |
| 20 | * Copyright (C) 2009-2015 Tails developers <tails@boum.org> ( GPLv3+) * |
| 21 | * Copyright (C) 2009 W. Trevor King <wking@drexel.edu> ( GPLv2+) * |
| 22 | * * |
| 23 | * Special thanks to Josh Drake for writing the original edward bot! :) * |
| 24 | * * |
| 25 | ************************************************************************ |
| 26 | |
| 27 | Code sourced from these projects: |
| 28 | |
| 29 | * http://agpl.fsf.org/emailselfdefense.fsf.org/edward/CURRENT/edward.tar.gz |
| 30 | * https://git-tails.immerda.ch/whisperback/tree/whisperBack/encryption.py?h=feature/python3 |
| 31 | * http://www.physics.drexel.edu/~wking/code/python/send_pgp_mime |
| 32 | |
| 33 | """ |
| 34 | |
| 35 | import sys |
| 36 | import gpgme |
| 37 | import re |
| 38 | import io |
| 39 | import os |
| 40 | |
| 41 | import email.parser |
| 42 | import email.message |
| 43 | import email.encoders |
| 44 | |
| 45 | from email.mime.multipart import MIMEMultipart |
| 46 | from email.mime.application import MIMEApplication |
| 47 | from email.mime.nonmultipart import MIMENonMultipart |
| 48 | |
| 49 | import edward_config |
| 50 | |
| 51 | def main (): |
| 52 | |
| 53 | handle_args() |
| 54 | |
| 55 | email_text = sys.stdin.read() |
| 56 | email_from, email_subject = email_from_subject(email_text) |
| 57 | |
| 58 | os.environ['GNUPGHOME'] = edward_config.gnupghome |
| 59 | gpgme_ctx = gpgme.Context() |
| 60 | gpgme_ctx.armor = True |
| 61 | |
| 62 | add_gpg_keys(email_text, gpgme_ctx) |
| 63 | |
| 64 | plaintext, keys = email_decode_flatten(email_text, gpgme_ctx) |
| 65 | encrypt_to_key = choose_reply_encryption_key(keys) |
| 66 | |
| 67 | reply_message = generate_reply(plaintext, email_from, \ |
| 68 | email_subject, encrypt_to_key, |
| 69 | edward_config.sign_with_key, |
| 70 | gpgme_ctx) |
| 71 | |
| 72 | print(reply_message) |
| 73 | |
| 74 | |
| 75 | def email_decode_flatten (email_text, gpgme_ctx): |
| 76 | |
| 77 | body = "" |
| 78 | keys = [] |
| 79 | |
| 80 | email_struct = email.parser.Parser().parsestr(email_text) |
| 81 | |
| 82 | for subpart in email_struct.walk(): |
| 83 | |
| 84 | payload, description, filename, content_type \ |
| 85 | = get_email_subpart_info(subpart) |
| 86 | |
| 87 | if payload == "": |
| 88 | continue |
| 89 | |
| 90 | if content_type == "multipart": |
| 91 | continue |
| 92 | |
| 93 | if content_type == "application/pgp-encrypted": |
| 94 | if description == "PGP/MIME version identification": |
| 95 | if payload.strip() != "Version: 1": |
| 96 | print(progname + ": Warning: unknown " \ |
| 97 | + description + ": " \ |
| 98 | + payload.strip(), file=sys.stderr) |
| 99 | continue |
| 100 | |
| 101 | |
| 102 | if (filename == "encrypted.asc") or (content_type == "pgp/mime"): |
| 103 | plaintext, more_keys = decrypt_text(payload, gpgme_ctx) |
| 104 | |
| 105 | body += plaintext |
| 106 | keys += more_keys |
| 107 | |
| 108 | elif content_type == "application/pgp-keys": |
| 109 | keys += add_gpg_keys(payload, gpgme_ctx) |
| 110 | |
| 111 | elif content_type == "text/plain": |
| 112 | body += payload + "\n" |
| 113 | |
| 114 | else: |
| 115 | body += payload + "\n" |
| 116 | |
| 117 | return body, keys |
| 118 | |
| 119 | |
| 120 | def email_from_subject (email_text): |
| 121 | |
| 122 | email_struct = email.parser.Parser().parsestr(email_text) |
| 123 | |
| 124 | email_from = email_struct['From'] |
| 125 | email_subject = email_struct['Subject'] |
| 126 | |
| 127 | return email_from, email_subject |
| 128 | |
| 129 | |
| 130 | def get_email_subpart_info (part): |
| 131 | |
| 132 | charset = part.get_content_charset() |
| 133 | payload_bytes = part.get_payload(decode=True) |
| 134 | |
| 135 | filename = part.get_filename() |
| 136 | content_type = part.get_content_type() |
| 137 | description_list = part.get_params(header='content-description') |
| 138 | |
| 139 | if charset == None: |
| 140 | charset = 'utf-8' |
| 141 | |
| 142 | if payload_bytes != None: |
| 143 | payload = payload_bytes.decode(charset) |
| 144 | else: |
| 145 | payload = "" |
| 146 | |
| 147 | if description_list != None: |
| 148 | description = description_list[0][0] |
| 149 | else: |
| 150 | description = "" |
| 151 | |
| 152 | return payload, description, filename, content_type |
| 153 | |
| 154 | |
| 155 | def add_gpg_keys (text, gpgme_ctx): |
| 156 | |
| 157 | key_blocks = scan_and_grab(text, |
| 158 | '-----BEGIN PGP PUBLIC KEY BLOCK-----', |
| 159 | '-----END PGP PUBLIC KEY BLOCK-----') |
| 160 | |
| 161 | keys = [] |
| 162 | for key_block in key_blocks: |
| 163 | fp = io.BytesIO(key_block.encode('ascii')) |
| 164 | |
| 165 | result = gpgme_ctx.import_(fp) |
| 166 | |
| 167 | fingerprint = result.imports[0][0] |
| 168 | debug("added gpg key: " + fingerprint) |
| 169 | |
| 170 | keys += [gpgme_ctx.get_key(fingerprint)] |
| 171 | |
| 172 | return keys |
| 173 | |
| 174 | |
| 175 | def decrypt_text (gpg_text, gpgme_ctx): |
| 176 | |
| 177 | body = "" |
| 178 | keys = [] |
| 179 | |
| 180 | msg_blocks = scan_and_grab(gpg_text, |
| 181 | '-----BEGIN PGP MESSAGE-----', |
| 182 | '-----END PGP MESSAGE-----') |
| 183 | |
| 184 | plaintexts_and_sigs = decrypt_blocks(msg_blocks, gpgme_ctx) |
| 185 | |
| 186 | for pair in plaintexts_and_sigs: |
| 187 | plaintext = pair[0] |
| 188 | sigs = pair[1] |
| 189 | |
| 190 | for sig in sigs: |
| 191 | keys += [gpgme_ctx.get_key(sig.fpr)] |
| 192 | |
| 193 | # recursive for nested layers of mime and/or gpg |
| 194 | plaintext, more_keys = email_decode_flatten(plaintext, gpgme_ctx) |
| 195 | |
| 196 | body += plaintext |
| 197 | keys += more_keys |
| 198 | |
| 199 | return body, keys |
| 200 | |
| 201 | |
| 202 | def scan_and_grab (text, start_text, end_text): |
| 203 | |
| 204 | matches = re.search('(' + start_text + '.*' + end_text + ')', |
| 205 | text, flags=re.DOTALL) |
| 206 | |
| 207 | if matches != None: |
| 208 | match_tuple = matches.groups() |
| 209 | else: |
| 210 | match_tuple = () |
| 211 | |
| 212 | return match_tuple |
| 213 | |
| 214 | |
| 215 | def decrypt_blocks (msg_blocks, gpgme_ctx): |
| 216 | |
| 217 | return [decrypt_block(block, gpgme_ctx) for block in msg_blocks] |
| 218 | |
| 219 | |
| 220 | def decrypt_block (msg_block, gpgme_ctx): |
| 221 | |
| 222 | block_b = io.BytesIO(msg_block.encode('ascii')) |
| 223 | plain_b = io.BytesIO() |
| 224 | |
| 225 | sigs = gpgme_ctx.decrypt_verify(block_b, plain_b) |
| 226 | |
| 227 | plaintext = plain_b.getvalue().decode('utf-8') |
| 228 | return (plaintext, sigs) |
| 229 | |
| 230 | |
| 231 | def choose_reply_encryption_key (keys): |
| 232 | |
| 233 | reply_key = None |
| 234 | for key in keys: |
| 235 | if (key.can_encrypt == True): |
| 236 | reply_key = key |
| 237 | break |
| 238 | |
| 239 | return reply_key |
| 240 | |
| 241 | |
| 242 | def generate_reply (plaintext, email_from, email_subject, encrypt_to_key, |
| 243 | sign_with_fingerprint, gpgme_ctx): |
| 244 | |
| 245 | |
| 246 | reply = "To: " + email_from + "\n" |
| 247 | reply += "Subject: " + email_subject + "\n" |
| 248 | |
| 249 | if (encrypt_to_key != None): |
| 250 | plaintext_reply = "thanks for the message!\n\n\n" |
| 251 | plaintext_reply += email_quote_text(plaintext) |
| 252 | |
| 253 | # quoted printable encoding lets most ascii characters look normal |
| 254 | # before the decrypted mime message is decoded. |
| 255 | char_set = email.charset.Charset("utf-8") |
| 256 | char_set.body_encoding = email.charset.QP |
| 257 | |
| 258 | # MIMEText doesn't allow setting the text encoding |
| 259 | # so we use MIMENonMultipart. |
| 260 | plaintext_mime = MIMENonMultipart('text', 'plain') |
| 261 | plaintext_mime.set_payload(plaintext_reply, charset=char_set) |
| 262 | |
| 263 | encrypted_text = encrypt_sign_message(plaintext_mime.as_string(), |
| 264 | encrypt_to_key, |
| 265 | sign_with_fingerprint, |
| 266 | gpgme_ctx) |
| 267 | |
| 268 | control_mime = MIMEApplication("Version: 1", |
| 269 | _subtype='pgp-encrypted', |
| 270 | _encoder=email.encoders.encode_7or8bit) |
| 271 | control_mime['Content-Description'] = 'PGP/MIME version identification' |
| 272 | control_mime.set_charset('us-ascii') |
| 273 | |
| 274 | encoded_mime = MIMEApplication(encrypted_text, |
| 275 | _subtype='octet-stream; name="encrypted.asc"', |
| 276 | _encoder=email.encoders.encode_7or8bit) |
| 277 | encoded_mime['Content-Description'] = 'OpenPGP encrypted message' |
| 278 | encoded_mime['Content-Disposition'] = 'inline; filename="encrypted.asc"' |
| 279 | encoded_mime.set_charset('us-ascii') |
| 280 | |
| 281 | message_mime = MIMEMultipart(_subtype="encrypted", protocol="application/pgp-encrypted") |
| 282 | message_mime.attach(control_mime) |
| 283 | message_mime.attach(encoded_mime) |
| 284 | message_mime['Content-Disposition'] = 'inline' |
| 285 | |
| 286 | reply += message_mime.as_string() |
| 287 | |
| 288 | else: |
| 289 | reply += "\n" |
| 290 | reply += "Sorry, i couldn't find your key.\n" |
| 291 | reply += "I'll need that to encrypt a message to you." |
| 292 | |
| 293 | return reply |
| 294 | |
| 295 | |
| 296 | def email_quote_text (text): |
| 297 | |
| 298 | quoted_message = re.sub(r'^', r'> ', text, flags=re.MULTILINE) |
| 299 | |
| 300 | return quoted_message |
| 301 | |
| 302 | |
| 303 | def encrypt_sign_message (plaintext, encrypt_to_key, sign_with_fingerprint, gpgme_ctx): |
| 304 | |
| 305 | sign_with_key = gpgme_ctx.get_key(sign_with_fingerprint) |
| 306 | gpgme_ctx.signers = [sign_with_key] |
| 307 | |
| 308 | plaintext_bytes = io.BytesIO(plaintext.encode('ascii')) |
| 309 | encrypted_bytes = io.BytesIO() |
| 310 | |
| 311 | gpgme_ctx.encrypt_sign([encrypt_to_key], gpgme.ENCRYPT_ALWAYS_TRUST, |
| 312 | plaintext_bytes, encrypted_bytes) |
| 313 | |
| 314 | encrypted_txt = encrypted_bytes.getvalue().decode('ascii') |
| 315 | return encrypted_txt |
| 316 | |
| 317 | |
| 318 | def debug (debug_msg): |
| 319 | |
| 320 | if edward_config.debug == True: |
| 321 | sys.stderr.write(debug_msg + "\n") |
| 322 | |
| 323 | |
| 324 | def handle_args (): |
| 325 | if __name__ == "__main__": |
| 326 | |
| 327 | global progname |
| 328 | progname = sys.argv[0] |
| 329 | |
| 330 | if len(sys.argv) > 1: |
| 331 | print(progname + ": error, this program doesn't " \ |
| 332 | "need any arguments.", file=sys.stderr) |
| 333 | exit(1) |
| 334 | |
| 335 | |
| 336 | main() |
| 337 | |