Skip to content
This repository has been archived by the owner on Jun 27, 2023. It is now read-only.

Updated hashing #298

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ Werkzeug<1.0.0
aniso8601
argparse
itsdangerous
pbkdf2
argon2-cffi
pylibmc
python-dateutil
pytz
Expand Down
18 changes: 11 additions & 7 deletions scoreboard/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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 '<User: %s <%s>>' % (self.nick.encode('utf-8'), self.email)
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand All @@ -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)
Expand Down
10 changes: 5 additions & 5 deletions scoreboard/tests/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
import logging
import os
import os.path
import pbkdf2
from argon2 import PasswordHasher
import time
import unittest

Expand Down Expand Up @@ -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)
Expand All @@ -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
Expand All @@ -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


Expand Down
8 changes: 4 additions & 4 deletions scoreboard/validators/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -31,7 +31,7 @@


def GetDefaultValidator():
return 'static_pbkdf2'
return 'static_argon2'


def GetValidatorForChallenge(challenge):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -27,24 +27,24 @@ 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)'

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())