move this here
[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)
56578eaf 99 email_from, email_subject = email_from_subject(email_text)
fafa21c3 100
1fccb295
AE
101 print(flatten_eddy(result))
102
56578eaf
AE
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
0bec96d6 109
0a064403
AE
110def get_gpg_context (gnupghome, sign_with_key_fp):
111
112 os.environ['GNUPGHOME'] = gnupghome
113
114 gpgme_ctx = gpgme.Context()
115 gpgme_ctx.armor = True
116
117 try:
118 sign_with_key = gpgme_ctx.get_key(sign_with_key_fp)
119 except:
120 error("unable to load signing key. is the gnupghome "
121 + "and signing key properly set in the edward_config.py?")
122 exit(1)
123
124 gpgme_ctx.signers = [sign_with_key]
125
126 return gpgme_ctx
127
128
38738401 129def parse_pgp_mime (email_text, gpgme_ctx):
394a1476
AE
130
131 email_struct = email.parser.Parser().parsestr(email_text)
132
56578eaf
AE
133 eddy_obj = parse_mime(email_struct)
134 eddy_obj = split_payloads(eddy_obj)
8f61c66a 135 eddy_obj = gpg_on_payloads(eddy_obj, gpgme_ctx)
8bb4b0d5 136
56578eaf 137 return eddy_obj
0bec96d6 138
0bec96d6 139
56578eaf 140def parse_mime(msg_struct):
0bec96d6 141
56578eaf 142 eddy_obj = EddyMsg()
8bb4b0d5 143
56578eaf
AE
144 if msg_struct.is_multipart() == True:
145 payloads = msg_struct.get_payload()
0bec96d6 146
56578eaf 147 eddy_obj.multipart = True
dd11a483
AE
148 eddy_obj.subparts = list(map(parse_mime, payloads))
149
56578eaf
AE
150 else:
151 eddy_obj = get_subpart_data(msg_struct)
394a1476 152
56578eaf 153 return eddy_obj
c267c233 154
80119cab 155
56578eaf 156def scan_and_split (payload_piece, match_type, pattern):
cf75de65 157
a5d37d44
AE
158 # don't try to re-split pieces containing gpg data
159 if payload_piece.piece_type != "text":
160 return [payload_piece]
161
56578eaf
AE
162 flags = re.DOTALL | re.MULTILINE
163 matches = re.search("(?P<beginning>.*?)(?P<match>" + pattern +
164 ")(?P<rest>.*)", payload_piece.string, flags=flags)
86663388 165
56578eaf
AE
166 if matches == None:
167 pieces = [payload_piece]
c96f3837 168
56578eaf 169 else:
d437f8b2 170
56578eaf
AE
171 beginning = PayloadPiece()
172 beginning.string = matches.group('beginning')
173 beginning.piece_type = payload_piece.piece_type
d437f8b2 174
56578eaf
AE
175 match = PayloadPiece()
176 match.string = matches.group('match')
177 match.piece_type = match_type
d437f8b2 178
56578eaf
AE
179 rest = PayloadPiece()
180 rest.string = matches.group('rest')
181 rest.piece_type = payload_piece.piece_type
d437f8b2 182
56578eaf 183 more_pieces = scan_and_split(rest, match_type, pattern)
4615b156 184 pieces = [beginning, match ] + more_pieces
d437f8b2 185
56578eaf 186 return pieces
d437f8b2 187
d437f8b2 188
56578eaf 189def get_subpart_data (part):
0bec96d6 190
56578eaf 191 obj = EddyMsg()
0bec96d6 192
56578eaf
AE
193 obj.charset = part.get_content_charset()
194 obj.payload_bytes = part.get_payload(decode=True)
195
196 obj.filename = part.get_filename()
197 obj.content_type = part.get_content_type()
198 obj.description_list = part['content-description']
199
200 # your guess is as good as a-myy-ee-ine...
201 if obj.charset == None:
202 obj.charset = 'utf-8'
203
204 if obj.payload_bytes != None:
0eb75d9c
AE
205 try:
206 payload = PayloadPiece()
207 payload.string = obj.payload_bytes.decode(obj.charset)
208 payload.piece_type = 'text'
209
210 obj.payload_pieces = [payload]
211 except UnicodeDecodeError:
212 pass
56578eaf
AE
213
214 return obj
215
216
dd11a483 217def do_to_eddys_pieces (function_to_do, eddy_obj, data):
56578eaf 218
e33518c8
AE
219 if eddy_obj == None:
220 return []
221
56578eaf 222 if eddy_obj.multipart == True:
dd11a483
AE
223 result_list = []
224 for sub in eddy_obj.subparts:
225 result_list += do_to_eddys_pieces(function_to_do, sub, data)
394a1476 226 else:
a5d37d44 227 result_list = [function_to_do(eddy_obj, data)]
dd11a483
AE
228
229 return result_list
230
231
a5d37d44
AE
232def split_payloads (eddy_obj):
233
234 for match_type in match_types:
235 do_to_eddys_pieces(split_payload_pieces, eddy_obj, match_type)
236
237 return eddy_obj
238
239
240def split_payload_pieces (eddy_obj, match_type):
241
242 (match_name, pattern) = match_type
243
244 new_pieces_list = []
245 for piece in eddy_obj.payload_pieces:
246 new_pieces_list += scan_and_split(piece, match_name, pattern)
247
248 eddy_obj.payload_pieces = new_pieces_list
249
250
8f61c66a 251def gpg_on_payloads (eddy_obj, gpgme_ctx):
38738401 252
8f61c66a 253 do_to_eddys_pieces(gpg_on_payload_pieces, eddy_obj, gpgme_ctx)
38738401
AE
254
255 return eddy_obj
256
257
8f61c66a 258def gpg_on_payload_pieces (eddy_obj, gpgme_ctx):
38738401 259
a5d37d44 260 for piece in eddy_obj.payload_pieces:
38738401
AE
261
262 if piece.piece_type == "text":
263 # don't transform the plaintext.
264 pass
265
266 elif piece.piece_type == "message":
267 (plaintext, sigs) = decrypt_block (piece.string, gpgme_ctx)
268
269 if plaintext:
270 piece.gpg_data = GPGData()
271 piece.gpg_data.sigs = sigs
272 # recurse!
273 piece.gpg_data.plainobj = parse_pgp_mime(plaintext, gpgme_ctx)
129543c3 274
8f61c66a
AE
275 elif piece.piece_type == "pubkey":
276 fingerprints = add_gpg_key(piece.string, gpgme_ctx)
277
278 if fingerprints != []:
279 piece.gpg_data = GPGData()
280 piece.gpg_data.keys = fingerprints
129543c3
AE
281
282 elif piece.piece_type == "clearsign":
283 (plaintext, fingerprints) = verify_clear_signature(piece.string, gpgme_ctx)
284
285 if fingerprints != []:
286 piece.gpg_data = GPGData()
287 piece.gpg_data.sigs = fingerprints
288 piece.gpg_data.plainobj = parse_pgp_mime(plaintext, gpgme_ctx)
289
38738401
AE
290 else:
291 pass
292
293
dd11a483
AE
294def flatten_eddy (eddy_obj):
295
296 string = "\n".join(do_to_eddys_pieces(flatten_payload_pieces, eddy_obj, None))
56578eaf
AE
297
298 return string
299
300
a5d37d44 301def flatten_payload_pieces (eddy_obj, _ignore):
0bec96d6 302
56578eaf 303 string = ""
a5d37d44 304 for piece in eddy_obj.payload_pieces:
38738401
AE
305 if piece.piece_type == "text":
306 string += piece.string
efe7336d
AE
307 elif piece.gpg_data == None:
308 string += "Hmmm... I wasn't able to get that part.\n"
38738401
AE
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