moved 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)
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#
bf79a93e
AE
105# reply_mime = generate_encrypted_mime(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
8f61c66a 324def add_gpg_key (key_block, gpgme_ctx):
c267c233 325
8f61c66a 326 fp = io.BytesIO(key_block.encode('ascii'))
c267c233 327
8f61c66a
AE
328 result = gpgme_ctx.import_(fp)
329 imports = result.imports
c267c233 330
8f61c66a 331 fingerprints = []
c267c233 332
8f61c66a
AE
333 if imports != []:
334 for import_ in imports:
335 fingerprint = import_[0]
d0489345 336 fingerprints += [fingerprint]
c267c233 337
e49673aa 338 debug("added gpg key: " + fingerprint)
ec1e779a 339
d0489345 340 return fingerprints
ec1e779a
AE
341
342
129543c3 343def verify_clear_signature (sig_block, gpgme_ctx):
cf75de65 344
129543c3
AE
345 # FIXME: this might require the un-decoded bytes
346 # or the correct re-encoding with the carset of the mime part.
347 msg_fp = io.BytesIO(sig_block.encode('utf-8'))
348 ptxt_fp = io.BytesIO()
cf75de65 349
129543c3 350 result = gpgme_ctx.verify(msg_fp, None, ptxt_fp)
cf75de65 351
129543c3
AE
352 # FIXME: this might require using the charset of the mime part.
353 plaintext = ptxt_fp.getvalue().decode('utf-8')
cf75de65 354
129543c3
AE
355 fingerprints = []
356 for res_ in result:
357 fingerprints += [res_.fpr]
cf75de65
AE
358
359 return plaintext, fingerprints
360
361
5b3053c1 362def decrypt_block (msg_block, gpgme_ctx):
0bec96d6 363
5b3053c1 364 block_b = io.BytesIO(msg_block.encode('ascii'))
0bec96d6
AE
365 plain_b = io.BytesIO()
366
afc1f64c
AE
367 try:
368 sigs = gpgme_ctx.decrypt_verify(block_b, plain_b)
369 except:
370 return ("",[])
0bec96d6 371
6aa41372 372 plaintext = plain_b.getvalue().decode('utf-8')
394a1476 373 return (plaintext, sigs)
0bec96d6
AE
374
375
d0489345 376def choose_reply_encryption_key (gpgme_ctx, fingerprints):
fafa21c3
AE
377
378 reply_key = None
d0489345
AE
379 for fp in fingerprints:
380 try:
381 key = gpgme_ctx.get_key(fp)
382
383 if (key.can_encrypt == True):
384 reply_key = key
385 break
386 except:
387 continue
388
fafa21c3 389
216708e9 390 return reply_key
fafa21c3
AE
391
392
d65993b8
AE
393def email_to_from_subject (email_text):
394
395 email_struct = email.parser.Parser().parsestr(email_text)
396
397 email_to = email_struct['To']
398 email_from = email_struct['From']
399 email_subject = email_struct['Subject']
400
401 return email_to, email_from, email_subject
402
403
bf79a93e 404def generate_encrypted_mime (plaintext, email_from, email_subject, encrypt_to_key,
0a064403 405 gpgme_ctx):
1da9b527 406
8bdfb6d4 407
1da9b527
AE
408 reply = "To: " + email_from + "\n"
409 reply += "Subject: " + email_subject + "\n"
216708e9
AE
410
411 if (encrypt_to_key != None):
412 plaintext_reply = "thanks for the message!\n\n\n"
413 plaintext_reply += email_quote_text(plaintext)
414
8bdfb6d4
AE
415 # quoted printable encoding lets most ascii characters look normal
416 # before the decrypted mime message is decoded.
417 char_set = email.charset.Charset("utf-8")
418 char_set.body_encoding = email.charset.QP
419
420 # MIMEText doesn't allow setting the text encoding
421 # so we use MIMENonMultipart.
422 plaintext_mime = MIMENonMultipart('text', 'plain')
423 plaintext_mime.set_payload(plaintext_reply, charset=char_set)
424
425 encrypted_text = encrypt_sign_message(plaintext_mime.as_string(),
426 encrypt_to_key,
40c37ab3 427 gpgme_ctx)
8bdfb6d4
AE
428
429 control_mime = MIMEApplication("Version: 1",
430 _subtype='pgp-encrypted',
431 _encoder=email.encoders.encode_7or8bit)
432 control_mime['Content-Description'] = 'PGP/MIME version identification'
433 control_mime.set_charset('us-ascii')
434
435 encoded_mime = MIMEApplication(encrypted_text,
436 _subtype='octet-stream; name="encrypted.asc"',
437 _encoder=email.encoders.encode_7or8bit)
438 encoded_mime['Content-Description'] = 'OpenPGP encrypted message'
439 encoded_mime['Content-Disposition'] = 'inline; filename="encrypted.asc"'
440 encoded_mime.set_charset('us-ascii')
441
442 message_mime = MIMEMultipart(_subtype="encrypted", protocol="application/pgp-encrypted")
443 message_mime.attach(control_mime)
444 message_mime.attach(encoded_mime)
445 message_mime['Content-Disposition'] = 'inline'
216708e9 446
8bdfb6d4 447 reply += message_mime.as_string()
216708e9
AE
448
449 else:
8bdfb6d4 450 reply += "\n"
216708e9
AE
451 reply += "Sorry, i couldn't find your key.\n"
452 reply += "I'll need that to encrypt a message to you."
1da9b527
AE
453
454 return reply
455
456
f87041f8
AE
457def email_quote_text (text):
458
459 quoted_message = re.sub(r'^', r'> ', text, flags=re.MULTILINE)
460
461 return quoted_message
462
463
0a064403 464def encrypt_sign_message (plaintext, encrypt_to_key, gpgme_ctx):
897cbaf6 465
6aa41372 466 plaintext_bytes = io.BytesIO(plaintext.encode('ascii'))
1da9b527
AE
467 encrypted_bytes = io.BytesIO()
468
897cbaf6 469 gpgme_ctx.encrypt_sign([encrypt_to_key], gpgme.ENCRYPT_ALWAYS_TRUST,
1da9b527
AE
470 plaintext_bytes, encrypted_bytes)
471
6aa41372 472 encrypted_txt = encrypted_bytes.getvalue().decode('ascii')
1da9b527
AE
473 return encrypted_txt
474
475
0a064403
AE
476def error (error_msg):
477
e4fb2ab2 478 sys.stderr.write(progname + ": " + str(error_msg) + "\n")
0a064403
AE
479
480
5e8f9094
AE
481def debug (debug_msg):
482
483 if edward_config.debug == True:
0a064403 484 error(debug_msg)
5e8f9094
AE
485
486
20f6e7c5
AE
487def handle_args ():
488 if __name__ == "__main__":
489
490 global progname
491 progname = sys.argv[0]
492
493 if len(sys.argv) > 1:
494 print(progname + ": error, this program doesn't " \
495 "need any arguments.", file=sys.stderr)
496 exit(1)
497
498
0bec96d6
AE
499main()
500