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