Commit 3259fc7caed421d812eaf44b7a425062fc607bc1

Authored by Miguel Barão
1 parent 5dae401a
Exists in master and in 1 other branch dev

- Modified login to prevent timing attacks.

- Sanity checks now support tests_right and tests_wrong in questions
(unit testing for questions)
- Application name is defined in __init__.py only, so that changing
name becomes easier.
aprendizations/initdb.py
1 1 #!/usr/bin/env python3
2 2  
3   -# base
  3 +# python standard libraries
4 4 import csv
5 5 import argparse
6 6 import re
7 7 from string import capwords
8 8 from concurrent.futures import ThreadPoolExecutor
9 9  
10   -# installed packages
  10 +# third party libraries
11 11 import bcrypt
12 12 import sqlalchemy as sa
13 13  
14 14 # this project
15   -from aprendizations.models import Base, Student
  15 +from .models import Base, Student
16 16  
17 17  
18 18 # ===========================================================================
... ...
aprendizations/knowledge.py
... ... @@ -5,12 +5,9 @@ from datetime import datetime
5 5 import logging
6 6 import asyncio
7 7  
8   -# libraries
  8 +# third party libraries
9 9 import networkx as nx
10 10  
11   -# this project
12   -# import questions
13   -
14 11 # setup logger for this module
15 12 logger = logging.getLogger(__name__)
16 13  
... ...
aprendizations/learnapp.py
... ... @@ -5,41 +5,25 @@ import logging
5 5 from contextlib import contextmanager # `with` statement in db sessions
6 6 import asyncio
7 7 from datetime import datetime
  8 +from random import random
8 9  
9   -# user installed libraries
  10 +# third party libraries
10 11 import bcrypt
11 12 from sqlalchemy import create_engine, func
12 13 from sqlalchemy.orm import sessionmaker
13 14 import networkx as nx
14 15  
15 16 # this project
16   -from aprendizations.models import Student, Answer, Topic, StudentTopic
17   -from aprendizations.knowledge import StudentKnowledge
18   -from aprendizations.questions import QFactory
19   -from aprendizations.tools import load_yaml
  17 +from .models import Student, Answer, Topic, StudentTopic
  18 +from .knowledge import StudentKnowledge
  19 +from .questions import QFactory
  20 +from .tools import load_yaml
20 21  
21 22 # setup logger for this module
22 23 logger = logging.getLogger(__name__)
23 24  
24 25  
25 26 # ============================================================================
26   -# helper functions
27   -# ============================================================================
28   -async def _bcrypt_hash(a, b):
29   - loop = asyncio.get_running_loop()
30   - return await loop.run_in_executor(None, bcrypt.hashpw,
31   - a.encode('utf-8'), b)
32   -
33   -
34   -async def check_password(try_pw: str, pw: str) -> bool:
35   - return pw == await _bcrypt_hash(try_pw, pw)
36   -
37   -
38   -async def bcrypt_hash_gen(new_pw: str):
39   - return await _bcrypt_hash(new_pw, bcrypt.gensalt())
40   -
41   -
42   -# ============================================================================
43 27 class LearnException(Exception):
44 28 pass
45 29  
... ... @@ -86,38 +70,74 @@ class LearnApp(object):
86 70  
87 71 # ------------------------------------------------------------------------
88 72 def sanity_check_questions(self):
89   - def check_question(qref, q):
90   - logger.debug(f'Generating {qref}...')
91   - try:
92   - q.generate()
93   - except Exception as e:
94   - logger.error(f'Sanity check failed in "{qref}"')
95   - raise e
  73 + logger.info('Starting sanity checks (may take a while...)')
96 74  
97   - logger.info('Starting sanity checks...')
98   - for qref, q in self.factory.items():
99   - check_question(qref, q)
100   - logger.info('Sanity checks passed.')
  75 + errors = 0
  76 + for qref in self.factory:
  77 + logger.debug(f'Checking "{qref}"...')
  78 + q = self.factory[qref].generate()
  79 + try:
  80 + q = self.factory[qref].generate()
  81 + except Exception:
  82 + logger.error(f'Failed to generate "{qref}".')
  83 + errors += 1
  84 + raise
  85 + continue
  86 +
  87 + if 'tests_right' in q:
  88 + for t in q['tests_right']:
  89 + q['answer'] = t
  90 + q.correct()
  91 + if q['grade'] < 1.0:
  92 + logger.error(f'Failed to correct right answer in '
  93 + f'"{qref}".')
  94 + errors += 1
  95 + continue # to next right test
  96 +
  97 + if 'tests_wrong' in q:
  98 + for t in q['tests_wrong']:
  99 + q['answer'] = t
  100 + q.correct()
  101 + if q['grade'] >= 1.0:
  102 + logger.error(f'Failed to correct right answer in '
  103 + f'"{qref}".')
  104 + errors += 1
  105 + continue # to next wrong test
  106 +
  107 + if errors > 0:
  108 + logger.info(f'{errors:>6} errors found.')
  109 + raise
  110 + else:
  111 + logger.info('No errors found.')
