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