Commit | Line | Data |
---|---|---|
0bec96d6 | 1 | #! /usr/bin/env python3 |
ff4136c7 | 2 | # -*- coding: utf-8 -*- |
0bec96d6 AE |
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+) * | |
8bdfb6d4 AE |
20 | * Copyright (C) 2009-2015 Tails developers <tails@boum.org> ( GPLv3+) * |
21 | * Copyright (C) 2009 W. Trevor King <wking@drexel.edu> ( GPLv2+) * | |
0bec96d6 AE |
22 | * * |
23 | * Special thanks to Josh Drake for writing the original edward bot! :) * | |
24 | * * | |
25 | ************************************************************************ | |
26 | ||
a5385c04 | 27 | Code sourced from these projects: |
0bec96d6 | 28 | |
8bdfb6d4 AE |
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 | |
0bec96d6 AE |
32 | """ |
33 | ||
34 | import sys | |
0bec96d6 AE |
35 | import gpgme |
36 | import re | |
37 | import io | |
40c37ab3 | 38 | import os |
0bec96d6 | 39 | |
8bdfb6d4 AE |
40 | import email.parser |
41 | import email.message | |
42 | import email.encoders | |
43 | ||
44 | from email.mime.multipart import MIMEMultipart | |
45 | from email.mime.application import MIMEApplication | |
46 | from email.mime.nonmultipart import MIMENonMultipart | |
47 | ||
40c37ab3 | 48 | import edward_config |
c96f3837 | 49 | |
38738401 AE |
50 | match_types = [('clearsign', |
51 | '-----BEGIN PGP SIGNED MESSAGE-----.*?-----BEGIN PGP SIGNATURE-----.*?-----END PGP SIGNATURE-----'), | |
52 | ('message', | |
56578eaf AE |
53 | '-----BEGIN PGP MESSAGE-----.*?-----END PGP MESSAGE-----'), |
54 | ('pubkey', | |
55 | '-----BEGIN PGP PUBLIC KEY BLOCK-----.*?-----END PGP PUBLIC KEY BLOCK-----'), | |
56 | ('detachedsig', | |
38738401 | 57 | '-----BEGIN PGP SIGNATURE-----.*?-----END PGP SIGNATURE-----')] |
56578eaf AE |
58 | |
59 | ||
60 | class EddyMsg (object): | |
61 | def __init__(self): | |
62 | self.multipart = False | |
63 | self.subparts = [] | |
64 | ||
65 | self.charset = None | |
66 | self.payload_bytes = None | |
67 | self.payload_pieces = [] | |
68 | ||
69 | self.filename = None | |
70 | self.content_type = None | |
71 | self.description_list = None | |
72 | ||
73 | ||
74 | class PayloadPiece (object): | |
75 | def __init__(self): | |
76 | self.piece_type = None | |
77 | self.string = None | |
78 | self.gpg_data = None | |
79 | ||
80 | ||
38738401 AE |
81 | class GPGData (object): |
82 | def __init__(self): | |
83 | self.decrypted = False | |
84 | ||
85 | self.plainobj = None | |
86 | self.sigs = [] | |
87 | self.keys = [] | |
88 | ||
89 | ||
0bec96d6 AE |
90 | def main (): |
91 | ||
20f6e7c5 | 92 | handle_args() |
0bec96d6 | 93 | |
0a064403 AE |
94 | gpgme_ctx = get_gpg_context(edward_config.gnupghome, |
95 | edward_config.sign_with_key) | |
96 | ||
c96f3837 | 97 | email_text = sys.stdin.read() |
38738401 | 98 | result = parse_pgp_mime(email_text, gpgme_ctx) |
65ed3800 | 99 | |
56578eaf | 100 | email_from, email_subject = email_from_subject(email_text) |
fafa21c3 | 101 | |
56578eaf AE |
102 | # plaintext, fingerprints = email_decode_flatten(email_text, gpgme_ctx, False) |
103 | # encrypt_to_key = choose_reply_encryption_key(gpgme_ctx, fingerprints) | |
104 | # | |
105 | # reply_message = generate_reply(plaintext, email_from, \ | |
106 | # email_subject, encrypt_to_key, | |
107 | # gpgme_ctx) | |
c96f3837 | 108 | |
56578eaf | 109 | print(flatten_eddy(result)) |
c96f3837 | 110 | |
0bec96d6 | 111 | |
0a064403 AE |
112 | def get_gpg_context (gnupghome, sign_with_key_fp): |
113 | ||
114 | os.environ['GNUPGHOME'] = gnupghome | |
115 | ||
116 | gpgme_ctx = gpgme.Context() | |
117 | gpgme_ctx.armor = True | |
118 | ||
119 | try: | |
120 | sign_with_key = gpgme_ctx.get_key(sign_with_key_fp) | |
121 | except: | |
122 | error("unable to load signing key. is the gnupghome " | |
123 | + "and signing key properly set in the edward_config.py?") | |
124 | exit(1) | |
125 | ||
126 | gpgme_ctx.signers = [sign_with_key] | |
127 | ||
128 | return gpgme_ctx | |
129 | ||
130 | ||
38738401 | 131 | def parse_pgp_mime (email_text, gpgme_ctx): |
394a1476 AE |
132 | |
133 | email_struct = email.parser.Parser().parsestr(email_text) | |
134 | ||
56578eaf AE |
135 | eddy_obj = parse_mime(email_struct) |
136 | eddy_obj = split_payloads(eddy_obj) | |
38738401 | 137 | eddy_obj = decrypt_payloads(eddy_obj, gpgme_ctx) |
8bb4b0d5 | 138 | |
56578eaf | 139 | return eddy_obj |
0bec96d6 | 140 | |
0bec96d6 | 141 | |
56578eaf | 142 | def parse_mime(msg_struct): |
0bec96d6 | 143 | |
56578eaf | 144 | eddy_obj = EddyMsg() |
8bb4b0d5 | 145 | |
56578eaf AE |
146 | if msg_struct.is_multipart() == True: |
147 | payloads = msg_struct.get_payload() | |
0bec96d6 | 148 | |
56578eaf | 149 | eddy_obj.multipart = True |
dd11a483 AE |
150 | eddy_obj.subparts = list(map(parse_mime, payloads)) |
151 | ||
56578eaf AE |
152 | else: |
153 | eddy_obj = get_subpart_data(msg_struct) | |
394a1476 | 154 | |
56578eaf | 155 | return eddy_obj |
c267c233 | 156 | |
80119cab | 157 | |
56578eaf | 158 | def split_payloads (eddy_obj): |
3a9d1426 | 159 | |
56578eaf | 160 | if eddy_obj.multipart == True: |
dd11a483 | 161 | eddy_obj.subparts = list(map(split_payloads, eddy_obj.subparts)) |
cf75de65 | 162 | |
56578eaf AE |
163 | else: |
164 | for (match_type, pattern) in match_types: | |
0bec96d6 | 165 | |
56578eaf AE |
166 | new_pieces_list = [] |
167 | for payload_piece in eddy_obj.payload_pieces: | |
168 | new_pieces_list += scan_and_split(payload_piece, | |
169 | match_type, pattern) | |
170 | eddy_obj.payload_pieces = new_pieces_list | |
86663388 | 171 | |
56578eaf | 172 | return eddy_obj |
cf75de65 AE |
173 | |
174 | ||
56578eaf | 175 | def scan_and_split (payload_piece, match_type, pattern): |
cf75de65 | 176 | |
56578eaf AE |
177 | flags = re.DOTALL | re.MULTILINE |
178 | matches = re.search("(?P<beginning>.*?)(?P<match>" + pattern + | |
179 | ")(?P<rest>.*)", payload_piece.string, flags=flags) | |
86663388 | 180 | |
56578eaf AE |
181 | if matches == None: |
182 | pieces = [payload_piece] | |
c96f3837 | 183 | |
56578eaf | 184 | else: |
d437f8b2 | 185 | |
56578eaf AE |
186 | beginning = PayloadPiece() |
187 | beginning.string = matches.group('beginning') | |
188 | beginning.piece_type = payload_piece.piece_type | |
d437f8b2 | 189 | |
56578eaf AE |
190 | match = PayloadPiece() |
191 | match.string = matches.group('match') | |
192 | match.piece_type = match_type | |
d437f8b2 | 193 | |
56578eaf AE |
194 | rest = PayloadPiece() |
195 | rest.string = matches.group('rest') | |
196 | rest.piece_type = payload_piece.piece_type | |
d437f8b2 | 197 | |
56578eaf | 198 | more_pieces = scan_and_split(rest, match_type, pattern) |
d437f8b2 | 199 | |
56578eaf AE |
200 | if more_pieces == None: |
201 | pieces = [beginning, match, rest] | |
202 | else: | |
203 | pieces = [beginning, match] + more_pieces | |
d437f8b2 | 204 | |
56578eaf | 205 | return pieces |
d437f8b2 | 206 | |
d437f8b2 | 207 | |
56578eaf | 208 | def get_subpart_data (part): |
0bec96d6 | 209 | |
56578eaf | 210 | obj = EddyMsg() |
0bec96d6 | 211 | |
56578eaf AE |
212 | obj.charset = part.get_content_charset() |
213 | obj.payload_bytes = part.get_payload(decode=True) | |
214 | ||
215 | obj.filename = part.get_filename() | |
216 | obj.content_type = part.get_content_type() | |
217 | obj.description_list = part['content-description'] | |
218 | ||
219 | # your guess is as good as a-myy-ee-ine... | |
220 | if obj.charset == None: | |
221 | obj.charset = 'utf-8' | |
222 | ||
223 | if obj.payload_bytes != None: | |
0eb75d9c AE |
224 | try: |
225 | payload = PayloadPiece() | |
226 | payload.string = obj.payload_bytes.decode(obj.charset) | |
227 | payload.piece_type = 'text' | |
228 | ||
229 | obj.payload_pieces = [payload] | |
230 | except UnicodeDecodeError: | |
231 | pass | |
56578eaf AE |
232 | |
233 | return obj | |
234 | ||
235 | ||
dd11a483 | 236 | def do_to_eddys_pieces (function_to_do, eddy_obj, data): |
56578eaf AE |
237 | |
238 | if eddy_obj.multipart == True: | |
dd11a483 AE |
239 | result_list = [] |
240 | for sub in eddy_obj.subparts: | |
241 | result_list += do_to_eddys_pieces(function_to_do, sub, data) | |
394a1476 | 242 | else: |
dd11a483 AE |
243 | result_list = [function_to_do(eddy_obj.payload_pieces, data)] |
244 | ||
245 | return result_list | |
246 | ||
247 | ||
38738401 AE |
248 | def decrypt_payloads (eddy_obj, gpgme_ctx): |
249 | ||
250 | do_to_eddys_pieces(decrypt_payload_pieces, eddy_obj, gpgme_ctx) | |
251 | ||
252 | return eddy_obj | |
253 | ||
254 | ||
255 | def decrypt_payload_pieces (payload_pieces, gpgme_ctx): | |
256 | ||
257 | for piece in payload_pieces: | |
258 | ||
259 | if piece.piece_type == "text": | |
260 | # don't transform the plaintext. | |
261 | pass | |
262 | ||
263 | elif piece.piece_type == "message": | |
264 | (plaintext, sigs) = decrypt_block (piece.string, gpgme_ctx) | |
265 | ||
266 | if plaintext: | |
267 | piece.gpg_data = GPGData() | |
268 | piece.gpg_data.sigs = sigs | |
269 | # recurse! | |
270 | piece.gpg_data.plainobj = parse_pgp_mime(plaintext, gpgme_ctx) | |
271 | else: | |
272 | pass | |
273 | ||
274 | ||
dd11a483 AE |
275 | def flatten_eddy (eddy_obj): |
276 | ||
277 | string = "\n".join(do_to_eddys_pieces(flatten_payload_pieces, eddy_obj, None)) | |
56578eaf AE |
278 | |
279 | return string | |
280 | ||
281 | ||
dd11a483 | 282 | def flatten_payload_pieces (payload_pieces, _ignore): |
0bec96d6 | 283 | |
56578eaf AE |
284 | string = "" |
285 | for piece in payload_pieces: | |
38738401 AE |
286 | if piece.piece_type == "text": |
287 | string += piece.string | |
288 | elif piece.piece_type == "message": | |
289 | # recursive! | |
290 | string += flatten_eddy(piece.gpg_data.plainobj) | |
56578eaf AE |
291 | |
292 | return string | |
293 | ||
294 | ||
295 | def email_from_subject (email_text): | |
296 | ||
297 | email_struct = email.parser.Parser().parsestr(email_text) | |
298 | ||
299 | email_from = email_struct['From'] | |
300 | email_subject = email_struct['Subject'] | |
301 | ||
302 | return email_from, email_subject | |
0bec96d6 | 303 | |
0bec96d6 | 304 | |
c267c233 AE |
305 | def add_gpg_keys (text, gpgme_ctx): |
306 | ||
83210634 AE |
307 | key_blocks = scan_and_grab(text, |
308 | '-----BEGIN PGP PUBLIC KEY BLOCK-----', | |
309 | '-----END PGP PUBLIC KEY BLOCK-----') | |
c267c233 | 310 | |
d0489345 | 311 | fingerprints = [] |
83210634 AE |
312 | for key_block in key_blocks: |
313 | fp = io.BytesIO(key_block.encode('ascii')) | |
c267c233 AE |
314 | |
315 | result = gpgme_ctx.import_(fp) | |
e49673aa | 316 | imports = result.imports |
c267c233 | 317 | |
e49673aa AE |
318 | if imports != []: |
319 | fingerprint = imports[0][0] | |
d0489345 | 320 | fingerprints += [fingerprint] |
c267c233 | 321 | |
e49673aa | 322 | debug("added gpg key: " + fingerprint) |
ec1e779a | 323 | |
d0489345 | 324 | return fingerprints |
ec1e779a AE |
325 | |
326 | ||
40c37ab3 | 327 | def decrypt_text (gpg_text, gpgme_ctx): |
86663388 | 328 | |
394a1476 | 329 | body = "" |
d0489345 | 330 | fingerprints = [] |
0bec96d6 | 331 | |
5b3053c1 | 332 | msg_blocks = scan_and_grab(gpg_text, |
c267c233 AE |
333 | '-----BEGIN PGP MESSAGE-----', |
334 | '-----END PGP MESSAGE-----') | |
86663388 | 335 | |
5b3053c1 | 336 | plaintexts_and_sigs = decrypt_blocks(msg_blocks, gpgme_ctx) |
0bec96d6 | 337 | |
5b3053c1 AE |
338 | for pair in plaintexts_and_sigs: |
339 | plaintext = pair[0] | |
340 | sigs = pair[1] | |
9eb78301 | 341 | |
394a1476 | 342 | for sig in sigs: |
d0489345 | 343 | fingerprints += [sig.fpr] |
0bec96d6 | 344 | |
394a1476 | 345 | # recursive for nested layers of mime and/or gpg |
d0489345 | 346 | plaintext, more_fps = email_decode_flatten(plaintext, gpgme_ctx, True) |
8bb4b0d5 | 347 | |
394a1476 | 348 | body += plaintext |
d0489345 | 349 | fingerprints += more_fps |
8bb4b0d5 | 350 | |
d0489345 | 351 | return body, fingerprints |
8bb4b0d5 | 352 | |
8bb4b0d5 | 353 | |
cf75de65 AE |
354 | def verify_clear_signature (text, gpgme_ctx): |
355 | ||
356 | sig_blocks = scan_and_grab(text, | |
357 | '-----BEGIN PGP SIGNED MESSAGE-----', | |
358 | '-----END PGP SIGNATURE-----') | |
359 | ||
360 | fingerprints = [] | |
361 | plaintext = "" | |
362 | ||
363 | for sig_block in sig_blocks: | |
364 | msg_fp = io.BytesIO(sig_block.encode('utf-8')) | |
365 | ptxt_fp = io.BytesIO() | |
366 | ||
367 | result = gpgme_ctx.verify(msg_fp, None, ptxt_fp) | |
368 | ||
369 | plaintext += ptxt_fp.getvalue().decode('utf-8') | |
370 | fingerprint = result[0].fpr | |
371 | ||
372 | fingerprints += [fingerprint] | |
373 | ||
374 | return plaintext, fingerprints | |
375 | ||
376 | ||
c267c233 | 377 | def scan_and_grab (text, start_text, end_text): |
0bec96d6 | 378 | |
c267c233 AE |
379 | matches = re.search('(' + start_text + '.*' + end_text + ')', |
380 | text, flags=re.DOTALL) | |
0bec96d6 | 381 | |
c267c233 AE |
382 | if matches != None: |
383 | match_tuple = matches.groups() | |
0bec96d6 | 384 | else: |
c267c233 | 385 | match_tuple = () |
7b06b980 | 386 | |
c267c233 | 387 | return match_tuple |
0bec96d6 AE |
388 | |
389 | ||
5b3053c1 | 390 | def decrypt_blocks (msg_blocks, gpgme_ctx): |
0bec96d6 | 391 | |
5b3053c1 | 392 | return [decrypt_block(block, gpgme_ctx) for block in msg_blocks] |
0bec96d6 | 393 | |
0bec96d6 | 394 | |
5b3053c1 | 395 | def decrypt_block (msg_block, gpgme_ctx): |
0bec96d6 | 396 | |
5b3053c1 | 397 | block_b = io.BytesIO(msg_block.encode('ascii')) |
0bec96d6 AE |
398 | plain_b = io.BytesIO() |
399 | ||
afc1f64c AE |
400 | try: |
401 | sigs = gpgme_ctx.decrypt_verify(block_b, plain_b) | |
402 | except: | |
403 | return ("",[]) | |
0bec96d6 | 404 | |
6aa41372 | 405 | plaintext = plain_b.getvalue().decode('utf-8') |
394a1476 | 406 | return (plaintext, sigs) |
0bec96d6 AE |
407 | |
408 | ||
d0489345 | 409 | def choose_reply_encryption_key (gpgme_ctx, fingerprints): |
fafa21c3 AE |
410 | |
411 | reply_key = None | |
d0489345 AE |
412 | for fp in fingerprints: |
413 | try: | |
414 | key = gpgme_ctx.get_key(fp) | |
415 | ||
416 | if (key.can_encrypt == True): | |
417 | reply_key = key | |
418 | break | |
419 | except: | |
420 | continue | |
421 | ||
fafa21c3 | 422 | |
216708e9 | 423 | return reply_key |
fafa21c3 AE |
424 | |
425 | ||
897cbaf6 | 426 | def generate_reply (plaintext, email_from, email_subject, encrypt_to_key, |
0a064403 | 427 | gpgme_ctx): |
1da9b527 | 428 | |
8bdfb6d4 | 429 | |
1da9b527 AE |
430 | reply = "To: " + email_from + "\n" |
431 | reply += "Subject: " + email_subject + "\n" | |
216708e9 AE |
432 | |
433 | if (encrypt_to_key != None): | |
434 | plaintext_reply = "thanks for the message!\n\n\n" | |
435 | plaintext_reply += email_quote_text(plaintext) | |
436 | ||
8bdfb6d4 AE |
437 | # quoted printable encoding lets most ascii characters look normal |
438 | # before the decrypted mime message is decoded. | |
439 | char_set = email.charset.Charset("utf-8") | |
440 | char_set.body_encoding = email.charset.QP | |
441 | ||
442 | # MIMEText doesn't allow setting the text encoding | |
443 | # so we use MIMENonMultipart. | |
444 | plaintext_mime = MIMENonMultipart('text', 'plain') | |
445 | plaintext_mime.set_payload(plaintext_reply, charset=char_set) | |
446 | ||
447 | encrypted_text = encrypt_sign_message(plaintext_mime.as_string(), | |
448 | encrypt_to_key, | |
40c37ab3 | 449 | gpgme_ctx) |
8bdfb6d4 AE |
450 | |
451 | control_mime = MIMEApplication("Version: 1", | |
452 | _subtype='pgp-encrypted', | |
453 | _encoder=email.encoders.encode_7or8bit) | |
454 | control_mime['Content-Description'] = 'PGP/MIME version identification' | |
455 | control_mime.set_charset('us-ascii') | |
456 | ||
457 | encoded_mime = MIMEApplication(encrypted_text, | |
458 | _subtype='octet-stream; name="encrypted.asc"', | |
459 | _encoder=email.encoders.encode_7or8bit) | |
460 | encoded_mime['Content-Description'] = 'OpenPGP encrypted message' | |
461 | encoded_mime['Content-Disposition'] = 'inline; filename="encrypted.asc"' | |
462 | encoded_mime.set_charset('us-ascii') | |
463 | ||
464 | message_mime = MIMEMultipart(_subtype="encrypted", protocol="application/pgp-encrypted") | |
465 | message_mime.attach(control_mime) | |
466 | message_mime.attach(encoded_mime) | |
467 | message_mime['Content-Disposition'] = 'inline' | |
216708e9 | 468 | |
8bdfb6d4 | 469 | reply += message_mime.as_string() |
216708e9 AE |
470 | |
471 | else: | |
8bdfb6d4 | 472 | reply += "\n" |
216708e9 AE |
473 | reply += "Sorry, i couldn't find your key.\n" |
474 | reply += "I'll need that to encrypt a message to you." | |
1da9b527 AE |
475 | |
476 | return reply | |
477 | ||
478 | ||
f87041f8 AE |
479 | def email_quote_text (text): |
480 | ||
481 | quoted_message = re.sub(r'^', r'> ', text, flags=re.MULTILINE) | |
482 | ||
483 | return quoted_message | |
484 | ||
485 | ||
0a064403 | 486 | def encrypt_sign_message (plaintext, encrypt_to_key, gpgme_ctx): |
897cbaf6 | 487 | |
6aa41372 | 488 | plaintext_bytes = io.BytesIO(plaintext.encode('ascii')) |
1da9b527 AE |
489 | encrypted_bytes = io.BytesIO() |
490 | ||
897cbaf6 | 491 | gpgme_ctx.encrypt_sign([encrypt_to_key], gpgme.ENCRYPT_ALWAYS_TRUST, |
1da9b527 AE |
492 | plaintext_bytes, encrypted_bytes) |
493 | ||
6aa41372 | 494 | encrypted_txt = encrypted_bytes.getvalue().decode('ascii') |
1da9b527 AE |
495 | return encrypted_txt |
496 | ||
497 | ||
0a064403 AE |
498 | def error (error_msg): |
499 | ||
500 | sys.stderr.write(progname + ": " + error_msg + "\n") | |
501 | ||
502 | ||
5e8f9094 AE |
503 | def debug (debug_msg): |
504 | ||
505 | if edward_config.debug == True: | |
0a064403 | 506 | error(debug_msg) |
5e8f9094 AE |
507 | |
508 | ||
20f6e7c5 AE |
509 | def handle_args (): |
510 | if __name__ == "__main__": | |
511 | ||
512 | global progname | |
513 | progname = sys.argv[0] | |
514 | ||
515 | if len(sys.argv) > 1: | |
516 | print(progname + ": error, this program doesn't " \ | |
517 | "need any arguments.", file=sys.stderr) | |
518 | exit(1) | |
519 | ||
520 | ||
0bec96d6 AE |
521 | main() |
522 |