added decryption function
[edward.git] / edward
CommitLineData
0bec96d6 1#! /usr/bin/env python3
ff4136c7 2# -*- coding: utf-8 -*-
0bec96d6
AE
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+) *
8bdfb6d4
AE
20* Copyright (C) 2009-2015 Tails developers <tails@boum.org> ( GPLv3+) *
21* Copyright (C) 2009 W. Trevor King <wking@drexel.edu> ( GPLv2+) *
0bec96d6
AE
22* *
23* Special thanks to Josh Drake for writing the original edward bot! :) *
24* *
25************************************************************************
26
a5385c04 27Code sourced from these projects:
0bec96d6 28
8bdfb6d4
AE
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
0bec96d6
AE
32"""
33
34import sys
0bec96d6
AE
35import gpgme
36import re
37import io
40c37ab3 38import os
0bec96d6 39
8bdfb6d4
AE
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
40c37ab3 48import edward_config
c96f3837 49
38738401
AE
50match_types = [('clearsign',
51 '-----BEGIN PGP SIGNED MESSAGE-----.*?-----BEGIN PGP SIGNATURE-----.*?-----END PGP SIGNATURE-----'),
52 ('message',
56578eaf
AE
53 '-----BEGIN PGP MESSAGE-----.*?-----END PGP MESSAGE-----'),
54 ('pubkey',
55 '-----BEGIN PGP PUBLIC KEY BLOCK-----.*?-----END PGP PUBLIC KEY BLOCK-----'),
56 ('detachedsig',
38738401 57 '-----BEGIN PGP SIGNATURE-----.*?-----END PGP SIGNATURE-----')]
56578eaf
AE
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
38738401
AE
81class GPGData (object):
82 def __init__(self):
83 self.decrypted = False
84
85 self.plainobj = None
86 self.sigs = []
87 self.keys = []
88
89
0bec96d6
AE
90def main ():
91
20f6e7c5 92 handle_args()
0bec96d6 93
0a064403
AE
94 gpgme_ctx = get_gpg_context(edward_config.gnupghome,
95 edward_config.sign_with_key)
96
c96f3837 97 email_text = sys.stdin.read()
38738401 98 result = parse_pgp_mime(email_text, gpgme_ctx)
65ed3800 99
56578eaf 100 email_from, email_subject = email_from_subject(email_text)
fafa21c3 101
56578eaf
AE
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)
c96f3837 108
56578eaf 109 print(flatten_eddy(result))
c96f3837 110
0bec96d6 111
0a064403
AE
112def 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
38738401 131def parse_pgp_mime (email_text, gpgme_ctx):
394a1476
AE
132
133 email_struct = email.parser.Parser().parsestr(email_text)
134
56578eaf
AE
135 eddy_obj = parse_mime(email_struct)
136 eddy_obj = split_payloads(eddy_obj)
38738401 137 eddy_obj = decrypt_payloads(eddy_obj, gpgme_ctx)
8bb4b0d5 138
56578eaf 139 return eddy_obj
0bec96d6 140
0bec96d6 141
56578eaf 142def parse_mime(msg_struct):
0bec96d6 143
56578eaf 144 eddy_obj = EddyMsg()
8bb4b0d5 145
56578eaf
AE
146 if msg_struct.is_multipart() == True:
147 payloads = msg_struct.get_payload()
0bec96d6 148
56578eaf 149 eddy_obj.multipart = True
dd11a483
AE
150 eddy_obj.subparts = list(map(parse_mime, payloads))
151
56578eaf
AE
152 else:
153 eddy_obj = get_subpart_data(msg_struct)
394a1476 154
56578eaf 155 return eddy_obj
c267c233 156
80119cab 157
56578eaf 158def split_payloads (eddy_obj):
3a9d1426 159
56578eaf 160 if eddy_obj.multipart == True:
dd11a483 161 eddy_obj.subparts = list(map(split_payloads, eddy_obj.subparts))
cf75de65 162
56578eaf
AE
163 else:
164 for (match_type, pattern) in match_types:
0bec96d6 165
56578eaf
AE
166 new_pieces_list = []
167 for payload_piece in eddy_obj.payload_pieces:
168 new_pieces_list += scan_and_split(payload_piece,
169 match_type, pattern)
170 eddy_obj.payload_pieces = new_pieces_list
86663388 171
56578eaf 172 return eddy_obj
cf75de65
AE
173
174
56578eaf 175def scan_and_split (payload_piece, match_type, pattern):
cf75de65 176
56578eaf
AE
177 flags = re.DOTALL | re.MULTILINE
178 matches = re.search("(?P<beginning>.*?)(?P<match>" + pattern +
179 ")(?P<rest>.*)", payload_piece.string, flags=flags)
86663388 180
56578eaf
AE
181 if matches == None:
182 pieces = [payload_piece]
c96f3837 183
56578eaf 184 else:
d437f8b2 185
56578eaf
AE
186 beginning = PayloadPiece()
187 beginning.string = matches.group('beginning')
188 beginning.piece_type = payload_piece.piece_type
d437f8b2 189
56578eaf
AE
190 match = PayloadPiece()
191 match.string = matches.group('match')
192 match.piece_type = match_type
d437f8b2 193
56578eaf
AE
194 rest = PayloadPiece()
195 rest.string = matches.group('rest')
196 rest.piece_type = payload_piece.piece_type
d437f8b2 197
56578eaf 198 more_pieces = scan_and_split(rest, match_type, pattern)
d437f8b2 199
56578eaf
AE
200 if more_pieces == None:
201 pieces = [beginning, match, rest]
202 else:
203 pieces = [beginning, match] + more_pieces
d437f8b2 204
56578eaf 205 return pieces
d437f8b2 206
d437f8b2 207
56578eaf 208def get_subpart_data (part):
0bec96d6 209
56578eaf 210 obj = EddyMsg()
0bec96d6 211
56578eaf
AE
212 obj.charset = part.get_content_charset()
213 obj.payload_bytes = part.get_payload(decode=True)
214
215 obj.filename = part.get_filename()
216 obj.content_type = part.get_content_type()
217 obj.description_list = part['content-description']
218
219 # your guess is as good as a-myy-ee-ine...
220 if obj.charset == None:
221 obj.charset = 'utf-8'
222
223 if obj.payload_bytes != None:
0eb75d9c
AE
224 try:
225 payload = PayloadPiece()
226 payload.string = obj.payload_bytes.decode(obj.charset)
227 payload.piece_type = 'text'
228
229 obj.payload_pieces = [payload]
230 except UnicodeDecodeError:
231 pass
56578eaf
AE
232
233 return obj
234
235
dd11a483 236def do_to_eddys_pieces (function_to_do, eddy_obj, data):
56578eaf
AE
237
238 if eddy_obj.multipart == True:
dd11a483
AE
239 result_list = []
240 for sub in eddy_obj.subparts:
241 result_list += do_to_eddys_pieces(function_to_do, sub, data)
394a1476 242 else:
dd11a483
AE
243 result_list = [function_to_do(eddy_obj.payload_pieces, data)]
244
245 return result_list
246
247
38738401
AE
248def decrypt_payloads (eddy_obj, gpgme_ctx):
249
250 do_to_eddys_pieces(decrypt_payload_pieces, eddy_obj, gpgme_ctx)
251
252 return eddy_obj
253
254
255def decrypt_payload_pieces (payload_pieces, gpgme_ctx):
256
257 for piece in payload_pieces:
258
259 if piece.piece_type == "text":
260 # don't transform the plaintext.
261 pass
262
263 elif piece.piece_type == "message":
264 (plaintext, sigs) = decrypt_block (piece.string, gpgme_ctx)
265
266 if plaintext:
267 piece.gpg_data = GPGData()
268 piece.gpg_data.sigs = sigs
269 # recurse!
270 piece.gpg_data.plainobj = parse_pgp_mime(plaintext, gpgme_ctx)
271 else:
272 pass
273
274
dd11a483
AE
275def flatten_eddy (eddy_obj):
276
277 string = "\n".join(do_to_eddys_pieces(flatten_payload_pieces, eddy_obj, None))
56578eaf
AE
278
279 return string
280
281
dd11a483 282def flatten_payload_pieces (payload_pieces, _ignore):
0bec96d6 283
56578eaf
AE
284 string = ""
285 for piece in payload_pieces:
38738401
AE
286 if piece.piece_type == "text":
287 string += piece.string
288 elif piece.piece_type == "message":
289 # recursive!
290 string += flatten_eddy(piece.gpg_data.plainobj)
56578eaf
AE
291
292 return string
293
294
295def email_from_subject (email_text):
296
297 email_struct = email.parser.Parser().parsestr(email_text)
298
299 email_from = email_struct['From']
300 email_subject = email_struct['Subject']
301
302 return email_from, email_subject
0bec96d6 303
0bec96d6 304
c267c233
AE
305def add_gpg_keys (text, gpgme_ctx):
306
83210634
AE
307 key_blocks = scan_and_grab(text,
308 '-----BEGIN PGP PUBLIC KEY BLOCK-----',
309 '-----END PGP PUBLIC KEY BLOCK-----')
c267c233 310
d0489345 311 fingerprints = []
83210634
AE
312 for key_block in key_blocks:
313 fp = io.BytesIO(key_block.encode('ascii'))
c267c233
AE
314
315 result = gpgme_ctx.import_(fp)
e49673aa 316 imports = result.imports
c267c233 317
e49673aa
AE
318 if imports != []:
319 fingerprint = imports[0][0]
d0489345 320 fingerprints += [fingerprint]
c267c233 321
e49673aa 322 debug("added gpg key: " + fingerprint)
ec1e779a 323
d0489345 324 return fingerprints
ec1e779a
AE
325
326
40c37ab3 327def decrypt_text (gpg_text, gpgme_ctx):
86663388 328
394a1476 329 body = ""
d0489345 330 fingerprints = []
0bec96d6 331
5b3053c1 332 msg_blocks = scan_and_grab(gpg_text,
c267c233
AE
333 '-----BEGIN PGP MESSAGE-----',
334 '-----END PGP MESSAGE-----')
86663388 335
5b3053c1 336 plaintexts_and_sigs = decrypt_blocks(msg_blocks, gpgme_ctx)
0bec96d6 337
5b3053c1
AE
338 for pair in plaintexts_and_sigs:
339 plaintext = pair[0]
340 sigs = pair[1]
9eb78301 341
394a1476 342 for sig in sigs:
d0489345 343 fingerprints += [sig.fpr]
0bec96d6 344
394a1476 345 # recursive for nested layers of mime and/or gpg
d0489345 346 plaintext, more_fps = email_decode_flatten(plaintext, gpgme_ctx, True)
8bb4b0d5 347
394a1476 348 body += plaintext
d0489345 349 fingerprints += more_fps
8bb4b0d5 350
d0489345 351 return body, fingerprints
8bb4b0d5 352
8bb4b0d5 353
cf75de65
AE
354def verify_clear_signature (text, gpgme_ctx):
355
356 sig_blocks = scan_and_grab(text,
357 '-----BEGIN PGP SIGNED MESSAGE-----',
358 '-----END PGP SIGNATURE-----')
359
360 fingerprints = []
361 plaintext = ""
362
363 for sig_block in sig_blocks:
364 msg_fp = io.BytesIO(sig_block.encode('utf-8'))
365 ptxt_fp = io.BytesIO()
366
367 result = gpgme_ctx.verify(msg_fp, None, ptxt_fp)
368
369 plaintext += ptxt_fp.getvalue().decode('utf-8')
370 fingerprint = result[0].fpr
371
372 fingerprints += [fingerprint]
373
374 return plaintext, fingerprints
375
376
c267c233 377def scan_and_grab (text, start_text, end_text):
0bec96d6 378
c267c233
AE
379 matches = re.search('(' + start_text + '.*' + end_text + ')',
380 text, flags=re.DOTALL)
0bec96d6 381
c267c233
AE
382 if matches != None:
383 match_tuple = matches.groups()
0bec96d6 384 else:
c267c233 385 match_tuple = ()
7b06b980 386
c267c233 387 return match_tuple
0bec96d6
AE
388
389
5b3053c1 390def decrypt_blocks (msg_blocks, gpgme_ctx):
0bec96d6 391
5b3053c1 392 return [decrypt_block(block, gpgme_ctx) for block in msg_blocks]
0bec96d6 393
0bec96d6 394
5b3053c1 395def decrypt_block (msg_block, gpgme_ctx):
0bec96d6 396
5b3053c1 397 block_b = io.BytesIO(msg_block.encode('ascii'))
0bec96d6
AE
398 plain_b = io.BytesIO()
399
afc1f64c
AE
400 try:
401 sigs = gpgme_ctx.decrypt_verify(block_b, plain_b)
402 except:
403 return ("",[])
0bec96d6 404
6aa41372 405 plaintext = plain_b.getvalue().decode('utf-8')
394a1476 406 return (plaintext, sigs)
0bec96d6
AE
407
408
d0489345 409def choose_reply_encryption_key (gpgme_ctx, fingerprints):
fafa21c3
AE
410
411 reply_key = None
d0489345
AE
412 for fp in fingerprints:
413 try:
414 key = gpgme_ctx.get_key(fp)
415
416 if (key.can_encrypt == True):
417 reply_key = key
418 break
419 except:
420 continue
421
fafa21c3 422
216708e9 423 return reply_key
fafa21c3
AE
424
425
897cbaf6 426def generate_reply (plaintext, email_from, email_subject, encrypt_to_key,
0a064403 427 gpgme_ctx):
1da9b527 428
8bdfb6d4 429
1da9b527
AE
430 reply = "To: " + email_from + "\n"
431 reply += "Subject: " + email_subject + "\n"
216708e9
AE
432
433 if (encrypt_to_key != None):
434 plaintext_reply = "thanks for the message!\n\n\n"
435 plaintext_reply += email_quote_text(plaintext)
436
8bdfb6d4
AE
437 # quoted printable encoding lets most ascii characters look normal
438 # before the decrypted mime message is decoded.
439 char_set = email.charset.Charset("utf-8")
440 char_set.body_encoding = email.charset.QP
441
442 # MIMEText doesn't allow setting the text encoding
443 # so we use MIMENonMultipart.
444 plaintext_mime = MIMENonMultipart('text', 'plain')
445 plaintext_mime.set_payload(plaintext_reply, charset=char_set)
446
447 encrypted_text = encrypt_sign_message(plaintext_mime.as_string(),
448 encrypt_to_key,
40c37ab3 449 gpgme_ctx)
8bdfb6d4
AE
450
451 control_mime = MIMEApplication("Version: 1",
452 _subtype='pgp-encrypted',
453 _encoder=email.encoders.encode_7or8bit)
454 control_mime['Content-Description'] = 'PGP/MIME version identification'
455 control_mime.set_charset('us-ascii')
456
457 encoded_mime = MIMEApplication(encrypted_text,
458 _subtype='octet-stream; name="encrypted.asc"',
459 _encoder=email.encoders.encode_7or8bit)
460 encoded_mime['Content-Description'] = 'OpenPGP encrypted message'
461 encoded_mime['Content-Disposition'] = 'inline; filename="encrypted.asc"'
462 encoded_mime.set_charset('us-ascii')
463
464 message_mime = MIMEMultipart(_subtype="encrypted", protocol="application/pgp-encrypted")
465 message_mime.attach(control_mime)
466 message_mime.attach(encoded_mime)
467 message_mime['Content-Disposition'] = 'inline'
216708e9 468
8bdfb6d4 469 reply += message_mime.as_string()
216708e9
AE
470
471 else:
8bdfb6d4 472 reply += "\n"
216708e9
AE
473 reply += "Sorry, i couldn't find your key.\n"
474 reply += "I'll need that to encrypt a message to you."
1da9b527
AE
475
476 return reply
477
478
f87041f8
AE
479def email_quote_text (text):
480
481 quoted_message = re.sub(r'^', r'> ', text, flags=re.MULTILINE)
482
483 return quoted_message
484
485
0a064403 486def encrypt_sign_message (plaintext, encrypt_to_key, gpgme_ctx):
897cbaf6 487
6aa41372 488 plaintext_bytes = io.BytesIO(plaintext.encode('ascii'))
1da9b527
AE
489 encrypted_bytes = io.BytesIO()
490
897cbaf6 491 gpgme_ctx.encrypt_sign([encrypt_to_key], gpgme.ENCRYPT_ALWAYS_TRUST,
1da9b527
AE
492 plaintext_bytes, encrypted_bytes)
493
6aa41372 494 encrypted_txt = encrypted_bytes.getvalue().decode('ascii')
1da9b527
AE
495 return encrypted_txt
496
497
0a064403
AE
498def error (error_msg):
499
500 sys.stderr.write(progname + ": " + error_msg + "\n")
501
502
5e8f9094
AE
503def debug (debug_msg):
504
505 if edward_config.debug == True:
0a064403 506 error(debug_msg)
5e8f9094
AE
507
508
20f6e7c5
AE
509def handle_args ():
510 if __name__ == "__main__":
511
512 global progname
513 progname = sys.argv[0]
514
515 if len(sys.argv) > 1:
516 print(progname + ": error, this program doesn't " \
517 "need any arguments.", file=sys.stderr)
518 exit(1)
519
520
0bec96d6
AE
521main()
522