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