Browse Source

improve encryption of logs

adam j hartz 6 months ago
parent
commit
3e7bfa3d4c

+ 8
- 2
CHANGELOG View File

@@ -25,8 +25,11 @@ ADDED:
25 25
   library of common check functions (to be used as 'csq_check_function' or
26 26
   similar).  It is accessible during normal page loads as 'csm_check'.
27 27
 
28
-* CAT-SOOP can now optionally encrypt (and compress) log entries, with keys
29
-  specified via 'cs_log_encryption_passphrase' and 'cs_log_encryption_salt'
28
+* CAT-SOOP can now optionally encrypt log entries, with keys specified via
29
+  'cs_log_encryption_passphrase' and 'cs_log_encryption_salt'
30
+
31
+* CAT-SOOP can now optionally compress log entries, by setting
32
+  'cs_log_compression' = True.
30 33
 
31 34
 CHANGED:
32 35
 
@@ -73,6 +76,9 @@ REMOVED:
73 76
 * Removed many bundled third-party applications, in favor of installation via
74 77
   pip.
75 78
 
79
+* 'cs_login_aes_key_location' has been removed.  The 'cs_log_encryption*'
80
+  variables should be used instead.
81
+
76 82
 FIXED:
77 83
 
78 84
 * Fixed an issue with error reporting when trying to log a tuple that contains

+ 10
- 0
LICENSE.included_software View File

@@ -39,6 +39,16 @@ For more information, please refer to <http://unlicense.org/>
39 39
 ########
40 40
 
41 41
 
