avoid a certain type of bug
[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)
8f61c66a 137 eddy_obj = gpg_on_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 scan_and_split (payload_piece, match_type, pattern):
cf75de65 159
a5d37d44
AE
160 # don't try to re-split pieces containing gpg data
161 if payload_piece.piece_type != "text":
162 return [payload_piece]
163
56578eaf
AE
164 flags = re.DOTALL | re.MULTILINE
165 matches = re.search("(?P<beginning>.*?)(?P<match>" + pattern +
166 ")(?P<rest>.*)", payload_piece.string, flags=flags)
86663388 167
56578eaf
AE
168 if matches == None:
169 pieces = [payload_piece]
c96f3837 170
56578eaf 171 else:
d437f8b2 172
56578eaf
AE
173 beginning = PayloadPiece()
174 beginning.string = matches.group('beginning')
175 beginning.piece_type = payload_piece.piece_type
d437f8b2 176
56578eaf
AE
177 match = PayloadPiece()
178 match.string = matches.group('match')
179 match.piece_type = match_type
d437f8b2 180
56578eaf
AE
181 rest = PayloadPiece()
182 rest.string = matches.group('rest')
183 rest.piece_type = payload_piece.piece_type
d437f8b2 184
56578eaf 185 more_pieces = scan_and_split(rest, match_type, pattern)
4615b156 186 pieces = [beginning, match ] + more_pieces
d437f8b2 187
56578eaf 188 return pieces
d437f8b2 189
d437f8b2 190
56578eaf 191def get_subpart_data (part):
0bec96d6 192
56578eaf 193 obj = EddyMsg()
0bec96d6 194
56578eaf
AE
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:
0eb75d9c
AE
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
56578eaf
AE
215
216 return obj
217
218
dd11a483 219def do_to_eddys_pieces (function_to_do, eddy_obj, data):
56578eaf 220
e33518c8
AE
221 if eddy_obj == None:
222 return []
223
56578eaf 224 if eddy_obj.multipart == True:
dd11a483
AE
225 result_list = []
226 for sub in eddy_obj.subparts:
227 result_list += do_to_eddys_pieces(function_to_do, sub, data)
394a1476 228 else:
a5d37d44 229 result_list = [function_to_do(eddy_obj, data)]
dd11a483
AE
230
231 return result_list
232
233
a5d37d44
AE
234def split_payloads (eddy_obj):
235
236 for match_type in match_types:
237 do_to_eddys_pieces(split_payload_pieces, eddy_obj, match_type)
238
239 return eddy_obj
240
241
242def split_payload_pieces (eddy_obj, match_type):
243
244 (match_name, pattern) = match_type
245
246 new_pieces_list = []
247 for piece in eddy_obj.payload_pieces:
248 new_pieces_list += scan_and_split(piece, match_name, pattern)
249
250 eddy_obj.payload_pieces = new_pieces_list
251
252
8f61c66a 253def gpg_on_payloads (eddy_obj, gpgme_ctx):
38738401 254
8f61c66a 255 do_to_eddys_pieces(gpg_on_payload_pieces, eddy_obj, gpgme_ctx)
38738401
AE
256
257 return eddy_obj
258
259
8f61c66a 260def gpg_on_payload_pieces (eddy_obj, gpgme_ctx):
38738401 261
a5d37d44 262 for piece in eddy_obj.payload_pieces:
38738401
AE
263
264 if piece.piece_type == "text":
265 # don't transform the plaintext.
266 pass
267
268 elif piece.piece_type == "message":
269 (plaintext, sigs) = decrypt_block (piece.string, gpgme_ctx)
270
271 if plaintext:
272 piece.gpg_data = GPGData()
273 piece.gpg_data.sigs = sigs
274 # recurse!
275 piece.gpg_data.plainobj = parse_pgp_mime(plaintext, gpgme_ctx)
129543c3 276
8f61c66a
AE
277 elif piece.piece_type == "pubkey":
278 fingerprints = add_gpg_key(piece.string, gpgme_ctx)
279
280 if fingerprints != []:
281 piece.gpg_data = GPGData()
282 piece.gpg_data.keys = fingerprints
129543c3
AE
283
284 elif piece.piece_type == "clearsign":
285 (plaintext, fingerprints) = verify_clear_signature(piece.string, gpgme_ctx)
286
287 if fingerprints != []:
288 piece.gpg_data = GPGData()
289 piece.gpg_data.sigs = fingerprints
290 piece.gpg_data.plainobj = parse_pgp_mime(plaintext, gpgme_ctx)
291
38738401
AE
292 else:
293 pass
294
295
dd11a483
AE
296def flatten_eddy (eddy_obj):
297
298 string = "\n".join(do_to_eddys_pieces(flatten_payload_pieces, eddy_obj, None))
56578eaf
AE
299
300 return string
301
302
a5d37d44 303def flatten_payload_pieces (eddy_obj, _ignore):
0bec96d6 304
56578eaf 305 string = ""
a5d37d44 306 for piece in eddy_obj.payload_pieces:
38738401
AE
307 if piece.piece_type == "text":
308 string += piece.string
309 elif piece.piece_type == "message":
310 # recursive!
311 string += flatten_eddy(piece.gpg_data.plainobj)
8f61c66a
AE
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
129543c3
AE
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 ***"
56578eaf
AE
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
0bec96d6 332
0bec96d6 333
8f61c66a 334def add_gpg_key (key_block, gpgme_ctx):
c267c233 335
8f61c66a 336 fp = io.BytesIO(key_block.encode('ascii'))
c267c233 337
8f61c66a
AE
338 result = gpgme_ctx.import_(fp)
339 imports = result.imports
c267c233 340
8f61c66a 341 fingerprints = []
c267c233 342
8f61c66a
AE
343 if imports != []:
344 for import_ in imports:
345 fingerprint = import_[0]
d0489345 346 fingerprints += [fingerprint]
c267c233 347
e49673aa 348 debug("added gpg key: " + fingerprint)
ec1e779a 349
d0489345 350 return fingerprints
ec1e779a
AE
351
352
129543c3 353def verify_clear_signature (sig_block, gpgme_ctx):
cf75de65 354
129543c3
AE
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()
cf75de65 359
129543c3 360 result = gpgme_ctx.verify(msg_fp, None, ptxt_fp)
cf75de65 361
129543c3
AE
362 # FIXME: this might require using the charset of the mime part.
363 plaintext = ptxt_fp.getvalue().decode('utf-8')
cf75de65 364
129543c3
AE
365 fingerprints = []
366 for res_ in result:
367 fingerprints += [res_.fpr]
cf75de65
AE
368
369 return plaintext, fingerprints
370
371
5b3053c1 372def decrypt_block (msg_block, gpgme_ctx):
0bec96d6 373
5b3053c1 374 block_b = io.BytesIO(msg_block.encode('ascii'))
0bec96d6
AE
375 plain_b = io.BytesIO()
376
afc1f64c
AE
377 try:
378 sigs = gpgme_ctx.decrypt_verify(block_b, plain_b)
379 except:
380 return ("",[])
0bec96d6 381
6aa41372 382 plaintext = plain_b.getvalue().decode('utf-8')
394a1476 383 return (plaintext, sigs)
0bec96d6
AE
384
385
d0489345 386def choose_reply_encryption_key (gpgme_ctx, fingerprints):
fafa21c3
AE
387
388 reply_key = None
d0489345
AE
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
fafa21c3 399
216708e9 400 return reply_key
fafa21c3
AE
401
402
897cbaf6 403def generate_reply (plaintext, email_from, email_subject, encrypt_to_key,
0a064403 404 gpgme_ctx):
1da9b527 405
8bdfb6d4 406
1da9b527
AE
407 reply = "To: " + email_from + "\n"
408 reply += "Subject: " + email_subject + "\n"
216708e9
AE
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
8bdfb6d4
AE
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,
40c37ab3 426 gpgme_ctx)
8bdfb6d4
AE
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'
216708e9 445
8bdfb6d4 446 reply += message_mime.as_string()
216708e9
AE
447
448 else:
8bdfb6d4 449 reply += "\n"
216708e9
AE
450 reply += "Sorry, i couldn't find your key.\n"
451 reply += "I'll need that to encrypt a message to you."
1da9b527
AE
452
453 return reply
454
455
f87041f8
AE
456def email_quote_text (text):
457
458 quoted_message = re.sub(r'^', r'> ', text, flags=re.MULTILINE)
459
460 return quoted_message
461
462
0a064403 463def encrypt_sign_message (plaintext, encrypt_to_key, gpgme_ctx):
897cbaf6 464
6aa41372 465 plaintext_bytes = io.BytesIO(plaintext.encode('ascii'))
1da9b527
AE
466 encrypted_bytes = io.BytesIO()
467
897cbaf6 468 gpgme_ctx.encrypt_sign([encrypt_to_key], gpgme.ENCRYPT_ALWAYS_TRUST,
1da9b527
AE
469 plaintext_bytes, encrypted_bytes)
470
6aa41372 471 encrypted_txt = encrypted_bytes.getvalue().decode('ascii')
1da9b527
AE
472 return encrypted_txt
473
474
0a064403
AE
475def error (error_msg):
476
e4fb2ab2 477 sys.stderr.write(progname + ": " + str(error_msg) + "\n")
0a064403
AE
478
479
5e8f9094
AE
480def debug (debug_msg):
481
482 if edward_config.debug == True:
0a064403 483 error(debug_msg)
5e8f9094
AE
484
485
20f6e7c5
AE
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
0bec96d6
AE
498main()
499