Removed clearsigning verification
[edward.git] / edward
1 #! /usr/bin/env python3
2 # -*- coding: utf-8 -*-
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+) *
20 * Copyright (C) 2009-2015 Tails developers <tails@boum.org> ( GPLv3+) *
21 * Copyright (C) 2009 W. Trevor King <wking@drexel.edu> ( GPLv2+) *
22 * *
23 * Special thanks to Josh Drake for writing the original edward bot! :) *
24 * *
25 ************************************************************************
26
27 Code sourced from these projects:
28
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
32 """
33
34 import sys
35 import gpgme
36 import re
37 import io
38 import os
39 import importlib
40
41 import email.parser
42 import email.message
43 import email.encoders
44
45 from email.mime.multipart import MIMEMultipart
46 from email.mime.application import MIMEApplication
47 from email.mime.nonmultipart import MIMENonMultipart
48
49 import edward_config
50
51 langs = ["de", "el", "en", "fr", "ja", "pt-br", "ro", "ru", "tr"]
52
53 """This list contains the abbreviated names of reply languages available to
54 edward."""
55
56
57 match_types = [('message',
58 '-----BEGIN PGP MESSAGE-----.*?-----END PGP MESSAGE-----'),
59 ('pubkey',
60 '-----BEGIN PGP PUBLIC KEY BLOCK-----.*?-----END PGP PUBLIC KEY BLOCK-----'),
61 ('detachedsig',
62 '-----BEGIN PGP SIGNATURE-----.*?-----END PGP SIGNATURE-----')]
63
64 """This list of tuples matches query names with re.search() queries used
65 to find GPG data for edward to process."""
66
67
68 class EddyMsg (object):
69 """
70 The EddyMsg class represents relevant parts of a mime message.
71
72 The represented message can be single-part or multi-part.
73
74 'multipart' is set to True if there are multiple mime parts.
75
76 'subparts' points to a list of mime sub-parts if it is a multi-part
77 message. Otherwise it points to an empty list.
78
79 'payload_bytes' contains the raw mime-decoded bytes that haven't been
80 encoded into a character set.
81
82 'payload_pieces' is a list of objects containing strings that when strung
83 together form the fully-decoded string representation of the mime part.
84
85 The 'charset' describes the character set of payload_bytes.
86
87 The 'filename', 'content_type' and 'description_list' come from the mime
88 part parameters.
89 """
90
91 multipart = False
92 subparts = []
93
94 payload_bytes = None
95 payload_pieces = []
96
97 charset = None
98 filename = None
99 content_type = None
100 description_list = None
101
102
103 class PayloadPiece (object):
104 """
105 PayloadPiece represents a complte or sub-section of a mime part.
106
107 Instances of this class are often strung together within one or more arrays
108 pointed to by each instance of the EddyMsg class.
109
110 'piece_type' refers to a string whose value describes the content of
111 'string'. Examples include "pubkey", for public keys, and "message", for
112 encrypted data (or armored signatures until they are known to be such.) The
113 names derive from the header and footer of each of these ascii-encoded gpg
114 blocks.
115
116 'string' contains some string of text, such as non-GPG text, an encrypted
117 block of text, a signature, or a public key.
118
119 'gpg_data' points to any instances of GPGData that have been created based
120 on the contents of 'string'.
121 """
122
123 piece_type = None
124 string = None
125 gpg_data = None
126
127
128 class GPGData (object):
129 """
130 GPGData holds info from decryption, sig. verification, and/or pub. keys.
131
132 Instances of this class contain decrypted information, signature
133 fingerprints and/or fingerprints of processed and imported public keys.
134
135 'decrypted' is set to True if 'plainobj' was created from encrypted data.
136
137 'plainobj' points to any decrypted, or signed part of, a GPG signature. It
138 is intended to be an instance of the EddyMsg class.
139
140 'sigs' is a list of fingerprints of keys used to sign the data in plainobj.
141
142 'keys' is a list of fingerprints of keys obtained in public key blocks.
143 """
144
145 decrypted = False
146
147 plainobj = None
148 sigs = []
149 keys = []
150
151
152 class ReplyInfo (object):
153 """
154 ReplyInfo contains details that edward uses in generating its reply.
155
156 Instances of this class contain information about whether a message was
157 successfully encrypted or signed, and whether a public key was attached, or
158 retrievable, from the local GPG store. It stores the fingerprints of
159 potential encryption key candidates and the message (if any at all) to
160 quote in edward's reply.
161
162 'replies' points one of the dictionaries of translated replies.
163
164 'target_key' refers to the fingerprint of a key used to sign encrypted
165 data. This is the preferred key, if it is set, and if is available.
166
167 'fallback_target_key' referst to the fingerprint of a key used to sign
168 unencrypted data; alternatively it may be a public key attached to the
169 message.
170
171 'msg_to_quote' refers to the part of a message which edward should quote in
172 his reply. This should remain as None if there was no encrypted and singed
173 part. This is to avoid making edward a service for decrypting other
174 people's messages to edward.
175
176 'success_decrypt' is set to True if edward could decrypt part of the
177 message.
178
179 'failed_decrypt' is set to True if edward failed to decrypt part of the
180 message.
181
182 'publick_key_received' is set to True if edward successfully imported a
183 public key.
184
185 'no_public_key' is set to True if edward doesn't have a key to encrypt to
186 when replying to the user.
187
188 'sig_success' is set to True if edward could to some extent verify the
189 signature of a signed part of the message to edward.
190
191 'sig_failure' is set to True if edward failed to some extent verify the
192 signature of a signed part of the message to edward.
193 """
194
195 replies = None
196
197 target_key = None
198 fallback_target_key = None
199 msg_to_quote = ""
200
201 success_decrypt = False
202 failed_decrypt = False
203 public_key_received = False
204 no_public_key = False
205 sig_success = False
206 sig_failure = False
207
208
209 def main ():
210
211 """
212 This is the main function for edward, a GPG reply bot.
213
214 Edward responds to GPG-encrypted and signed mail, encrypting and signing
215 the response if the user's public key is, or was, included in the message.
216
217 Args:
218 None
219
220 Returns:
221 Nothing
222
223 Pre:
224 Mime or plaintext email passing in through standard input. Portions of
225 the email may be encrypted. If the To: address contains the text
226 "edward-ja", then the reply will contain a reply written in the
227 Japanese language. There are other languages as well. The default
228 language is English.
229
230 Post:
231 A reply email will be printed to standard output. The contents of the
232 reply email depends on whether the original email was encrypted or not,
233 has or doesn't have a signature, whether a public key used in the
234 original message is provided or locally stored, and the language
235 implied by the To: address in the original email.
236 """
237
238 handle_args()
239
240 gpgme_ctx = get_gpg_context(edward_config.gnupghome,
241 edward_config.sign_with_key)
242
243 email_text = sys.stdin.read()
244 email_struct = parse_pgp_mime(email_text, gpgme_ctx)
245
246 email_to, email_from, email_subject = email_to_from_subject(email_text)
247 lang = import_lang(email_to)
248
249 replyinfo_obj = ReplyInfo()
250 replyinfo_obj.replies = lang.replies
251
252 prepare_for_reply(email_struct, replyinfo_obj)
253 encrypt_to_key = get_key_from_fp(replyinfo_obj, gpgme_ctx)
254 reply_plaintext = write_reply(replyinfo_obj)
255
256 reply_mime = generate_encrypted_mime(reply_plaintext, email_from, \
257 email_subject, encrypt_to_key,
258 gpgme_ctx)
259
260 print(reply_mime)
261
262
263 def get_gpg_context (gnupghome, sign_with_key_fp):
264 """
265 This function returns the GPG context needed for encryption and signing.
266
267 The context is needed by other functions which use GPG functionality.
268
269 Args:
270 gnupghome: The path to "~/.gnupg/" or its alternative.
271 sign_with_key: The fingerprint of the key to sign with
272
273 Returns:
274 A gpgme context to be used for GPG functions.
275
276 Post:
277 the 'armor' flag is set to True and the list of signing keys contains
278 the single specified key
279 """
280
281 os.environ['GNUPGHOME'] = gnupghome
282
283 gpgme_ctx = gpgme.Context()
284 gpgme_ctx.armor = True
285
286 try:
287 sign_with_key = gpgme_ctx.get_key(sign_with_key_fp)
288 except:
289 error("unable to load signing key. is the gnupghome "
290 + "and signing key properly set in the edward_config.py?")
291 exit(1)
292
293 gpgme_ctx.signers = [sign_with_key]
294
295 return gpgme_ctx
296
297
298 def parse_pgp_mime (email_text, gpgme_ctx):
299 """Parses the email for mime payloads and decrypts/verfies signatures.
300
301 This function creates a representation of a mime or plaintext email with
302 the EddyMsg class. It then splits each mime payload into one or more pieces
303 which may be plain text or GPG data. It then decrypts encrypted parts and
304 does some very basic signature verification on those parts.
305
306 Args:
307 email_text: an email message in string format
308 gpgme_ctx: a gpgme context
309
310 Returns:
311 A message as an instance of EddyMsg
312
313 Post:
314 the returned EddyMsg instance has split, decrypted, verified and pubkey
315 imported payloads
316 """
317
318 email_struct = email.parser.Parser().parsestr(email_text)
319
320 eddymsg_obj = parse_mime(email_struct)
321 split_payloads(eddymsg_obj)
322 gpg_on_payloads(eddymsg_obj, gpgme_ctx)
323
324 return eddymsg_obj
325
326
327 def parse_mime(msg_struct):
328 """Translates python's email.parser format into an EddyMsg format
329
330 If the message is multi-part, then a recursive object is created, where
331 each sub-part is also a EddyMsg instance.
332
333 Args:
334 msg_struct: an email parsed with email.parser.Parser(), which can be
335 multi-part
336
337 Returns:
338 an instance of EddyMsg, potentially a recursive one.
339 """
340
341 eddymsg_obj = EddyMsg()
342
343 if msg_struct.is_multipart() == True:
344 payloads = msg_struct.get_payload()
345
346 eddymsg_obj.multipart = True
347 eddymsg_obj.subparts = list(map(parse_mime, payloads))
348
349 else:
350 eddymsg_obj = get_subpart_data(msg_struct)
351
352 return eddymsg_obj
353
354
355 def scan_and_split (payload_piece, match_type, pattern):
356 """This splits the payloads of an EddyMsg object into GPG and text parts.
357
358 An EddyMsg object's payload_pieces starts off as a list containing a single
359 PayloadPiece object. This function returns a list of these objects which
360 have been split into GPG data and regular text, if such splits need to be/
361 can be made.
362
363 Args:
364 payload_piece: a single payload or a split part of a payload
365 match_type: the type of data to try to spit out from the payload piece
366 pattern: the search pattern to be used for finding that type of data
367
368 Returns:
369 a list of objects of the PayloadPiece class, in the order that the
370 string part of payload_piece originally was, broken up according to
371 matches specified by 'pattern'.
372 """
373
374 # don't try to re-split pieces containing gpg data
375 if payload_piece.piece_type != "text":
376 return [payload_piece]
377
378 flags = re.DOTALL | re.MULTILINE
379 matches = re.search("(?P<beginning>.*?)(?P<match>" + pattern +
380 ")(?P<rest>.*)", payload_piece.string, flags=flags)
381
382 if matches == None:
383 pieces = [payload_piece]
384
385 else:
386
387 beginning = PayloadPiece()
388 beginning.string = matches.group('beginning')
389 beginning.piece_type = payload_piece.piece_type
390
391 match = PayloadPiece()
392 match.string = matches.group('match')
393 match.piece_type = match_type
394
395 rest = PayloadPiece()
396 rest.string = matches.group('rest')
397 rest.piece_type = payload_piece.piece_type
398
399 more_pieces = scan_and_split(rest, match_type, pattern)
400 pieces = [beginning, match ] + more_pieces
401
402 return pieces
403
404
405 def get_subpart_data (part):
406 """This function grabs information from a single part mime object.
407
408 It copies needed data from a single part email.parser.Parser() object over
409 to an EddyMsg object.
410
411 Args:
412 part: a non-multi-part mime.parser.Parser() object
413
414 Returns:
415 a single-part EddyMsg() object
416 """
417
418 obj = EddyMsg()
419
420 obj.charset = part.get_content_charset()
421 obj.payload_bytes = part.get_payload(decode=True)
422
423 obj.filename = part.get_filename()
424 obj.content_type = part.get_content_type()
425 obj.description_list = part['content-description']
426
427 # your guess is as good as a-myy-ee-ine...
428 if obj.charset == None:
429 obj.charset = 'utf-8'
430
431 if obj.payload_bytes != None:
432 try:
433 payload = PayloadPiece()
434 payload.string = obj.payload_bytes.decode(obj.charset)
435 payload.piece_type = 'text'
436
437 obj.payload_pieces = [payload]
438 except UnicodeDecodeError:
439 pass
440
441 return obj
442
443
444 def do_to_eddys_pieces (function_to_do, eddymsg_obj, data):
445 """A function which maps another function onto a message's subparts.
446
447 This is a higer-order function which recursively performs a specified
448 function on each subpart of a multi-part message. Each single-part sub-part
449 has the function applied to it. This function also works if the part passed
450 in is single-part.
451
452 Args:
453 function_to_do: function to perform on sub-parts
454 eddymsg_obj: a single part or multi-part EddyMsg object
455 data: a second argument to pass to 'function_to_do'
456
457 Returns:
458 Nothing
459
460 Post:
461 The passed-in EddyMsg object is transformed recursively on its
462 sub-parts according to 'function_to_do'.
463 """
464
465 if eddymsg_obj.multipart == True:
466 for sub in eddymsg_obj.subparts:
467 do_to_eddys_pieces(function_to_do, sub, data)
468 else:
469 function_to_do(eddymsg_obj, data)
470
471
472 def split_payloads (eddymsg_obj):
473 """Splits all (sub-)payloads of a message into GPG data and regular text.
474
475 Recursively performs payload splitting on all sub-parts of an EddyMsg
476 object into the various GPG data types, such as GPG messages, public key
477 blocks and signed text.
478
479 Args:
480 eddymsg_obj: an instance of EddyMsg
481
482 Returns:
483 Nothing
484
485 Pre:
486 The EddyMsg object has payloads that are unsplit (by may be split)..
487
488 Post:
489 The EddyMsg object's payloads are all split into GPG and non-GPG parts.
490 """
491
492 for match_type in match_types:
493 do_to_eddys_pieces(split_payload_pieces, eddymsg_obj, match_type)
494
495
496 def split_payload_pieces (eddymsg_obj, match_type):
497 """A helper function for split_payloads(); works on PayloadPiece objects.
498
499 This function splits up PayloadPiece objects into multipe PayloadPiece
500 objects and replaces the EddyMsg object's previous list of payload pieces
501 with the new split up one.
502
503 Args:
504 eddymsg_obj: a single-part EddyMsg object.
505 match_type: a tuple from the match_types list, which specifies a match
506 name and a match pattern.
507
508 Returns:
509 Nothing
510
511 Pre:
512 The payload piece(s) of an EddyMsg object may be already split or
513 unsplit.
514
515 Post:
516 The EddyMsg object's payload piece(s) are split into a list of pieces
517 if matches of the match_type are found.
518 """
519
520 (match_name, pattern) = match_type
521
522 new_pieces_list = []
523 for piece in eddymsg_obj.payload_pieces:
524 new_pieces_list += scan_and_split(piece, match_name, pattern)
525
526 eddymsg_obj.payload_pieces = new_pieces_list
527
528
529 def gpg_on_payloads (eddymsg_obj, gpgme_ctx, prev_parts=[]):
530 """Performs GPG operations on the GPG parts of the message
531
532 This function decrypts text, verifies signatures, and imports public keys
533 included in an email.
534
535 Args:
536 eddymsg_obj: an EddyMsg object with its payload_pieces split into GPG
537 and non-GPG sections by split_payloads()
538 gpgme_ctx: a gpgme context
539
540 prev_parts: a list of mime parts that occur before the eddymsg_obj
541 part, under the same multi-part mime part. This is used for
542 verifying detached signatures. For the root mime part, this should
543 be an empty list, which is the default value if this paramater is
544 omitted.
545
546 Return:
547 Nothing
548
549 Pre:
550 eddymsg_obj should have its payloads split into gpg and non-gpg pieces.
551
552 Post:
553 Decryption, verification and key imports occur. the gpg_data member of
554 PayloadPiece objects get filled in with GPGData objects.
555 """
556
557 if eddymsg_obj.multipart == True:
558 prev_parts=[]
559 for sub in eddymsg_obj.subparts:
560 gpg_on_payloads (sub, gpgme_ctx, prev_parts)
561 prev_parts += [sub]
562
563 return
564
565 for piece in eddymsg_obj.payload_pieces:
566
567 if piece.piece_type == "text":
568 # don't transform the plaintext.
569 pass
570
571 elif piece.piece_type == "message":
572 (plaintext, sigs) = decrypt_block(piece.string, gpgme_ctx)
573
574 if plaintext:
575 piece.gpg_data = GPGData()
576 piece.gpg_data.decrypted = True
577 piece.gpg_data.sigs = sigs
578 # recurse!
579 piece.gpg_data.plainobj = parse_pgp_mime(plaintext, gpgme_ctx)
580 continue
581
582 # if not encrypted, check to see if this is an armored signature.
583 (plaintext, sigs) = verify_sig_message(piece.string, gpgme_ctx)
584
585 if plaintext:
586 piece.piece_type = "signature"
587 piece.gpg_data = GPGData()
588 piece.gpg_data.sigs = sigs
589 # recurse!
590 piece.gpg_data.plainobj = parse_pgp_mime(plaintext, gpgme_ctx)
591
592 # FIXME: handle pubkeys first, so that signatures can be validated
593 # on freshly imported keys
594 elif piece.piece_type == "pubkey":
595 key_fps = add_gpg_key(piece.string, gpgme_ctx)
596
597 if key_fps != []:
598 piece.gpg_data = GPGData()
599 piece.gpg_data.keys = key_fps
600
601 elif piece.piece_type == "detachedsig":
602 for prev in prev_parts:
603 payload_bytes = prev.payload_bytes
604 sig_fps = verify_detached_signature(piece.string, payload_bytes, gpgme_ctx)
605
606 if sig_fps != []:
607 piece.gpg_data = GPGData()
608 piece.gpg_data.sigs = sig_fps
609 piece.gpg_data.plainobj = prev
610 break
611
612 else:
613 pass
614
615
616 def prepare_for_reply (eddymsg_obj, replyinfo_obj):
617 """Updates replyinfo_obj with info on the message's GPG success/failures
618
619 This function marks replyinfo_obj with information about whether encrypted
620 text in eddymsg_obj was successfully decrypted, signatures were verified
621 and whether a public key was found or not.
622
623 Args:
624 eddymsg_obj: a message in the EddyMsg format
625 replyinfo_obj: an instance of ReplyInfo
626
627 Returns:
628 Nothing
629
630 Pre:
631 eddymsg_obj has had its gpg_data created by gpg_on_payloads
632
633 Post:
634 replyinfo_obj has been updated with info about decryption/sig
635 verififcation status, etc. However the desired key isn't imported until
636 later, so the success or failure of that updates the values set here.
637 """
638
639 do_to_eddys_pieces(prepare_for_reply_pieces, eddymsg_obj, replyinfo_obj)
640
641 def prepare_for_reply_pieces (eddymsg_obj, replyinfo_obj):
642 """A helper function for prepare_for_reply
643
644 It updates replyinfo_obj with GPG success/failure information, when
645 supplied a single-part EddyMsg object.
646
647 Args:
648 eddymsg_obj: a single-part message in the EddyMsg format
649 replyinfo_obj: an object which holds information about the message's
650 GPG status
651
652 Returns:
653 Nothing
654
655 Pre:
656 eddymsg_obj is a single-part message. (it may be a part of a multi-part
657 message.) It has had its gpg_data created by gpg_on_payloads if it has
658 gpg data.
659
660 Post:
661 replyinfo_obj has been updated with gpg success/failure information
662 """
663
664 for piece in eddymsg_obj.payload_pieces:
665 if piece.piece_type == "text":
666 # don't quote the plaintext part.
667 pass
668
669 elif piece.piece_type == "message":
670 prepare_for_reply_message(piece, replyinfo_obj)
671
672 elif piece.piece_type == "pubkey":
673 prepare_for_reply_pubkey(piece, replyinfo_obj)
674
675 elif (piece.piece_type == "detachedsig") \
676 or (piece.piece_type == "signature"):
677 prepare_for_reply_sig(piece, replyinfo_obj)
678
679
680 def prepare_for_reply_message (piece, replyinfo_obj):
681 """Helper function for prepare_for_reply()
682
683 This function is called when the piece_type of a payload piece is
684 "message", or GPG Message block. This should be encrypted text. If the
685 encryted block is signed, a sig will be attached to .target_key unless
686 there is already one there.
687
688 Args:
689 piece: a PayloadPiece object.
690 replyinfo_obj: object which gets updated with decryption status, etc.
691
692
693 Returns:
694 Nothing
695
696 Pre:
697 the piece.payload_piece value should be "message".
698
699 Post:
700 replyinfo_obj gets updated with decryption status, signing status and a
701 potential signing key.
702 """
703
704 if piece.gpg_data == None:
705 replyinfo_obj.failed_decrypt = True
706 return
707
708 replyinfo_obj.success_decrypt = True
709
710 # we already have a key (and a message)
711 if replyinfo_obj.target_key != None:
712 return
713
714 if piece.gpg_data.sigs != []:
715 replyinfo_obj.target_key = piece.gpg_data.sigs[0]
716 get_signed_part = False
717 else:
718 # only include a signed message in the reply.
719 get_signed_part = True
720
721 flatten_decrypted_payloads(piece.gpg_data.plainobj, replyinfo_obj, get_signed_part)
722
723 # to catch public keys in encrypted blocks
724 prepare_for_reply(piece.gpg_data.plainobj, replyinfo_obj)
725
726
727 def prepare_for_reply_pubkey (piece, replyinfo_obj):
728 """Helper function for prepare_for_reply(). Marks pubkey import status.
729
730 Marks replyinfo_obj with pub key import status.
731
732 Args:
733 piece: a PayloadPiece object
734 replyinfo_obj: a ReplyInfo object
735
736 Pre:
737 piece.piece_type should be set to "pubkey".
738
739 Post:
740 replyinfo_obj has its fields updated.
741 """
742
743 if piece.gpg_data == None or piece.gpg_data.keys == []:
744 replyinfo_obj.no_public_key = True
745 else:
746 replyinfo_obj.public_key_received = True
747
748 if replyinfo_obj.fallback_target_key == None:
749 replyinfo_obj.fallback_target_key = piece.gpg_data.keys[0]
750
751
752 def prepare_for_reply_sig (piece, replyinfo_obj):
753 """Helper function for prepare_for_reply(). Marks sig verification status.
754
755 Marks replyinfo_obj with signature verification status.
756
757 Args:
758 piece: a PayloadPiece object
759 replyinfo_obj: a ReplyInfo object
760
761 Pre:
762 piece.piece_type should be set to "signature", or "detachedsig".
763
764 Post:
765 replyinfo_obj has its fields updated.
766 """
767
768 if piece.gpg_data == None or piece.gpg_data.sigs == []:
769 replyinfo_obj.sig_failure = True
770 else:
771 replyinfo_obj.sig_success = True
772
773 if replyinfo_obj.fallback_target_key == None:
774 replyinfo_obj.fallback_target_key = piece.gpg_data.sigs[0]
775
776
777 def flatten_decrypted_payloads (eddymsg_obj, replyinfo_obj, get_signed_part):
778 """For creating a string representation of a signed, encrypted part.
779
780 When given a decrypted payload, it will add either the plaintext or signed
781 plaintext to the reply message, depeding on 'get_signed_part'. This is
782 useful for ensuring that the reply message only comes from a signed and
783 ecrypted GPG message. It also sets the target_key for encrypting the reply
784 if it's told to get signed text only.
785
786 Args:
787 eddymsg_obj: the message in EddyMsg format created by decrypting GPG
788 text
789 replyinfo_obj: a ReplyInfo object for holding the message to quote and
790 the target_key to encrypt to.
791 get_signed_part: True if we should only include text that contains a
792 further signature. If False, then include plain text.
793
794 Returns:
795 Nothing
796
797 Pre:
798 The EddyMsg instance passed in should be a piece.gpg_data.plainobj
799 which represents decrypted text. It may or may not be signed on that
800 level.
801
802 Post:
803 the ReplyInfo instance may have a new 'target_key' set and its
804 'msg_to_quote' will be updated with (possibly signed) plaintext, if any
805 could be found.
806 """
807
808 if eddymsg_obj == None:
809 return
810
811 # recurse on multi-part mime
812 if eddymsg_obj.multipart == True:
813 for sub in eddymsg_obj.subparts:
814 flatten_decrypted_payloads(sub, replyinfo_obj, get_signed_part)
815
816 for piece in eddymsg_obj.payload_pieces:
817 if (get_signed_part):
818 if ((piece.piece_type == "detachedsig") \
819 or (piece.piece_type == "signature")) \
820 and (piece.gpg_data != None):
821 flatten_decrypted_payloads(piece.gpg_data.plainobj, replyinfo_obj, False)
822 replyinfo_obj.target_key = piece.gpg_data.sigs[0]
823 break
824 else:
825 if piece.piece_type == "text":
826 replyinfo_obj.msg_to_quote += piece.string
827
828
829 def get_key_from_fp (replyinfo_obj, gpgme_ctx):
830 """Obtains a public key object from a key fingerprint
831
832 If the .target_key is not set, then we use .fallback_target_key.
833
834 Args:
835 replyinfo_obj: ReplyInfo instance
836 gpgme_ctx: the gpgme context
837
838 Return:
839 The key object of the key of either the target_key or the fallback one
840 if .target_key is not set. If the key cannot be loaded, then return
841 None.
842
843 Pre:
844 Loading a key requires that we have the public key imported. This
845 requires that they email contains the pub key block, or that it was
846 previously sent to edward.
847
848 Post:
849 If the key cannot be loaded, then the replyinfo_obj is marked for
850 having no public key available.
851 """
852
853 if replyinfo_obj.target_key == None:
854 replyinfo_obj.target_key = replyinfo_obj.fallback_target_key
855
856 if replyinfo_obj.target_key != None:
857 try:
858 encrypt_to_key = gpgme_ctx.get_key(replyinfo_obj.target_key)
859 return encrypt_to_key
860
861 except:
862 pass
863
864 # no available key to use
865 replyinfo_obj.target_key = None
866 replyinfo_obj.fallback_target_key = None
867
868 replyinfo_obj.no_public_key = True
869 replyinfo_obj.public_key_received = False
870
871 return None
872
873
874 def write_reply (replyinfo_obj):
875 """Write the reply email body about the GPG successes/failures.
876
877 The reply is about whether decryption, sig verification and key
878 import/loading was successful or failed. If text was successfully decrypted
879 and verified, then the first instance of such text will be included in
880 quoted form.
881
882 Args:
883 replyinfo_obj: contains details of GPG processing status
884
885 Returns:
886 the plaintext message to be sent to the user
887
888 Pre:
889 replyinfo_obj should be populated with info about GPG processing status.
890 """
891
892 reply_plain = ""
893
894 if replyinfo_obj.success_decrypt == True:
895 reply_plain += replyinfo_obj.replies['success_decrypt']
896
897 if replyinfo_obj.no_public_key == False:
898 quoted_text = email_quote_text(replyinfo_obj.msg_to_quote)
899 reply_plain += quoted_text
900
901 elif replyinfo_obj.failed_decrypt == True:
902 reply_plain += replyinfo_obj.replies['failed_decrypt']
903
904
905 if replyinfo_obj.sig_success == True:
906 reply_plain += "\n\n"
907 reply_plain += replyinfo_obj.replies['sig_success']
908
909 elif replyinfo_obj.sig_failure == True:
910 reply_plain += "\n\n"
911 reply_plain += replyinfo_obj.replies['sig_failure']
912
913
914 if replyinfo_obj.public_key_received == True:
915 reply_plain += "\n\n"
916 reply_plain += replyinfo_obj.replies['public_key_received']
917
918 elif replyinfo_obj.no_public_key == True:
919 reply_plain += "\n\n"
920 reply_plain += replyinfo_obj.replies['no_public_key']
921
922
923 reply_plain += "\n\n"
924 reply_plain += replyinfo_obj.replies['signature']
925
926 return reply_plain
927
928
929 def add_gpg_key (key_block, gpgme_ctx):
930 """Adds a GPG pubkey to the local keystore
931
932 This adds keys received through email into the key store so they can be
933 used later.
934
935 Args:
936 key_block: the string form of the ascii-armored public key block
937 gpgme_ctx: the gpgme context
938
939 Returns:
940 the fingerprint(s) of the imported key(s)
941 """
942
943 fp = io.BytesIO(key_block.encode('ascii'))
944
945 result = gpgme_ctx.import_(fp)
946 imports = result.imports
947
948 key_fingerprints = []
949
950 if imports != []:
951 for import_ in imports:
952 fingerprint = import_[0]
953 key_fingerprints += [fingerprint]
954
955 debug("added gpg key: " + fingerprint)
956
957 return key_fingerprints
958
959
960 def verify_sig_message (msg_block, gpgme_ctx):
961 """Verifies the signature of a signed, ascii-armored block of text.
962
963 It encodes the string into ascii, since binary GPG files are currently
964 unsupported, and alternative, the ascii-armored format is encodable into
965 ascii.
966
967 Args:
968 msg_block: a GPG Message block in string form. It may be encrypted or
969 not. If it is encrypted, it will return empty results.
970 gpgme_ctx: the gpgme context
971
972 Returns:
973 A tuple of the plaintext of the signed part and the list of
974 fingerprints of keys signing the data. If verification failed, perhaps
975 because the message was also encrypted, then empty results are
976 returned.
977 """
978
979 block_b = io.BytesIO(msg_block.encode('ascii'))
980 plain_b = io.BytesIO()
981
982 try:
983 sigs = gpgme_ctx.verify(block_b, None, plain_b)
984 except:
985 return ("",[])
986
987 plaintext = plain_b.getvalue().decode('utf-8')
988
989 fingerprints = []
990 for sig in sigs:
991 fingerprints += [sig.fpr]
992 return (plaintext, fingerprints)
993
994
995 def verify_detached_signature (detached_sig, plaintext_bytes, gpgme_ctx):
996 """Verifies the signature of a detached signature.
997
998 This requires the signature part and the signed part as separate arguments.
999
1000 Args:
1001 detached_sig: the signature part of the detached signature
1002 plaintext_bytes: the byte form of the message being signed.
1003 gpgme_ctx: the gpgme context
1004
1005 Returns:
1006 A list of signing fingerprints if the signature verification was
1007 sucessful. Otherwise, an empty list is returned.
1008 """
1009
1010 detached_sig_fp = io.BytesIO(detached_sig.encode('ascii'))
1011 plaintext_fp = io.BytesIO(plaintext_bytes)
1012 ptxt_fp = io.BytesIO()
1013
1014 result = gpgme_ctx.verify(detached_sig_fp, plaintext_fp, None)
1015
1016 sig_fingerprints = []
1017 for res_ in result:
1018 sig_fingerprints += [res_.fpr]
1019
1020 return sig_fingerprints
1021
1022
1023 def decrypt_block (msg_block, gpgme_ctx):
1024 """Decrypts a block of GPG text, and verifies any included sigatures.
1025
1026 Some encypted messages have embeded signatures, so those are verified too.
1027
1028 Args:
1029 msg_block: the encrypted(/signed) text
1030 gpgme_ctx: the gpgme context
1031
1032 Returns:
1033 A tuple of plaintext and signatures, if the decryption and signature
1034 verification were successful, respectively.
1035 """
1036
1037 block_b = io.BytesIO(msg_block.encode('ascii'))
1038 plain_b = io.BytesIO()
1039
1040 try:
1041 sigs = gpgme_ctx.decrypt_verify(block_b, plain_b)
1042 except:
1043 return ("",[])
1044
1045 plaintext = plain_b.getvalue().decode('utf-8')
1046
1047 fingerprints = []
1048 for sig in sigs:
1049 fingerprints += [sig.fpr]
1050 return (plaintext, fingerprints)
1051
1052
1053 def email_to_from_subject (email_text):
1054 """Returns the values of the email's To:, From: and Subject: fields
1055
1056 Returns this information from an email.
1057
1058 Args:
1059 email_text: the string form of the email
1060
1061 Returns:
1062 the email To:, From:, and Subject: fields as strings
1063 """
1064
1065 email_struct = email.parser.Parser().parsestr(email_text)
1066
1067 email_to = email_struct['To']
1068 email_from = email_struct['From']
1069 email_subject = email_struct['Subject']
1070
1071 return email_to, email_from, email_subject
1072
1073
1074 def import_lang(email_to):
1075 """Imports appropriate language file for basic i18n support
1076
1077 The language imported depends on the To: address of the email received by
1078 edward. an -en ending implies the English language, whereas a -ja ending
1079 implies Japanese. The list of supported languages is listed in the 'langs'
1080 list at the beginning of the program.
1081
1082 Args:
1083 email_to: the string containing the email address that the mail was
1084 sent to.
1085
1086 Returns:
1087 the reference to the imported language module. The only variable in
1088 this file is the 'replies' dictionary.
1089 """
1090
1091 if email_to != None:
1092 for lang in langs:
1093 if "edward-" + lang in email_to:
1094 lang = "lang." + re.sub('-', '_', lang)
1095 language = importlib.import_module(lang)
1096
1097 return language
1098
1099 return importlib.import_module("lang.en")
1100
1101
1102 def generate_encrypted_mime (plaintext, email_to, email_subject, encrypt_to_key,
1103 gpgme_ctx):
1104 """This function creates the mime email reply. It can encrypt the email.
1105
1106 If the encrypt_key is included, then the email is encrypted and signed.
1107 Otherwise it is unencrypted.
1108
1109 Args:
1110 plaintext: the plaintext body of the message to create.
1111 email_to: the email address to reply to
1112 email_subject: the subject to use in reply
1113 encrypt_to_key: the key object to use for encrypting the email. (or
1114 None)
1115 gpgme_ctx: the gpgme context
1116
1117 Returns
1118 A string version of the mime message, possibly encrypted and signed.
1119 """
1120
1121 # quoted printable encoding lets most ascii characters look normal
1122 # before the mime message is decoded.
1123 char_set = email.charset.Charset("utf-8")
1124 char_set.body_encoding = email.charset.QP
1125
1126 # MIMEText doesn't allow setting the text encoding
1127 # so we use MIMENonMultipart.
1128 plaintext_mime = MIMENonMultipart('text', 'plain')
1129 plaintext_mime.set_payload(plaintext, charset=char_set)
1130
1131 if (encrypt_to_key != None):
1132
1133 encrypted_text = encrypt_sign_message(plaintext_mime.as_string(),
1134 encrypt_to_key,
1135 gpgme_ctx)
1136 gpg_payload = encrypted_text
1137
1138 else:
1139 signed_text = sign_message(plaintext_mime.as_string(), gpgme_ctx)
1140 gpg_payload = signed_text
1141
1142 control_mime = MIMEApplication("Version: 1",
1143 _subtype='pgp-encrypted',
1144 _encoder=email.encoders.encode_7or8bit)
1145 control_mime['Content-Description'] = 'PGP/MIME version identification'
1146 control_mime.set_charset('us-ascii')
1147
1148 encoded_mime = MIMEApplication(gpg_payload,
1149 _subtype='octet-stream; name="encrypted.asc"',
1150 _encoder=email.encoders.encode_7or8bit)
1151 encoded_mime['Content-Description'] = 'OpenPGP encrypted message'
1152 encoded_mime['Content-Disposition'] = 'inline; filename="encrypted.asc"'
1153 encoded_mime.set_charset('us-ascii')
1154
1155 message_mime = MIMEMultipart(_subtype="encrypted", protocol="application/pgp-encrypted")
1156 message_mime.attach(control_mime)
1157 message_mime.attach(encoded_mime)
1158 message_mime['Content-Disposition'] = 'inline'
1159
1160
1161 message_mime['To'] = email_to
1162 message_mime['Subject'] = email_subject
1163
1164 reply = message_mime.as_string()
1165
1166 return reply
1167
1168
1169 def email_quote_text (text):
1170 """Quotes input text by inserting "> "s
1171
1172 This is useful for quoting a text for the reply message. It inserts "> "
1173 strings at the beginning of lines.
1174
1175 Args:
1176 text: plain text to quote
1177
1178 Returns:
1179 Quoted text
1180 """
1181
1182 quoted_message = re.sub(r'^', r'> ', text, flags=re.MULTILINE)
1183
1184 return quoted_message
1185
1186
1187 def encrypt_sign_message (plaintext, encrypt_to_key, gpgme_ctx):
1188 """Encrypts and signs plaintext
1189
1190 This encrypts and signs a message.
1191
1192 Args:
1193 plaintext: text to sign and ecrypt
1194 encrypt_to_key: the key object to encrypt to
1195 gpgme_ctx: the gpgme context
1196
1197 Returns:
1198 An encrypted and signed string of text
1199 """
1200
1201 # the plaintext should be mime encoded in an ascii-compatible form
1202 plaintext_bytes = io.BytesIO(plaintext.encode('ascii'))
1203 encrypted_bytes = io.BytesIO()
1204
1205 gpgme_ctx.encrypt_sign([encrypt_to_key], gpgme.ENCRYPT_ALWAYS_TRUST,
1206 plaintext_bytes, encrypted_bytes)
1207
1208 encrypted_txt = encrypted_bytes.getvalue().decode('ascii')
1209 return encrypted_txt
1210
1211
1212 def sign_message (plaintext, gpgme_ctx):
1213 """Signs plaintext
1214
1215 This signs a message.
1216
1217 Args:
1218 plaintext: text to sign
1219 gpgme_ctx: the gpgme context
1220
1221 Returns:
1222 An armored signature as a string of text
1223 """
1224
1225 # the plaintext should be mime encoded in an ascii-compatible form
1226 plaintext_bytes = io.BytesIO(plaintext.encode('ascii'))
1227 signed_bytes = io.BytesIO()
1228
1229 gpgme_ctx.sign(plaintext_bytes, signed_bytes, gpgme.SIG_MODE_NORMAL)
1230
1231 signed_txt = signed_bytes.getvalue().decode('ascii')
1232 return signed_txt
1233
1234
1235 def error (error_msg):
1236 """Write an error message to stdout
1237
1238 The error message includes the program name.
1239
1240 Args:
1241 error_msg: the message to print
1242
1243 Returns:
1244 Nothing
1245
1246 Post:
1247 An error message is printed to stdout
1248 """
1249
1250 sys.stderr.write(progname + ": " + str(error_msg) + "\n")
1251
1252
1253 def debug (debug_msg):
1254 """Writes a debug message to stdout if debug == True
1255
1256 If the debug option is set in edward_config.py, then the passed message
1257 gets printed to stdout.
1258
1259 Args:
1260 debug_msg: the message to print to stdout
1261
1262 Returns:
1263 Nothing
1264
1265 Post:
1266 A debug message is printed to stdout
1267 """
1268
1269 if edward_config.debug == True:
1270 error(debug_msg)
1271
1272
1273 def handle_args ():
1274 """Sets the progname variable and complains about any arguments
1275
1276 If there are any arguments, then edward complains and quits, because input
1277 is read from stdin.
1278
1279 Args:
1280 None
1281
1282 Returns:
1283 None
1284
1285 Post:
1286 Exits with error 1 if there are arguments, otherwise returns to the
1287 calling function, such as main().
1288 """
1289
1290 global progname
1291 progname = sys.argv[0]
1292
1293 if len(sys.argv) > 1:
1294 print(progname + ": error, this program doesn't " \
1295 "need any arguments.", file=sys.stderr)
1296 exit(1)
1297
1298
1299 if __name__ == "__main__":
1300 """Executes main if this file is not loaded interactively"""
1301
1302 main()
1303