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