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