avoid a certain type of 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
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 = gpg_on_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 scan_and_split (payload_piece, match_type, pattern):
159
160 # don't try to re-split pieces containing gpg data
161 if payload_piece.piece_type != "text":
162 return [payload_piece]
163
164 flags = re.DOTALL | re.MULTILINE
165 matches = re.search("(?P<beginning>.*?)(?P<match>" + pattern +
166 ")(?P<rest>.*)", payload_piece.string, flags=flags)
167
168 if matches == None:
169 pieces = [payload_piece]
170
171 else:
172
173 beginning = PayloadPiece()
174 beginning.string = matches.group('beginning')
175 beginning.piece_type = payload_piece.piece_type
176
177 match = PayloadPiece()
178 match.string = matches.group('match')
179 match.piece_type = match_type
180
181 rest = PayloadPiece()
182 rest.string = matches.group('rest')
183 rest.piece_type = payload_piece.piece_type
184
185 more_pieces = scan_and_split(rest, match_type, pattern)
186 pieces = [beginning, match ] + more_pieces
187
188 return pieces
189
190
191 def get_subpart_data (part):
192
193 obj = EddyMsg()
194
195 obj.charset = part.get_content_charset()
196 obj.payload_bytes = part.get_payload(decode=True)
197
198 obj.filename = part.get_filename()
199 obj.content_type = part.get_content_type()
200 obj.description_list = part['content-description']
201
202 # your guess is as good as a-myy-ee-ine...
203 if obj.charset == None:
204 obj.charset = 'utf-8'
205
206 if obj.payload_bytes != None:
207 try:
208 payload = PayloadPiece()
209 payload.string = obj.payload_bytes.decode(obj.charset)
210 payload.piece_type = 'text'
211
212 obj.payload_pieces = [payload]
213 except UnicodeDecodeError:
214 pass
215
216 return obj
217
218
219 def do_to_eddys_pieces (function_to_do, eddy_obj, data):
220
221 if eddy_obj == None:
222 return []
223
224 if eddy_obj.multipart == True:
225 result_list = []
226 for sub in eddy_obj.subparts:
227 result_list += do_to_eddys_pieces(function_to_do, sub, data)
228 else:
229 result_list = [function_to_do(eddy_obj, data)]
230
231 return result_list
232
233
234 def split_payloads (eddy_obj):
235
236 for match_type in match_types:
237 do_to_eddys_pieces(split_payload_pieces, eddy_obj, match_type)
238
239 return eddy_obj
240
241
242 def split_payload_pieces (eddy_obj, match_type):
243
244 (match_name, pattern) = match_type
245
246 new_pieces_list = []
247 for piece in eddy_obj.payload_pieces:
248 new_pieces_list += scan_and_split(piece, match_name, pattern)
249
250 eddy_obj.payload_pieces = new_pieces_list
251
252
253 def gpg_on_payloads (eddy_obj, gpgme_ctx):
254
255 do_to_eddys_pieces(gpg_on_payload_pieces, eddy_obj, gpgme_ctx)
256
257 return eddy_obj
258
259
260 def gpg_on_payload_pieces (eddy_obj, gpgme_ctx):
261
262 for piece in eddy_obj.payload_pieces:
263
264 if piece.piece_type == "text":
265 # don't transform the plaintext.
266 pass
267
268 elif piece.piece_type == "message":
269 (plaintext, sigs) = decrypt_block (piece.string, gpgme_ctx)
270
271 if plaintext:
272 piece.gpg_data = GPGData()
273 piece.gpg_data.sigs = sigs
274 # recurse!
275 piece.gpg_data.plainobj = parse_pgp_mime(plaintext, gpgme_ctx)
276
277 elif piece.piece_type == "pubkey":
278 fingerprints = add_gpg_key(piece.string, gpgme_ctx)
279
280 if fingerprints != []:
281 piece.gpg_data = GPGData()
282 piece.gpg_data.keys = fingerprints
283
284 elif piece.piece_type == "clearsign":
285 (plaintext, fingerprints) = verify_clear_signature(piece.string, gpgme_ctx)
286
287 if fingerprints != []:
288 piece.gpg_data = GPGData()
289 piece.gpg_data.sigs = fingerprints
290 piece.gpg_data.plainobj = parse_pgp_mime(plaintext, gpgme_ctx)
291
292 else:
293 pass
294
295
296 def flatten_eddy (eddy_obj):
297
298 string = "\n".join(do_to_eddys_pieces(flatten_payload_pieces, eddy_obj, None))
299
300 return string
301
302
303 def flatten_payload_pieces (eddy_obj, _ignore):
304
305 string = ""
306 for piece in eddy_obj.payload_pieces:
307 if piece.piece_type == "text":
308 string += piece.string
309 elif piece.piece_type == "message":
310 # recursive!
311 string += flatten_eddy(piece.gpg_data.plainobj)
312 elif piece.piece_type == "pubkey":
313 string += "thanks for your public key:"
314 for key in piece.gpg_data.keys:
315 string += "\n" + key
316 elif piece.piece_type == "clearsign":
317 string += "*** Begin signed part ***\n"
318 string += flatten_eddy(piece.gpg_data.plainobj)
319 string += "\n*** End signed part ***"
320
321 return string
322
323
324 def email_from_subject (email_text):
325
326 email_struct = email.parser.Parser().parsestr(email_text)
327
328 email_from = email_struct['From']
329 email_subject = email_struct['Subject']
330
331 return email_from, email_subject
332
333
334 def add_gpg_key (key_block, gpgme_ctx):
335
336 fp = io.BytesIO(key_block.encode('ascii'))
337
338 result = gpgme_ctx.import_(fp)
339 imports = result.imports
340
341 fingerprints = []
342
343 if imports != []:
344 for import_ in imports:
345 fingerprint = import_[0]
346 fingerprints += [fingerprint]
347
348 debug("added gpg key: " + fingerprint)
349
350 return fingerprints
351
352
353 def verify_clear_signature (sig_block, gpgme_ctx):
354
355 # FIXME: this might require the un-decoded bytes
356 # or the correct re-encoding with the carset of the mime part.
357 msg_fp = io.BytesIO(sig_block.encode('utf-8'))
358 ptxt_fp = io.BytesIO()
359
360 result = gpgme_ctx.verify(msg_fp, None, ptxt_fp)
361
362 # FIXME: this might require using the charset of the mime part.
363 plaintext = ptxt_fp.getvalue().decode('utf-8')
364
365 fingerprints = []
366 for res_ in result:
367 fingerprints += [res_.fpr]
368
369 return plaintext, fingerprints
370
371
372 def decrypt_block (msg_block, gpgme_ctx):
373
374 block_b = io.BytesIO(msg_block.encode('ascii'))
375 plain_b = io.BytesIO()
376
377 try:
378 sigs = gpgme_ctx.decrypt_verify(block_b, plain_b)
379 except:
380 return ("",[])
381
382 plaintext = plain_b.getvalue().decode('utf-8')
383 return (plaintext, sigs)
384
385
386 def choose_reply_encryption_key (gpgme_ctx, fingerprints):
387
388 reply_key = None
389 for fp in fingerprints:
390 try:
391 key = gpgme_ctx.get_key(fp)
392
393 if (key.can_encrypt == True):
394 reply_key = key
395 break
396 except:
397 continue
398
399
400 return reply_key
401
402
403 def generate_reply (plaintext, email_from, email_subject, encrypt_to_key,
404 gpgme_ctx):
405
406
407 reply = "To: " + email_from + "\n"
408 reply += "Subject: " + email_subject + "\n"
409
410 if (encrypt_to_key != None):
411 plaintext_reply = "thanks for the message!\n\n\n"
412 plaintext_reply += email_quote_text(plaintext)
413
414 # quoted printable encoding lets most ascii characters look normal
415 # before the decrypted mime message is decoded.
416 char_set = email.charset.Charset("utf-8")
417 char_set.body_encoding = email.charset.QP
418
419 # MIMEText doesn't allow setting the text encoding
420 # so we use MIMENonMultipart.
421 plaintext_mime = MIMENonMultipart('text', 'plain')
422 plaintext_mime.set_payload(plaintext_reply, charset=char_set)
423
424 encrypted_text = encrypt_sign_message(plaintext_mime.as_string(),
425 encrypt_to_key,
426 gpgme_ctx)
427
428 control_mime = MIMEApplication("Version: 1",
429 _subtype='pgp-encrypted',
430 _encoder=email.encoders.encode_7or8bit)
431 control_mime['Content-Description'] = 'PGP/MIME version identification'
432 control_mime.set_charset('us-ascii')
433
434 encoded_mime = MIMEApplication(encrypted_text,
435 _subtype='octet-stream; name="encrypted.asc"',
436 _encoder=email.encoders.encode_7or8bit)
437 encoded_mime['Content-Description'] = 'OpenPGP encrypted message'
438 encoded_mime['Content-Disposition'] = 'inline; filename="encrypted.asc"'
439 encoded_mime.set_charset('us-ascii')
440
441 message_mime = MIMEMultipart(_subtype="encrypted", protocol="application/pgp-encrypted")
442 message_mime.attach(control_mime)
443 message_mime.attach(encoded_mime)
444 message_mime['Content-Disposition'] = 'inline'
445
446 reply += message_mime.as_string()
447
448 else:
449 reply += "\n"
450 reply += "Sorry, i couldn't find your key.\n"
451 reply += "I'll need that to encrypt a message to you."
452
453 return reply
454
455
456 def email_quote_text (text):
457
458 quoted_message = re.sub(r'^', r'> ', text, flags=re.MULTILINE)
459
460 return quoted_message
461
462
463 def encrypt_sign_message (plaintext, encrypt_to_key, gpgme_ctx):
464
465 plaintext_bytes = io.BytesIO(plaintext.encode('ascii'))
466 encrypted_bytes = io.BytesIO()
467
468 gpgme_ctx.encrypt_sign([encrypt_to_key], gpgme.ENCRYPT_ALWAYS_TRUST,
469 plaintext_bytes, encrypted_bytes)
470
471 encrypted_txt = encrypted_bytes.getvalue().decode('ascii')
472 return encrypted_txt
473
474
475 def error (error_msg):
476
477 sys.stderr.write(progname + ": " + str(error_msg) + "\n")
478
479
480 def debug (debug_msg):
481
482 if edward_config.debug == True:
483 error(debug_msg)
484
485
486 def handle_args ():
487 if __name__ == "__main__":
488
489 global progname
490 progname = sys.argv[0]
491
492 if len(sys.argv) > 1:
493 print(progname + ": error, this program doesn't " \
494 "need any arguments.", file=sys.stderr)
495 exit(1)
496
497
498 main()
499