fb92076d46af04bb17624504797873f8f1177fd9
[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
35 import sys
36 import gpgme
37 import re
38 import io
39 import os
40
41 import email.parser
42 import email.message
43 import email.encoders
44
45 from email.mime.multipart import MIMEMultipart
46 from email.mime.application import MIMEApplication
47 from email.mime.nonmultipart import MIMENonMultipart
48
49 import edward_config
50
51 def main ():
52
53 handle_args()
54
55 email_text = sys.stdin.read()
56 email_from, email_subject = email_from_subject(email_text)
57
58 os.environ['GNUPGHOME'] = edward_config.gnupghome
59 gpgme_ctx = gpgme.Context()
60 gpgme_ctx.armor = True
61
62 add_gpg_keys(email_text, gpgme_ctx)
63
64 plaintext, keys = email_decode_flatten(email_text, gpgme_ctx)
65 encrypt_to_key = choose_reply_encryption_key(keys)
66
67 reply_message = generate_reply(plaintext, email_from, \
68 email_subject, encrypt_to_key,
69 edward_config.sign_with_key,
70 gpgme_ctx)
71
72 print(reply_message)
73
74
75 def email_decode_flatten (email_text, gpgme_ctx):
76
77 body = ""
78 keys = []
79
80 email_struct = email.parser.Parser().parsestr(email_text)
81
82 for subpart in email_struct.walk():
83
84 payload, description, filename, content_type \
85 = get_email_subpart_info(subpart)
86
87 if payload == "":
88 continue
89
90 if content_type == "multipart":
91 continue
92
93 if content_type == "application/pgp-encrypted":
94 if description == "PGP/MIME version identification":
95 if payload.strip() != "Version: 1":
96 print(progname + ": Warning: unknown " \
97 + description + ": " \
98 + payload.strip(), file=sys.stderr)
99 continue
100
101
102 if (filename == "encrypted.asc") or (content_type == "pgp/mime"):
103 plaintext, more_keys = decrypt_text(payload, gpgme_ctx)
104
105 body += plaintext
106 keys += more_keys
107
108 elif content_type == "application/pgp-keys":
109 add_gpg_keys(payload, gpgme_ctx)
110
111 elif content_type == "text/plain":
112 body += payload + "\n"
113
114 else:
115 body += payload + "\n"
116
117 return body, keys
118
119
120 def email_from_subject (email_text):
121
122 email_struct = email.parser.Parser().parsestr(email_text)
123
124 email_from = email_struct['From']
125 email_subject = email_struct['Subject']
126
127 return email_from, email_subject
128
129
130 def get_email_subpart_info (part):
131
132 charset = part.get_content_charset()
133 payload_bytes = part.get_payload(decode=True)
134
135 filename = part.get_filename()
136 content_type = part.get_content_type()
137 description_list = part.get_params(header='content-description')
138
139 if charset == None:
140 charset = 'utf-8'
141
142 if payload_bytes != None:
143 payload = payload_bytes.decode(charset)
144 else:
145 payload = ""
146
147 if description_list != None:
148 description = description_list[0][0]
149 else:
150 description = ""
151
152 return payload, description, filename, content_type
153
154
155 def add_gpg_keys (text, gpgme_ctx):
156
157 keys = scan_and_grab(text,
158 '-----BEGIN PGP PUBLIC KEY BLOCK-----',
159 '-----END PGP PUBLIC KEY BLOCK-----')
160
161 for key in keys:
162 fp = io.BytesIO(key.encode('ascii'))
163
164 result = gpgme_ctx.import_(fp)
165 fingerprint = result.imports[0][0]
166
167 debug("added gpg key: " + fingerprint)
168
169 def decrypt_text (gpg_text, gpgme_ctx):
170
171 body = ""
172 keys = []
173
174 gpg_chunks = scan_and_grab(gpg_text,
175 '-----BEGIN PGP MESSAGE-----',
176 '-----END PGP MESSAGE-----')
177
178 plaintext_and_sigs_chunks = decrypt_chunks(gpg_chunks, gpgme_ctx)
179
180 for chunk in plaintext_and_sigs_chunks:
181 plaintext = chunk[0]
182 sigs = chunk[1]
183
184 for sig in sigs:
185 key = get_pub_key(sig, gpgme_ctx)
186 keys += [key]
187
188 # recursive for nested layers of mime and/or gpg
189 plaintext, more_keys = email_decode_flatten(plaintext, gpgme_ctx)
190
191 body += plaintext
192 keys += more_keys
193
194 return body, keys
195
196
197 def get_pub_key (sig, gpgme_ctx):
198
199 fingerprint = sig.fpr
200 key = gpgme_ctx.get_key(fingerprint)
201
202 return key
203
204
205 def scan_and_grab (text, start_text, end_text):
206
207 matches = re.search('(' + start_text + '.*' + end_text + ')',
208 text, flags=re.DOTALL)
209
210 if matches != None:
211 match_tuple = matches.groups()
212 else:
213 match_tuple = ()
214
215 return match_tuple
216
217
218 def decrypt_chunks (gpg_chunks, gpgme_ctx):
219
220 return [decrypt_chunk(chunk, gpgme_ctx) for chunk in gpg_chunks]
221
222
223 def decrypt_chunk (gpg_chunk, gpgme_ctx):
224
225 chunk_b = io.BytesIO(gpg_chunk.encode('ascii'))
226 plain_b = io.BytesIO()
227
228 sigs = gpgme_ctx.decrypt_verify(chunk_b, plain_b)
229
230 plaintext = plain_b.getvalue().decode('utf-8')
231 return (plaintext, sigs)
232
233
234 def choose_reply_encryption_key (keys):
235
236 reply_key = None
237 for key in keys:
238 if (key.can_encrypt == True):
239 reply_key = key
240 break
241
242 return reply_key
243
244
245 def generate_reply (plaintext, email_from, email_subject, encrypt_to_key,
246 sign_with_fingerprint, gpgme_ctx):
247
248
249 reply = "To: " + email_from + "\n"
250 reply += "Subject: " + email_subject + "\n"
251
252 if (encrypt_to_key != None):
253 plaintext_reply = "thanks for the message!\n\n\n"
254 plaintext_reply += email_quote_text(plaintext)
255
256 # quoted printable encoding lets most ascii characters look normal
257 # before the decrypted mime message is decoded.
258 char_set = email.charset.Charset("utf-8")
259 char_set.body_encoding = email.charset.QP
260
261 # MIMEText doesn't allow setting the text encoding
262 # so we use MIMENonMultipart.
263 plaintext_mime = MIMENonMultipart('text', 'plain')
264 plaintext_mime.set_payload(plaintext_reply, charset=char_set)
265
266 encrypted_text = encrypt_sign_message(plaintext_mime.as_string(),
267 encrypt_to_key,
268 sign_with_fingerprint,
269 gpgme_ctx)
270
271 control_mime = MIMEApplication("Version: 1",
272 _subtype='pgp-encrypted',
273 _encoder=email.encoders.encode_7or8bit)
274 control_mime['Content-Description'] = 'PGP/MIME version identification'
275 control_mime.set_charset('us-ascii')
276
277 encoded_mime = MIMEApplication(encrypted_text,
278 _subtype='octet-stream; name="encrypted.asc"',
279 _encoder=email.encoders.encode_7or8bit)
280 encoded_mime['Content-Description'] = 'OpenPGP encrypted message'
281 encoded_mime['Content-Disposition'] = 'inline; filename="encrypted.asc"'
282 encoded_mime.set_charset('us-ascii')
283
284 message_mime = MIMEMultipart(_subtype="encrypted", protocol="application/pgp-encrypted")
285 message_mime.attach(control_mime)
286 message_mime.attach(encoded_mime)
287 message_mime['Content-Disposition'] = 'inline'
288
289 reply += message_mime.as_string()
290
291 else:
292 reply += "\n"
293 reply += "Sorry, i couldn't find your key.\n"
294 reply += "I'll need that to encrypt a message to you."
295
296 return reply
297
298
299 def email_quote_text (text):
300
301 quoted_message = re.sub(r'^', r'> ', text, flags=re.MULTILINE)
302
303 return quoted_message
304
305
306 def encrypt_sign_message (plaintext, encrypt_to_key, sign_with_fingerprint, gpgme_ctx):
307
308 sign_with_key = gpgme_ctx.get_key(sign_with_fingerprint)
309 gpgme_ctx.signers = [sign_with_key]
310
311 plaintext_bytes = io.BytesIO(plaintext.encode('ascii'))
312 encrypted_bytes = io.BytesIO()
313
314 gpgme_ctx.encrypt_sign([encrypt_to_key], gpgme.ENCRYPT_ALWAYS_TRUST,
315 plaintext_bytes, encrypted_bytes)
316
317 encrypted_txt = encrypted_bytes.getvalue().decode('ascii')
318 return encrypted_txt
319
320
321 def debug (debug_msg):
322
323 if edward_config.debug == True:
324 sys.stderr.write(debug_msg + "\n")
325
326
327 def handle_args ():
328 if __name__ == "__main__":
329
330 global progname
331 progname = sys.argv[0]
332
333 if len(sys.argv) > 1:
334 print(progname + ": error, this program doesn't " \
335 "need any arguments.", file=sys.stderr)
336 exit(1)
337
338
339 main()
340