42
+CAT-SOOP contains a slightly-modified version of fernet.py from the
43
+cryptography Python library (https://github.com/pyca/cryptography).  It was
44
+modified to work with raw bytes instead of with URL-safe base64-encoded
45
+strings.  The original file was dual-licensed under the BSD 3-clause license OR
46
+the Apache License (version 2.0).
47
+
48
+
49
+########
50
+
51
+
42 52
 CAT-SOOP contains a slightly-modified version of highlight.js, a Javascript
43 53
 library for syntax highlighting (https://highlightjs.org/).  In particular,
44 54
 syntax highlighting of Python code was modified to include more built-in names.

+ 11
- 20
__AUTH__/login/login.py View File

@@ -16,7 +16,7 @@
16 16
 
17 17
 import os
18 18
 import re
19
-import base64
19
+import struct
20 20
 import random
21 21
 import string
22 22
 import hashlib
@@ -41,8 +41,6 @@ def get_logged_in_user(context):
41 41
     hash_iterations = context.get('cs_password_hash_iterations', 500000)
42 42
     url = _get_base_url(context)
43 43
 
44
-    aes_key_loc = context.get('cs_login_aes_key_location', None)
45
-
46 44
     # if the user is trying to log out, do that.
47 45
     if action == 'logout':
48 46
         context['cs_session_data'] = {}
@@ -96,7 +94,7 @@ def get_logged_in_user(context):
96 94
                 clear_session_vars(context, 'login_message')
97 95
                 # store new password.
98 96
                 salt = get_new_password_salt()
99
-                phash = compute_password_hash(passwd, salt, hash_iterations, aes_key_loc=aes_key_loc)
97
+                phash = compute_password_hash(context, passwd, salt, hash_iterations)
100 98
                 login_info['password_salt'] = salt
101 99
                 login_info['password_hash'] = phash
102 100
                 logging.update_log('_logininfo', [], uname, login_info)
@@ -236,7 +234,7 @@ def get_logged_in_user(context):
236 234
                 # store new password.
237 235
                 login_info = logging.most_recent('_logininfo', [], u, {})
238 236
                 salt = get_new_password_salt()
239
-                phash = compute_password_hash(passwd, salt, hash_iterations, aes_key_loc=aes_key_loc)
237
+                phash = compute_password_hash(context, passwd, salt, hash_iterations)
240 238
                 login_info['password_salt'] = salt
241 239
                 login_info['password_hash'] = phash
242 240
                 logging.update_log('_logininfo', [], u, login_info)
@@ -413,7 +411,7 @@ def get_logged_in_user(context):
413 411
                 clear_session_vars(context, 'login_message', 'last_form')
414 412
                 # generate new salt and password hash
415 413
                 salt = get_new_password_salt()
416
-                phash = compute_password_hash(passwd, salt, hash_iterations, aes_key_loc=aes_key_loc)
414
+                phash = compute_password_hash(context, passwd, salt, hash_iterations)
417 415
                 # if necessary, send confirmation e-mail
418 416
                 # otherwise, treat like already confirmed
419 417
                 if (mail.can_send_email(context) and
@@ -514,11 +512,11 @@ def check_password(context, provided, uname, iterations=500000):
514 512
     user_login_info = logging.most_recent('_logininfo', [], uname, {})
515 513
     pass_hash = user_login_info.get('password_hash', None)
516 514
     if pass_hash is not None:
515
+        if context['csm_cslog'].ENCRYPT_KEY is not None:
516
+            pass_hash = context['csm_cslog'].FERNET.decrypt(pass_hash)
517 517
         salt = user_login_info.get('password_salt', None)
518
-        hashed_pass = compute_password_hash(provided, salt, iterations,
519
-                                            aes_key_loc=context.get('cs_login_aes_key_location', None))
520
-        if hashed_pass == pass_hash:
521
-            return True
518
+        hashed_pass = compute_password_hash(context, provided, salt, iterations, encrypt=False)
519
+        return hashed_pass == pass_hash
522 520
     return False
523 521
 
524 522
 
@@ -541,22 +539,15 @@ def _ensure_bytes(x):
541 539
         return x
542 540
 
543 541
 
544
-def compute_password_hash(password, salt=None, iterations=500000, aes_key_loc=None):
542
+def compute_password_hash(context, password, salt=None, iterations=500000, encrypt=True):
545 543
     """
546 544
     Given a password, and (optionally) an associated salt, return a hash value.
547 545
     """
548 546
     hash_ = hashlib.pbkdf2_hmac('sha512', _ensure_bytes(password),
549 547
                                 _ensure_bytes(salt),
550 548
                                 iterations)
551
-    if aes_key_loc is not None:
552
-        if not os.path.isfile(aes_key_loc):
553
-            with open(aes_key_loc, 'wb') as f:
554
-                key = get_new_password_salt(32)
555
-                f.write(_ensure_bytes(key))
556
-        with open(aes_key_loc, 'rb') as f:
557
-            key = f.read()
558
-        aes = pyaes.AESModeOfOperationCTR(key)
559
-        hash_ = aes.encrypt(hash_)
549
+    if encrypt and (context['csm_cslog'].ENCRYPT_KEY is not None):
550
+        hash_ = context['csm_cslog'].FERNET.encrypt(hash_)
560 551
     return hash_
561 552
 
562 553
 

+ 20
- 7
__HANDLERS__/default/default.py View File

@@ -23,6 +23,7 @@ import random
23 23
 import shutil
24 24
 import string
25 25
 import hashlib
26
+import binascii
26 27
 import tempfile
27 28
 import traceback
28 29
 import collections
@@ -43,7 +44,7 @@ def new_entry(context, qname, action):
43 44
            'action': action}
44 45
     loc = os.path.join(tempfile.gettempdir(), 'staging', id_)
45 46
     os.makedirs(os.path.dirname(loc), exist_ok=True)
46
-    with open(loc, 'w') as f:
47
+    with open(loc, 'wb') as f:
47 48
         f.write(context['csm_cslog'].prep(obj))
48 49
     newloc = os.path.join(context['cs_data_root'], '__LOGS__', '_checker', 'queued', '%s_%s' % (time.time(), id_))
49 50
     shutil.move(loc, newloc)
@@ -112,7 +113,7 @@ def handle_get_state(context):
112 113
         try:
113 114
             with open(os.path.join(context['cs_data_root'], '__LOGS__',
114 115
                                    '_checker', 'results', v[0], v[1], v),
115
-                      'r') as f:
116
+                      'rb') as f:
116 117
                 row = context['csm_cslog'].unprep(f.read())
117 118
         except:
118 119
             row = None
@@ -1356,7 +1357,7 @@ def render_question(elt, context, lastsubmit, wrap=True):
1356 1357
                                    '_checker', 'results',
1357 1358
                                    magic[0], magic[1], magic)
