fixed a comment
[edward.git] / edward
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 import sys
35 import gpgme
36 import re
37 import io
38 import os
39 import importlib
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 langs = ["de", "el", "en", "fr", "ja", "pt-br", "ro", "ru", "tr"]
52
53 match_types = [('clearsign',
54 '-----BEGIN PGP SIGNED MESSAGE-----.*?-----BEGIN PGP SIGNATURE-----.*?-----END PGP SIGNATURE-----'),
55 ('message',
56 '-----BEGIN PGP MESSAGE-----.*?-----END PGP MESSAGE-----'),
57 ('pubkey',
58 '-----BEGIN PGP PUBLIC KEY BLOCK-----.*?-----END PGP PUBLIC KEY BLOCK-----'),
59 ('detachedsig',
60 '-----BEGIN PGP SIGNATURE-----.*?-----END PGP SIGNATURE-----')]
61
62
63 class EddyMsg (object):
64 def __init__(self):
65 self.multipart = False
66 self.subparts = []
67
68 self.charset = None
69 self.payload_bytes = None
70 self.payload_pieces = []
71
72 self.filename = None
73 self.content_type = None
74 self.description_list = None
75
76
77 class PayloadPiece (object):
78 def __init__(self):
79 self.piece_type = None
80 self.string = None
81 self.gpg_data = None
82
83
84 class GPGData (object):
85 def __init__(self):
86 self.decrypted = False
87
88 self.plainobj = None
89 self.sigs = []
90 self.keys = []
91
92 class ReplyInfo (object):
93 def __init__(self):
94 self.replies = None
95
96 self.target_key = None
97 self.fallback_target_key = None
98 self.msg_to_quote = ""
99
100 self.success_decrypt = False
101 self.failed_decrypt = False
102 self.public_key_received = False
103 self.no_public_key = False
104 self.sig_success = False
105 self.sig_failure = False
106
107
108 def main ():
109
110 handle_args()
111
112 gpgme_ctx = get_gpg_context(edward_config.gnupghome,
113 edward_config.sign_with_key)
114
115 email_text = sys.stdin.read()
116 email_struct = parse_pgp_mime(email_text, gpgme_ctx)
117
118 email_to, email_from, email_subject = email_to_from_subject(email_text)
119 lang = import_lang(email_to)
120
121 replyinfo_obj = ReplyInfo()
122 replyinfo_obj.replies = lang.replies
123
124 prepare_for_reply(email_struct, replyinfo_obj)
125 encrypt_to_key = get_key_from_fp(replyinfo_obj, gpgme_ctx)
126 reply_plaintext = write_reply(replyinfo_obj)
127
128 reply_mime = generate_encrypted_mime(reply_plaintext, email_from, \
129 email_subject, encrypt_to_key,
130 gpgme_ctx)
131
132 print(reply_mime)
133
134
135 def get_gpg_context (gnupghome, sign_with_key_fp):
136
137 os.environ['GNUPGHOME'] = gnupghome
138
139 gpgme_ctx = gpgme.Context()
140 gpgme_ctx.armor = True
141
142 try:
143 sign_with_key = gpgme_ctx.get_key(sign_with_key_fp)
144 except:
145 error("unable to load signing key. is the gnupghome "
146 + "and signing key properly set in the edward_config.py?")
147 exit(1)
148
149 gpgme_ctx.signers = [sign_with_key]
150
151 return gpgme_ctx
152
153
154 def parse_pgp_mime (email_text, gpgme_ctx):
155
156 email_struct = email.parser.Parser().parsestr(email_text)
157
158 eddymsg_obj = parse_mime(email_struct)
159 split_payloads(eddymsg_obj)
160 gpg_on_payloads(eddymsg_obj, gpgme_ctx)
161
162 return eddymsg_obj
163
164
165 def parse_mime(msg_struct):
166
167 eddymsg_obj = EddyMsg()
168
169 if msg_struct.is_multipart() == True:
170 payloads = msg_struct.get_payload()
171
172 eddymsg_obj.multipart = True
173 eddymsg_obj.subparts = list(map(parse_mime, payloads))
174
175 else:
176 eddymsg_obj = get_subpart_data(msg_struct)
177
178 return eddymsg_obj
179
180
181 def scan_and_split (payload_piece, match_type, pattern):
182
183 # don't try to re-split pieces containing gpg data
184 if payload_piece.piece_type != "text":
185 return [payload_piece]
186
187 flags = re.DOTALL | re.MULTILINE
188 matches = re.search("(?P<beginning>.*?)(?P<match>" + pattern +
189 ")(?P<rest>.*)", payload_piece.string, flags=flags)
190
191 if matches == None:
192 pieces = [payload_piece]
193
194 else:
195
196 beginning = PayloadPiece()
197 beginning.string = matches.group('beginning')
198 beginning.piece_type = payload_piece.piece_type
199
200 match = PayloadPiece()
201 match.string = matches.group('match')
202 match.piece_type = match_type
203
204 rest = PayloadPiece()
205 rest.string = matches.group('rest')
206 rest.piece_type = payload_piece.piece_type
207
208 more_pieces = scan_and_split(rest, match_type, pattern)
209 pieces = [beginning, match ] + more_pieces
210
211 return pieces
212
213
214 def get_subpart_data (part):
215
216 obj = EddyMsg()
217
218 obj.charset = part.get_content_charset()
219 obj.payload_bytes = part.get_payload(decode=True)
220
221 obj.filename = part.get_filename()
222 obj.content_type = part.get_content_type()
223 obj.description_list = part['content-description']
224
225 # your guess is as good as a-myy-ee-ine...
226 if obj.charset == None:
227 obj.charset = 'utf-8'
228
229 if obj.payload_bytes != None:
230 try:
231 payload = PayloadPiece()
232 payload.string = obj.payload_bytes.decode(obj.charset)
233 payload.piece_type = 'text'
234
235 obj.payload_pieces = [payload]
236 except UnicodeDecodeError:
237 pass
238
239 return obj
240
241
242 def do_to_eddys_pieces (function_to_do, eddymsg_obj, data):
243
244 if eddymsg_obj.multipart == True:
245 for sub in eddymsg_obj.subparts:
246 do_to_eddys_pieces(function_to_do, sub, data)
247 else:
248 function_to_do(eddymsg_obj, data)
249
250
251 def split_payloads (eddymsg_obj):
252
253 for match_type in match_types:
254 do_to_eddys_pieces(split_payload_pieces, eddymsg_obj, match_type)
255
256
257 def split_payload_pieces (eddymsg_obj, match_type):
258
259 (match_name, pattern) = match_type
260
261 new_pieces_list = []
262 for piece in eddymsg_obj.payload_pieces:
263 new_pieces_list += scan_and_split(piece, match_name, pattern)
264
265 eddymsg_obj.payload_pieces = new_pieces_list
266
267
268 def gpg_on_payloads (eddymsg_obj, gpgme_ctx, prev_parts=[]):
269
270 if eddymsg_obj.multipart == True:
271 prev_parts=[]
272 for sub in eddymsg_obj.subparts:
273 gpg_on_payloads (sub, gpgme_ctx, prev_parts)
274 prev_parts += [sub]
275
276 return
277
278 for piece in eddymsg_obj.payload_pieces:
279
280 if piece.piece_type == "text":
281 # don't transform the plaintext.
282 pass
283
284 elif piece.piece_type == "message":
285 (plaintext, sigs) = decrypt_block(piece.string, gpgme_ctx)
286
287 if plaintext:
288 piece.gpg_data = GPGData()
289 piece.gpg_data.decrypted = True
290 piece.gpg_data.sigs = sigs
291 # recurse!
292 piece.gpg_data.plainobj = parse_pgp_mime(plaintext, gpgme_ctx)
293 continue
294
295 # if not encrypted, check to see if this is an armored signature.
296 (plaintext, sigs) = verify_sig_message(piece.string, gpgme_ctx)
297
298 if plaintext:
299 piece.piece_type = "signature"
300 piece.gpg_data = GPGData()
301 piece.gpg_data.sigs = sigs
302 # recurse!
303 piece.gpg_data.plainobj = parse_pgp_mime(plaintext, gpgme_ctx)
304
305 elif piece.piece_type == "pubkey":
306 key_fps = add_gpg_key(piece.string, gpgme_ctx)
307
308 if key_fps != []:
309 piece.gpg_data = GPGData()
310 piece.gpg_data.keys = key_fps
311
312 elif piece.piece_type == "clearsign":
313 (plaintext, sig_fps) = verify_clear_signature(piece.string, gpgme_ctx)
314
315 if sig_fps != []:
316 piece.gpg_data = GPGData()
317 piece.gpg_data.sigs = sig_fps
318 piece.gpg_data.plainobj = parse_pgp_mime(plaintext, gpgme_ctx)
319
320 elif piece.piece_type == "detachedsig":
321 for prev in prev_parts:
322 payload_bytes = prev.payload_bytes
323 sig_fps = verify_detached_signature(piece.string, payload_bytes, gpgme_ctx)
324
325 if sig_fps != []:
326 piece.gpg_data = GPGData()
327 piece.gpg_data.sigs = sig_fps
328 piece.gpg_data.plainobj = prev
329 break
330
331 else:
332 pass
333
334
335 def prepare_for_reply (eddymsg_obj, replyinfo_obj):
336
337 do_to_eddys_pieces(prepare_for_reply_pieces, eddymsg_obj, replyinfo_obj)
338
339 def prepare_for_reply_pieces (eddymsg_obj, replyinfo_obj):
340
341 for piece in eddymsg_obj.payload_pieces:
342 if piece.piece_type == "text":
343 # don't quote the plaintext part.
344 pass
345
346 elif piece.piece_type == "message":
347 prepare_for_reply_message(piece, replyinfo_obj)
348
349 elif piece.piece_type == "pubkey":
350 prepare_for_reply_pubkey(piece, replyinfo_obj)
351
352 elif (piece.piece_type == "clearsign") \
353 or (piece.piece_type == "detachedsig") \
354 or (piece.piece_type == "signature"):
355 prepare_for_reply_sig(piece, replyinfo_obj)
356
357
358 def prepare_for_reply_message (piece, replyinfo_obj):
359
360 if piece.gpg_data == None:
361 replyinfo_obj.failed_decrypt = True
362 return
363
364 replyinfo_obj.success_decrypt = True
365
366 # we already have a key (and a message)
367 if replyinfo_obj.target_key != None:
368 return
369
370 if piece.gpg_data.sigs != []:
371 replyinfo_obj.target_key = piece.gpg_data.sigs[0]
372 get_signed_part = False
373 else:
374 # only include a signed message in the reply.
375 get_signed_part = True
376
377 replyinfo_obj.msg_to_quote = flatten_decrypted_payloads(piece.gpg_data.plainobj, get_signed_part)
378
379 # to catch public keys in encrypted blocks
380 prepare_for_reply(piece.gpg_data.plainobj, replyinfo_obj)
381
382
383 def prepare_for_reply_pubkey (piece, replyinfo_obj):
384
385 if piece.gpg_data == None or piece.gpg_data.keys == []:
386 replyinfo_obj.no_public_key = True
387 else:
388 replyinfo_obj.public_key_received = True
389
390 if replyinfo_obj.fallback_target_key == None:
391 replyinfo_obj.fallback_target_key = piece.gpg_data.keys[0]
392
393
394 def prepare_for_reply_sig (piece, replyinfo_obj):
395
396 if piece.gpg_data == None or piece.gpg_data.sigs == []:
397 replyinfo_obj.sig_failure = True
398 else:
399 replyinfo_obj.sig_success = True
400
401 if replyinfo_obj.fallback_target_key == None:
402 replyinfo_obj.fallback_target_key = piece.gpg_data.sigs[0]
403
404
405
406 def flatten_decrypted_payloads (eddymsg_obj, get_signed_part):
407
408 flat_string = ""
409
410 if eddymsg_obj == None:
411 return ""
412
413 # recurse on multi-part mime
414 if eddymsg_obj.multipart == True:
415 for sub in eddymsg_obj.subparts:
416 flat_string += flatten_decrypted_payloads (sub, get_signed_part)
417
418 return flat_string
419
420 for piece in eddymsg_obj.payload_pieces:
421 if piece.piece_type == "text":
422 flat_string += piece.string
423
424 if (get_signed_part):
425 # don't include nested encryption
426 if (piece.piece_type == "message") \
427 and (piece.gpg_data != None) \
428 and (piece.gpg_data.decrypted == False):
429 flat_string += flatten_decrypted_payloads(piece.gpg_data.plainobj, get_signed_part)
430
431 elif ((piece.piece_type == "clearsign") \
432 or (piece.piece_type == "detachedsig") \
433 or (piece.piece_type == "signature")) \
434 and (piece.gpg_data != None):
435 # FIXME: the key used to sign this message needs to be the one that is used for the encrypted reply.
436 flat_string += flatten_decrypted_payloads (piece.gpg_data.plainobj, get_signed_part)
437
438 return flat_string
439
440
441 def get_key_from_fp (replyinfo_obj, gpgme_ctx):
442
443 if replyinfo_obj.target_key == None:
444 replyinfo_obj.target_key = replyinfo_obj.fallback_target_key
445
446 if replyinfo_obj.target_key != None:
447 try:
448 encrypt_to_key = gpgme_ctx.get_key(replyinfo_obj.target_key)
449 return encrypt_to_key
450
451 except:
452 pass
453
454 # no available key to use
455 replyinfo_obj.target_key = None
456 replyinfo_obj.fallback_target_key = None
457
458 replyinfo_obj.no_public_key = True
459 replyinfo_obj.public_key_received = False
460
461 return None
462
463
464 def write_reply (replyinfo_obj):
465
466 reply_plain = ""
467
468 if replyinfo_obj.success_decrypt == True:
469 reply_plain += replyinfo_obj.replies['success_decrypt']
470
471 if replyinfo_obj.no_public_key == False:
472 quoted_text = email_quote_text(replyinfo_obj.msg_to_quote)
473 reply_plain += quoted_text
474
475 elif replyinfo_obj.failed_decrypt == True:
476 reply_plain += replyinfo_obj.replies['failed_decrypt']
477
478
479 if replyinfo_obj.sig_success == True:
480 reply_plain += "\n\n"
481 reply_plain += replyinfo_obj.replies['sig_success']
482
483 elif replyinfo_obj.sig_failure == True:
484 reply_plain += "\n\n"
485 reply_plain += replyinfo_obj.replies['sig_failure']
486
487
488 if replyinfo_obj.public_key_received == True:
489 reply_plain += "\n\n"
490 reply_plain += replyinfo_obj.replies['public_key_received']
491
492 elif replyinfo_obj.no_public_key == True:
493 reply_plain += "\n\n"
494 reply_plain += replyinfo_obj.replies['no_public_key']
495
496
497 reply_plain += "\n\n"
498 reply_plain += replyinfo_obj.replies['signature']
499
500 return reply_plain
501
502
503 def add_gpg_key (key_block, gpgme_ctx):
504
505 fp = io.BytesIO(key_block.encode('ascii'))
506
507 result = gpgme_ctx.import_(fp)
508 imports = result.imports
509
510 key_fingerprints = []
511
512 if imports != []:
513 for import_ in imports:
514 fingerprint = import_[0]
515 key_fingerprints += [fingerprint]
516
517 debug("added gpg key: " + fingerprint)
518
519 return key_fingerprints
520
521
522 def verify_sig_message (msg_block, gpgme_ctx):
523
524 block_b = io.BytesIO(msg_block.encode('ascii'))
525 plain_b = io.BytesIO()
526
527 try:
528 sigs = gpgme_ctx.verify(block_b, None, plain_b)
529 except:
530 return ("",[])
531
532 plaintext = plain_b.getvalue().decode('utf-8')
533
534 fingerprints = []
535 for sig in sigs:
536 fingerprints += [sig.fpr]
537 return (plaintext, fingerprints)
538
539
540 def verify_clear_signature (sig_block, gpgme_ctx):
541
542 # FIXME: this might require the un-decoded bytes
543 # or the correct re-encoding with the carset of the mime part.
544 msg_fp = io.BytesIO(sig_block.encode('utf-8'))
545 ptxt_fp = io.BytesIO()
546
547 result = gpgme_ctx.verify(msg_fp, None, ptxt_fp)
548
549 # FIXME: this might require using the charset of the mime part.
550 plaintext = ptxt_fp.getvalue().decode('utf-8')
551
552 sig_fingerprints = []
553 for res_ in result:
554 sig_fingerprints += [res_.fpr]
555
556 return plaintext, sig_fingerprints
557
558
559 def verify_detached_signature (detached_sig, plaintext_bytes, gpgme_ctx):
560
561 detached_sig_fp = io.BytesIO(detached_sig.encode('ascii'))
562 plaintext_fp = io.BytesIO(plaintext_bytes)
563 ptxt_fp = io.BytesIO()
564
565 result = gpgme_ctx.verify(detached_sig_fp, plaintext_fp, None)
566
567 sig_fingerprints = []
568 for res_ in result:
569 sig_fingerprints += [res_.fpr]
570
571 return sig_fingerprints
572
573
574 def decrypt_block (msg_block, gpgme_ctx):
575
576 block_b = io.BytesIO(msg_block.encode('ascii'))
577 plain_b = io.BytesIO()
578
579 try:
580 sigs = gpgme_ctx.decrypt_verify(block_b, plain_b)
581 except:
582 return ("",[])
583
584 plaintext = plain_b.getvalue().decode('utf-8')
585
586 fingerprints = []
587 for sig in sigs:
588 fingerprints += [sig.fpr]
589 return (plaintext, fingerprints)
590
591
592 def choose_reply_encryption_key (gpgme_ctx, fingerprints):
593
594 reply_key = None
595 for fp in fingerprints:
596 try:
597 key = gpgme_ctx.get_key(fp)
598
599 if (key.can_encrypt == True):
600 reply_key = key
601 break
602 except:
603 continue
604
605
606 return reply_key
607
608
609 def email_to_from_subject (email_text):
610
611 email_struct = email.parser.Parser().parsestr(email_text)
612
613 email_to = email_struct['To']
614 email_from = email_struct['From']
615 email_subject = email_struct['Subject']
616
617 return email_to, email_from, email_subject
618
619
620 def import_lang(email_to):
621
622 if email_to != None:
623 for lang in langs:
624 if "edward-" + lang in email_to:
625 lang = "lang." + re.sub('-', '_', lang)
626 language = importlib.import_module(lang)
627
628 return language
629
630 return importlib.import_module("lang.en")
631
632
633 def generate_encrypted_mime (plaintext, email_from, email_subject, encrypt_to_key,
634 gpgme_ctx):
635
636 # quoted printable encoding lets most ascii characters look normal
637 # before the mime message is decoded.
638 char_set = email.charset.Charset("utf-8")
639 char_set.body_encoding = email.charset.QP
640
641 # MIMEText doesn't allow setting the text encoding
642 # so we use MIMENonMultipart.
643 plaintext_mime = MIMENonMultipart('text', 'plain')
644 plaintext_mime.set_payload(plaintext, charset=char_set)
645
646 if (encrypt_to_key != None):
647
648 encrypted_text = encrypt_sign_message(plaintext_mime.as_string(),
649 encrypt_to_key,
650 gpgme_ctx)
651
652 control_mime = MIMEApplication("Version: 1",
653 _subtype='pgp-encrypted',
654 _encoder=email.encoders.encode_7or8bit)
655 control_mime['Content-Description'] = 'PGP/MIME version identification'
656 control_mime.set_charset('us-ascii')
657
658 encoded_mime = MIMEApplication(encrypted_text,
659 _subtype='octet-stream; name="encrypted.asc"',
660 _encoder=email.encoders.encode_7or8bit)
661 encoded_mime['Content-Description'] = 'OpenPGP encrypted message'
662 encoded_mime['Content-Disposition'] = 'inline; filename="encrypted.asc"'
663 encoded_mime.set_charset('us-ascii')
664
665 message_mime = MIMEMultipart(_subtype="encrypted", protocol="application/pgp-encrypted")
666 message_mime.attach(control_mime)
667 message_mime.attach(encoded_mime)
668 message_mime['Content-Disposition'] = 'inline'
669
670 else:
671 message_mime = plaintext_mime
672
673 message_mime['To'] = email_from
674 message_mime['Subject'] = email_subject
675
676 reply = message_mime.as_string()
677
678 return reply
679
680
681 def email_quote_text (text):
682
683 quoted_message = re.sub(r'^', r'> ', text, flags=re.MULTILINE)
684
685 return quoted_message
686
687
688 def encrypt_sign_message (plaintext, encrypt_to_key, gpgme_ctx):
689
690 plaintext_bytes = io.BytesIO(plaintext.encode('ascii'))
691 encrypted_bytes = io.BytesIO()
692
693 gpgme_ctx.encrypt_sign([encrypt_to_key], gpgme.ENCRYPT_ALWAYS_TRUST,
694 plaintext_bytes, encrypted_bytes)
695
696 encrypted_txt = encrypted_bytes.getvalue().decode('ascii')
697 return encrypted_txt
698
699
700 def error (error_msg):
701
702 sys.stderr.write(progname + ": " + str(error_msg) + "\n")
703
704
705 def debug (debug_msg):
706
707 if edward_config.debug == True:
708 error(debug_msg)
709
710
711 def handle_args ():
712 if __name__ == "__main__":
713
714 global progname
715 progname = sys.argv[0]
716
717 if len(sys.argv) > 1:
718 print(progname + ": error, this program doesn't " \
719 "need any arguments.", file=sys.stderr)
720 exit(1)
721
722
723 main()
724