move this here
[edward.git] / edward
... / ...
CommitLineData
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
27Code 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
34import sys
35import gpgme
36import re
37import io
38import os
39
40import email.parser
41import email.message
42import email.encoders
43
44from email.mime.multipart import MIMEMultipart
45from email.mime.application import MIMEApplication
46from email.mime.nonmultipart import MIMENonMultipart
47
48import edward_config
49
50match_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
60class 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
74class PayloadPiece (object):
75 def __init__(self):
76 self.piece_type = None
77 self.string = None
78 self.gpg_data = None
79
80
81class GPGData (object):
82 def __init__(self):
83 self.decrypted = False
84
85 self.plainobj = None
86 self.sigs = []
87 self.keys = []
88
89
90def 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 email_from, email_subject = email_from_subject(email_text)
100
101 print(flatten_eddy(result))
102
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
110def get_gpg_context (gnupghome, sign_with_key_fp):
111
112 os.environ['GNUPGHOME'] = gnupghome
113
114 gpgme_ctx = gpgme.Context()
115 gpgme_ctx.armor = True
116
117 try:
118 sign_with_key = gpgme_ctx.get_key(sign_with_key_fp)
119 except:
120 error("unable to load signing key. is the gnupghome "
121 + "and signing key properly set in the edward_config.py?")
122 exit(1)
123
124 gpgme_ctx.signers = [sign_with_key]
125
126 return gpgme_ctx
127
128
129def parse_pgp_mime (email_text, gpgme_ctx):
130
131 email_struct = email.parser.Parser().parsestr(email_text)
132
133 eddy_obj = parse_mime(email_struct)
134 eddy_obj = split_payloads(eddy_obj)
135 eddy_obj = gpg_on_payloads(eddy_obj, gpgme_ctx)
136
137 return eddy_obj
138
139
140def parse_mime(msg_struct):
141
142 eddy_obj = EddyMsg()
143
144 if msg_struct.is_multipart() == True:
145 payloads = msg_struct.get_payload()
146
147 eddy_obj.multipart = True
148 eddy_obj.subparts = list(map(parse_mime, payloads))
149
150 else:
151 eddy_obj = get_subpart_data(msg_struct)
152
153 return eddy_obj
154
155
156def scan_and_split (payload_piece, match_type, pattern):
157
158 # don't try to re-split pieces containing gpg data
159 if payload_piece.piece_type != "text":
160 return [payload_piece]
161
162 flags = re.DOTALL | re.MULTILINE
163 matches = re.search("(?P<beginning>.*?)(?P<match>" + pattern +
164 ")(?P<rest>.*)", payload_piece.string, flags=flags)
165
166 if matches == None:
167 pieces = [payload_piece]
168
169 else:
170
171 beginning = PayloadPiece()
172 beginning.string = matches.group('beginning')
173 beginning.piece_type = payload_piece.piece_type
174
175 match = PayloadPiece()
176 match.string = matches.group('match')
177 match.piece_type = match_type
178
179 rest = PayloadPiece()
180 rest.string = matches.group('rest')
181 rest.piece_type = payload_piece.piece_type
182
183 more_pieces = scan_and_split(rest, match_type, pattern)
184 pieces = [beginning, match ] + more_pieces
185
186 return pieces
187
188
189def get_subpart_data (part):
190
191 obj = EddyMsg()
192
193 obj.charset = part.get_content_charset()
194 obj.payload_bytes = part.get_payload(decode=True)
195
196 obj.filename = part.get_filename()
197 obj.content_type = part.get_content_type()
198 obj.description_list = part['content-description']
199
200 # your guess is as good as a-myy-ee-ine...
201 if obj.charset == None:
202 obj.charset = 'utf-8'
203
204 if obj.payload_bytes != None:
205 try:
206 payload = PayloadPiece()
207 payload.string = obj.payload_bytes.decode(obj.charset)
208 payload.piece_type = 'text'
209
210 obj.payload_pieces = [payload]
211 except UnicodeDecodeError:
212 pass
213
214 return obj
215
216
217def do_to_eddys_pieces (function_to_do, eddy_obj, data):
218
219 if eddy_obj == None:
220 return []
221
222 if eddy_obj.multipart == True:
223 result_list = []
224 for sub in eddy_obj.subparts:
225 result_list += do_to_eddys_pieces(function_to_do, sub, data)
226 else:
227 result_list = [function_to_do(eddy_obj, data)]
228
229 return result_list
230
231
232def split_payloads (eddy_obj):
233
234 for match_type in match_types:
235 do_to_eddys_pieces(split_payload_pieces, eddy_obj, match_type)
236
237 return eddy_obj
238
239
240def split_payload_pieces (eddy_obj, match_type):
241
242 (match_name, pattern) = match_type
243
244 new_pieces_list = []
245 for piece in eddy_obj.payload_pieces:
246 new_pieces_list += scan_and_split(piece, match_name, pattern)
247
248 eddy_obj.payload_pieces = new_pieces_list
249
250
251def gpg_on_payloads (eddy_obj, gpgme_ctx):
252
253 do_to_eddys_pieces(gpg_on_payload_pieces, eddy_obj, gpgme_ctx)
254
255 return eddy_obj
256
257
258def gpg_on_payload_pieces (eddy_obj, gpgme_ctx):
259
260 for piece in eddy_obj.payload_pieces:
261
262 if piece.piece_type == "text":
263 # don't transform the plaintext.
264 pass
265
266 elif piece.piece_type == "message":
267 (plaintext, sigs) = decrypt_block (piece.string, gpgme_ctx)
268
269 if plaintext:
270 piece.gpg_data = GPGData()
271 piece.gpg_data.sigs = sigs
272 # recurse!
273 piece.gpg_data.plainobj = parse_pgp_mime(plaintext, gpgme_ctx)
274
275 elif piece.piece_type == "pubkey":
276 fingerprints = add_gpg_key(piece.string, gpgme_ctx)
277
278 if fingerprints != []:
279 piece.gpg_data = GPGData()
280 piece.gpg_data.keys = fingerprints
281
282 elif piece.piece_type == "clearsign":
283 (plaintext, fingerprints) = verify_clear_signature(piece.string, gpgme_ctx)
284
285 if fingerprints != []:
286 piece.gpg_data = GPGData()
287 piece.gpg_data.sigs = fingerprints
288 piece.gpg_data.plainobj = parse_pgp_mime(plaintext, gpgme_ctx)
289
290 else:
291 pass
292
293
294def flatten_eddy (eddy_obj):
295
296 string = "\n".join(do_to_eddys_pieces(flatten_payload_pieces, eddy_obj, None))
297
298 return string
299
300
301def flatten_payload_pieces (eddy_obj, _ignore):
302
303 string = ""
304 for piece in eddy_obj.payload_pieces:
305 if piece.piece_type == "text":
306 string += piece.string
307 elif piece.gpg_data == None:
308 string += "Hmmm... I wasn't able to get that part.\n"
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
324def 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
334def 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
353def 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
372def 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
386def 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
403def 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
456def email_quote_text (text):
457
458 quoted_message = re.sub(r'^', r'> ', text, flags=re.MULTILINE)
459
460 return quoted_message
461
462
463def 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
475def error (error_msg):
476
477 sys.stderr.write(progname + ": " + str(error_msg) + "\n")
478
479
480def debug (debug_msg):
481
482 if edward_config.debug == True:
483 error(debug_msg)
484
485
486def 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
498main()
499