add signed txt, etc. if it's in an encrypted block
[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 self.msg_to_quote = ""
96
97 self.success_decrypt = False
98 self.failed_decrypt = False
99 self.public_key_received = False
100 self.no_public_key = False
101 self.sig_success = False
102 self.sig_failure = False
103
104
105 def main ():
106
107 handle_args()
108
109 gpgme_ctx = get_gpg_context(edward_config.gnupghome,
110 edward_config.sign_with_key)
111
112 email_text = sys.stdin.read()
113 email_struct = parse_pgp_mime(email_text, gpgme_ctx)
114
115 email_to, email_from, email_subject = email_to_from_subject(email_text)
116 lang = import_lang(email_to)
117
118 replyinfo_obj = ReplyInfo()
119 replyinfo_obj.replies = lang.replies
120
121 prepare_for_reply(email_struct, replyinfo_obj)
122 reply_plaintext = write_reply(replyinfo_obj)
123
124 print(reply_plaintext)
125
126 # encrypt_to_key = choose_reply_encryption_key(gpgme_ctx, fingerprints)
127 #
128 # reply_mime = generate_encrypted_mime(plaintext, email_from, \
129 # email_subject, encrypt_to_key,
130 # gpgme_ctx)
131
132
133 def get_gpg_context (gnupghome, sign_with_key_fp):
134
135 os.environ['GNUPGHOME'] = gnupghome
136
137 gpgme_ctx = gpgme.Context()
138 gpgme_ctx.armor = True
139
140 try:
141 sign_with_key = gpgme_ctx.get_key(sign_with_key_fp)
142 except:
143 error("unable to load signing key. is the gnupghome "
144 + "and signing key properly set in the edward_config.py?")
145 exit(1)
146
147 gpgme_ctx.signers = [sign_with_key]
148
149 return gpgme_ctx
150
151
152 def parse_pgp_mime (email_text, gpgme_ctx):
153
154 email_struct = email.parser.Parser().parsestr(email_text)
155
156 eddymsg_obj = parse_mime(email_struct)
157 split_payloads(eddymsg_obj)
158 gpg_on_payloads(eddymsg_obj, gpgme_ctx)
159
160 return eddymsg_obj
161
162
163 def parse_mime(msg_struct):
164
165 eddymsg_obj = EddyMsg()
166
167 if msg_struct.is_multipart() == True:
168 payloads = msg_struct.get_payload()
169
170 eddymsg_obj.multipart = True
171 eddymsg_obj.subparts = list(map(parse_mime, payloads))
172
173 else:
174 eddymsg_obj = get_subpart_data(msg_struct)
175
176 return eddymsg_obj
177
178
179 def scan_and_split (payload_piece, match_type, pattern):
180
181 # don't try to re-split pieces containing gpg data
182 if payload_piece.piece_type != "text":
183 return [payload_piece]
184
185 flags = re.DOTALL | re.MULTILINE
186 matches = re.search("(?P<beginning>.*?)(?P<match>" + pattern +
187 ")(?P<rest>.*)", payload_piece.string, flags=flags)
188
189 if matches == None:
190 pieces = [payload_piece]
191
192 else:
193
194 beginning = PayloadPiece()
195 beginning.string = matches.group('beginning')
196 beginning.piece_type = payload_piece.piece_type
197
198 match = PayloadPiece()
199 match.string = matches.group('match')
200 match.piece_type = match_type
201
202 rest = PayloadPiece()
203 rest.string = matches.group('rest')
204 rest.piece_type = payload_piece.piece_type
205
206 more_pieces = scan_and_split(rest, match_type, pattern)
207 pieces = [beginning, match ] + more_pieces
208
209 return pieces
210
211
212 def get_subpart_data (part):
213
214 obj = EddyMsg()
215
216 obj.charset = part.get_content_charset()
217 obj.payload_bytes = part.get_payload(decode=True)
218
219 obj.filename = part.get_filename()
220 obj.content_type = part.get_content_type()
221 obj.description_list = part['content-description']
222
223 # your guess is as good as a-myy-ee-ine...
224 if obj.charset == None:
225 obj.charset = 'utf-8'
226
227 if obj.payload_bytes != None:
228 try:
229 payload = PayloadPiece()
230 payload.string = obj.payload_bytes.decode(obj.charset)
231 payload.piece_type = 'text'
232
233 obj.payload_pieces = [payload]
234 except UnicodeDecodeError:
235 pass
236
237 return obj
238
239
240 def do_to_eddys_pieces (function_to_do, eddymsg_obj, data):
241
242 if eddymsg_obj.multipart == True:
243 for sub in eddymsg_obj.subparts:
244 do_to_eddys_pieces(function_to_do, sub, data)
245 else:
246 function_to_do(eddymsg_obj, data)
247
248
249 def split_payloads (eddymsg_obj):
250
251 for match_type in match_types:
252 do_to_eddys_pieces(split_payload_pieces, eddymsg_obj, match_type)
253
254
255 def split_payload_pieces (eddymsg_obj, match_type):
256
257 (match_name, pattern) = match_type
258
259 new_pieces_list = []
260 for piece in eddymsg_obj.payload_pieces:
261 new_pieces_list += scan_and_split(piece, match_name, pattern)
262
263 eddymsg_obj.payload_pieces = new_pieces_list
264
265
266 def gpg_on_payloads (eddymsg_obj, gpgme_ctx, prev_parts=[]):
267
268 if eddymsg_obj.multipart == True:
269 prev_parts=[]
270 for sub in eddymsg_obj.subparts:
271 gpg_on_payloads (sub, gpgme_ctx, prev_parts)
272 prev_parts += [sub]
273
274 return
275
276 for piece in eddymsg_obj.payload_pieces:
277
278 if piece.piece_type == "text":
279 # don't transform the plaintext.
280 pass
281
282 elif piece.piece_type == "message":
283 (plaintext, sigs) = decrypt_block (piece.string, gpgme_ctx)
284
285 if plaintext:
286 piece.gpg_data = GPGData()
287 piece.gpg_data.sigs = sigs
288 # recurse!
289 piece.gpg_data.plainobj = parse_pgp_mime(plaintext, gpgme_ctx)
290
291 elif piece.piece_type == "pubkey":
292 key_fps = add_gpg_key(piece.string, gpgme_ctx)
293
294 if key_fps != []:
295 piece.gpg_data = GPGData()
296 piece.gpg_data.keys = key_fps
297
298 elif piece.piece_type == "clearsign":
299 (plaintext, sig_fps) = verify_clear_signature(piece.string, gpgme_ctx)
300
301 if sig_fps != []:
302 piece.gpg_data = GPGData()
303 piece.gpg_data.sigs = sig_fps
304 piece.gpg_data.plainobj = parse_pgp_mime(plaintext, gpgme_ctx)
305
306 elif piece.piece_type == "detachedsig":
307 for prev in prev_parts:
308 payload_bytes = prev.payload_bytes
309 sig_fps = verify_detached_signature(piece.string, payload_bytes, gpgme_ctx)
310
311 if sig_fps != []:
312 piece.gpg_data = GPGData()
313 piece.gpg_data.sigs = sig_fps
314 piece.gpg_data.plainobj = prev
315 break
316 else:
317 pass
318
319
320 def prepare_for_reply (eddymsg_obj, replyinfo_obj):
321
322 do_to_eddys_pieces(prepare_for_reply_pieces, eddymsg_obj, replyinfo_obj)
323
324
325 def prepare_for_reply_pieces (eddymsg_obj, replyinfo_obj):
326
327 for piece in eddymsg_obj.payload_pieces:
328 if piece.piece_type == "text":
329 # don't quote the plaintext part.
330 pass
331
332 elif piece.piece_type == "message":
333 if piece.gpg_data == None:
334 replyinfo_obj.failed_decrypt = True
335 else:
336 replyinfo_obj.success_decrypt = True
337 # TODO: only quote it if it is also signed by the encrypter.
338 replyinfo_obj.msg_to_quote += flatten_payloads(piece.gpg_data.plainobj)
339
340 prepare_for_reply(piece.gpg_data.plainobj, replyinfo_obj)
341
342 elif piece.piece_type == "pubkey":
343 if piece.gpg_data == None:
344 replyinfo_obj.no_public_key = True
345 else:
346 replyinfo_obj.public_key_received = True
347
348 elif (piece.piece_type == "clearsign") \
349 or (piece.piece_type == "detachedsig"):
350 if piece.gpg_data == None:
351 replyinfo_obj.sig_failure = True
352 else:
353 replyinfo_obj.sig_success = True
354
355
356 def flatten_payloads (eddymsg_obj):
357
358 flat_string = ""
359
360 if eddymsg_obj == None:
361 return ""
362
363 if eddymsg_obj.multipart == True:
364 for sub in eddymsg_obj.subparts:
365 flat_string += flatten_payloads (sub)
366
367 return flat_string
368
369 for piece in eddymsg_obj.payload_pieces:
370 if piece.piece_type == "text":
371 flat_string += piece.string
372 elif piece.piece_type == "message":
373 flat_string += flatten_payloads(piece.plainobj)
374 elif ((piece.piece_type == "clearsign") \
375 or (piece.piece_type == "detachedsig")) \
376 and (piece.gpg_data != None):
377 flat_string += flatten_payloads (piece.gpg_data.plainobj)
378
379
380 return flat_string
381
382
383 def write_reply (replyinfo_obj):
384
385 reply_plain = ""
386
387 if replyinfo_obj.success_decrypt == True:
388 quoted_text = email_quote_text(replyinfo_obj.msg_to_quote)
389 reply_plain += replyinfo_obj.replies['success_decrypt']
390 reply_plain += quoted_text
391
392 elif replyinfo_obj.failed_decrypt == True:
393 reply_plain += replyinfo_obj.replies['failed_decrypt']
394
395
396 if replyinfo_obj.sig_success == True:
397 reply_plain += "\n\n"
398 reply_plain += replyinfo_obj.replies['sig_success']
399
400 elif replyinfo_obj.sig_failure == True:
401 reply_plain += "\n\n"
402 reply_plain += replyinfo_obj.replies['sig_failure']
403
404
405 if replyinfo_obj.public_key_received == True:
406 reply_plain += "\n\n"
407 reply_plain += replyinfo_obj.replies['public_key_received']
408
409 elif replyinfo_obj.no_public_key == True:
410 reply_plain += "\n\n"
411 reply_plain += replyinfo_obj.replies['no_public_key']
412
413
414 reply_plain += "\n\n"
415 reply_plain += replyinfo_obj.replies['signature']
416
417 return reply_plain
418
419
420 def add_gpg_key (key_block, gpgme_ctx):
421
422 fp = io.BytesIO(key_block.encode('ascii'))
423
424 result = gpgme_ctx.import_(fp)
425 imports = result.imports
426
427 key_fingerprints = []
428
429 if imports != []:
430 for import_ in imports:
431 fingerprint = import_[0]
432 key_fingerprints += [fingerprint]
433
434 debug("added gpg key: " + fingerprint)
435
436 return key_fingerprints
437
438
439 def verify_clear_signature (sig_block, gpgme_ctx):
440
441 # FIXME: this might require the un-decoded bytes
442 # or the correct re-encoding with the carset of the mime part.
443 msg_fp = io.BytesIO(sig_block.encode('utf-8'))
444 ptxt_fp = io.BytesIO()
445
446 result = gpgme_ctx.verify(msg_fp, None, ptxt_fp)
447
448 # FIXME: this might require using the charset of the mime part.
449 plaintext = ptxt_fp.getvalue().decode('utf-8')
450
451 sig_fingerprints = []
452 for res_ in result:
453 sig_fingerprints += [res_.fpr]
454
455 return plaintext, sig_fingerprints
456
457
458 def verify_detached_signature (detached_sig, plaintext_bytes, gpgme_ctx):
459
460 detached_sig_fp = io.BytesIO(detached_sig.encode('ascii'))
461 plaintext_fp = io.BytesIO(plaintext_bytes)
462 ptxt_fp = io.BytesIO()
463
464 result = gpgme_ctx.verify(detached_sig_fp, plaintext_fp, None)
465
466 sig_fingerprints = []
467 for res_ in result:
468 sig_fingerprints += [res_.fpr]
469
470 return sig_fingerprints
471
472
473 def decrypt_block (msg_block, gpgme_ctx):
474
475 block_b = io.BytesIO(msg_block.encode('ascii'))
476 plain_b = io.BytesIO()
477
478 try:
479 sigs = gpgme_ctx.decrypt_verify(block_b, plain_b)
480 except:
481 return ("",[])
482
483 plaintext = plain_b.getvalue().decode('utf-8')
484 return (plaintext, sigs)
485
486
487 def choose_reply_encryption_key (gpgme_ctx, fingerprints):
488
489 reply_key = None
490 for fp in fingerprints:
491 try:
492 key = gpgme_ctx.get_key(fp)
493
494 if (key.can_encrypt == True):
495 reply_key = key
496 break
497 except:
498 continue
499
500
501 return reply_key
502
503
504 def email_to_from_subject (email_text):
505
506 email_struct = email.parser.Parser().parsestr(email_text)
507
508 email_to = email_struct['To']
509 email_from = email_struct['From']
510 email_subject = email_struct['Subject']
511
512 return email_to, email_from, email_subject
513
514
515 def import_lang(email_to):
516
517 if email_to != None:
518 for lang in langs:
519 if "edward-" + lang in email_to:
520 lang = "lang." + re.sub('-', '_', lang)
521 language = importlib.import_module(lang)
522
523 return language
524
525 return importlib.import_module("lang.en")
526
527
528 def generate_encrypted_mime (plaintext, email_from, email_subject, encrypt_to_key,
529 gpgme_ctx):
530
531
532 reply = "To: " + email_from + "\n"
533 reply += "Subject: " + email_subject + "\n"
534
535 if (encrypt_to_key != None):
536 plaintext_reply = "thanks for the message!\n\n\n"
537 plaintext_reply += email_quote_text(plaintext)
538
539 # quoted printable encoding lets most ascii characters look normal
540 # before the decrypted mime message is decoded.
541 char_set = email.charset.Charset("utf-8")
542 char_set.body_encoding = email.charset.QP
543
544 # MIMEText doesn't allow setting the text encoding
545 # so we use MIMENonMultipart.
546 plaintext_mime = MIMENonMultipart('text', 'plain')
547 plaintext_mime.set_payload(plaintext_reply, charset=char_set)
548
549 encrypted_text = encrypt_sign_message(plaintext_mime.as_string(),
550 encrypt_to_key,
551 gpgme_ctx)
552
553 control_mime = MIMEApplication("Version: 1",
554 _subtype='pgp-encrypted',
555 _encoder=email.encoders.encode_7or8bit)
556 control_mime['Content-Description'] = 'PGP/MIME version identification'
557 control_mime.set_charset('us-ascii')
558
559 encoded_mime = MIMEApplication(encrypted_text,
560 _subtype='octet-stream; name="encrypted.asc"',
561 _encoder=email.encoders.encode_7or8bit)
562 encoded_mime['Content-Description'] = 'OpenPGP encrypted message'
563 encoded_mime['Content-Disposition'] = 'inline; filename="encrypted.asc"'
564 encoded_mime.set_charset('us-ascii')
565
566 message_mime = MIMEMultipart(_subtype="encrypted", protocol="application/pgp-encrypted")
567 message_mime.attach(control_mime)
568 message_mime.attach(encoded_mime)
569 message_mime['Content-Disposition'] = 'inline'
570
571 reply += message_mime.as_string()
572
573 else:
574 reply += "\n"
575 reply += "Sorry, i couldn't find your key.\n"
576 reply += "I'll need that to encrypt a message to you."
577
578 return reply
579
580
581 def email_quote_text (text):
582
583 quoted_message = re.sub(r'^', r'> ', text, flags=re.MULTILINE)
584
585 return quoted_message
586
587
588 def encrypt_sign_message (plaintext, encrypt_to_key, gpgme_ctx):
589
590 plaintext_bytes = io.BytesIO(plaintext.encode('ascii'))
591 encrypted_bytes = io.BytesIO()
592
593 gpgme_ctx.encrypt_sign([encrypt_to_key], gpgme.ENCRYPT_ALWAYS_TRUST,
594 plaintext_bytes, encrypted_bytes)
595
596 encrypted_txt = encrypted_bytes.getvalue().decode('ascii')
597 return encrypted_txt
598
599
600 def error (error_msg):
601
602 sys.stderr.write(progname + ": " + str(error_msg) + "\n")
603
604
605 def debug (debug_msg):
606
607 if edward_config.debug == True:
608 error(debug_msg)
609
610
611 def handle_args ():
612 if __name__ == "__main__":
613
614 global progname
615 progname = sys.argv[0]
616
617 if len(sys.argv) > 1:
618 print(progname + ": error, this program doesn't " \
619 "need any arguments.", file=sys.stderr)
620 exit(1)
621
622
623 main()
624