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