101 112  
102 113 # ------------------------------------------------------------------------
103 114 # login
104 115 # ------------------------------------------------------------------------
105   - async def login(self, uid, try_pw):
  116 + async def login(self, uid, pw):
106 117 with self.db_session() as s:
107   - try:
108   - name, password = s.query(Student.name, Student.password) \
109   - .filter_by(id=uid) \
110   - .one()
111   - except Exception:
112   - logger.info(f'User "{uid}" does not exist')
113   - return False
  118 + found = s.query(Student.name, Student.password) \
  119 + .filter_by(id=uid) \
  120 + .one_or_none()
  121 +
  122 + # wait random time to minimize timing attacks
  123 + await asyncio.sleep(random())
  124 +
  125 + loop = asyncio.get_running_loop()
  126 + if found is None:
  127 + logger.info(f'User "{uid}" does not exist')
  128 + await loop.run_in_executor(None, bcrypt.hashpw, b'',
  129 + bcrypt.gensalt()) # just spend time
  130 + return False
114 131  
115   - pw_ok = await check_password(try_pw, password) # async bcrypt
  132 + else:
  133 + name, hashed_pw = found
  134 + pw_ok = await loop.run_in_executor(None, bcrypt.checkpw,
  135 + pw.encode('utf-8'), hashed_pw)
116 136  
117 137 if pw_ok:
118 138 if uid in self.online:
119   - logger.warning(f'User "{uid}" already logged in, overwriting')
120   - counter = self.online[uid]['counter'] # simultaneous logins
  139 + logger.warning(f'User "{uid}" already logged in')
  140 + counter = self.online[uid]['counter']
121 141 else:
122 142 logger.info(f'User "{uid}" logged in')
123 143 counter = 0
... ... @@ -158,7 +178,9 @@ class LearnApp(object):
158 178 if not pw:
159 179 return False
160 180  
161   - pw = await bcrypt_hash_gen(pw)
  181 + loop = asyncio.get_running_loop()
  182 + pw = await loop.run_in_executor(None, bcrypt.hashpw,
  183 + pw.encode('utf-8'), bcrypt.gensalt())
162 184  
163 185 with self.db_session() as s:
164 186 u = s.query(Student).get(uid)
... ...
aprendizations/models.py
1 1  
  2 +# third party libraries
2 3 from sqlalchemy import Column, ForeignKey, Integer, Float, String
3 4 from sqlalchemy.ext.declarative import declarative_base
4 5 from sqlalchemy.orm import relationship
... ...
aprendizations/questions.py
... ... @@ -7,7 +7,7 @@ import logging
7 7 import asyncio
8 8  
9 9 # this project
10   -from aprendizations.tools import run_script
  10 +from .tools import run_script
11 11  
12 12 # setup logger for this module
13 13 logger = logging.getLogger(__name__)
... ...
aprendizations/redirect.py
1 1 #!/usr/bin/env python3
2 2  
  3 +# python standard libraries
  4 +import argparse
  5 +
  6 +# third party libraries
3 7 from tornado.web import RedirectHandler, Application
4 8 from tornado.ioloop import IOLoop
5   -import argparse
6 9  
7 10  
8 11 def main():
... ...
aprendizations/serve.py
... ... @@ -14,16 +14,16 @@ import functools
14 14 import ssl
15 15 import asyncio
16 16  
17   -# user installed libraries
  17 +# third party libraries
18 18 import tornado.ioloop
19 19 import tornado.web
20 20 import tornado.httpserver
21 21 from tornado.escape import to_unicode
22 22  
23 23 # this project
24   -from aprendizations.learnapp import LearnApp
25   -from aprendizations.tools import load_yaml, md_to_html
26   -from aprendizations import APP_NAME
  24 +from .learnapp import LearnApp
  25 +from .tools import load_yaml, md_to_html
  26 +from . import APP_NAME
