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
97 gpgme_ctx
= get_gpg_context(edward_config
.gnupghome
,
98 edward_config
.sign_with_key
)
100 email_text
= sys
.stdin
.read()
101 email_struct
= parse_pgp_mime(email_text
, gpgme_ctx
)
103 email_to
, email_from
, email_subject
= email_to_from_subject(email_text
)
104 lang
= import_lang(email_to
)
106 reply_plaintext
= build_reply(email_struct
)
108 debug(lang
.replies
['success_decrypt'])
109 print(reply_plaintext
)
111 # encrypt_to_key = choose_reply_encryption_key(gpgme_ctx, fingerprints)
113 # reply_mime = generate_encrypted_mime(plaintext, email_from, \
114 # email_subject, encrypt_to_key,
118 def get_gpg_context (gnupghome
, sign_with_key_fp
):
120 os
.environ
['GNUPGHOME'] = gnupghome
122 gpgme_ctx
= gpgme
.Context()
123 gpgme_ctx
.armor
= True
126 sign_with_key
= gpgme_ctx
.get_key(sign_with_key_fp
)
128 error("unable to load signing key. is the gnupghome "
129 + "and signing key properly set in the edward_config.py?")
132 gpgme_ctx
.signers
= [sign_with_key
]
137 def parse_pgp_mime (email_text
, gpgme_ctx
):
139 email_struct
= email
.parser
.Parser().parsestr(email_text
)
141 eddy_obj
= parse_mime(email_struct
)
142 eddy_obj
= split_payloads(eddy_obj
)
143 eddy_obj
= gpg_on_payloads(eddy_obj
, gpgme_ctx
)
148 def parse_mime(msg_struct
):
152 if msg_struct
.is_multipart() == True:
153 payloads
= msg_struct
.get_payload()
155 eddy_obj
.multipart
= True
156 eddy_obj
.subparts
= list(map(parse_mime
, payloads
))
159 eddy_obj
= get_subpart_data(msg_struct
)
164 def scan_and_split (payload_piece
, match_type
, pattern
):
166 # don't try to re-split pieces containing gpg data
167 if payload_piece
.piece_type
!= "text":
168 return [payload_piece
]
170 flags
= re
.DOTALL | re
.MULTILINE
171 matches
= re
.search("(?P<beginning>.*?)(?P<match>" + pattern
+
172 ")(?P<rest>.*)", payload_piece
.string
, flags
=flags
)
175 pieces
= [payload_piece
]
179 beginning
= PayloadPiece()
180 beginning
.string
= matches
.group('beginning')
181 beginning
.piece_type
= payload_piece
.piece_type
183 match
= PayloadPiece()
184 match
.string
= matches
.group('match')
185 match
.piece_type
= match_type
187 rest
= PayloadPiece()
188 rest
.string
= matches
.group('rest')
189 rest
.piece_type
= payload_piece
.piece_type
191 more_pieces
= scan_and_split(rest
, match_type
, pattern
)
192 pieces
= [beginning
, match
] + more_pieces
197 def get_subpart_data (part
):
201 obj
.charset
= part
.get_content_charset()
202 obj
.payload_bytes
= part
.get_payload(decode
=True)
204 obj
.filename
= part
.get_filename()
205 obj
.content_type
= part
.get_content_type()
206 obj
.description_list
= part
['content-description']
208 # your guess is as good as a-myy-ee-ine...
209 if obj
.charset
== None:
210 obj
.charset
= 'utf-8'
212 if obj
.payload_bytes
!= None:
214 payload
= PayloadPiece()
215 payload
.string
= obj
.payload_bytes
.decode(obj
.charset
)
216 payload
.piece_type
= 'text'
218 obj
.payload_pieces
= [payload
]
219 except UnicodeDecodeError:
225 def do_to_eddys_pieces (function_to_do
, eddy_obj
, data
):
230 if eddy_obj
.multipart
== True:
232 for sub
in eddy_obj
.subparts
:
233 result_list
+= do_to_eddys_pieces(function_to_do
, sub
, data
)
235 result_list
= [function_to_do(eddy_obj
, data
)]
240 def split_payloads (eddy_obj
):
242 for match_type
in match_types
:
243 do_to_eddys_pieces(split_payload_pieces
, eddy_obj
, match_type
)
248 def split_payload_pieces (eddy_obj
, match_type
):
250 (match_name
, pattern
) = match_type
253 for piece
in eddy_obj
.payload_pieces
:
254 new_pieces_list
+= scan_and_split(piece
, match_name
, pattern
)
256 eddy_obj
.payload_pieces
= new_pieces_list
259 def gpg_on_payloads (eddy_obj
, gpgme_ctx
):
261 do_to_eddys_pieces(gpg_on_payload_pieces
, eddy_obj
, gpgme_ctx
)
266 def gpg_on_payload_pieces (eddy_obj
, gpgme_ctx
):
268 for piece
in eddy_obj
.payload_pieces
:
270 if piece
.piece_type
== "text":
271 # don't transform the plaintext.
274 elif piece
.piece_type
== "message":
275 (plaintext
, sigs
) = decrypt_block (piece
.string
, gpgme_ctx
)
278 piece
.gpg_data
= GPGData()
279 piece
.gpg_data
.sigs
= sigs
281 piece
.gpg_data
.plainobj
= parse_pgp_mime(plaintext
, gpgme_ctx
)
283 elif piece
.piece_type
== "pubkey":
284 fingerprints
= add_gpg_key(piece
.string
, gpgme_ctx
)
286 if fingerprints
!= []:
287 piece
.gpg_data
= GPGData()
288 piece
.gpg_data
.keys
= fingerprints
290 elif piece
.piece_type
== "clearsign":
291 (plaintext
, fingerprints
) = verify_clear_signature(piece
.string
, gpgme_ctx
)
293 if fingerprints
!= []:
294 piece
.gpg_data
= GPGData()
295 piece
.gpg_data
.sigs
= fingerprints
296 piece
.gpg_data
.plainobj
= parse_pgp_mime(plaintext
, gpgme_ctx
)
302 def build_reply (eddy_obj
):
304 string
= "\n".join(do_to_eddys_pieces(build_reply_pieces
, eddy_obj
, None))
309 def build_reply_pieces (eddy_obj
, _ignore
):
312 for piece
in eddy_obj
.payload_pieces
:
313 if piece
.piece_type
== "text":
314 string
+= piece
.string
315 elif piece
.gpg_data
== None:
316 string
+= "Hmmm... I wasn't able to get that part.\n"
317 elif piece
.piece_type
== "message":
319 string
+= build_reply(piece
.gpg_data
.plainobj
)
320 elif piece
.piece_type
== "pubkey":
321 string
+= "thanks for your public key:"
322 for key
in piece
.gpg_data
.keys
:
324 elif piece
.piece_type
== "clearsign":
325 string
+= "*** Begin signed part ***\n"
326 string
+= build_reply(piece
.gpg_data
.plainobj
)
327 string
+= "\n*** End signed part ***"
332 def add_gpg_key (key_block
, gpgme_ctx
):
334 fp
= io
.BytesIO(key_block
.encode('ascii'))
336 result
= gpgme_ctx
.import_(fp
)
337 imports
= result
.imports
342 for import_
in imports
:
343 fingerprint
= import_
[0]
344 fingerprints
+= [fingerprint
]
346 debug("added gpg key: " + fingerprint
)
351 def verify_clear_signature (sig_block
, gpgme_ctx
):
353 # FIXME: this might require the un-decoded bytes
354 # or the correct re-encoding with the carset of the mime part.
355 msg_fp
= io
.BytesIO(sig_block
.encode('utf-8'))
356 ptxt_fp
= io
.BytesIO()
358 result
= gpgme_ctx
.verify(msg_fp
, None, ptxt_fp
)
360 # FIXME: this might require using the charset of the mime part.
361 plaintext
= ptxt_fp
.getvalue().decode('utf-8')
365 fingerprints
+= [res_
.fpr
]
367 return plaintext
, fingerprints
370 def decrypt_block (msg_block
, gpgme_ctx
):
372 block_b
= io
.BytesIO(msg_block
.encode('ascii'))
373 plain_b
= io
.BytesIO()
376 sigs
= gpgme_ctx
.decrypt_verify(block_b
, plain_b
)
380 plaintext
= plain_b
.getvalue().decode('utf-8')
381 return (plaintext
, sigs
)
384 def choose_reply_encryption_key (gpgme_ctx
, fingerprints
):
387 for fp
in fingerprints
:
389 key
= gpgme_ctx
.get_key(fp
)
391 if (key
.can_encrypt
== True):
401 def email_to_from_subject (email_text
):
403 email_struct
= email
.parser
.Parser().parsestr(email_text
)
405 email_to
= email_struct
['To']
406 email_from
= email_struct
['From']
407 email_subject
= email_struct
['Subject']
409 return email_to
, email_from
, email_subject
412 def import_lang(email_to
):
416 if "edward-" + lang
in email_to
:
417 lang
= "lang." + re
.sub('-', '_', lang
)
418 language
= importlib
.import_module(lang
)
422 return importlib
.import_module("lang.en")
425 def generate_encrypted_mime (plaintext
, email_from
, email_subject
, encrypt_to_key
,
429 reply
= "To: " + email_from
+ "\n"
430 reply
+= "Subject: " + email_subject
+ "\n"
432 if (encrypt_to_key
!= None):
433 plaintext_reply
= "thanks for the message!\n\n\n"
434 plaintext_reply
+= email_quote_text(plaintext
)
436 # quoted printable encoding lets most ascii characters look normal
437 # before the decrypted mime message is decoded.
438 char_set
= email
.charset
.Charset("utf-8")
439 char_set
.body_encoding
= email
.charset
.QP
441 # MIMEText doesn't allow setting the text encoding
442 # so we use MIMENonMultipart.
443 plaintext_mime
= MIMENonMultipart('text', 'plain')
444 plaintext_mime
.set_payload(plaintext_reply
, charset
=char_set
)
446 encrypted_text
= encrypt_sign_message(plaintext_mime
.as_string(),
450 control_mime
= MIMEApplication("Version: 1",
451 _subtype
='pgp-encrypted',
452 _encoder
=email
.encoders
.encode_7or8bit
)
453 control_mime
['Content-Description'] = 'PGP/MIME version identification'
454 control_mime
.set_charset('us-ascii')
456 encoded_mime
= MIMEApplication(encrypted_text
,
457 _subtype
='octet-stream; name="encrypted.asc"',
458 _encoder
=email
.encoders
.encode_7or8bit
)
459 encoded_mime
['Content-Description'] = 'OpenPGP encrypted message'
460 encoded_mime
['Content-Disposition'] = 'inline; filename="encrypted.asc"'
461 encoded_mime
.set_charset('us-ascii')
463 message_mime
= MIMEMultipart(_subtype
="encrypted", protocol
="application/pgp-encrypted")
464 message_mime
.attach(control_mime
)
465 message_mime
.attach(encoded_mime
)
466 message_mime
['Content-Disposition'] = 'inline'
468 reply
+= message_mime
.as_string()
472 reply
+= "Sorry, i couldn't find your key.\n"
473 reply
+= "I'll need that to encrypt a message to you."
478 def email_quote_text (text
):
480 quoted_message
= re
.sub(r
'^', r
'> ', text
, flags
=re
.MULTILINE
)
482 return quoted_message
485 def encrypt_sign_message (plaintext
, encrypt_to_key
, gpgme_ctx
):
487 plaintext_bytes
= io
.BytesIO(plaintext
.encode('ascii'))
488 encrypted_bytes
= io
.BytesIO()
490 gpgme_ctx
.encrypt_sign([encrypt_to_key
], gpgme
.ENCRYPT_ALWAYS_TRUST
,
491 plaintext_bytes
, encrypted_bytes
)
493 encrypted_txt
= encrypted_bytes
.getvalue().decode('ascii')
497 def error (error_msg
):
499 sys
.stderr
.write(progname
+ ": " + str(error_msg
) + "\n")
502 def debug (debug_msg
):
504 if edward_config
.debug
== True:
509 if __name__
== "__main__":
512 progname
= sys
.argv
[0]
514 if len(sys
.argv
) > 1:
515 print(progname
+ ": error, this program doesn't " \
516 "need any arguments.", file=sys
.stderr
)