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