diff --git a/requirements.txt b/requirements.txt index dc29c54..9425c02 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,7 +10,7 @@ Werkzeug<1.0.0 aniso8601 argparse itsdangerous -pbkdf2 +argon2-cffi pylibmc python-dateutil pytz diff --git a/scoreboard/models.py b/scoreboard/models.py index 35cdd46..814919d 100644 --- a/scoreboard/models.py +++ b/scoreboard/models.py @@ -22,11 +22,12 @@ import logging import math import os -import pbkdf2 import re import sqlalchemy as sqlalchemy_base import time +from argon2 import PasswordHasher + from sqlalchemy import exc from sqlalchemy import func from sqlalchemy import orm @@ -159,7 +160,7 @@ class User(db.Model): uid = db.Column(db.Integer, primary_key=True) email = db.Column(db.String(120), unique=True, nullable=False, index=True) nick = db.Column(db.String(80), unique=True, nullable=False, index=True) - pwhash = db.Column(db.String(48)) # pbkdf2.crypt == 48 bytes + pwhash = db.Column(db.String(48)) # argon2.PasswordHasher().hash admin = db.Column(db.Boolean, default=False, index=True) team_tid = db.Column(db.Integer, db.ForeignKey('team.tid')) create_ip = db.Column(db.String(45)) # max 45 bytes for IPv6 @@ -168,7 +169,8 @@ class User(db.Model): api_key_updated = db.Column(db.DateTime) def set_password(self, password): - self.pwhash = pbkdf2.crypt(password) + ph = PasswordHasher() + self.pwhash = ph.hash(password) def __repr__(self): return '>' % (self.nick.encode('utf-8'), self.email) @@ -253,7 +255,8 @@ def login_user(cls, email, password): user = cls.query.filter_by(email=email).one() except exc.InvalidRequestError: return None - if pbkdf2.crypt(password, user.pwhash) == user.pwhash: + ph = PasswordHasher() + if ph.verify(user.pwhash, password): if flask.has_request_context(): user.last_login_ip = flask.request.remote_addr db.session.commit() @@ -372,7 +375,7 @@ class Challenge(db.Model): points = db.Column(db.Integer, nullable=False) min_points = db.Column(db.Integer, nullable=True) validator = db.Column(db.String(24), nullable=False, - default='static_pbkdf2') + default='static_argon2') answer_hash = db.Column(db.String(48)) # Protect answers unlocked = db.Column(db.Boolean, default=False) weight = db.Column(db.Integer, nullable=False) # Order for display @@ -493,7 +496,7 @@ def prereq_solved(self, prereq, team): @classmethod def create(cls, name, description, points, answer, unlocked=False, - validator='static_pbkdf2'): + validator='static_argon2'): challenge = cls() challenge.name = name challenge.description = description @@ -652,6 +655,7 @@ class Answer(db.Model): @classmethod def create(cls, challenge, team, answer_text): + ph = PasswordHasher() answer = cls() answer.first_blood = 0 if not challenge.solves: @@ -661,7 +665,7 @@ def create(cls, challenge, team, answer_text): answer.team = team answer.timestamp = datetime.datetime.utcnow() if answer_text: - answer.answer_hash = pbkdf2.crypt(team.name + answer_text) + answer.answer_hash = ph.hash(team.name + answer_text) if flask.request: answer.submit_ip = flask.request.remote_addr db.session.add(answer) diff --git a/scoreboard/tests/base.py b/scoreboard/tests/base.py index 38d0bc3..932fbdd 100644 --- a/scoreboard/tests/base.py +++ b/scoreboard/tests/base.py @@ -21,7 +21,7 @@ import logging import os import os.path -import pbkdf2 +from argon2 import PasswordHasher import time import unittest @@ -107,8 +107,8 @@ class RestTestCase(BaseTestCase): def setUp(self): super(RestTestCase, self).setUp() # Monkey patch pbkdf2 for speed - self._orig_pbkdf2 = pbkdf2.crypt - pbkdf2.crypt = self._pbkdf2_dummy + self._orig_argon2 = PasswordHasher().hash + PasswordHasher().hash = self._argon2_dummy # Setup some special clients self.admin_client = AdminClient( self.app, self.app.response_class) @@ -117,7 +117,7 @@ def setUp(self): def tearDown(self): super(RestTestCase, self).tearDown() - pbkdf2.crypt = self._orig_pbkdf2 + PasswordHasher().hash = self._orig_argon2 def postJSON(self, path, data, client=None): client = client or self.client @@ -139,7 +139,7 @@ def swapClient(self, client): self.client = old_client @staticmethod - def _pbkdf2_dummy(value, *unused_args): + def _argon2_dummy(value, *unused_args): return value diff --git a/scoreboard/validators/__init__.py b/scoreboard/validators/__init__.py index 816411d..7569c03 100644 --- a/scoreboard/validators/__init__.py +++ b/scoreboard/validators/__init__.py @@ -13,14 +13,14 @@ # limitations under the License. -from . import static_pbkdf2 +from . import static_argon2 from . import per_team from . import nonce from . import regex _Validators = { - 'static_pbkdf2': static_pbkdf2.StaticPBKDF2Validator, - 'static_pbkdf2_ci': static_pbkdf2.CaseStaticPBKDF2Validator, + 'static_argon2': static_argon2.StaticArgon2Validator, + 'static_argon2_ci': static_argon2.CaseStaticArgon2Validator, 'per_team': per_team.PerTeamValidator, 'nonce_166432': nonce.Nonce_16_64_Base32_Validator, 'nonce_245632': nonce.Nonce_24_56_Base32_Validator, @@ -31,7 +31,7 @@ def GetDefaultValidator(): - return 'static_pbkdf2' + return 'static_argon2' def GetValidatorForChallenge(challenge): diff --git a/scoreboard/validators/static_pbkdf2.py b/scoreboard/validators/static_argon2.py similarity index 76% rename from scoreboard/validators/static_pbkdf2.py rename to scoreboard/validators/static_argon2.py index 54247c8..00aa836 100644 --- a/scoreboard/validators/static_pbkdf2.py +++ b/scoreboard/validators/static_argon2.py @@ -12,13 +12,13 @@ # See the License for the specific language governing permissions and # limitations under the License. -import pbkdf2 +import argon2 from scoreboard import utils from scoreboard.validators import base -class StaticPBKDF2Validator(base.BaseValidator): +class StaticArgon2Validator(base.BaseValidator): """PBKDF2-based secrets, everyone gets the same flag.""" name = 'Static' @@ -27,14 +27,14 @@ def validate_answer(self, answer, unused_team): if not self.challenge.answer_hash: return False return utils.compare_digest( - pbkdf2.crypt(answer, self.challenge.answer_hash), + argon2.PasswordHasher().hash(answer, self.challenge.answer_hash), self.challenge.answer_hash) def change_answer(self, answer): - self.challenge.answer_hash = pbkdf2.crypt(answer) + self.challenge.answer_hash = argon2.PasswordHasher().hash(answer) -class CaseStaticPBKDF2Validator(StaticPBKDF2Validator): +class CaseStaticArgon2Validator(StaticArgon2Validator): """PBKDF2-based secrets, case insensitive.""" name = 'Static (Case Insensitive)' @@ -42,9 +42,9 @@ class CaseStaticPBKDF2Validator(StaticPBKDF2Validator): def validate_answer(self, answer, team): if not isinstance(answer, str): return False - return super(CaseStaticPBKDF2Validator, self).validate_answer( + return super(CaseStaticArgon2Validator, self).validate_answer( answer.lower(), team) def change_answer(self, answer): - return super(CaseStaticPBKDF2Validator, self).change_answer( + return super(CaseStaticArgon2Validator, self).change_answer( answer.lower())