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