59d648244eb7e427103f499742b72eb39afa5ffb
[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 = ["an", "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
294 elif piece.piece_type == "pubkey":
295 key_fps = add_gpg_key(piece.string, gpgme_ctx)
296
297 if key_fps != []:
298 piece.gpg_data = GPGData()
299 piece.gpg_data.keys = key_fps
300
301 elif piece.piece_type == "clearsign":
302 (plaintext, sig_fps) = verify_clear_signature(piece.string, gpgme_ctx)
303
304 if sig_fps != []:
305 piece.gpg_data = GPGData()
306 piece.gpg_data.sigs = sig_fps
307 piece.gpg_data.plainobj = parse_pgp_mime(plaintext, gpgme_ctx)
308
309 elif piece.piece_type == "detachedsig":
310 for prev in prev_parts:
311 payload_bytes = prev.payload_bytes
312 sig_fps = verify_detached_signature(piece.string, payload_bytes, gpgme_ctx)
313
314 if sig_fps != []:
315 piece.gpg_data = GPGData()
316 piece.gpg_data.sigs = sig_fps
317 piece.gpg_data.plainobj = prev
318 break
319
320 else:
321 pass
322
323
324 def prepare_for_reply (eddymsg_obj, replyinfo_obj):
325
326 do_to_eddys_pieces(prepare_for_reply_pieces, eddymsg_obj, replyinfo_obj)
327
328 def prepare_for_reply_pieces (eddymsg_obj, replyinfo_obj):
329
330 for piece in eddymsg_obj.payload_pieces:
331 if piece.piece_type == "text":
332 # don't quote the plaintext part.
333 pass
334
335 elif piece.piece_type == "message":
336 prepare_for_reply_message(piece, replyinfo_obj)
337
338 elif piece.piece_type == "pubkey":
339 prepare_for_reply_pubkey(piece, replyinfo_obj)
340
341 elif (piece.piece_type == "clearsign") \
342 or (piece.piece_type == "detachedsig"):
343 prepare_for_reply_sig(piece, replyinfo_obj)
344
345
346 def prepare_for_reply_message (piece, replyinfo_obj):
347
348 if piece.gpg_data == None:
349 replyinfo_obj.failed_decrypt = True
350 return
351
352 if piece.gpg_data.decrypted == False:
353 prepare_for_reply_sig(piece, replyinfo_obj)
354 return
355 else:
356 replyinfo_obj.success_decrypt = True
357
358 # we already have a key (and a message)
359 if replyinfo_obj.target_key != None:
360 return
361
362 if piece.gpg_data.sigs != []:
363 replyinfo_obj.target_key = piece.gpg_data.sigs[0]
364 get_signed_part = False
365 else:
366 # only include a signed message in the reply.
367 get_signed_part = True
368
369 replyinfo_obj.msg_to_quote = flatten_decrypted_payloads(piece.gpg_data.plainobj, get_signed_part)
370
371 # to catch public keys in encrypted blocks
372 prepare_for_reply(piece.gpg_data.plainobj, replyinfo_obj)
373
374
375 def prepare_for_reply_pubkey (piece, replyinfo_obj):
376
377 if piece.gpg_data == None or piece.gpg_data.keys == []:
378 replyinfo_obj.no_public_key = True
379 else:
380 replyinfo_obj.public_key_received = True
381
382 if replyinfo_obj.fallback_target_key == None:
383 replyinfo_obj.fallback_target_key = piece.gpg_data.keys[0]
384
385
386 def prepare_for_reply_sig (piece, replyinfo_obj):
387
388 if piece.gpg_data == None or piece.gpg_data.sigs == []:
389 replyinfo_obj.sig_failure = True
390 else:
391 replyinfo_obj.sig_success = True
392
393 if replyinfo_obj.fallback_target_key == None:
394 replyinfo_obj.fallback_target_key = piece.gpg_data.sigs[0]
395
396
397
398 def flatten_decrypted_payloads (eddymsg_obj, get_signed_part):
399
400 flat_string = ""
401
402 if eddymsg_obj == None:
403 return ""
404
405 # recurse on multi-part mime
406 if eddymsg_obj.multipart == True:
407 for sub in eddymsg_obj.subparts:
408 flat_string += flatten_decrypted_payloads (sub, get_signed_part)
409
410 return flat_string
411
412 for piece in eddymsg_obj.payload_pieces:
413 if piece.piece_type == "text":
414 flat_string += piece.string
415
416 if (get_signed_part):
417 # don't include nested encryption
418 if (piece.piece_type == "message") \
419 and (piece.gpg_data != None) \
420 and (piece.gpg_data.decrypted == False):
421 flat_string += flatten_decrypted_payloads(piece.gpg_data.plainobj, get_signed_part)
422
423 elif ((piece.piece_type == "clearsign") \
424 or (piece.piece_type == "detachedsig")) \
425 and (piece.gpg_data != None):
426 # FIXME: the key used to sign this message needs to be the one that is used for the encrypted reply.
427 flat_string += flatten_decrypted_payloads (piece.gpg_data.plainobj, get_signed_part)
428
429 return flat_string
430
431
432 def get_key_from_fp (replyinfo_obj, gpgme_ctx):
433
434 if replyinfo_obj.target_key == None:
435 replyinfo_obj.target_key = replyinfo_obj.fallback_target_key
436
437 if replyinfo_obj.target_key != None:
438 try:
439 encrypt_to_key = gpgme_ctx.get_key(replyinfo_obj.target_key)
440 return encrypt_to_key
441
442 except:
443 pass
444
445 # no available key to use
446 replyinfo_obj.target_key = None
447 replyinfo_obj.fallback_target_key = None
448
449 replyinfo_obj.no_public_key = True
450 replyinfo_obj.public_key_received = False
451
452 return None
453
454
455 def write_reply (replyinfo_obj):
456
457 reply_plain = ""
458
459 if replyinfo_obj.success_decrypt == True:
460 reply_plain += replyinfo_obj.replies['success_decrypt']
461
462 if replyinfo_obj.no_public_key == False:
463 quoted_text = email_quote_text(replyinfo_obj.msg_to_quote)
464 reply_plain += quoted_text
465
466 elif replyinfo_obj.failed_decrypt == True:
467 reply_plain += replyinfo_obj.replies['failed_decrypt']
468
469
470 if replyinfo_obj.sig_success == True:
471 reply_plain += "\n\n"
472 reply_plain += replyinfo_obj.replies['sig_success']
473
474 elif replyinfo_obj.sig_failure == True:
475 reply_plain += "\n\n"
476 reply_plain += replyinfo_obj.replies['sig_failure']
477
478
479 if replyinfo_obj.public_key_received == True:
480 reply_plain += "\n\n"
481 reply_plain += replyinfo_obj.replies['public_key_received']
482
483 elif replyinfo_obj.no_public_key == True:
484 reply_plain += "\n\n"
485 reply_plain += replyinfo_obj.replies['no_public_key']
486
487
488 reply_plain += "\n\n"
489 reply_plain += replyinfo_obj.replies['signature']
490
491 return reply_plain
492
493
494 def add_gpg_key (key_block, gpgme_ctx):
495
496 fp = io.BytesIO(key_block.encode('ascii'))
497
498 result = gpgme_ctx.import_(fp)
499 imports = result.imports
500
501 key_fingerprints = []
502
503 if imports != []:
504 for import_ in imports:
505 fingerprint = import_[0]
506 key_fingerprints += [fingerprint]
507
508 debug("added gpg key: " + fingerprint)
509
510 return key_fingerprints
511
512
513 def verify_clear_signature (sig_block, gpgme_ctx):
514
515 # FIXME: this might require the un-decoded bytes
516 # or the correct re-encoding with the carset of the mime part.
517 msg_fp = io.BytesIO(sig_block.encode('utf-8'))
518 ptxt_fp = io.BytesIO()
519
520 result = gpgme_ctx.verify(msg_fp, None, ptxt_fp)
521
522 # FIXME: this might require using the charset of the mime part.
523 plaintext = ptxt_fp.getvalue().decode('utf-8')
524
525 sig_fingerprints = []
526 for res_ in result:
527 sig_fingerprints += [res_.fpr]
528
529 return plaintext, sig_fingerprints
530
531
532 def verify_detached_signature (detached_sig, plaintext_bytes, gpgme_ctx):
533
534 detached_sig_fp = io.BytesIO(detached_sig.encode('ascii'))
535 plaintext_fp = io.BytesIO(plaintext_bytes)
536 ptxt_fp = io.BytesIO()
537
538 result = gpgme_ctx.verify(detached_sig_fp, plaintext_fp, None)
539
540 sig_fingerprints = []
541 for res_ in result:
542 sig_fingerprints += [res_.fpr]
543
544 return sig_fingerprints
545
546
547 def decrypt_block (msg_block, gpgme_ctx):
548
549 block_b = io.BytesIO(msg_block.encode('ascii'))
550 plain_b = io.BytesIO()
551
552 try:
553 sigs = gpgme_ctx.decrypt_verify(block_b, plain_b)
554 except:
555 return ("",[])
556
557 plaintext = plain_b.getvalue().decode('utf-8')
558
559 fingerprints = []
560 for sig in sigs:
561 fingerprints += [sig.fpr]
562 return (plaintext, fingerprints)
563
564
565 def choose_reply_encryption_key (gpgme_ctx, fingerprints):
566
567 reply_key = None
568 for fp in fingerprints:
569 try:
570 key = gpgme_ctx.get_key(fp)
571
572 if (key.can_encrypt == True):
573 reply_key = key
574 break
575 except:
576 continue
577
578
579 return reply_key
580
581
582 def email_to_from_subject (email_text):
583
584 email_struct = email.parser.Parser().parsestr(email_text)
585
586 email_to = email_struct['To']
587 email_from = email_struct['From']
588 email_subject = email_struct['Subject']
589
590 return email_to, email_from, email_subject
591
592
593 def import_lang(email_to):
594
595 if email_to != None:
596 for lang in langs:
597 if "edward-" + lang in email_to:
598 lang = "lang." + re.sub('-', '_', lang)
599 language = importlib.import_module(lang)
600
601 return language
602
603 return importlib.import_module("lang.en")
604
605
606 def generate_encrypted_mime (plaintext, email_from, email_subject, encrypt_to_key,
607 gpgme_ctx):
608
609 # quoted printable encoding lets most ascii characters look normal
610 # before the decrypted mime message is decoded.
611 char_set = email.charset.Charset("utf-8")
612 char_set.body_encoding = email.charset.QP
613
614 # MIMEText doesn't allow setting the text encoding
615 # so we use MIMENonMultipart.
616 plaintext_mime = MIMENonMultipart('text', 'plain')
617 plaintext_mime.set_payload(plaintext, charset=char_set)
618
619 if (encrypt_to_key != None):
620
621 encrypted_text = encrypt_sign_message(plaintext_mime.as_string(),
622 encrypt_to_key,
623 gpgme_ctx)
624
625 control_mime = MIMEApplication("Version: 1",
626 _subtype='pgp-encrypted',
627 _encoder=email.encoders.encode_7or8bit)
628 control_mime['Content-Description'] = 'PGP/MIME version identification'
629 control_mime.set_charset('us-ascii')
630
631 encoded_mime = MIMEApplication(encrypted_text,
632 _subtype='octet-stream; name="encrypted.asc"',
633 _encoder=email.encoders.encode_7or8bit)
634 encoded_mime['Content-Description'] = 'OpenPGP encrypted message'
635 encoded_mime['Content-Disposition'] = 'inline; filename="encrypted.asc"'
636 encoded_mime.set_charset('us-ascii')
637
638 message_mime = MIMEMultipart(_subtype="encrypted", protocol="application/pgp-encrypted")
639 message_mime.attach(control_mime)
640 message_mime.attach(encoded_mime)
641 message_mime['Content-Disposition'] = 'inline'
642
643 else:
644 message_mime = plaintext_mime
645
646 message_mime['To'] = email_from
647 message_mime['Subject'] = email_subject
648
649 reply = message_mime.as_string()
650
651 return reply
652
653
654 def email_quote_text (text):
655
656 quoted_message = re.sub(r'^', r'> ', text, flags=re.MULTILINE)
657
658 return quoted_message
659
660
661 def encrypt_sign_message (plaintext, encrypt_to_key, gpgme_ctx):
662
663 plaintext_bytes = io.BytesIO(plaintext.encode('ascii'))
664 encrypted_bytes = io.BytesIO()
665
666 gpgme_ctx.encrypt_sign([encrypt_to_key], gpgme.ENCRYPT_ALWAYS_TRUST,
667 plaintext_bytes, encrypted_bytes)
668
669 encrypted_txt = encrypted_bytes.getvalue().decode('ascii')
670 return encrypted_txt
671
672
673 def error (error_msg):
674
675 sys.stderr.write(progname + ": " + str(error_msg) + "\n")
676
677
678 def debug (debug_msg):
679
680 if edward_config.debug == True:
681 error(debug_msg)
682
683
684 def handle_args ():
685 if __name__ == "__main__":
686
687 global progname
688 progname = sys.argv[0]
689
690 if len(sys.argv) > 1:
691 print(progname + ": error, this program doesn't " \
692 "need any arguments.", file=sys.stderr)
693 exit(1)
694
695
696 main()
697