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