1358 1359
         if os.path.isfile(checker_loc):
1359
-            with open(checker_loc, 'r') as f:
1360
+            with open(checker_loc, 'rb') as f:
1360 1361
                 result = context['csm_cslog'].unprep(f.read())
1361 1362
             message = '\n<script type="text/javascript">document.getElementById("%s_score_display").innerHTML = %r;</script>' % (name, result['score_box'])
1362 1363
             try:
@@ -1647,15 +1648,27 @@ def pre_handle(context):
1647 1648
                     continue
1648 1649
                 if isinstance(value, list):
1649 1650
                     data = csm_thirdparty.data_uri.DataURI(value[1]).data
1650
-                    dir_ = os.path.join(context['cs_data_root'], '__LOGS__', '_uploads', *context['cs_path_info'])
1651
+                    if context['csm_cslog'].ENCRYPT_KEY is not None:
1652
+                        _path = [context['csm_cslog']._e(i, repr(context['cs_path_info'])) for i in context['cs_path_info']]
1653
+                    else:
1654
+                        _path = context['cs_path_info']
1655
+                    dir_ = os.path.join(context['cs_data_root'], '__LOGS__', '_uploads', *_path)
1651 1656
                     os.makedirs(dir_, exist_ok=True)
1652 1657
                     value[0] = value[0].replace('<', '').replace('>', '').replace('"', '').replace('"', '')
1658
+                    if '.' in value[0]:
1659
+                        fname, fileext = value[0].rsplit('.', 1)
1660
+                    else:
1661
+                        fname, fileext = value[0], ''
1653 1662
                     hstring = hashlib.md5(data).hexdigest()
1654
-                    fname = '%s___%s___%.06f___%s___%s' % (context['cs_username'], name, time.time(), hstring, value[0])
1663
+                    fname = '%s___%s___%.06f___%s___%s' % (context['cs_username'], name, time.time(), hstring, fname)
1664
+                    if context['csm_cslog'].ENCRYPT_KEY is not None:
1665
+                        fname = context['csm_cslog']._e(fname, context['cs_username']+repr(context['cs_path_info']))
1666
+                    if fileext:
1667
+                        fname = '.'.join([fname, fileext])
1655 1668
                     fullname = os.path.join(dir_, fname)
1656 1669
                     with open(fullname, 'wb') as f:
1657
-                        f.write(data)
1658
-                    value[1] = os.path.join(*context['cs_path_info'], fname)
1670
+                        f.write(context['csm_cslog'].compress_encrypt(data))
1671
+                    value[1] = os.path.join(*_path, fname)
1659 1672
         elif context['cs_upload_management'] == 'db':
1660 1673
             pass
1661 1674
         else:

+ 5
- 1
__QTYPES__/fileupload/fileupload.py View File

@@ -63,7 +63,11 @@ def render_html(last_log, **info):
63 63
         try:
64 64
             fname, loc = ll
65 65
             loc = os.path.basename(loc)
