add pubkey as a fallback 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 # TODO error handling for missing keys and setting .no_public_key
129 reply_mime = generate_encrypted_mime(reply_plaintext, email_from, \
130 email_subject, encrypt_to_key,
131 gpgme_ctx)
132
133 print(reply_mime)
134
135
136 def get_gpg_context (gnupghome, sign_with_key_fp):
137
138 os.environ['GNUPGHOME'] = gnupghome
139
140 gpgme_ctx = gpgme.Context()
141 gpgme_ctx.armor = True
142
143 try:
144 sign_with_key = gpgme_ctx.get_key(sign_with_key_fp)
145 except:
146 error("unable to load signing key. is the gnupghome "
147 + "and signing key properly set in the edward_config.py?")
148 exit(1)
149
150 gpgme_ctx.signers = [sign_with_key]
151
152 return gpgme_ctx
153
154
155 def parse_pgp_mime (email_text, gpgme_ctx):
156
157 email_struct = email.parser.Parser().parsestr(email_text)
158
159 eddymsg_obj = parse_mime(email_struct)
160 split_payloads(eddymsg_obj)
161 gpg_on_payloads(eddymsg_obj, gpgme_ctx)
162
163 return eddymsg_obj
164
165
166 def parse_mime(msg_struct):
167
168 eddymsg_obj = EddyMsg()
169
170 if msg_struct.is_multipart() == True:
171 payloads = msg_struct.get_payload()
172
173 eddymsg_obj.multipart = True
174 eddymsg_obj.subparts = list(map(parse_mime, payloads))
175
176 else:
177 eddymsg_obj = get_subpart_data(msg_struct)
178
179 return eddymsg_obj
180
181
182 def scan_and_split (payload_piece, match_type, pattern):
183
184 # don't try to re-split pieces containing gpg data
185 if payload_piece.piece_type != "text":
186 return [payload_piece]
187
188 flags = re.DOTALL | re.MULTILINE
189 matches = re.search("(?P<beginning>.*?)(?P<match>" + pattern +
190 ")(?P<rest>.*)", payload_piece.string, flags=flags)
191
192 if matches == None:
193 pieces = [payload_piece]
194
195 else:
196
197 beginning = PayloadPiece()
198 beginning.string = matches.group('beginning')
199 beginning.piece_type = payload_piece.piece_type
200
201 match = PayloadPiece()
202 match.string = matches.group('match')
203 match.piece_type = match_type
204
205 rest = PayloadPiece()
206 rest.string = matches.group('rest')
207 rest.piece_type = payload_piece.piece_type
208
209 more_pieces = scan_and_split(rest, match_type, pattern)
210 pieces = [beginning, match ] + more_pieces
211
212 return pieces
213
214
215 def get_subpart_data (part):
216
217 obj = EddyMsg()
218
219 obj.charset = part.get_content_charset()
220 obj.payload_bytes = part.get_payload(decode=True)
221
222 obj.filename = part.get_filename()
223 obj.content_type = part.get_content_type()
224 obj.description_list = part['content-description']
225
226 # your guess is as good as a-myy-ee-ine...
227 if obj.charset == None:
228 obj.charset = 'utf-8'
229
230 if obj.payload_bytes != None:
231 try:
232 payload = PayloadPiece()
233 payload.string = obj.payload_bytes.decode(obj.charset)
234 payload.piece_type = 'text'
235
236 obj.payload_pieces = [payload]
237 except UnicodeDecodeError:
238 pass
239
240 return obj
241
242
243 def do_to_eddys_pieces (function_to_do, eddymsg_obj, data):
244
245 if eddymsg_obj.multipart == True:
246 for sub in eddymsg_obj.subparts:
247 do_to_eddys_pieces(function_to_do, sub, data)
248 else:
249 function_to_do(eddymsg_obj, data)
250
251
252 def split_payloads (eddymsg_obj):
253
254 for match_type in match_types:
255 do_to_eddys_pieces(split_payload_pieces, eddymsg_obj, match_type)
256
257
258 def split_payload_pieces (eddymsg_obj, match_type):
259
260 (match_name, pattern) = match_type
261
262 new_pieces_list = []
263 for piece in eddymsg_obj.payload_pieces:
264 new_pieces_list += scan_and_split(piece, match_name, pattern)
265
266 eddymsg_obj.payload_pieces = new_pieces_list
267
268
269 def gpg_on_payloads (eddymsg_obj, gpgme_ctx, prev_parts=[]):
270
271 if eddymsg_obj.multipart == True:
272 prev_parts=[]
273 for sub in eddymsg_obj.subparts:
274 gpg_on_payloads (sub, gpgme_ctx, prev_parts)
275 prev_parts += [sub]
276
277 return
278
279 for piece in eddymsg_obj.payload_pieces:
280
281 if piece.piece_type == "text":
282 # don't transform the plaintext.
283 pass
284
285 elif piece.piece_type == "message":
286 (plaintext, sigs) = decrypt_block(piece.string, gpgme_ctx)
287
288 if plaintext:
289 piece.gpg_data = GPGData()
290 piece.gpg_data.decrypted = True
291 piece.gpg_data.sigs = sigs
292 # recurse!
293 piece.gpg_data.plainobj = parse_pgp_mime(plaintext, gpgme_ctx)
294
295 elif piece.piece_type == "pubkey":
296 key_fps = add_gpg_key(piece.string, gpgme_ctx)
297
298 if key_fps != []:
299 piece.gpg_data = GPGData()
300 piece.gpg_data.keys = key_fps
301
302 elif piece.piece_type == "clearsign":
303 (plaintext, sig_fps) = verify_clear_signature(piece.string, gpgme_ctx)
304
305 if sig_fps != []:
306 piece.gpg_data = GPGData()
307 piece.gpg_data.sigs = sig_fps
308 piece.gpg_data.plainobj = parse_pgp_mime(plaintext, gpgme_ctx)
309
310 elif piece.piece_type == "detachedsig":
311 for prev in prev_parts:
312 payload_bytes = prev.payload_bytes
313 sig_fps = verify_detached_signature(piece.string, payload_bytes, gpgme_ctx)
314
315 if sig_fps != []:
316 piece.gpg_data = GPGData()
317 piece.gpg_data.sigs = sig_fps
318 piece.gpg_data.plainobj = prev
319 break
320
321 else:
322 pass
323
324
325 def prepare_for_reply (eddymsg_obj, replyinfo_obj):
326
327 do_to_eddys_pieces(prepare_for_reply_pieces, eddymsg_obj, replyinfo_obj)
328
329 def prepare_for_reply_pieces (eddymsg_obj, replyinfo_obj):
330
331 for piece in eddymsg_obj.payload_pieces:
332 if piece.piece_type == "text":
333 # don't quote the plaintext part.
334 pass
335
336 elif piece.piece_type == "message":
337 if piece.gpg_data == None:
338 replyinfo_obj.failed_decrypt = True
339 else:
340 replyinfo_obj.success_decrypt = True
341
342 if replyinfo_obj.target_key != None:
343 continue
344 if piece.gpg_data.sigs != []:
345 replyinfo_obj.target_key = piece.gpg_data.sigs[0]
346 replyinfo_obj.msg_to_quote = flatten_payloads(piece.gpg_data.plainobj)
347
348 # to catch public keys in encrypted blocks
349 prepare_for_reply(piece.gpg_data.plainobj, replyinfo_obj)
350
351 elif piece.piece_type == "pubkey":
352 if piece.gpg_data == None:
353 replyinfo_obj.no_public_key = True
354 else:
355 replyinfo_obj.public_key_received = True
356
357 if replyinfo_obj.fallback_target_key == None:
358 replyinfo_obj.fallback_target_key = piece.gpg_data.keys[0]
359
360 elif (piece.piece_type == "clearsign") \
361 or (piece.piece_type == "detachedsig"):
362 if piece.gpg_data == None:
363 replyinfo_obj.sig_failure = True
364 else:
365 replyinfo_obj.sig_success = True
366
367 if replyinfo_obj.fallback_target_key == None:
368 replyinfo_obj.fallback_target_key = piece.gpg_data.sigs[0]
369
370
371
372 def flatten_payloads (eddymsg_obj):
373
374 flat_string = ""
375
376 if eddymsg_obj == None:
377 return ""
378
379 if eddymsg_obj.multipart == True:
380 for sub in eddymsg_obj.subparts:
381 flat_string += flatten_payloads (sub)
382
383 return flat_string
384
385 # todo: don't include nested decrypted messages.
386 for piece in eddymsg_obj.payload_pieces:
387 if piece.piece_type == "text":
388 flat_string += piece.string
389 elif piece.piece_type == "message":
390 flat_string += flatten_payloads(piece.plainobj)
391 elif ((piece.piece_type == "clearsign") \
392 or (piece.piece_type == "detachedsig")) \
393 and (piece.gpg_data != None):
394 flat_string += flatten_payloads (piece.gpg_data.plainobj)
395
396
397 return flat_string
398
399
400 def get_key_from_fp (replyinfo_obj, gpgme_ctx):
401
402 if replyinfo_obj.target_key == None:
403 replyinfo_obj.target_key = replyinfo_obj.fallback_target_key
404
405 if replyinfo_obj.target_key != None:
406 try:
407 encrypt_to_key = gpgme_ctx.get_key(replyinfo_obj.target_key)
408 return encrypt_to_key
409
410 except:
411 pass
412
413 # no available key to use
414 replyinfo_obj.target_key = None
415 replyinfo_obj.fallback_target_key = None
416
417 replyinfo_obj.no_public_key = True
418 replyinfo_obj.public_key_received = False
419
420 return None
421
422
423 def write_reply (replyinfo_obj):
424
425 reply_plain = ""
426
427 if replyinfo_obj.success_decrypt == True:
428 reply_plain += replyinfo_obj.replies['success_decrypt']
429
430 if replyinfo_obj.no_public_key == False:
431 quoted_text = email_quote_text(replyinfo_obj.msg_to_quote)
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 # quoted printable encoding lets most ascii characters look normal
578 # before the decrypted mime message is decoded.
579 char_set = email.charset.Charset("utf-8")
580 char_set.body_encoding = email.charset.QP
581
582 # MIMEText doesn't allow setting the text encoding
583 # so we use MIMENonMultipart.
584 plaintext_mime = MIMENonMultipart('text', 'plain')
585 plaintext_mime.set_payload(plaintext, charset=char_set)
586
587 if (encrypt_to_key != None):
588
589 encrypted_text = encrypt_sign_message(plaintext_mime.as_string(),
590 encrypt_to_key,
591 gpgme_ctx)
592
593 control_mime = MIMEApplication("Version: 1",
594 _subtype='pgp-encrypted',
595 _encoder=email.encoders.encode_7or8bit)
596 control_mime['Content-Description'] = 'PGP/MIME version identification'
597 control_mime.set_charset('us-ascii')
598
599 encoded_mime = MIMEApplication(encrypted_text,
600 _subtype='octet-stream; name="encrypted.asc"',
601 _encoder=email.encoders.encode_7or8bit)
602 encoded_mime['Content-Description'] = 'OpenPGP encrypted message'
603 encoded_mime['Content-Disposition'] = 'inline; filename="encrypted.asc"'
604 encoded_mime.set_charset('us-ascii')
605
606 message_mime = MIMEMultipart(_subtype="encrypted", protocol="application/pgp-encrypted")
607 message_mime.attach(control_mime)
608 message_mime.attach(encoded_mime)
609 message_mime['Content-Disposition'] = 'inline'
610
611 else:
612 message_mime = plaintext_mime
613
614 message_mime['To'] = email_from
615 message_mime['Subject'] = email_subject
616
617 reply = message_mime.as_string()
618
619 return reply
620
621
622 def email_quote_text (text):
623
624 quoted_message = re.sub(r'^', r'> ', text, flags=re.MULTILINE)
625
626 return quoted_message
627
628
629 def encrypt_sign_message (plaintext, encrypt_to_key, gpgme_ctx):
630
631 plaintext_bytes = io.BytesIO(plaintext.encode('ascii'))
632 encrypted_bytes = io.BytesIO()
633
634 gpgme_ctx.encrypt_sign([encrypt_to_key], gpgme.ENCRYPT_ALWAYS_TRUST,
635 plaintext_bytes, encrypted_bytes)
636
637 encrypted_txt = encrypted_bytes.getvalue().decode('ascii')
638 return encrypted_txt
639
640
641 def error (error_msg):
642
643 sys.stderr.write(progname + ": " + str(error_msg) + "\n")
644
645
646 def debug (debug_msg):
647
648 if edward_config.debug == True:
649 error(debug_msg)
650
651
652 def handle_args ():
653 if __name__ == "__main__":
654
655 global progname
656 progname = sys.argv[0]
657
658 if len(sys.argv) > 1:
659 print(progname + ": error, this program doesn't " \
660 "need any arguments.", file=sys.stderr)
661 exit(1)
662
663
664 main()
665