27 27  
28 28  
29 29 # ----------------------------------------------------------------------------
... ... @@ -97,6 +97,7 @@ class RankingsHandler(BaseHandler):
97 97 uid = self.current_user
98 98 rankings = self.learn.get_rankings(uid)
99 99 self.render('rankings.html',
  100 + appname=APP_NAME,
100 101 uid=uid,
101 102 name=self.learn.get_student_name(uid),
102 103 rankings=rankings)
... ... @@ -107,7 +108,9 @@ class RankingsHandler(BaseHandler):
107 108 # ----------------------------------------------------------------------------
108 109 class LoginHandler(BaseHandler):
109 110 def get(self):
110   - self.render('login.html', error='')
  111 + self.render('login.html',
  112 + appname=APP_NAME,
  113 + error='')
111 114  
112 115 async def post(self):
113 116 uid = self.get_body_argument('uid').lstrip('l')
... ... @@ -121,7 +124,9 @@ class LoginHandler(BaseHandler):
121 124 self.set_secure_cookie('counter', counter)
122 125 self.redirect('/')
123 126 else:
124   - self.render('login.html', error='Número ou senha incorrectos')
  127 + self.render('login.html',
  128 + appname=APP_NAME,
  129 + error='Número ou senha incorrectos')
125 130  
126 131  
127 132 # ----------------------------------------------------------------------------
... ... @@ -168,6 +173,7 @@ class RootHandler(BaseHandler):
168 173 def get(self):
169 174 uid = self.current_user
170 175 self.render('maintopics-table.html',
  176 + appname=APP_NAME,
171 177 uid=uid,
172 178 name=self.learn.get_student_name(uid),
173 179 state=self.learn.get_student_state(uid),
... ... @@ -193,6 +199,7 @@ class TopicHandler(BaseHandler):
193 199 self.redirect('/')
194 200 else:
195 201 self.render('topic.html',
  202 + appname=APP_NAME,
196 203 uid=uid,
197 204 name=self.learn.get_student_name(uid),
198 205 )
... ... @@ -201,9 +208,6 @@ class TopicHandler(BaseHandler):
201 208 # ----------------------------------------------------------------------------
202 209 # Serves files from the /public subdir of the topics.
203 210 # ----------------------------------------------------------------------------
204   -
205   -# FIXME error in many situations... images are not shown...
206   -# seems to happen when the browser sends two GET requests at the same time
207 211 class FileHandler(BaseHandler):
208 212 SUPPORTED_METHODS = ['GET']
209 213  
... ...
aprendizations/templates/login.html
1 1 <!DOCTYPE html>
2 2 <html lang="en">
3 3 <head>
4   - <title>iLearn</title>
  4 + <title>{{appname}}</title>
5 5 <link rel="icon" href="/static/favicon.ico">
6 6  
7 7 <meta charset="utf-8">
... ...
aprendizations/templates/maintopics-table.html
... ... @@ -3,7 +3,7 @@
3 3 <!doctype html>
4 4 <html lang="pt-PT">
5 5 <head>
6   - <title>iLearn</title>
  6 + <title>{{appname}}</title>
7 7 <link rel="icon" href="/static/favicon.ico">
8 8  
9 9 <meta charset="utf-8">
... ...
aprendizations/templates/rankings.html
... ... @@ -3,7 +3,7 @@
3 3 <!doctype html>
4 4 <html lang="pt-PT">
5 5 <head>
6   - <title>iLearn</title>
  6 + <title>{{appname}}</title>
7 7 <link rel="icon" href="/static/favicon.ico">
8 8  
9 9 <meta charset="utf-8">
... ...
aprendizations/templates/topic.html
1 1 <!DOCTYPE html>
2 2 <html>
3 3 <head>
4   - <title>iLearn</title>
  4 + <title>{{appname}}</title>
5 5 <link rel="icon" href="/static/favicon.ico">
6 6  
7 7 <meta charset="utf-8">
... ...
aprendizations/tools.py
... ... @@ -5,7 +5,7 @@ import subprocess
5 5 import logging
6 6 import re
7 7  
8   -# user installed libraries
  8 +# third party libraries
9 9 import yaml
10 10 import mistune
11 11 from pygments import highlight
... ...
package.json
... ... @@ -6,5 +6,6 @@
6 6 "codemirror": "^5.45.0",
7 7 "mathjax": "^2.7.5",
8 8 "mdbootstrap": "^4.7.6"
9   - }
  9 + },
  10 + "private": true
10 11 }
... ...