ca8d4f7594a502cc2dbe0ef4bbe970788458a97d
[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 quoted_text = email_quote_text(replyinfo_obj.msg_to_quote)
427 reply_plain += replyinfo_obj.replies['success_decrypt']
428 reply_plain += quoted_text
429
430 elif replyinfo_obj.failed_decrypt == True:
431 reply_plain += replyinfo_obj.replies['failed_decrypt']
432
433
434 if replyinfo_obj.sig_success == True:
435 reply_plain += "\n\n"
436 reply_plain += replyinfo_obj.replies['sig_success']
437
438 elif replyinfo_obj.sig_failure == True:
439 reply_plain += "\n\n"
440 reply_plain += replyinfo_obj.replies['sig_failure']
441
442
443 if replyinfo_obj.public_key_received == True:
444 reply_plain += "\n\n"
445 reply_plain += replyinfo_obj.replies['public_key_received']
446
447 elif replyinfo_obj.no_public_key == True:
448 reply_plain += "\n\n"
449 reply_plain += replyinfo_obj.replies['no_public_key']
450
451
452 reply_plain += "\n\n"
453 reply_plain += replyinfo_obj.replies['signature']
454
455 return reply_plain
456
457
458 def add_gpg_key (key_block, gpgme_ctx):
459
460 fp = io.BytesIO(key_block.encode('ascii'))
461
462 result = gpgme_ctx.import_(fp)
463 imports = result.imports
464
465 key_fingerprints = []
466
467 if imports != []:
468 for import_ in imports:
469 fingerprint = import_[0]
470 key_fingerprints += [fingerprint]
471
472 debug("added gpg key: " + fingerprint)
473
474 return key_fingerprints
475
476
477 def verify_clear_signature (sig_block, gpgme_ctx):
478
479 # FIXME: this might require the un-decoded bytes
480 # or the correct re-encoding with the carset of the mime part.
481 msg_fp = io.BytesIO(sig_block.encode('utf-8'))
482 ptxt_fp = io.BytesIO()
483
484 result = gpgme_ctx.verify(msg_fp, None, ptxt_fp)
485
486 # FIXME: this might require using the charset of the mime part.
487 plaintext = ptxt_fp.getvalue().decode('utf-8')
488
489 sig_fingerprints = []
490 for res_ in result:
491 sig_fingerprints += [res_.fpr]
492
493 return plaintext, sig_fingerprints
494
495
496 def verify_detached_signature (detached_sig, plaintext_bytes, gpgme_ctx):
497
498 detached_sig_fp = io.BytesIO(detached_sig.encode('ascii'))
499 plaintext_fp = io.BytesIO(plaintext_bytes)
500 ptxt_fp = io.BytesIO()
501
502 result = gpgme_ctx.verify(detached_sig_fp, plaintext_fp, None)
503
504 sig_fingerprints = []
505 for res_ in result:
506 sig_fingerprints += [res_.fpr]
507
508 return sig_fingerprints
509
510
511 def decrypt_block (msg_block, gpgme_ctx):
512
513 block_b = io.BytesIO(msg_block.encode('ascii'))
514 plain_b = io.BytesIO()
515
516 try:
517 sigs = gpgme_ctx.decrypt_verify(block_b, plain_b)
518 except:
519 return ("",[])
520
521 plaintext = plain_b.getvalue().decode('utf-8')
522
523 fingerprints = []
524 for sig in sigs:
525 fingerprints += [sig.fpr]
526 return (plaintext, fingerprints)
527
528
529 def choose_reply_encryption_key (gpgme_ctx, fingerprints):
530
531 reply_key = None
532 for fp in fingerprints:
533 try:
534 key = gpgme_ctx.get_key(fp)
535
536 if (key.can_encrypt == True):
537 reply_key = key
538 break
539 except:
540 continue
541
542
543 return reply_key
544
545
546 def email_to_from_subject (email_text):
547
548 email_struct = email.parser.Parser().parsestr(email_text)
549
550 email_to = email_struct['To']
551 email_from = email_struct['From']
552 email_subject = email_struct['Subject']
553
554 return email_to, email_from, email_subject
555
556
557 def import_lang(email_to):
558
559 if email_to != None:
560 for lang in langs:
561 if "edward-" + lang in email_to:
562 lang = "lang." + re.sub('-', '_', lang)
563 language = importlib.import_module(lang)
564
565 return language
566
567 return importlib.import_module("lang.en")
568
569
570 def generate_encrypted_mime (plaintext, email_from, email_subject, encrypt_to_key,
571 gpgme_ctx):
572
573
574 reply = "To: " + email_from + "\n"
575 reply += "Subject: " + email_subject + "\n"
576
577 if (encrypt_to_key != None):
578 plaintext_reply = "thanks for the message!\n\n\n"
579 plaintext_reply += email_quote_text(plaintext)
580
581 # quoted printable encoding lets most ascii characters look normal
582 # before the decrypted mime message is decoded.
583 char_set = email.charset.Charset("utf-8")
584 char_set.body_encoding = email.charset.QP
585
586 # MIMEText doesn't allow setting the text encoding
587 # so we use MIMENonMultipart.
588 plaintext_mime = MIMENonMultipart('text', 'plain')
589 plaintext_mime.set_payload(plaintext_reply, charset=char_set)
590
591 encrypted_text = encrypt_sign_message(plaintext_mime.as_string(),
592 encrypt_to_key,
593 gpgme_ctx)
594
595 control_mime = MIMEApplication("Version: 1",
596 _subtype='pgp-encrypted',
597 _encoder=email.encoders.encode_7or8bit)
598 control_mime['Content-Description'] = 'PGP/MIME version identification'
599 control_mime.set_charset('us-ascii')
600
601 encoded_mime = MIMEApplication(encrypted_text,
602 _subtype='octet-stream; name="encrypted.asc"',
603 _encoder=email.encoders.encode_7or8bit)
604 encoded_mime['Content-Description'] = 'OpenPGP encrypted message'
605 encoded_mime['Content-Disposition'] = 'inline; filename="encrypted.asc"'
606 encoded_mime.set_charset('us-ascii')
607
608 message_mime = MIMEMultipart(_subtype="encrypted", protocol="application/pgp-encrypted")
609 message_mime.attach(control_mime)
610 message_mime.attach(encoded_mime)
611 message_mime['Content-Disposition'] = 'inline'
612
613 reply += message_mime.as_string()
614
615 else:
616 reply += "\n"
617 reply += "Sorry, i couldn't find your key.\n"
618 reply += "I'll need that to encrypt a message to you."
619
620 return reply
621
622
623 def email_quote_text (text):
624
625 quoted_message = re.sub(r'^', r'> ', text, flags=re.MULTILINE)
626
627 return quoted_message
628
629
630 def encrypt_sign_message (plaintext, encrypt_to_key, gpgme_ctx):
631
632 plaintext_bytes = io.BytesIO(plaintext.encode('ascii'))
633 encrypted_bytes = io.BytesIO()
634
635 gpgme_ctx.encrypt_sign([encrypt_to_key], gpgme.ENCRYPT_ALWAYS_TRUST,
636 plaintext_bytes, encrypted_bytes)
637
638 encrypted_txt = encrypted_bytes.getvalue().decode('ascii')
639 return encrypted_txt
640
641
642 def error (error_msg):
643
644 sys.stderr.write(progname + ": " + str(error_msg) + "\n")
645
646
647 def debug (debug_msg):
648
649 if edward_config.debug == True:
650 error(debug_msg)
651
652
653 def handle_args ():
654 if __name__ == "__main__":
655
656 global progname
657 progname = sys.argv[0]
658
659 if len(sys.argv) > 1:
660 print(progname + ": error, this program doesn't " \
661 "need any arguments.", file=sys.stderr)
662 exit(1)
663
664
665 main()
666