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