66
-            qstring = urlencode({'path': json.dumps(info['cs_path_info']),
66
+            if info['csm_cslog'].ENCRYPT_KEY is not None:
67
+                _path = [info['csm_cslog']._e(i, repr(info['cs_path_info'])) for i in info['cs_path_info']]
68
+            else:
69
+                _path = info['cs_path_info']
70
+            qstring = urlencode({'path': json.dumps(_path),
67 71
                                  'fname': loc})
68 72
             safe_fname = fname.replace('<', '').replace('>', '').replace('"', '').replace("'", '')
69 73
             out += '<br/>'

+ 5
- 1
__QTYPES__/pythoncode/pythoncode.py View File

@@ -365,7 +365,11 @@ def render_html_upload(last_log, **info):
365 365
         try:
366 366
             fname, loc = last_log[name]
367 367
             loc = os.path.basename(loc)
368
-            qstring = urlencode({'path': json.dumps(info['cs_path_info']),
368
+            if info['csm_cslog'].ENCRYPT_KEY is not None:
369
+                _path = [info['csm_cslog']._e(i, repr(info['cs_path_info'])) for i in info['cs_path_info']]
370
+            else:
371
+                _path = info['cs_path_info']
372
+            qstring = urlencode({'path': json.dumps(_path),
369 373
                                  'fname': loc})
370 374
             out += '<br/>'
371 375
             safe_fname = fname.replace('<', '').replace('>', '').replace('"', '').replace("'", '')

+ 2
- 0
__UTIL__/get_upload/content.py View File

@@ -41,6 +41,8 @@ if error is None:
41 41
         content_type = mimetypes.guess_type(fname)[0] or 'text/plain'
42 42
         with open(loc, 'rb') as f:
43 43
             response = f.read()
44
+        if csm_cslog.ENCRYPT_KEY is not None:
45
+            response = csm_cslog.decompress_decrypt(response)
44 46
     except:
45 47
         error = 'There was an error retrieving the file.'
46 48
 

+ 7
- 2
catsoop/base_context.py View File

@@ -262,6 +262,11 @@ running
262 262
 
263 263
 # Log Encryption
264 264
 
265
+cs_log_compression = False
266
+"""
267
+Special: Boolean indicating whether log entries should be compressed.
268
+"""
269
+
265 270
 cs_log_encryption_passphrase = None
266 271
 """
267 272
 Special: Passphrase to be used when encrypting logs, or None for no encryption.
@@ -326,8 +331,8 @@ except Exception as e:
326 331
 
327 332
 cs_all_pieces = [
328 333
     'api', 'auth', 'base_context', 'check', 'cslog', 'dispatch', 'errors',
329
-    'groups', 'language', 'loader', 'mail', 'process', 'session', 'time',
330
-    'thirdparty', 'tutor', 'util',
334
+    'fernet', 'groups', 'language', 'loader', 'mail', 'process', 'session',
335
+    'time', 'thirdparty', 'tutor', 'util',
331 336
 ]
332 337
 
333 338
 cs_all_thirdparty = ['data_uri']

+ 80
- 48
catsoop/cslog.py View File

@@ -34,9 +34,12 @@ add new Python objects to a log.
34 34
 import os
35 35
 import re
36 36
 import ast
37
-import zlib
37
+import lzma
38 38
 import pyaes
39
+import base64
39 40
 import pprint
41
+import random
42
+import struct
40 43
 import hashlib
41 44
 import binascii
42 45
 import importlib
@@ -45,6 +48,8 @@ import contextlib
45 48
 from collections import OrderedDict
46 49
 from datetime import datetime, timedelta
47 50
 
51
+from .fernet import RawFernet
52
+
48 53
 _nodoc = {'passthrough', 'FileLock', 'SEP_CHARS', 'get_separator',
49 54
           'good_separator', 'modify_most_recent', 'NoneType', 'OrderedDict',
50 55
           'datetime', 'timedelta'}
@@ -58,15 +63,18 @@ from filelock import FileLock
58 63
 
59 64
 importlib.reload(base_context)
60 65
 
61
-ENCRYPT_KEY = base_context.cs_log_encryption_passphrase
66
+COMPRESS = base_context.cs_log_compression
67
+
68
+ENCRYPT_PASS = ENCRYPT_KEY = base_context.cs_log_encryption_passphrase
62 69
 if ENCRYPT_KEY is not None:
63
-    salt = base_context.cs_log_encryption_salt
64
-    if not isinstance(salt, bytes):
70
+    SALT = base_context.cs_log_encryption_salt
71
+    if not isinstance(SALT, bytes):
65 72
         try:
66
-            salt = binascii.unhexlify(salt)
73
+            SALT = binascii.unhexlify(SALT)
67 74
         except:
68
-            salt = salt.encode()
69
-    ENCRYPT_KEY = hashlib.pbkdf2_hmac('sha256', ENCRYPT_KEY.encode(), salt, 100000, dklen=32)
75
+            SALT = SALT.encode('utf8')
76
+    ENCRYPT_KEY = hashlib.pbkdf2_hmac('sha256', ENCRYPT_KEY.encode('utf8'), SALT, 100000, dklen=32)
77
+    FERNET = RawFernet(ENCRYPT_KEY)
70 78
 
71 79
 
72 80
 def _split_path(path, sofar=[]):
@@ -82,6 +90,7 @@ def _split_path(path, sofar=[]):
82 90
 _CHARACTER_MAP = {chr(o): '_%s' % chr(o+32) for o in range(65, 91)}
83 91
 _CHARACTER_MAP.update({'.': '_.', '_': '__'})
84 92
 
93
+
85 94
 def _convert_path(path):
86 95
     return (''.join(_CHARACTER_MAP.get(i, i) for i in w) for w in _split_path(path))
87 96
 
@@ -91,37 +100,56 @@ def log_lock(path):
91 100
     os.makedirs(os.path.dirname(lock_loc), exist_ok=True)
92 101
     return FileLock(lock_loc)
93 102
 
94
-def encrypt(x):
95
-    aes = pyaes.AESModeOfOperationCTR(ENCRYPT_KEY)
96
-    return binascii.hexlify(aes.encrypt(zlib.compress(x.encode()))).decode()
97
-
98
-def decrypt(x):
99
-    aes = pyaes.AESModeOfOperationCTR(ENCRYPT_KEY)
100
-    return zlib.decompress(aes.decrypt(binascii.unhexlify(x))).decode()
101
-
102
-
103
-if ENCRYPT_KEY is None:
104
-    def prep(x):
105
-        """
106
-        Helper function to serialize a Python object.
107
-        """
108
-        return pprint.pformat(x).replace('datetime.', '')
109
-    def unprep(x):
110
-        """
111
-        Helper function to deserialize a Python object.
112
-        """
113
-        return literal_eval(x)
114
-else:
115
-    def prep(x):
116
-        """
117
-        Helper function to serialize a Python object.
118
-        """
119
-        return encrypt(pprint.pformat(x).replace('datetime.', ''))
120
-    def unprep(x):
121
-        """
122
-        Helper function to deserialize a Python object.
123
-        """
124
-        return literal_eval(decrypt(x))
103
+def compress_encrypt(x):
104
+    if COMPRESS:
105
+        x = lzma.compress(x)
106
+    if ENCRYPT_KEY is not None:
107
+        x = FERNET.encrypt(x)
108
+    return x
109
+
110
+
111
+def prep(x):
112
+    """
113
+    Helper function to serialize a Python object.
114
+    """
115
+    out = compress_encrypt(pprint.pformat(x).replace('datetime.', '').encode('utf8'))
116
+    if COMPRESS or (ENCRYPT_KEY is not None):
117
+        out = base64.b85encode(out)
118
+    return out
119
+
120
+
121
+def decompress_decrypt(x):
122
+    if ENCRYPT_KEY is not None:
123
+        x = FERNET.decrypt(x)
124
+    if COMPRESS:
125
+        x = lzma.decompress(x)
126
+    return x
127
+
128
+
129
+def unprep(x):
130
+    """
131
+    Helper function to deserialize a Python object.
132
+    """
133
+    if COMPRESS or (ENCRYPT_KEY is not None):
134
+        x = base64.b85decode(x)
135
+    return literal_eval(decompress_decrypt(x).decode('utf8'))
136
+
137
+
138
+def _e(x, seed=0):
139
+    r = random.Random()
140
+    r.seed(seed)
141
+    cstr = bytes(r.randint(0, 255) for i in range(2))
142
+    cnt = struct.unpack('H', cstr)[0]
143
+    ctext = pyaes.AESModeOfOperationCTR(ENCRYPT_KEY, counter=pyaes.Counter(cnt)).encrypt(x.encode('utf8'))
144
+    return binascii.hexlify(cstr + ctext).decode('utf8')
145
+
146
+
147
+def _d(x):
148
+    x = binascii.unhexlify(x)
149
+    cstr = x[:2]
150
+    cnt = struct.unpack('H', cstr)[0]
151
+    ctext = x[2:]
152
+    return pyaes.AESModeOfOperationCTR(ENCRYPT_KEY, counter=pyaes.Counter(cnt)).decrypt(ctext).decode('utf8')
125 153
 
126 154
 
127 155
 def get_log_filename(db_name, path, logname):
@@ -135,9 +163,10 @@ def get_log_filename(db_name, path, logname):
135 163
     * `logname`: the name of the log
136 164
     '''
137 165
     if ENCRYPT_KEY is not None:
138
-        path = [encrypt(i) for i in path]
139
-        db_name = encrypt(db_name)
140
-        logname = encrypt(logname)
166
+        seed = ENCRYPT_PASS + (path[0] if path else db_name)
167
+        path = [_e(i, seed+i) for i in path]
168
+        db_name = _e(db_name, seed+db_name)
169
+        logname = _e(logname, seed+repr(path))
141 170
     if path:
142 171
         course = path[0]
143 172
         return os.path.join(base_context.cs_data_root, '__LOGS__', '_courses', course, db_name, *(path[1:]), '%s.log' % logname)
@@ -145,14 +174,16 @@ def get_log_filename(db_name, path, logname):
145 174
         return os.path.join(base_context.cs_data_root, '__LOGS__', db_name, *path, '%s.log' % logname)
146 175
 
147 176
 
148
-sep = '\n\n'
177
+sep = b'\n\n'
149 178
 
150 179
 
151 180
 def _update_log(fname, new):
152 181
         assert can_log(new), "Can't log: %r" % (new, )
153 182
         os.makedirs(os.path.dirname(fname), exist_ok=True)
154
-        with open(fname, 'a') as f:
155
-            f.write(prep(new) + sep)
183
+        with open(fname, 'ab') as f:
184
+            f.write(prep(new))
185
+            f.write(sep)
186
+
156 187
 
157 188
 def update_log(db_name, path, logname, new, lock=True):
158 189
     """
@@ -181,8 +212,9 @@ def update_log(db_name, path, logname, new, lock=True):
181 212
 def _overwrite_log(fname, new):
182 213
     assert can_log(new), "Can't log: %r" % (new, )
183 214
     os.makedirs(os.path.dirname(fname), exist_ok=True)
184
-    with open(fname, 'w') as f:
185
-        f.write(prep(new) + sep)
215
+    with open(fname, 'wb') as f:
216
+        f.write(prep(new))
217
+        f.write(sep)
186 218
 
187 219
 
188 220
 def overwrite_log(db_name, path, logname, new, lock=True):
@@ -214,7 +246,7 @@ def _read_log(db_name, path, logname, lock=True):
214 246
     cm = log_lock(fname) if lock else passthrough()
215 247
     with cm as lock:
216 248
         try:
217
-            f = open(fname, 'r')
249
+            f = open(fname, 'rb')
218 250
             for i in f.read().split(sep):
219 251
                 if i:
220 252
                     yield unprep(i)
@@ -276,7 +308,7 @@ def most_recent(db_name, path, logname, default=None, lock=True):
276 308
     #get an exclusive lock on this file before reading it
277 309
     cm = log_lock(fname) if lock else passthrough()
278 310
     with cm as lock:
279
-        with open(fname, 'r') as f:
311
+        with open(fname, 'rb') as f:
280 312
             return unprep(f.read().rsplit(sep, 2)[-2])
281 313
 
282 314
 

+ 155
- 0
catsoop/fernet.py View File

@@ -0,0 +1,155 @@
1
+# This file is part of CAT-SOOP
2
+# Copyright (c) 2011-2018 Adam Hartz <hz@mit.edu>
3
+#
4
+# This program is free software: you can redistribute it and/or modify it under
5
+# the terms of the GNU Affero General Public License as published by the Free
6
+# Software Foundation, either version 3 of the License, or (at your option) any
7
+# later version.
8
+#
9
+# This program is distributed in the hope that it will be useful, but WITHOUT
10
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
11
+# FOR A PARTICULAR PURPOSE.  See the GNU Affero General Public License for more
12
+# details.
13
+#
14
+# You should have received a copy of the GNU Affero General Public License
15
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
16
+
17
+# This file is a modified version of the file available at:
18
+# https://github.com/pyca/cryptography/blob/master/src/cryptography/fernet.py
19
+
20
+# This original file, part of the cryptography Python3 package
21
+# (https://cryptography.io/en/latest/) was dual licensed under the terms of the
22
+# Apache License, Version 2.0, and the BSD License. See
23
+# https://github.com/pyca/cryptography/blob/master/LICENSE for complete
24
+# details.
25
+
26
+import binascii
27
+import os
28
+import struct
29
+import time
30
+
31
+import six
32
+
33
+from cryptography.exceptions import InvalidSignature
34
+from cryptography.hazmat.backends import default_backend
35
+from cryptography.hazmat.primitives import hashes, padding
36
+from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
37
+from cryptography.hazmat.primitives.hmac import HMAC
38
+
39
+
40
+class InvalidToken(Exception):
41
+    pass
42
+
43
+
44
+_MAX_CLOCK_SKEW = 60
45
+
46
+
47
+class RawFernet(object):
48
+    def __init__(self, key, backend=None):
49
+        if backend is None:
50
+            backend = default_backend()
51
+
52
+        if len(key) != 32:
53
+            raise ValueError(
54
+                "Fernet key must be 32 bytes."
55
+            )
56
+
57
+        self._signing_key = key[:16]
58
+        self._encryption_key = key[16:]
59
+        self._backend = backend
60
+
61
+    @classmethod
62
+    def generate_key(cls):
63
+        return os.urandom(32)
64
+
65
+    def encrypt(self, data):
66
+        current_time = int(time.time())
67
+        iv = os.urandom(16)
68
+        return self._encrypt_from_parts(data, current_time, iv)
69
+
70
+    def _encrypt_from_parts(self, data, current_time, iv):
71
+        if not isinstance(data, bytes):
72
+            raise TypeError("data must be bytes.")
73
+
74
+        padder = padding.PKCS7(algorithms.AES.block_size).padder()
75
+        padded_data = padder.update(data) + padder.finalize()
76
+        encryptor = Cipher(
77
+            algorithms.AES(self._encryption_key), modes.CBC(iv), self._backend
78
+        ).encryptor()
79
+        ciphertext = encryptor.update(padded_data) + encryptor.finalize()
80
+
81
+        basic_parts = (
82
+            b"\x80" + struct.pack(">Q", current_time) + iv + ciphertext
83
+        )
84
+
85
+        h = HMAC(self._signing_key, hashes.SHA256(), backend=self._backend)
86
+        h.update(basic_parts)
87
+        hmac = h.finalize()
88
+        return basic_parts + hmac
89
+
90
+    def decrypt(self, token, ttl=None):
91
+        timestamp, data = RawFernet._get_unverified_token_data(token)
92
+        return self._decrypt_data(data, timestamp, ttl)
93
+
94
+    def extract_timestamp(self, token):
95
+        timestamp, data = RawFernet._get_unverified_token_data(token)
96
+        # Verify the token was not tampered with.
97
+        self._verify_signature(data)
98
+        return timestamp
99
+
100
+    @staticmethod
101
+    def _get_unverified_token_data(token):
102
+        if not isinstance(token, bytes):
103
+            raise TypeError("token must be bytes.")
104
+
105
+        try:
106
+            data = token
107
+        except (TypeError, binascii.Error):
108
+            raise InvalidToken
109
+
110
+        if not data or six.indexbytes(data, 0) != 0x80:
111
+            raise InvalidToken
112
+
113
+        try:
114
+            timestamp, = struct.unpack(">Q", data[1:9])
115
+        except struct.error:
116
+            raise InvalidToken
117
+        return timestamp, data
118
+
119
+    def _verify_signature(self, data):
120
+        h = HMAC(self._signing_key, hashes.SHA256(), backend=self._backend)
121
+        h.update(data[:-32])
122
+        try:
123
+            h.verify(data[-32:])
124
+        except InvalidSignature:
125
+            raise InvalidToken
126
+
127
+    def _decrypt_data(self, data, timestamp, ttl):
128
+        current_time = int(time.time())
129
+        if ttl is not None:
130
+            if timestamp + ttl < current_time:
131
+                raise InvalidToken
132
+
133
+            if current_time + _MAX_CLOCK_SKEW < timestamp:
134
+                raise InvalidToken
135
+
136
+        self._verify_signature(data)
137
+
138
+        iv = data[9:25]
139
+        ciphertext = data[25:-32]
140
+        decryptor = Cipher(
141
+            algorithms.AES(self._encryption_key), modes.CBC(iv), self._backend
142
+        ).decryptor()
143
+        plaintext_padded = decryptor.update(ciphertext)
144
+        try:
145
+            plaintext_padded += decryptor.finalize()
146
+        except ValueError:
147
+            raise InvalidToken
148
+        unpadder = padding.PKCS7(algorithms.AES.block_size).unpadder()
149
+
150
+        unpadded = unpadder.update(plaintext_padded)
151
+        try:
152
+            unpadded += unpadder.finalize()
153
+        except ValueError:
154
+            raise InvalidToken
155
+        return unpadded

+ 2
- 2
catsoop/session.py View File

@@ -118,7 +118,7 @@ def get_session_data(context, sid):
118 118
     fname = os.path.join(SESSION_DIR, sid)
119 119
     with cslog.log_lock(fname) as lock:
120 120
         try:
121
-            with open(fname, 'r') as f:
121
+            with open(fname, 'rb') as f:
122 122
                 out = cslog.unprep(f.read())
123 123
         except:
124 124
             out = {}  # default to returning empty session
@@ -140,5 +140,5 @@ def set_session_data(context, sid, data):
140 140
     make_session_dir()
141 141
     fname = os.path.join(SESSION_DIR, sid)
142 142
     with cslog.log_lock(fname) as lock:
143
-        with open(fname, 'w') as f:
143
+        with open(fname, 'wb') as f:
144 144
             f.write(cslog.prep(data))

+ 1
- 1
catsoop/tutor.py View File

@@ -186,7 +186,7 @@ def read_checker_result(context, magic):
186 186
             question types that don't return extra data
187 187
     """
188 188
     with open(os.path.join(context['cs_data_root'], '__LOGS__', '_checker',
189
-              'results', magic[0], magic[1], magic), 'r') as f:
189
+              'results', magic[0], magic[1], magic), 'rb') as f:
190 190
         out = context['csm_cslog'].unprep(f.read())
191 191
     return out
192 192
 

+ 1
- 3
requirements.txt View File

@@ -1,6 +1,6 @@
1 1
 bs4
2 2
 cheroot
3
-ecdsa
3
+cryptography
4 4
 filelock
5 5
 html5lib
6 6
 jose
@@ -8,6 +8,4 @@ markdown
8 8
 mpmath
9 9
 ply
10 10
 pyaes
11
-pyasn1
12
-rsa
13 11
 websockets

+ 3
- 3
scripts/checker.py View File

@@ -119,7 +119,7 @@ def do_check(row):
119 119
 
120 120
         # make temporary file to write results to
121 121
         _, temploc = tempfile.mkstemp()
122
-        with open(temploc, 'w') as f:
122
+        with open(temploc, 'wb') as f:
123 123
             f.write(context['csm_cslog'].prep(row))
124 124
         # move that file to results, close the handle to it.
125 125
         magic = row['magic']
@@ -166,7 +166,7 @@ while True:
166 166
                                    "because the checker ran for too long.</b></font>")
167 167
                 magic = row['magic']
168 168
                 newloc = os.path.join(RESULTS, magic[0], magic[1], magic)
169
-                with open(newloc, 'w') as f:
169
+                with open(newloc, 'wb') as f:
170 170
                     f.write(cslog.prep(row))
171 171
                 # then remove from running
172 172
                 os.unlink(os.path.join(RUNNING, row['magic']))
@@ -185,7 +185,7 @@ while True:
185 185
         if waiting:
186 186
             # grab the first thing off the queue, move it to the "running" dir
187 187
             first = waiting[0]
188
-            with open(os.path.join(QUEUED, first), 'r') as f:
188
+            with open(os.path.join(QUEUED, first), 'rb') as f:
189 189
                 row = cslog.unprep(f.read())
190 190
             _, magic = first.split('_')
191 191
             row['magic'] = magic

+ 1
- 1
scripts/reporter.py View File

@@ -98,7 +98,7 @@ async def reporter(websocket, path):
98 98
             msg = {'type': 'running', 'started': start, 'now': time.time()}
99 99
         elif status == 'results':
100 100
             try:
101
-                with open(os.path.join(RESULTS, magic[0], magic[1], magic), 'r') as f:
101
+                with open(os.path.join(RESULTS, magic[0], magic[1], magic), 'rb') as f:
102 102
                     m = unprep(f.read())
103 103
             except:
104 104
                 return

Loading…
Cancel
Save