summaryrefslogtreecommitdiff
path: root/matweb
diff options
context:
space:
mode:
authorjfriedli2020-04-26 09:50:14 -0700
committerjfriedli2020-04-26 09:50:14 -0700
commitc301e472bd7fd79d675c5df089db0b16fd1e2cfe (patch)
treec3332e0f974edc09881b5534c35becc5b9fffa3b /matweb
parente1bac8b6a7fd857f38b7bcb678398c82baaa8fd5 (diff)
Resolve "Use a HMAC instead of a hash"
Diffstat (limited to 'matweb')
-rw-r--r--matweb/frontend.py14
-rw-r--r--matweb/rest_api.py19
-rw-r--r--matweb/utils.py35
3 files changed, 47 insertions, 21 deletions
diff --git a/matweb/frontend.py b/matweb/frontend.py
index 93432b4..2e25467 100644
--- a/matweb/frontend.py
+++ b/matweb/frontend.py
@@ -18,8 +18,8 @@ def info():
18 ) 18 )
19 19
20 20
21@routes.route('/download/<string:key>/<string:filename>') 21@routes.route('/download/<string:key>/<string:secret>/<string:filename>')
22def download_file(key: str, filename: str): 22def download_file(key: str, secret: str, filename: str):
23 if filename != secure_filename(filename): 23 if filename != secure_filename(filename):
24 return redirect(url_for('routes.upload_file')) 24 return redirect(url_for('routes.upload_file'))
25 25
@@ -28,7 +28,7 @@ def download_file(key: str, filename: str):
28 28
29 if not os.path.exists(complete_path): 29 if not os.path.exists(complete_path):
30 return redirect(url_for('routes.upload_file')) 30 return redirect(url_for('routes.upload_file'))
31 if hmac.compare_digest(utils.hash_file(complete_path), key) is False: 31 if hmac.compare_digest(utils.hash_file(complete_path, secret), key) is False:
32 return redirect(url_for('routes.upload_file')) 32 return redirect(url_for('routes.upload_file'))
33 33
34 @after_this_request 34 @after_this_request
@@ -67,10 +67,14 @@ def upload_file():
67 flash('Unable to clean %s' % mime) 67 flash('Unable to clean %s' % mime)
68 return redirect(url_for('routes.upload_file')) 68 return redirect(url_for('routes.upload_file'))
69 69
70 key, meta_after, output_filename = utils.cleanup(parser, filepath, current_app.config['UPLOAD_FOLDER']) 70 key, secret, meta_after, output_filename = utils.cleanup(parser, filepath, current_app.config['UPLOAD_FOLDER'])
71 71
72 return render_template( 72 return render_template(
73 'download.html', mimetypes=mime_types, meta=meta, filename=output_filename, meta_after=meta_after, key=key 73 'download.html',
74 mimetypes=mime_types,
75 meta=meta,
76 download_uri=url_for('routes.download_file', key=key, secret=secret, filename=output_filename),
77 meta_after=meta_after,
74 ) 78 )
75 79
76 max_file_size = int(current_app.config['MAX_CONTENT_LENGTH'] / 1024 / 1024) 80 max_file_size = int(current_app.config['MAX_CONTENT_LENGTH'] / 1024 / 1024)
diff --git a/matweb/rest_api.py b/matweb/rest_api.py
index 60d834f..4098050 100644
--- a/matweb/rest_api.py
+++ b/matweb/rest_api.py
@@ -42,14 +42,15 @@ class APIUpload(Resource):
42 if not parser.remove_all(): 42 if not parser.remove_all():
43 abort(500, message='Unable to clean %s' % mime) 43 abort(500, message='Unable to clean %s' % mime)
44 44
45 key, meta_after, output_filename = utils.cleanup(parser, filepath, self.upload_folder) 45 key, secret, meta_after, output_filename = utils.cleanup(parser, filepath, self.upload_folder)
46 return utils.return_file_created_response( 46 return utils.return_file_created_response(
47 output_filename, 47 output_filename,
48 mime, 48 mime,
49 key, 49 key,
50 secret,
50 meta, 51 meta,
51 meta_after, 52 meta_after,
52 urljoin(request.host_url, '%s/%s/%s/%s' % ('api', 'download', key, output_filename)) 53 urljoin(request.host_url, '%s/%s/%s/%s/%s' % ('api', 'download', key, secret, output_filename))
53 ) 54 )
54 55
55 56
@@ -58,8 +59,8 @@ class APIDownload(Resource):
58 def __init__(self, **kwargs): 59 def __init__(self, **kwargs):
59 self.upload_folder = kwargs['upload_folder'] 60 self.upload_folder = kwargs['upload_folder']
60 61
61 def get(self, key: str, filename: str): 62 def get(self, key: str, secret: str, filename: str):
62 complete_path, filepath = utils.is_valid_api_download_file(filename, key, self.upload_folder) 63 complete_path, filepath = utils.is_valid_api_download_file(filename, key, secret, self.upload_folder)
63 # Make sure the file is NOT deleted on HEAD requests 64 # Make sure the file is NOT deleted on HEAD requests
64 if request.method == 'GET': 65 if request.method == 'GET':
65 file_removal_scheduler.run_file_removal_job(self.upload_folder) 66 file_removal_scheduler.run_file_removal_job(self.upload_folder)
@@ -87,6 +88,7 @@ class APIBulkDownloadCreator(Resource):
87 'type': 'dict', 88 'type': 'dict',
88 'schema': { 89 'schema': {
89 'key': {'type': 'string', 'required': True}, 90 'key': {'type': 'string', 'required': True},
91 'secret': {'type': 'string', 'required': True},
90 'file_name': {'type': 'string', 'required': True} 92 'file_name': {'type': 'string', 'required': True}
91 } 93 }
92 } 94 }
@@ -108,6 +110,7 @@ class APIBulkDownloadCreator(Resource):
108 complete_path, file_path = utils.is_valid_api_download_file( 110 complete_path, file_path = utils.is_valid_api_download_file(
109 file_candidate['file_name'], 111 file_candidate['file_name'],
110 file_candidate['key'], 112 file_candidate['key'],
113 file_candidate['secret'],
111 self.upload_folder 114 self.upload_folder
112 ) 115 )
113 try: 116 try:
@@ -124,13 +127,17 @@ class APIBulkDownloadCreator(Resource):
124 parser, mime = utils.get_file_parser(zip_path) 127 parser, mime = utils.get_file_parser(zip_path)
125 if not parser.remove_all(): 128 if not parser.remove_all():
126 abort(500, message='Unable to clean %s' % mime) 129 abort(500, message='Unable to clean %s' % mime)
127 key, meta_after, output_filename = utils.cleanup(parser, zip_path, self.upload_folder) 130 key, secret, meta_after, output_filename = utils.cleanup(parser, zip_path, self.upload_folder)
128 return { 131 return {
129 'output_filename': output_filename, 132 'output_filename': output_filename,
130 'mime': mime, 133 'mime': mime,
131 'key': key, 134 'key': key,
135 'secret': secret,
132 'meta_after': meta_after, 136 'meta_after': meta_after,
133 'download_link': urljoin(request.host_url, '%s/%s/%s/%s' % ('api', 'download', key, output_filename)) 137 'download_link': urljoin(
138 request.host_url,
139 '%s/%s/%s/%s/%s' % ('api', 'download', key, secret, output_filename)
140 )
134 }, 201 141 }, 201
135 142
136 143
diff --git a/matweb/utils.py b/matweb/utils.py
index 8dfff45..ec9b99c 100644
--- a/matweb/utils.py
+++ b/matweb/utils.py
@@ -12,15 +12,21 @@ def get_allow_origin_header_value():
12 return os.environ.get('MAT2_ALLOW_ORIGIN_WHITELIST', '*').split(" ") 12 return os.environ.get('MAT2_ALLOW_ORIGIN_WHITELIST', '*').split(" ")
13 13
14 14
15def hash_file(filepath: str) -> str: 15def hash_file(filepath: str, secret: str) -> str:
16 sha256 = hashlib.sha256() 16 """
17 The goal of the hmac is to ONLY make the hashes unpredictable
18 :param filepath: Path of the file
19 :param secret: a server side generated secret
20 :return: digest, secret
21 """
22 mac = hmac.new(secret.encode(), None, hashlib.sha256)
17 with open(filepath, 'rb') as f: 23 with open(filepath, 'rb') as f:
18 while True: 24 while True:
19 data = f.read(65536) # read the file by chunk of 64k 25 data = f.read(65536) # read the file by chunk of 64k
20 if not data: 26 if not data:
21 break 27 break
22 sha256.update(data) 28 mac.update(data)
23 return sha256.hexdigest() 29 return mac.hexdigest()
24 30
25 31
26def check_upload_folder(upload_folder): 32def check_upload_folder(upload_folder):
@@ -28,11 +34,20 @@ def check_upload_folder(upload_folder):
28 os.mkdir(upload_folder) 34 os.mkdir(upload_folder)
29 35
30 36
31def return_file_created_response(output_filename, mime, key, meta, meta_after, download_link): 37def return_file_created_response(
38 output_filename: str,
39 mime: str,
40 key: str,
41 secret: str,
42 meta: list,
43 meta_after: list,
44 download_link: str
45) -> dict:
32 return { 46 return {
33 'output_filename': output_filename, 47 'output_filename': output_filename,
34 'mime': mime, 48 'mime': mime,
35 'key': key, 49 'key': key,
50 'secret': secret,
36 'meta': meta, 51 'meta': meta,
37 'meta_after': meta_after, 52 'meta_after': meta_after,
38 'download_link': download_link 53 'download_link': download_link
@@ -65,9 +80,9 @@ def cleanup(parser, filepath, upload_folder):
65 parser, _ = parser_factory.get_parser(parser.output_filename) 80 parser, _ = parser_factory.get_parser(parser.output_filename)
66 meta_after = parser.get_meta() 81 meta_after = parser.get_meta()
67 os.remove(filepath) 82 os.remove(filepath)
68 83 secret = os.urandom(32).hex()
69 key = hash_file(os.path.join(upload_folder, output_filename)) 84 key = hash_file(os.path.join(upload_folder, output_filename), secret)
70 return key, meta_after, output_filename 85 return key, secret, meta_after, output_filename
71 86
72 87
73def get_file_paths(filename, upload_folder): 88def get_file_paths(filename, upload_folder):
@@ -77,7 +92,7 @@ def get_file_paths(filename, upload_folder):
77 return complete_path, filepath 92 return complete_path, filepath
78 93
79 94
80def is_valid_api_download_file(filename, key, upload_folder): 95def is_valid_api_download_file(filename: str, key: str, secret: str, upload_folder: str) -> [str, str]:
81 if filename != secure_filename(filename): 96 if filename != secure_filename(filename):
82 abort(400, message='Insecure filename') 97 abort(400, message='Insecure filename')
83 98
@@ -86,6 +101,6 @@ def is_valid_api_download_file(filename, key, upload_folder):
86 if not os.path.exists(complete_path): 101 if not os.path.exists(complete_path):
87 abort(404, message='File not found') 102 abort(404, message='File not found')
88 103
89 if hmac.compare_digest(hash_file(complete_path), key) is False: 104 if hmac.compare_digest(hash_file(complete_path, secret), key) is False:
90 abort(400, message='The file hash does not match') 105 abort(400, message='The file hash does not match')
91 return complete_path, filepath 106 return complete_path, filepath