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