beb17f1ac2b2b5e526e1493b400c88ecf14d3475
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. *
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. *
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/>. *
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+) *
23 * Special thanks to Josh Drake for writing the original edward bot! :) *
25 ************************************************************************
27 Code sourced from these projects:
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
45 from email
.mime
.multipart
import MIMEMultipart
46 from email
.mime
.application
import MIMEApplication
47 from email
.mime
.nonmultipart
import MIMENonMultipart
51 langs
= ["an", "de", "el", "en", "fr", "ja", "pt-br", "ro", "ru", "tr"]
53 match_types
= [('clearsign',
54 '-----BEGIN PGP SIGNED MESSAGE-----.*?-----BEGIN PGP SIGNATURE-----.*?-----END PGP SIGNATURE-----'),
56 '-----BEGIN PGP MESSAGE-----.*?-----END PGP MESSAGE-----'),
58 '-----BEGIN PGP PUBLIC KEY BLOCK-----.*?-----END PGP PUBLIC KEY BLOCK-----'),
60 '-----BEGIN PGP SIGNATURE-----.*?-----END PGP SIGNATURE-----')]
63 class EddyMsg (object):
65 self
.multipart
= False
69 self
.payload_bytes
= None
70 self
.payload_pieces
= []
73 self
.content_type
= None
74 self
.description_list
= None
77 class PayloadPiece (object):
79 self
.piece_type
= None
84 class GPGData (object):
86 self
.decrypted
= False
92 class ReplyInfo (object):
96 self
.target_key
= None
97 self
.fallback_target_key
= None
98 self
.msg_to_quote
= ""
100 self
.success_decrypt
= False
101 self
.failed_decrypt
= False
102 self
.public_key_received
= False
103 self
.no_public_key
= False
104 self
.sig_success
= False
105 self
.sig_failure
= False
112 gpgme_ctx
= get_gpg_context(edward_config
.gnupghome
,
113 edward_config
.sign_with_key
)
115 email_text
= sys
.stdin
.read()
116 email_struct
= parse_pgp_mime(email_text
, gpgme_ctx
)
118 email_to
, email_from
, email_subject
= email_to_from_subject(email_text
)
119 lang
= import_lang(email_to
)
121 replyinfo_obj
= ReplyInfo()
122 replyinfo_obj
.replies
= lang
.replies
124 prepare_for_reply(email_struct
, replyinfo_obj
)
125 encrypt_to_key
= get_key_from_fp(replyinfo_obj
, gpgme_ctx
)
126 reply_plaintext
= write_reply(replyinfo_obj
)
128 reply_mime
= generate_encrypted_mime(reply_plaintext
, email_from
, \
129 email_subject
, encrypt_to_key
,
135 def get_gpg_context (gnupghome
, sign_with_key_fp
):
137 os
.environ
['GNUPGHOME'] = gnupghome
139 gpgme_ctx
= gpgme
.Context()
140 gpgme_ctx
.armor
= True
143 sign_with_key
= gpgme_ctx
.get_key(sign_with_key_fp
)
145 error("unable to load signing key. is the gnupghome "
146 + "and signing key properly set in the edward_config.py?")
149 gpgme_ctx
.signers
= [sign_with_key
]
154 def parse_pgp_mime (email_text
, gpgme_ctx
):
156 email_struct
= email
.parser
.Parser().parsestr(email_text
)
158 eddymsg_obj
= parse_mime(email_struct
)
159 split_payloads(eddymsg_obj
)
160 gpg_on_payloads(eddymsg_obj
, gpgme_ctx
)
165 def parse_mime(msg_struct
):
167 eddymsg_obj
= EddyMsg()
169 if msg_struct
.is_multipart() == True:
170 payloads
= msg_struct
.get_payload()
172 eddymsg_obj
.multipart
= True
173 eddymsg_obj
.subparts
= list(map(parse_mime
, payloads
))
176 eddymsg_obj
= get_subpart_data(msg_struct
)
181 def scan_and_split (payload_piece
, match_type
, pattern
):
183 # don't try to re-split pieces containing gpg data
184 if payload_piece
.piece_type
!= "text":
185 return [payload_piece
]
187 flags
= re
.DOTALL | re
.MULTILINE
188 matches
= re
.search("(?P<beginning>.*?)(?P<match>" + pattern
+
189 ")(?P<rest>.*)", payload_piece
.string
, flags
=flags
)
192 pieces
= [payload_piece
]
196 beginning
= PayloadPiece()
197 beginning
.string
= matches
.group('beginning')
198 beginning
.piece_type
= payload_piece
.piece_type
200 match
= PayloadPiece()
201 match
.string
= matches
.group('match')
202 match
.piece_type
= match_type
204 rest
= PayloadPiece()
205 rest
.string
= matches
.group('rest')
206 rest
.piece_type
= payload_piece
.piece_type
208 more_pieces
= scan_and_split(rest
, match_type
, pattern
)
209 pieces
= [beginning
, match
] + more_pieces
214 def get_subpart_data (part
):
218 obj
.charset
= part
.get_content_charset()
219 obj
.payload_bytes
= part
.get_payload(decode
=True)
221 obj
.filename
= part
.get_filename()
222 obj
.content_type
= part
.get_content_type()
223 obj
.description_list
= part
['content-description']
225 # your guess is as good as a-myy-ee-ine...
226 if obj
.charset
== None:
227 obj
.charset
= 'utf-8'
229 if obj
.payload_bytes
!= None:
231 payload
= PayloadPiece()
232 payload
.string
= obj
.payload_bytes
.decode(obj
.charset
)
233 payload
.piece_type
= 'text'
235 obj
.payload_pieces
= [payload
]
236 except UnicodeDecodeError:
242 def do_to_eddys_pieces (function_to_do
, eddymsg_obj
, data
):
244 if eddymsg_obj
.multipart
== True:
245 for sub
in eddymsg_obj
.subparts
:
246 do_to_eddys_pieces(function_to_do
, sub
, data
)
248 function_to_do(eddymsg_obj
, data
)
251 def split_payloads (eddymsg_obj
):
253 for match_type
in match_types
:
254 do_to_eddys_pieces(split_payload_pieces
, eddymsg_obj
, match_type
)
257 def split_payload_pieces (eddymsg_obj
, match_type
):
259 (match_name
, pattern
) = match_type
262 for piece
in eddymsg_obj
.payload_pieces
:
263 new_pieces_list
+= scan_and_split(piece
, match_name
, pattern
)
265 eddymsg_obj
.payload_pieces
= new_pieces_list
268 def gpg_on_payloads (eddymsg_obj
, gpgme_ctx
, prev_parts
=[]):
270 if eddymsg_obj
.multipart
== True:
272 for sub
in eddymsg_obj
.subparts
:
273 gpg_on_payloads (sub
, gpgme_ctx
, prev_parts
)
278 for piece
in eddymsg_obj
.payload_pieces
:
280 if piece
.piece_type
== "text":
281 # don't transform the plaintext.
284 elif piece
.piece_type
== "message":
285 (plaintext
, sigs
) = decrypt_block(piece
.string
, gpgme_ctx
)
288 piece
.gpg_data
= GPGData()
289 piece
.gpg_data
.decrypted
= True
290 piece
.gpg_data
.sigs
= sigs
292 piece
.gpg_data
.plainobj
= parse_pgp_mime(plaintext
, gpgme_ctx
)
295 # if not encrypted, check to see if this is an armored signature.
296 (plaintext
, sigs
) = verify_sig_message(piece
.string
, gpgme_ctx
)
299 piece
.piece_type
= "signature"
300 piece
.gpg_data
= GPGData()
301 piece
.gpg_data
.sigs
= sigs
303 piece
.gpg_data
.plainobj
= parse_pgp_mime(plaintext
, gpgme_ctx
)
305 elif piece
.piece_type
== "pubkey":
306 key_fps
= add_gpg_key(piece
.string
, gpgme_ctx
)
309 piece
.gpg_data
= GPGData()
310 piece
.gpg_data
.keys
= key_fps
312 elif piece
.piece_type
== "clearsign":
313 (plaintext
, sig_fps
) = verify_clear_signature(piece
.string
, gpgme_ctx
)
316 piece
.gpg_data
= GPGData()
317 piece
.gpg_data
.sigs
= sig_fps
318 piece
.gpg_data
.plainobj
= parse_pgp_mime(plaintext
, gpgme_ctx
)
320 elif piece
.piece_type
== "detachedsig":
321 for prev
in prev_parts
:
322 payload_bytes
= prev
.payload_bytes
323 sig_fps
= verify_detached_signature(piece
.string
, payload_bytes
, gpgme_ctx
)
326 piece
.gpg_data
= GPGData()
327 piece
.gpg_data
.sigs
= sig_fps
328 piece
.gpg_data
.plainobj
= prev
335 def prepare_for_reply (eddymsg_obj
, replyinfo_obj
):
337 do_to_eddys_pieces(prepare_for_reply_pieces
, eddymsg_obj
, replyinfo_obj
)
339 def prepare_for_reply_pieces (eddymsg_obj
, replyinfo_obj
):
341 for piece
in eddymsg_obj
.payload_pieces
:
342 if piece
.piece_type
== "text":
343 # don't quote the plaintext part.
346 elif piece
.piece_type
== "message":
347 prepare_for_reply_message(piece
, replyinfo_obj
)
349 elif piece
.piece_type
== "pubkey":
350 prepare_for_reply_pubkey(piece
, replyinfo_obj
)
352 elif (piece
.piece_type
== "clearsign") \
353 or (piece
.piece_type
== "detachedsig") \
354 or (piece
.piece_type
== "signature"):
355 prepare_for_reply_sig(piece
, replyinfo_obj
)
358 def prepare_for_reply_message (piece
, replyinfo_obj
):
360 if piece
.gpg_data
== None:
361 replyinfo_obj
.failed_decrypt
= True
364 replyinfo_obj
.success_decrypt
= True
366 # we already have a key (and a message)
367 if replyinfo_obj
.target_key
!= None:
370 if piece
.gpg_data
.sigs
!= []:
371 replyinfo_obj
.target_key
= piece
.gpg_data
.sigs
[0]
372 get_signed_part
= False
374 # only include a signed message in the reply.
375 get_signed_part
= True
377 replyinfo_obj
.msg_to_quote
= flatten_decrypted_payloads(piece
.gpg_data
.plainobj
, get_signed_part
)
379 # to catch public keys in encrypted blocks
380 prepare_for_reply(piece
.gpg_data
.plainobj
, replyinfo_obj
)
383 def prepare_for_reply_pubkey (piece
, replyinfo_obj
):
385 if piece
.gpg_data
== None or piece
.gpg_data
.keys
== []:
386 replyinfo_obj
.no_public_key
= True
388 replyinfo_obj
.public_key_received
= True
390 if replyinfo_obj
.fallback_target_key
== None:
391 replyinfo_obj
.fallback_target_key
= piece
.gpg_data
.keys
[0]
394 def prepare_for_reply_sig (piece
, replyinfo_obj
):
396 if piece
.gpg_data
== None or piece
.gpg_data
.sigs
== []:
397 replyinfo_obj
.sig_failure
= True
399 replyinfo_obj
.sig_success
= True
401 if replyinfo_obj
.fallback_target_key
== None:
402 replyinfo_obj
.fallback_target_key
= piece
.gpg_data
.sigs
[0]
406 def flatten_decrypted_payloads (eddymsg_obj
, get_signed_part
):
410 if eddymsg_obj
== None:
413 # recurse on multi-part mime
414 if eddymsg_obj
.multipart
== True:
415 for sub
in eddymsg_obj
.subparts
:
416 flat_string
+= flatten_decrypted_payloads (sub
, get_signed_part
)
420 for piece
in eddymsg_obj
.payload_pieces
:
421 if piece
.piece_type
== "text":
422 flat_string
+= piece
.string
424 if (get_signed_part
):
425 # don't include nested encryption
426 if (piece
.piece_type
== "message") \
427 and (piece
.gpg_data
!= None) \
428 and (piece
.gpg_data
.decrypted
== False):
429 flat_string
+= flatten_decrypted_payloads(piece
.gpg_data
.plainobj
, get_signed_part
)
431 elif ((piece
.piece_type
== "clearsign") \
432 or (piece
.piece_type
== "detachedsig") \
433 or (piece
.piece_type
== "signature")) \
434 and (piece
.gpg_data
!= None):
435 # FIXME: the key used to sign this message needs to be the one that is used for the encrypted reply.
436 flat_string
+= flatten_decrypted_payloads (piece
.gpg_data
.plainobj
, get_signed_part
)
441 def get_key_from_fp (replyinfo_obj
, gpgme_ctx
):
443 if replyinfo_obj
.target_key
== None:
444 replyinfo_obj
.target_key
= replyinfo_obj
.fallback_target_key
446 if replyinfo_obj
.target_key
!= None:
448 encrypt_to_key
= gpgme_ctx
.get_key(replyinfo_obj
.target_key
)
449 return encrypt_to_key
454 # no available key to use
455 replyinfo_obj
.target_key
= None
456 replyinfo_obj
.fallback_target_key
= None
458 replyinfo_obj
.no_public_key
= True
459 replyinfo_obj
.public_key_received
= False
464 def write_reply (replyinfo_obj
):
468 if replyinfo_obj
.success_decrypt
== True:
469 reply_plain
+= replyinfo_obj
.replies
['success_decrypt']
471 if replyinfo_obj
.no_public_key
== False:
472 quoted_text
= email_quote_text(replyinfo_obj
.msg_to_quote
)
473 reply_plain
+= quoted_text
475 elif replyinfo_obj
.failed_decrypt
== True:
476 reply_plain
+= replyinfo_obj
.replies
['failed_decrypt']
479 if replyinfo_obj
.sig_success
== True:
480 reply_plain
+= "\n\n"
481 reply_plain
+= replyinfo_obj
.replies
['sig_success']
483 elif replyinfo_obj
.sig_failure
== True:
484 reply_plain
+= "\n\n"
485 reply_plain
+= replyinfo_obj
.replies
['sig_failure']
488 if replyinfo_obj
.public_key_received
== True:
489 reply_plain
+= "\n\n"
490 reply_plain
+= replyinfo_obj
.replies
['public_key_received']
492 elif replyinfo_obj
.no_public_key
== True:
493 reply_plain
+= "\n\n"
494 reply_plain
+= replyinfo_obj
.replies
['no_public_key']
497 reply_plain
+= "\n\n"
498 reply_plain
+= replyinfo_obj
.replies
['signature']
503 def add_gpg_key (key_block
, gpgme_ctx
):
505 fp
= io
.BytesIO(key_block
.encode('ascii'))
507 result
= gpgme_ctx
.import_(fp
)
508 imports
= result
.imports
510 key_fingerprints
= []
513 for import_
in imports
:
514 fingerprint
= import_
[0]
515 key_fingerprints
+= [fingerprint
]
517 debug("added gpg key: " + fingerprint
)
519 return key_fingerprints
522 def verify_sig_message (msg_block
, gpgme_ctx
):
524 block_b
= io
.BytesIO(msg_block
.encode('ascii'))
525 plain_b
= io
.BytesIO()
528 sigs
= gpgme_ctx
.verify(block_b
, None, plain_b
)
532 plaintext
= plain_b
.getvalue().decode('utf-8')
536 fingerprints
+= [sig
.fpr
]
537 return (plaintext
, fingerprints
)
540 def verify_clear_signature (sig_block
, gpgme_ctx
):
542 # FIXME: this might require the un-decoded bytes
543 # or the correct re-encoding with the carset of the mime part.
544 msg_fp
= io
.BytesIO(sig_block
.encode('utf-8'))
545 ptxt_fp
= io
.BytesIO()
547 result
= gpgme_ctx
.verify(msg_fp
, None, ptxt_fp
)
549 # FIXME: this might require using the charset of the mime part.
550 plaintext
= ptxt_fp
.getvalue().decode('utf-8')
552 sig_fingerprints
= []
554 sig_fingerprints
+= [res_
.fpr
]
556 return plaintext
, sig_fingerprints
559 def verify_detached_signature (detached_sig
, plaintext_bytes
, gpgme_ctx
):
561 detached_sig_fp
= io
.BytesIO(detached_sig
.encode('ascii'))
562 plaintext_fp
= io
.BytesIO(plaintext_bytes
)
563 ptxt_fp
= io
.BytesIO()
565 result
= gpgme_ctx
.verify(detached_sig_fp
, plaintext_fp
, None)
567 sig_fingerprints
= []
569 sig_fingerprints
+= [res_
.fpr
]
571 return sig_fingerprints
574 def decrypt_block (msg_block
, gpgme_ctx
):
576 block_b
= io
.BytesIO(msg_block
.encode('ascii'))
577 plain_b
= io
.BytesIO()
580 sigs
= gpgme_ctx
.decrypt_verify(block_b
, plain_b
)
584 plaintext
= plain_b
.getvalue().decode('utf-8')
588 fingerprints
+= [sig
.fpr
]
589 return (plaintext
, fingerprints
)
592 def choose_reply_encryption_key (gpgme_ctx
, fingerprints
):
595 for fp
in fingerprints
:
597 key
= gpgme_ctx
.get_key(fp
)
599 if (key
.can_encrypt
== True):
609 def email_to_from_subject (email_text
):
611 email_struct
= email
.parser
.Parser().parsestr(email_text
)
613 email_to
= email_struct
['To']
614 email_from
= email_struct
['From']
615 email_subject
= email_struct
['Subject']
617 return email_to
, email_from
, email_subject
620 def import_lang(email_to
):
624 if "edward-" + lang
in email_to
:
625 lang
= "lang." + re
.sub('-', '_', lang
)
626 language
= importlib
.import_module(lang
)
630 return importlib
.import_module("lang.en")
633 def generate_encrypted_mime (plaintext
, email_from
, email_subject
, encrypt_to_key
,
636 # quoted printable encoding lets most ascii characters look normal
637 # before the decrypted mime message is decoded.
638 char_set
= email
.charset
.Charset("utf-8")
639 char_set
.body_encoding
= email
.charset
.QP
641 # MIMEText doesn't allow setting the text encoding
642 # so we use MIMENonMultipart.
643 plaintext_mime
= MIMENonMultipart('text', 'plain')
644 plaintext_mime
.set_payload(plaintext
, charset
=char_set
)
646 if (encrypt_to_key
!= None):
648 encrypted_text
= encrypt_sign_message(plaintext_mime
.as_string(),
652 control_mime
= MIMEApplication("Version: 1",
653 _subtype
='pgp-encrypted',
654 _encoder
=email
.encoders
.encode_7or8bit
)
655 control_mime
['Content-Description'] = 'PGP/MIME version identification'
656 control_mime
.set_charset('us-ascii')
658 encoded_mime
= MIMEApplication(encrypted_text
,
659 _subtype
='octet-stream; name="encrypted.asc"',
660 _encoder
=email
.encoders
.encode_7or8bit
)
661 encoded_mime
['Content-Description'] = 'OpenPGP encrypted message'
662 encoded_mime
['Content-Disposition'] = 'inline; filename="encrypted.asc"'
663 encoded_mime
.set_charset('us-ascii')
665 message_mime
= MIMEMultipart(_subtype
="encrypted", protocol
="application/pgp-encrypted")
666 message_mime
.attach(control_mime
)
667 message_mime
.attach(encoded_mime
)
668 message_mime
['Content-Disposition'] = 'inline'
671 message_mime
= plaintext_mime
673 message_mime
['To'] = email_from
674 message_mime
['Subject'] = email_subject
676 reply
= message_mime
.as_string()
681 def email_quote_text (text
):
683 quoted_message
= re
.sub(r
'^', r
'> ', text
, flags
=re
.MULTILINE
)
685 return quoted_message
688 def encrypt_sign_message (plaintext
, encrypt_to_key
, gpgme_ctx
):
690 plaintext_bytes
= io
.BytesIO(plaintext
.encode('ascii'))
691 encrypted_bytes
= io
.BytesIO()
693 gpgme_ctx
.encrypt_sign([encrypt_to_key
], gpgme
.ENCRYPT_ALWAYS_TRUST
,
694 plaintext_bytes
, encrypted_bytes
)
696 encrypted_txt
= encrypted_bytes
.getvalue().decode('ascii')
700 def error (error_msg
):
702 sys
.stderr
.write(progname
+ ": " + str(error_msg
) + "\n")
705 def debug (debug_msg
):
707 if edward_config
.debug
== True:
712 if __name__
== "__main__":
715 progname
= sys
.argv
[0]
717 if len(sys
.argv
) > 1:
718 print(progname
+ ": error, this program doesn't " \
719 "need any arguments.", file=sys
.stderr
)