for better debug functionality
[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)
4615b156 199 pieces = [beginning, match ] + more_pieces
d437f8b2 200
56578eaf 201 return pieces
d437f8b2 202
d437f8b2 203
56578eaf 204def get_subpart_data (part):
0bec96d6 205
56578eaf 206 obj = EddyMsg()
0bec96d6 207
56578eaf
AE
208 obj.charset = part.get_content_charset()
209 obj.payload_bytes = part.get_payload(decode=True)
210
211 obj.filename = part.get_filename()
212 obj.content_type = part.get_content_type()
213 obj.description_list = part['content-description']
214
215 # your guess is as good as a-myy-ee-ine...
216 if obj.charset == None:
217 obj.charset = 'utf-8'
218
219 if obj.payload_bytes != None:
0eb75d9c
AE
220 try:
221 payload = PayloadPiece()
222 payload.string = obj.payload_bytes.decode(obj.charset)
223 payload.piece_type = 'text'
224
225 obj.payload_pieces = [payload]
226 except UnicodeDecodeError:
227 pass
56578eaf
AE
228
229 return obj
230
231
dd11a483 232def do_to_eddys_pieces (function_to_do, eddy_obj, data):
56578eaf
AE
233
234 if eddy_obj.multipart == True:
dd11a483
AE
235 result_list = []
236 for sub in eddy_obj.subparts:
237 result_list += do_to_eddys_pieces(function_to_do, sub, data)
394a1476 238 else:
dd11a483
AE
239 result_list = [function_to_do(eddy_obj.payload_pieces, data)]
240
241 return result_list
242
243
38738401
AE
244def decrypt_payloads (eddy_obj, gpgme_ctx):
245
246 do_to_eddys_pieces(decrypt_payload_pieces, eddy_obj, gpgme_ctx)
247
248 return eddy_obj
249
250
251def decrypt_payload_pieces (payload_pieces, gpgme_ctx):
252
253 for piece in payload_pieces:
254
255 if piece.piece_type == "text":
256 # don't transform the plaintext.
257 pass
258
259 elif piece.piece_type == "message":
260 (plaintext, sigs) = decrypt_block (piece.string, gpgme_ctx)
261
262 if plaintext:
263 piece.gpg_data = GPGData()
264 piece.gpg_data.sigs = sigs
265 # recurse!
266 piece.gpg_data.plainobj = parse_pgp_mime(plaintext, gpgme_ctx)
267 else:
268 pass
269
270
dd11a483
AE
271def flatten_eddy (eddy_obj):
272
273 string = "\n".join(do_to_eddys_pieces(flatten_payload_pieces, eddy_obj, None))
56578eaf
AE
274
275 return string
276
277
dd11a483 278def flatten_payload_pieces (payload_pieces, _ignore):
0bec96d6 279
56578eaf
AE
280 string = ""
281 for piece in payload_pieces:
38738401
AE
282 if piece.piece_type == "text":
283 string += piece.string
284 elif piece.piece_type == "message":
285 # recursive!
286 string += flatten_eddy(piece.gpg_data.plainobj)
56578eaf
AE
287
288 return string
289
290
291def email_from_subject (email_text):
292
293 email_struct = email.parser.Parser().parsestr(email_text)
294
295 email_from = email_struct['From']
296 email_subject = email_struct['Subject']
297
298 return email_from, email_subject
0bec96d6 299
0bec96d6 300
c267c233
AE
301def add_gpg_keys (text, gpgme_ctx):
302
83210634
AE
303 key_blocks = scan_and_grab(text,
304 '-----BEGIN PGP PUBLIC KEY BLOCK-----',
305 '-----END PGP PUBLIC KEY BLOCK-----')
c267c233 306
d0489345 307 fingerprints = []
83210634
AE
308 for key_block in key_blocks:
309 fp = io.BytesIO(key_block.encode('ascii'))
c267c233
AE
310
311 result = gpgme_ctx.import_(fp)
e49673aa 312 imports = result.imports
c267c233 313
e49673aa
AE
314 if imports != []:
315 fingerprint = imports[0][0]
d0489345 316 fingerprints += [fingerprint]
c267c233 317
e49673aa 318 debug("added gpg key: " + fingerprint)
ec1e779a 319
d0489345 320 return fingerprints
ec1e779a
AE
321
322
40c37ab3 323def decrypt_text (gpg_text, gpgme_ctx):
86663388 324
394a1476 325 body = ""
d0489345 326 fingerprints = []
0bec96d6 327
5b3053c1 328 msg_blocks = scan_and_grab(gpg_text,
c267c233
AE
329 '-----BEGIN PGP MESSAGE-----',
330 '-----END PGP MESSAGE-----')
86663388 331
5b3053c1 332 plaintexts_and_sigs = decrypt_blocks(msg_blocks, gpgme_ctx)
0bec96d6 333
5b3053c1
AE
334 for pair in plaintexts_and_sigs:
335 plaintext = pair[0]
336 sigs = pair[1]
9eb78301 337
394a1476 338 for sig in sigs:
d0489345 339 fingerprints += [sig.fpr]
0bec96d6 340
394a1476 341 # recursive for nested layers of mime and/or gpg
d0489345 342 plaintext, more_fps = email_decode_flatten(plaintext, gpgme_ctx, True)
8bb4b0d5 343
394a1476 344 body += plaintext
d0489345 345 fingerprints += more_fps
8bb4b0d5 346
d0489345 347 return body, fingerprints
8bb4b0d5 348
8bb4b0d5 349
cf75de65
AE
350def verify_clear_signature (text, gpgme_ctx):
351
352 sig_blocks = scan_and_grab(text,
353 '-----BEGIN PGP SIGNED MESSAGE-----',
354 '-----END PGP SIGNATURE-----')
355
356 fingerprints = []
357 plaintext = ""
358
359 for sig_block in sig_blocks:
360 msg_fp = io.BytesIO(sig_block.encode('utf-8'))
361 ptxt_fp = io.BytesIO()
362
363 result = gpgme_ctx.verify(msg_fp, None, ptxt_fp)
364
365 plaintext += ptxt_fp.getvalue().decode('utf-8')
366 fingerprint = result[0].fpr
367
368 fingerprints += [fingerprint]
369
370 return plaintext, fingerprints
371
372
c267c233 373def scan_and_grab (text, start_text, end_text):
0bec96d6 374
c267c233
AE
375 matches = re.search('(' + start_text + '.*' + end_text + ')',
376 text, flags=re.DOTALL)
0bec96d6 377
c267c233
AE
378 if matches != None:
379 match_tuple = matches.groups()
0bec96d6 380 else:
c267c233 381 match_tuple = ()
7b06b980 382
c267c233 383 return match_tuple
0bec96d6
AE
384
385
5b3053c1 386def decrypt_blocks (msg_blocks, gpgme_ctx):
0bec96d6 387
5b3053c1 388 return [decrypt_block(block, gpgme_ctx) for block in msg_blocks]
0bec96d6 389
0bec96d6 390
5b3053c1 391def decrypt_block (msg_block, gpgme_ctx):
0bec96d6 392
5b3053c1 393 block_b = io.BytesIO(msg_block.encode('ascii'))
0bec96d6
AE
394 plain_b = io.BytesIO()
395
afc1f64c
AE
396 try:
397 sigs = gpgme_ctx.decrypt_verify(block_b, plain_b)
398 except:
399 return ("",[])
0bec96d6 400
6aa41372 401 plaintext = plain_b.getvalue().decode('utf-8')
394a1476 402 return (plaintext, sigs)
0bec96d6
AE
403
404
d0489345 405def choose_reply_encryption_key (gpgme_ctx, fingerprints):
fafa21c3
AE
406
407 reply_key = None
d0489345
AE
408 for fp in fingerprints:
409 try:
410 key = gpgme_ctx.get_key(fp)
411
412 if (key.can_encrypt == True):
413 reply_key = key
414 break
415 except:
416 continue
417
fafa21c3 418
216708e9 419 return reply_key
fafa21c3
AE
420
421
897cbaf6 422def generate_reply (plaintext, email_from, email_subject, encrypt_to_key,
0a064403 423 gpgme_ctx):
1da9b527 424
8bdfb6d4 425
1da9b527
AE
426 reply = "To: " + email_from + "\n"
427 reply += "Subject: " + email_subject + "\n"
216708e9
AE
428
429 if (encrypt_to_key != None):
430 plaintext_reply = "thanks for the message!\n\n\n"
431 plaintext_reply += email_quote_text(plaintext)
432
8bdfb6d4
AE
433 # quoted printable encoding lets most ascii characters look normal
434 # before the decrypted mime message is decoded.
435 char_set = email.charset.Charset("utf-8")
436 char_set.body_encoding = email.charset.QP
437
438 # MIMEText doesn't allow setting the text encoding
439 # so we use MIMENonMultipart.
440 plaintext_mime = MIMENonMultipart('text', 'plain')
441 plaintext_mime.set_payload(plaintext_reply, charset=char_set)
442
443 encrypted_text = encrypt_sign_message(plaintext_mime.as_string(),
444 encrypt_to_key,
40c37ab3 445 gpgme_ctx)
8bdfb6d4
AE
446
447 control_mime = MIMEApplication("Version: 1",
448 _subtype='pgp-encrypted',
449 _encoder=email.encoders.encode_7or8bit)
450 control_mime['Content-Description'] = 'PGP/MIME version identification'
451 control_mime.set_charset('us-ascii')
452
453 encoded_mime = MIMEApplication(encrypted_text,
454 _subtype='octet-stream; name="encrypted.asc"',
455 _encoder=email.encoders.encode_7or8bit)
456 encoded_mime['Content-Description'] = 'OpenPGP encrypted message'
457 encoded_mime['Content-Disposition'] = 'inline; filename="encrypted.asc"'
458 encoded_mime.set_charset('us-ascii')
459
460 message_mime = MIMEMultipart(_subtype="encrypted", protocol="application/pgp-encrypted")
461 message_mime.attach(control_mime)
462 message_mime.attach(encoded_mime)
463 message_mime['Content-Disposition'] = 'inline'
216708e9 464
8bdfb6d4 465 reply += message_mime.as_string()
216708e9
AE
466
467 else:
8bdfb6d4 468 reply += "\n"
216708e9
AE
469 reply += "Sorry, i couldn't find your key.\n"
470 reply += "I'll need that to encrypt a message to you."
1da9b527
AE
471
472 return reply
473
474
f87041f8
AE
475def email_quote_text (text):
476
477 quoted_message = re.sub(r'^', r'> ', text, flags=re.MULTILINE)
478
479 return quoted_message
480
481
0a064403 482def encrypt_sign_message (plaintext, encrypt_to_key, gpgme_ctx):
897cbaf6 483
6aa41372 484 plaintext_bytes = io.BytesIO(plaintext.encode('ascii'))
1da9b527
AE
485 encrypted_bytes = io.BytesIO()
486
897cbaf6 487 gpgme_ctx.encrypt_sign([encrypt_to_key], gpgme.ENCRYPT_ALWAYS_TRUST,
1da9b527
AE
488 plaintext_bytes, encrypted_bytes)
489
6aa41372 490 encrypted_txt = encrypted_bytes.getvalue().decode('ascii')
1da9b527
AE
491 return encrypted_txt
492
493
0a064403
AE
494def error (error_msg):
495
e4fb2ab2 496 sys.stderr.write(progname + ": " + str(error_msg) + "\n")
0a064403
AE
497
498
5e8f9094
AE
499def debug (debug_msg):
500
501 if edward_config.debug == True:
0a064403 502 error(debug_msg)
5e8f9094
AE
503
504
20f6e7c5
AE
505def handle_args ():
506 if __name__ == "__main__":
507
508 global progname
509 progname = sys.argv[0]
510
511 if len(sys.argv) > 1:
512 print(progname + ": error, this program doesn't " \
513 "need any arguments.", file=sys.stderr)
514 exit(1)
515
516
0bec96d6
AE
517main()
518