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