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