return fingerprints not sig objects
[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
adcef2f7 39import importlib
0bec96d6 40
8bdfb6d4
AE
41import email.parser
42import email.message
43import email.encoders
44
45from email.mime.multipart import MIMEMultipart
46from email.mime.application import MIMEApplication
47from email.mime.nonmultipart import MIMENonMultipart
48
40c37ab3 49import edward_config
c96f3837 50
adcef2f7
AE
51langs = ["an", "de", "el", "en", "fr", "ja", "pt-br", "ro", "ru", "tr"]
52
38738401
AE
53match_types = [('clearsign',
54 '-----BEGIN PGP SIGNED MESSAGE-----.*?-----BEGIN PGP SIGNATURE-----.*?-----END PGP SIGNATURE-----'),
55 ('message',
56578eaf
AE
56 '-----BEGIN PGP MESSAGE-----.*?-----END PGP MESSAGE-----'),
57 ('pubkey',
58 '-----BEGIN PGP PUBLIC KEY BLOCK-----.*?-----END PGP PUBLIC KEY BLOCK-----'),
59 ('detachedsig',
38738401 60 '-----BEGIN PGP SIGNATURE-----.*?-----END PGP SIGNATURE-----')]
56578eaf
AE
61
62
63class EddyMsg (object):
64 def __init__(self):
65 self.multipart = False
66 self.subparts = []
67
68 self.charset = None
69 self.payload_bytes = None
70 self.payload_pieces = []
71
72 self.filename = None
73 self.content_type = None
74 self.description_list = None
75
76
77class PayloadPiece (object):
78 def __init__(self):
79 self.piece_type = None
80 self.string = None
81 self.gpg_data = None
82
83
38738401
AE
84class GPGData (object):
85 def __init__(self):
86 self.decrypted = False
87
88 self.plainobj = None
89 self.sigs = []
90 self.keys = []
91
d873ff48
AE
92class ReplyInfo (object):
93 def __init__(self):
94 self.replies = None
95 self.msg_to_quote = ""
96
97 self.success_decrypt = False
98 self.failed_decrypt = False
99 self.public_key_received = False
100 self.no_public_key = False
101 self.sig_success = False
102 self.sig_failure = False
103
38738401 104
0bec96d6
AE
105def main ():
106
20f6e7c5 107 handle_args()
0bec96d6 108
0a064403
AE
109 gpgme_ctx = get_gpg_context(edward_config.gnupghome,
110 edward_config.sign_with_key)
111
c96f3837 112 email_text = sys.stdin.read()
adcef2f7
AE
113 email_struct = parse_pgp_mime(email_text, gpgme_ctx)
114
115 email_to, email_from, email_subject = email_to_from_subject(email_text)
116 lang = import_lang(email_to)
fafa21c3 117
d873ff48
AE
118 replyinfo_obj = ReplyInfo()
119 replyinfo_obj.replies = lang.replies
120
121 prepare_for_reply(email_struct, replyinfo_obj)
122 reply_plaintext = write_reply(replyinfo_obj)
adcef2f7 123
adcef2f7 124 print(reply_plaintext)
1fccb295 125
56578eaf
AE
126# encrypt_to_key = choose_reply_encryption_key(gpgme_ctx, fingerprints)
127#
bf79a93e
AE
128# reply_mime = generate_encrypted_mime(plaintext, email_from, \
129# email_subject, encrypt_to_key,
130# gpgme_ctx)
c96f3837 131
0bec96d6 132
0a064403
AE
133def get_gpg_context (gnupghome, sign_with_key_fp):
134
135 os.environ['GNUPGHOME'] = gnupghome
136
137 gpgme_ctx = gpgme.Context()
138 gpgme_ctx.armor = True
139
140 try:
141 sign_with_key = gpgme_ctx.get_key(sign_with_key_fp)
142 except:
143 error("unable to load signing key. is the gnupghome "
144 + "and signing key properly set in the edward_config.py?")
145 exit(1)
146
147 gpgme_ctx.signers = [sign_with_key]
148
149 return gpgme_ctx
150
151
38738401 152def parse_pgp_mime (email_text, gpgme_ctx):
394a1476
AE
153
154 email_struct = email.parser.Parser().parsestr(email_text)
155
928e3819
AE
156 eddymsg_obj = parse_mime(email_struct)
157 split_payloads(eddymsg_obj)
158 gpg_on_payloads(eddymsg_obj, gpgme_ctx)
8bb4b0d5 159
928e3819 160 return eddymsg_obj
0bec96d6 161
0bec96d6 162
56578eaf 163def parse_mime(msg_struct):
0bec96d6 164
928e3819 165 eddymsg_obj = EddyMsg()
8bb4b0d5 166
56578eaf
AE
167 if msg_struct.is_multipart() == True:
168 payloads = msg_struct.get_payload()
0bec96d6 169
928e3819
AE
170 eddymsg_obj.multipart = True
171 eddymsg_obj.subparts = list(map(parse_mime, payloads))
dd11a483 172
56578eaf 173 else:
928e3819 174 eddymsg_obj = get_subpart_data(msg_struct)
394a1476 175
928e3819 176 return eddymsg_obj
c267c233 177
80119cab 178
56578eaf 179def scan_and_split (payload_piece, match_type, pattern):
cf75de65 180
a5d37d44
AE
181 # don't try to re-split pieces containing gpg data
182 if payload_piece.piece_type != "text":
183 return [payload_piece]
184
56578eaf
AE
185 flags = re.DOTALL | re.MULTILINE
186 matches = re.search("(?P<beginning>.*?)(?P<match>" + pattern +
187 ")(?P<rest>.*)", payload_piece.string, flags=flags)
86663388 188
56578eaf
AE
189 if matches == None:
190 pieces = [payload_piece]
c96f3837 191
56578eaf 192 else:
d437f8b2 193
56578eaf
AE
194 beginning = PayloadPiece()
195 beginning.string = matches.group('beginning')
196 beginning.piece_type = payload_piece.piece_type
d437f8b2 197
56578eaf
AE
198 match = PayloadPiece()
199 match.string = matches.group('match')
200 match.piece_type = match_type
d437f8b2 201
56578eaf
AE
202 rest = PayloadPiece()
203 rest.string = matches.group('rest')
204 rest.piece_type = payload_piece.piece_type
d437f8b2 205
56578eaf 206 more_pieces = scan_and_split(rest, match_type, pattern)
4615b156 207 pieces = [beginning, match ] + more_pieces
d437f8b2 208
56578eaf 209 return pieces
d437f8b2 210
d437f8b2 211
56578eaf 212def get_subpart_data (part):
0bec96d6 213
56578eaf 214 obj = EddyMsg()
0bec96d6 215
56578eaf
AE
216 obj.charset = part.get_content_charset()
217 obj.payload_bytes = part.get_payload(decode=True)
218
219 obj.filename = part.get_filename()
220 obj.content_type = part.get_content_type()
221 obj.description_list = part['content-description']
222
223 # your guess is as good as a-myy-ee-ine...
224 if obj.charset == None:
225 obj.charset = 'utf-8'
226
227 if obj.payload_bytes != None:
0eb75d9c
AE
228 try:
229 payload = PayloadPiece()
230 payload.string = obj.payload_bytes.decode(obj.charset)
231 payload.piece_type = 'text'
232
233 obj.payload_pieces = [payload]
234 except UnicodeDecodeError:
235 pass
56578eaf
AE
236
237 return obj
238
239
928e3819 240def do_to_eddys_pieces (function_to_do, eddymsg_obj, data):
56578eaf 241
928e3819
AE
242 if eddymsg_obj.multipart == True:
243 for sub in eddymsg_obj.subparts:
d873ff48 244 do_to_eddys_pieces(function_to_do, sub, data)
394a1476 245 else:
928e3819 246 function_to_do(eddymsg_obj, data)
dd11a483
AE
247
248
928e3819 249def split_payloads (eddymsg_obj):
a5d37d44
AE
250
251 for match_type in match_types:
928e3819 252 do_to_eddys_pieces(split_payload_pieces, eddymsg_obj, match_type)
a5d37d44 253
a5d37d44 254
928e3819 255def split_payload_pieces (eddymsg_obj, match_type):
a5d37d44
AE
256
257 (match_name, pattern) = match_type
258
259 new_pieces_list = []
928e3819 260 for piece in eddymsg_obj.payload_pieces:
a5d37d44
AE
261 new_pieces_list += scan_and_split(piece, match_name, pattern)
262
928e3819 263 eddymsg_obj.payload_pieces = new_pieces_list
a5d37d44
AE
264
265
928e3819 266def gpg_on_payloads (eddymsg_obj, gpgme_ctx, prev_parts=[]):
38738401 267
928e3819 268 if eddymsg_obj.multipart == True:
101d54a8 269 prev_parts=[]
928e3819 270 for sub in eddymsg_obj.subparts:
101d54a8
AE
271 gpg_on_payloads (sub, gpgme_ctx, prev_parts)
272 prev_parts += [sub]
38738401 273
d873ff48 274 return
38738401 275
928e3819 276 for piece in eddymsg_obj.payload_pieces:
38738401
AE
277
278 if piece.piece_type == "text":
279 # don't transform the plaintext.
280 pass
281
282 elif piece.piece_type == "message":
283 (plaintext, sigs) = decrypt_block (piece.string, gpgme_ctx)
284
285 if plaintext:
286 piece.gpg_data = GPGData()
287 piece.gpg_data.sigs = sigs
288 # recurse!
289 piece.gpg_data.plainobj = parse_pgp_mime(plaintext, gpgme_ctx)
129543c3 290
8f61c66a 291 elif piece.piece_type == "pubkey":
f8ee6bd3 292 key_fps = add_gpg_key(piece.string, gpgme_ctx)
8f61c66a 293
f8ee6bd3 294 if key_fps != []:
8f61c66a 295 piece.gpg_data = GPGData()
f8ee6bd3 296 piece.gpg_data.keys = key_fps
129543c3
AE
297
298 elif piece.piece_type == "clearsign":
f8ee6bd3 299 (plaintext, sig_fps) = verify_clear_signature(piece.string, gpgme_ctx)
129543c3 300
f8ee6bd3 301 if sig_fps != []:
129543c3 302 piece.gpg_data = GPGData()
f8ee6bd3 303 piece.gpg_data.sigs = sig_fps
129543c3
AE
304 piece.gpg_data.plainobj = parse_pgp_mime(plaintext, gpgme_ctx)
305
101d54a8
AE
306 elif piece.piece_type == "detachedsig":
307 for prev in prev_parts:
308 payload_bytes = prev.payload_bytes
6d240f68 309 sig_fps = verify_detached_signature(piece.string, payload_bytes, gpgme_ctx)
101d54a8 310
6d240f68
AE
311 if sig_fps != []:
312 piece.gpg_data = GPGData()
313 piece.gpg_data.sigs = sig_fps
314 piece.gpg_data.plainobj = prev
315 break
38738401
AE
316 else:
317 pass
318
319
928e3819 320def prepare_for_reply (eddymsg_obj, replyinfo_obj):
dd11a483 321
928e3819 322 do_to_eddys_pieces(prepare_for_reply_pieces, eddymsg_obj, replyinfo_obj)
56578eaf 323
56578eaf 324
928e3819 325def prepare_for_reply_pieces (eddymsg_obj, replyinfo_obj):
56578eaf 326
928e3819 327 for piece in eddymsg_obj.payload_pieces:
38738401 328 if piece.piece_type == "text":
d873ff48
AE
329 # don't quote the plaintext part.
330 pass
331
38738401 332 elif piece.piece_type == "message":
d873ff48
AE
333 if piece.gpg_data == None:
334 replyinfo_obj.failed_decrypt = True
335 else:
336 replyinfo_obj.success_decrypt = True
337 # TODO: only quote it if it is also signed by the encrypter.
338 replyinfo_obj.msg_to_quote += flatten_payloads(piece.gpg_data.plainobj)
339
340 prepare_for_reply(piece.gpg_data.plainobj, replyinfo_obj)
341
8f61c66a 342 elif piece.piece_type == "pubkey":
d873ff48
AE
343 if piece.gpg_data == None:
344 replyinfo_obj.no_public_key = True
345 else:
346 replyinfo_obj.public_key_received = True
347
348 elif (piece.piece_type == "clearsign") \
349 or (piece.piece_type == "detachedsig"):
350 if piece.gpg_data == None:
351 replyinfo_obj.sig_failure = True
352 else:
353 replyinfo_obj.sig_success = True
354
355
928e3819 356def flatten_payloads (eddymsg_obj):
d873ff48
AE
357
358 flat_string = ""
359
0aa88c27
AE
360 if eddymsg_obj == None:
361 return ""
362
928e3819
AE
363 if eddymsg_obj.multipart == True:
364 for sub in eddymsg_obj.subparts:
d873ff48
AE
365 flat_string += flatten_payloads (sub)
366
367 return flat_string
368
928e3819 369 for piece in eddymsg_obj.payload_pieces:
d873ff48
AE
370 if piece.piece_type == "text":
371 flat_string += piece.string
0aa88c27
AE
372 elif piece.piece_type == "message":
373 flat_string += flatten_payloads(piece.plainobj)
374 elif ((piece.piece_type == "clearsign") \
375 or (piece.piece_type == "detachedsig")) \
376 and (piece.gpg_data != None):
377 flat_string += flatten_payloads (piece.gpg_data.plainobj)
378
d873ff48
AE
379
380 return flat_string
381
382
383def write_reply (replyinfo_obj):
384
385 reply_plain = ""
386
387 if replyinfo_obj.success_decrypt == True:
388 quoted_text = email_quote_text(replyinfo_obj.msg_to_quote)
389 reply_plain += replyinfo_obj.replies['success_decrypt']
390 reply_plain += quoted_text
391
392 elif replyinfo_obj.failed_decrypt == True:
393 reply_plain += replyinfo_obj.replies['failed_decrypt']
394
395
396 if replyinfo_obj.sig_success == True:
397 reply_plain += "\n\n"
398 reply_plain += replyinfo_obj.replies['sig_success']
399
400 elif replyinfo_obj.sig_failure == True:
401 reply_plain += "\n\n"
402 reply_plain += replyinfo_obj.replies['sig_failure']
403
404
405 if replyinfo_obj.public_key_received == True:
406 reply_plain += "\n\n"
407 reply_plain += replyinfo_obj.replies['public_key_received']
408
409 elif replyinfo_obj.no_public_key == True:
410 reply_plain += "\n\n"
411 reply_plain += replyinfo_obj.replies['no_public_key']
412
413
414 reply_plain += "\n\n"
415 reply_plain += replyinfo_obj.replies['signature']
56578eaf 416
d873ff48 417 return reply_plain
56578eaf
AE
418
419
8f61c66a 420def add_gpg_key (key_block, gpgme_ctx):
c267c233 421
8f61c66a 422 fp = io.BytesIO(key_block.encode('ascii'))
c267c233 423
8f61c66a
AE
424 result = gpgme_ctx.import_(fp)
425 imports = result.imports
c267c233 426
f8ee6bd3 427 key_fingerprints = []
c267c233 428
8f61c66a
AE
429 if imports != []:
430 for import_ in imports:
431 fingerprint = import_[0]
f8ee6bd3 432 key_fingerprints += [fingerprint]
c267c233 433
e49673aa 434 debug("added gpg key: " + fingerprint)
ec1e779a 435
f8ee6bd3 436 return key_fingerprints
ec1e779a
AE
437
438
129543c3 439def verify_clear_signature (sig_block, gpgme_ctx):
cf75de65 440
129543c3
AE
441 # FIXME: this might require the un-decoded bytes
442 # or the correct re-encoding with the carset of the mime part.
443 msg_fp = io.BytesIO(sig_block.encode('utf-8'))
444 ptxt_fp = io.BytesIO()
cf75de65 445
129543c3 446 result = gpgme_ctx.verify(msg_fp, None, ptxt_fp)
cf75de65 447
129543c3
AE
448 # FIXME: this might require using the charset of the mime part.
449 plaintext = ptxt_fp.getvalue().decode('utf-8')
cf75de65 450
f8ee6bd3 451 sig_fingerprints = []
129543c3 452 for res_ in result:
f8ee6bd3 453 sig_fingerprints += [res_.fpr]
cf75de65 454
f8ee6bd3 455 return plaintext, sig_fingerprints
cf75de65
AE
456
457
101d54a8
AE
458def verify_detached_signature (detached_sig, plaintext_bytes, gpgme_ctx):
459
460 detached_sig_fp = io.BytesIO(detached_sig.encode('ascii'))
461 plaintext_fp = io.BytesIO(plaintext_bytes)
462 ptxt_fp = io.BytesIO()
463
464 result = gpgme_ctx.verify(detached_sig_fp, plaintext_fp, None)
465
466 sig_fingerprints = []
467 for res_ in result:
468 sig_fingerprints += [res_.fpr]
469
470 return sig_fingerprints
471
472
5b3053c1 473def decrypt_block (msg_block, gpgme_ctx):
0bec96d6 474
5b3053c1 475 block_b = io.BytesIO(msg_block.encode('ascii'))
0bec96d6
AE
476 plain_b = io.BytesIO()
477
afc1f64c
AE
478 try:
479 sigs = gpgme_ctx.decrypt_verify(block_b, plain_b)
480 except:
481 return ("",[])
0bec96d6 482
6aa41372 483 plaintext = plain_b.getvalue().decode('utf-8')
cbdf22c1
AE
484
485 fingerprints = []
486 for sig in sigs:
487 fingerprints += [sig.fpr]
488 return (plaintext, fingerprints)
0bec96d6
AE
489
490
d0489345 491def choose_reply_encryption_key (gpgme_ctx, fingerprints):
fafa21c3
AE
492
493 reply_key = None
d0489345
AE
494 for fp in fingerprints:
495 try:
496 key = gpgme_ctx.get_key(fp)
497
498 if (key.can_encrypt == True):
499 reply_key = key
500 break
501 except:
502 continue
503
fafa21c3 504
216708e9 505 return reply_key
fafa21c3
AE
506
507
d65993b8
AE
508def email_to_from_subject (email_text):
509
510 email_struct = email.parser.Parser().parsestr(email_text)
511
512 email_to = email_struct['To']
513 email_from = email_struct['From']
514 email_subject = email_struct['Subject']
515
516 return email_to, email_from, email_subject
517
518
adcef2f7
AE
519def import_lang(email_to):
520
5250b3b8
AE
521 if email_to != None:
522 for lang in langs:
523 if "edward-" + lang in email_to:
524 lang = "lang." + re.sub('-', '_', lang)
525 language = importlib.import_module(lang)
adcef2f7 526
5250b3b8 527 return language
adcef2f7
AE
528
529 return importlib.import_module("lang.en")
530
531
bf79a93e 532def generate_encrypted_mime (plaintext, email_from, email_subject, encrypt_to_key,
0a064403 533 gpgme_ctx):
1da9b527 534
8bdfb6d4 535
1da9b527
AE
536 reply = "To: " + email_from + "\n"
537 reply += "Subject: " + email_subject + "\n"
216708e9
AE
538
539 if (encrypt_to_key != None):
540 plaintext_reply = "thanks for the message!\n\n\n"
541 plaintext_reply += email_quote_text(plaintext)
542
8bdfb6d4
AE
543 # quoted printable encoding lets most ascii characters look normal
544 # before the decrypted mime message is decoded.
545 char_set = email.charset.Charset("utf-8")
546 char_set.body_encoding = email.charset.QP
547
548 # MIMEText doesn't allow setting the text encoding
549 # so we use MIMENonMultipart.
550 plaintext_mime = MIMENonMultipart('text', 'plain')
551 plaintext_mime.set_payload(plaintext_reply, charset=char_set)
552
553 encrypted_text = encrypt_sign_message(plaintext_mime.as_string(),
554 encrypt_to_key,
40c37ab3 555 gpgme_ctx)
8bdfb6d4
AE
556
557 control_mime = MIMEApplication("Version: 1",
558 _subtype='pgp-encrypted',
559 _encoder=email.encoders.encode_7or8bit)
560 control_mime['Content-Description'] = 'PGP/MIME version identification'
561 control_mime.set_charset('us-ascii')
562
563 encoded_mime = MIMEApplication(encrypted_text,
564 _subtype='octet-stream; name="encrypted.asc"',
565 _encoder=email.encoders.encode_7or8bit)
566 encoded_mime['Content-Description'] = 'OpenPGP encrypted message'
567 encoded_mime['Content-Disposition'] = 'inline; filename="encrypted.asc"'
568 encoded_mime.set_charset('us-ascii')
569
570 message_mime = MIMEMultipart(_subtype="encrypted", protocol="application/pgp-encrypted")
571 message_mime.attach(control_mime)
572 message_mime.attach(encoded_mime)
573 message_mime['Content-Disposition'] = 'inline'
216708e9 574
8bdfb6d4 575 reply += message_mime.as_string()
216708e9
AE
576
577 else:
8bdfb6d4 578 reply += "\n"
216708e9
AE
579 reply += "Sorry, i couldn't find your key.\n"
580 reply += "I'll need that to encrypt a message to you."
1da9b527
AE
581
582 return reply
583
584
f87041f8
AE
585def email_quote_text (text):
586
587 quoted_message = re.sub(r'^', r'> ', text, flags=re.MULTILINE)
588
589 return quoted_message
590
591
0a064403 592def encrypt_sign_message (plaintext, encrypt_to_key, gpgme_ctx):
897cbaf6 593
6aa41372 594 plaintext_bytes = io.BytesIO(plaintext.encode('ascii'))
1da9b527
AE
595 encrypted_bytes = io.BytesIO()
596
897cbaf6 597 gpgme_ctx.encrypt_sign([encrypt_to_key], gpgme.ENCRYPT_ALWAYS_TRUST,
1da9b527
AE
598 plaintext_bytes, encrypted_bytes)
599
6aa41372 600 encrypted_txt = encrypted_bytes.getvalue().decode('ascii')
1da9b527
AE
601 return encrypted_txt
602
603
0a064403
AE
604def error (error_msg):
605
e4fb2ab2 606 sys.stderr.write(progname + ": " + str(error_msg) + "\n")
0a064403
AE
607
608
5e8f9094
AE
609def debug (debug_msg):
610
611 if edward_config.debug == True:
0a064403 612 error(debug_msg)
5e8f9094
AE
613
614
20f6e7c5
AE
615def handle_args ():
616 if __name__ == "__main__":
617
618 global progname
619 progname = sys.argv[0]
620
621 if len(sys.argv) > 1:
622 print(progname + ": error, this program doesn't " \
623 "need any arguments.", file=sys.stderr)
624 exit(1)
625
626
0bec96d6
AE
627main()
628