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