fixed bug
[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 eddy_obj = split_payloads(eddy_obj)
143 eddy_obj = 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 == None:
228 return []
229
230 if eddy_obj.multipart == True:
231 result_list = []
232 for sub in eddy_obj.subparts:
233 result_list += do_to_eddys_pieces(function_to_do, sub, data)
234 else:
235 result_list = [function_to_do(eddy_obj, data)]
236
237 return result_list
238
239
240 def split_payloads (eddy_obj):
241
242 for match_type in match_types:
243 do_to_eddys_pieces(split_payload_pieces, eddy_obj, match_type)
244
245 return eddy_obj
246
247
248 def split_payload_pieces (eddy_obj, match_type):
249
250 (match_name, pattern) = match_type
251
252 new_pieces_list = []
253 for piece in eddy_obj.payload_pieces:
254 new_pieces_list += scan_and_split(piece, match_name, pattern)
255
256 eddy_obj.payload_pieces = new_pieces_list
257
258
259 def gpg_on_payloads (eddy_obj, gpgme_ctx):
260
261 do_to_eddys_pieces(gpg_on_payload_pieces, eddy_obj, gpgme_ctx)
262
263 return eddy_obj
264
265
266 def gpg_on_payload_pieces (eddy_obj, gpgme_ctx):
267
268 for piece in eddy_obj.payload_pieces:
269
270 if piece.piece_type == "text":
271 # don't transform the plaintext.
272 pass
273
274 elif piece.piece_type == "message":
275 (plaintext, sigs) = decrypt_block (piece.string, gpgme_ctx)
276
277 if plaintext:
278 piece.gpg_data = GPGData()
279 piece.gpg_data.sigs = sigs
280 # recurse!
281 piece.gpg_data.plainobj = parse_pgp_mime(plaintext, gpgme_ctx)
282
283 elif piece.piece_type == "pubkey":
284 fingerprints = add_gpg_key(piece.string, gpgme_ctx)
285
286 if fingerprints != []:
287 piece.gpg_data = GPGData()
288 piece.gpg_data.keys = fingerprints
289
290 elif piece.piece_type == "clearsign":
291 (plaintext, fingerprints) = verify_clear_signature(piece.string, gpgme_ctx)
292
293 if fingerprints != []:
294 piece.gpg_data = GPGData()
295 piece.gpg_data.sigs = fingerprints
296 piece.gpg_data.plainobj = parse_pgp_mime(plaintext, gpgme_ctx)
297
298 else:
299 pass
300
301
302 def build_reply (eddy_obj):
303
304 string = "\n".join(do_to_eddys_pieces(build_reply_pieces, eddy_obj, None))
305
306 return string
307
308
309 def build_reply_pieces (eddy_obj, _ignore):
310
311 string = ""
312 for piece in eddy_obj.payload_pieces:
313 if piece.piece_type == "text":
314 string += piece.string
315 elif piece.gpg_data == None:
316 string += "Hmmm... I wasn't able to get that part.\n"
317 elif piece.piece_type == "message":
318 # recursive!
319 string += build_reply(piece.gpg_data.plainobj)
320 elif piece.piece_type == "pubkey":
321 string += "thanks for your public key:"
322 for key in piece.gpg_data.keys:
323 string += "\n" + key
324 elif piece.piece_type == "clearsign":
325 string += "*** Begin signed part ***\n"
326 string += build_reply(piece.gpg_data.plainobj)
327 string += "\n*** End signed part ***"
328
329 return string
330
331
332 def add_gpg_key (key_block, gpgme_ctx):
333
334 fp = io.BytesIO(key_block.encode('ascii'))
335
336 result = gpgme_ctx.import_(fp)
337 imports = result.imports
338
339 fingerprints = []
340
341 if imports != []:
342 for import_ in imports:
343 fingerprint = import_[0]
344 fingerprints += [fingerprint]
345
346 debug("added gpg key: " + fingerprint)
347
348 return fingerprints
349
350
351 def verify_clear_signature (sig_block, gpgme_ctx):
352
353 # FIXME: this might require the un-decoded bytes
354 # or the correct re-encoding with the carset of the mime part.
355 msg_fp = io.BytesIO(sig_block.encode('utf-8'))
356 ptxt_fp = io.BytesIO()
357
358 result = gpgme_ctx.verify(msg_fp, None, ptxt_fp)
359
360 # FIXME: this might require using the charset of the mime part.
361 plaintext = ptxt_fp.getvalue().decode('utf-8')
362
363 fingerprints = []
364 for res_ in result:
365 fingerprints += [res_.fpr]
366
367 return plaintext, fingerprints
368
369
370 def decrypt_block (msg_block, gpgme_ctx):
371
372 block_b = io.BytesIO(msg_block.encode('ascii'))
373 plain_b = io.BytesIO()
374
375 try:
376 sigs = gpgme_ctx.decrypt_verify(block_b, plain_b)
377 except:
378 return ("",[])
379
380 plaintext = plain_b.getvalue().decode('utf-8')
381 return (plaintext, sigs)
382
383
384 def choose_reply_encryption_key (gpgme_ctx, fingerprints):
385
386 reply_key = None
387 for fp in fingerprints:
388 try:
389 key = gpgme_ctx.get_key(fp)
390
391 if (key.can_encrypt == True):
392 reply_key = key
393 break
394 except:
395 continue
396
397
398 return reply_key
399
400
401 def email_to_from_subject (email_text):
402
403 email_struct = email.parser.Parser().parsestr(email_text)
404
405 email_to = email_struct['To']
406 email_from = email_struct['From']
407 email_subject = email_struct['Subject']
408
409 return email_to, email_from, email_subject
410
411
412 def import_lang(email_to):
413
414 if email_to != None:
415 for lang in langs:
416 if "edward-" + lang in email_to:
417 lang = "lang." + re.sub('-', '_', lang)
418 language = importlib.import_module(lang)
419
420 return language
421
422 return importlib.import_module("lang.en")
423
424
425 def generate_encrypted_mime (plaintext, email_from, email_subject, encrypt_to_key,
426 gpgme_ctx):
427
428
429 reply = "To: " + email_from + "\n"
430 reply += "Subject: " + email_subject + "\n"
431
432 if (encrypt_to_key != None):
433 plaintext_reply = "thanks for the message!\n\n\n"
434 plaintext_reply += email_quote_text(plaintext)
435
436 # quoted printable encoding lets most ascii characters look normal
437 # before the decrypted mime message is decoded.
438 char_set = email.charset.Charset("utf-8")
439 char_set.body_encoding = email.charset.QP
440
441 # MIMEText doesn't allow setting the text encoding
442 # so we use MIMENonMultipart.
443 plaintext_mime = MIMENonMultipart('text', 'plain')
444 plaintext_mime.set_payload(plaintext_reply, charset=char_set)
445
446 encrypted_text = encrypt_sign_message(plaintext_mime.as_string(),
447 encrypt_to_key,
448 gpgme_ctx)
449
450 control_mime = MIMEApplication("Version: 1",
451 _subtype='pgp-encrypted',
452 _encoder=email.encoders.encode_7or8bit)
453 control_mime['Content-Description'] = 'PGP/MIME version identification'
454 control_mime.set_charset('us-ascii')
455
456 encoded_mime = MIMEApplication(encrypted_text,
457 _subtype='octet-stream; name="encrypted.asc"',
458 _encoder=email.encoders.encode_7or8bit)
459 encoded_mime['Content-Description'] = 'OpenPGP encrypted message'
460 encoded_mime['Content-Disposition'] = 'inline; filename="encrypted.asc"'
461 encoded_mime.set_charset('us-ascii')
462
463 message_mime = MIMEMultipart(_subtype="encrypted", protocol="application/pgp-encrypted")
464 message_mime.attach(control_mime)
465 message_mime.attach(encoded_mime)
466 message_mime['Content-Disposition'] = 'inline'
467
468 reply += message_mime.as_string()
469
470 else:
471 reply += "\n"
472 reply += "Sorry, i couldn't find your key.\n"
473 reply += "I'll need that to encrypt a message to you."
474
475 return reply
476
477
478 def email_quote_text (text):
479
480 quoted_message = re.sub(r'^', r'> ', text, flags=re.MULTILINE)
481
482 return quoted_message
483
484
485 def encrypt_sign_message (plaintext, encrypt_to_key, gpgme_ctx):
486
487 plaintext_bytes = io.BytesIO(plaintext.encode('ascii'))
488 encrypted_bytes = io.BytesIO()
489
490 gpgme_ctx.encrypt_sign([encrypt_to_key], gpgme.ENCRYPT_ALWAYS_TRUST,
491 plaintext_bytes, encrypted_bytes)
492
493 encrypted_txt = encrypted_bytes.getvalue().decode('ascii')
494 return encrypted_txt
495
496
497 def error (error_msg):
498
499 sys.stderr.write(progname + ": " + str(error_msg) + "\n")
500
501
502 def debug (debug_msg):
503
504 if edward_config.debug == True:
505 error(debug_msg)
506
507
508 def handle_args ():
509 if __name__ == "__main__":
510
511 global progname
512 progname = sys.argv[0]
513
514 if len(sys.argv) > 1:
515 print(progname + ": error, this program doesn't " \
516 "need any arguments.", file=sys.stderr)
517 exit(1)
518
519
520 main()
521