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