Skip to content
This repository has been archived by the owner on Jan 5, 2019. It is now read-only.

Commit

Permalink
refactor validation process:
Browse files Browse the repository at this point in the history
* Move most validation parts from form to auth backends as django
* check cnonce against replay attacks
  • Loading branch information
jedie committed May 11, 2015
1 parent 6931448 commit f69a488
Show file tree
Hide file tree
Showing 13 changed files with 271 additions and 226 deletions.
3 changes: 3 additions & 0 deletions README.creole
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,9 @@ to run the Live-Server-Tests, install [[https://pypi.python.org/pypi/selenium|se

== changelog

* v0.3.0 - **dev**
** check cnonce against replay attacks
** refactor validation process
* v0.2.0 - 10.05.2015:
** increase default PBKDF2 iteration after test on a Raspberry Pi 1
** more unitests
Expand Down
8 changes: 0 additions & 8 deletions example_project/example_project/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,14 +38,6 @@
ALLOWED_HOSTS = ["*"]


# Disable cache
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.dummy.DummyCache',
}
}


# Application definition

INSTALLED_APPS = (
Expand Down
36 changes: 30 additions & 6 deletions secure_js_login/auth_backends.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,11 @@
from __future__ import unicode_literals

import logging
from django.contrib.auth import get_user_model

from django.contrib.auth.backends import ModelBackend
from django.core.exceptions import ObjectDoesNotExist
from secure_js_login.models import UserProfile

from secure_js_login.utils import crypt

Expand All @@ -30,18 +33,39 @@ def authenticate(self, username=None, **kwargs):
# log.debug("authenticate with SecureLoginAuthBackend")
# log.debug("Check with: %r" % repr(kwargs))

if username is None:
# log.error("No username given.")
try:
server_challenge=kwargs.pop("server_challenge")
except KeyError:
# log.error("No 'server_callenge' given.")
return

if tuple(kwargs.keys()) == ("password",):
# log.debug("normal auth, e.g.: normal django admin login pages was used")
UserModel = get_user_model()
if username is None:
username = kwargs.get(UserModel.USERNAME_FIELD)
# log.error("No username given, use: %r", username)
# else:
# log.debug("Username: %r", username)

try:
user = UserModel._default_manager.get_by_natural_key(username)
except UserModel.DoesNotExist:
# log.error("User %r not exists.", username)
return

user = kwargs.pop("user")
try:
user_profile = UserProfile.objects.get_user_profile(user)
except UserProfile.DoesNotExist as err:
# log.error("Profile for user %r doesn't exists!" % user.username)
return

check = crypt.check_secure_js_login(**kwargs)
# log.debug("Call crypt.check_secure_js_login with: %s", repr(kwargs))
check = crypt.check_secure_js_login(
secure_password=kwargs["password"],
encrypted_part=user_profile.encrypted_part,
server_challenge=server_challenge,
)
if check == True:
# log.debug("Check ok!")
user.previous_login = user.last_login # Save for: secure_js_login.views.display_login_info()
return user
# log.debug("Check failed!")
172 changes: 27 additions & 145 deletions secure_js_login/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,28 +39,16 @@ class WrongUserError(ObjectDoesNotExist):
ERROR_MESSAGE = AuthenticationForm.error_messages["invalid_login"]


class UsernameForm(forms.Form):
class UsernameForm(AuthenticationForm):
"""
similar to django.contrib.auth.forms.AuthenticationForm
Used to get the salt from UserProfile
"""
username = forms.CharField(min_length=1, max_length=254)
password = forms.CharField(required=False)

def __init__(self, request=None, *args, **kwargs):
"""
'request' parameter like auth.forms.AuthenticationForm
"""
self.request = request
self.user = None
super(UsernameForm, self).__init__(request, *args, **kwargs)
self.user_profile = None

super(UsernameForm, self).__init__(*args, **kwargs)

# Set the label for the "username" field.
UserModel = get_user_model()
self.username_field = UserModel._meta.get_field(UserModel.USERNAME_FIELD)
if self.fields['username'].label is None:
self.fields['username'].label = capfirst(self.username_field.verbose_name)

def _raise_validate_error(self, msg):
# log.debug("%s error: %s", self.__class__.__name__, msg)
if not settings.DEBUG:
Expand All @@ -78,43 +66,23 @@ def clean(self):

def clean_username(self):
# log.debug("%s.clean_username()", self.__class__.__name__)

username = self.cleaned_data['username']
try:
self.user = get_user_model().objects.get(username=username)
self.user_cache = get_user_model().objects.get(username=username)
except ObjectDoesNotExist as err:
raise self._raise_validate_error("User %r doesn't exists!" % username)

try:
self.user_profile = UserProfile.objects.get_user_profile(self.user)
self.user_profile = UserProfile.objects.get_user_profile(self.user_cache)
except ObjectDoesNotExist as err:
raise self._raise_validate_error(
"Profile for user %r doesn't exists!" % self.user.username
"Profile for user %r doesn't exists!" % self.user_cache.username
)
return username


# PBKDF2_BYTE_LENGTH*2 + "$" + PBKDF2_BYTE_LENGTH + "$" + CLIENT_NONCE_LENGTH
# or:
# PBKDF2_HEX_LENGTH + "$" + PBKDF2_HALF_HEX_LENGTH + "$" + CLIENT_NONCE_LENGTH
CLIENT_DATA_LEN = crypt.PBKDF2_HEX_LENGTH + crypt.PBKDF2_HALF_HEX_LENGTH + app_settings.CLIENT_NONCE_LENGTH + 2


class HashValidator(object):
def __init__(self, name, length):
self.name = name
self.length = length
self.regexp = re.compile(r"^[a-f0-9]{%i}$" % length)

def validate(self, value):
if len(value)!=self.length:
raise ValidationError("%s length error" % self.name)

if not self.regexp.match(value):
raise ValidationError("%s regexp error" % self.name)

PBKDF2_HASH_Validator = HashValidator(name="pbkdf2_hash", length=crypt.PBKDF2_HEX_LENGTH)
SECOND_PBKDF2_PART_Validator = HashValidator(name="second_pbkdf2_part", length=crypt.PBKDF2_HALF_HEX_LENGTH)
CLIENT_NONCE_HEX_Validator = HashValidator(name="cnonce", length=app_settings.CLIENT_NONCE_LENGTH)


class SecureLoginForm(UsernameForm):
Expand All @@ -123,116 +91,30 @@ class SecureLoginForm(UsernameForm):
send pbkdf2_hash1, second-pbkdf2-part and cnonce to the server
"""
password=forms.CharField(
min_length=CLIENT_DATA_LEN,
max_length=CLIENT_DATA_LEN,
min_length=crypt.CLIENT_DATA_LEN,
max_length=crypt.CLIENT_DATA_LEN,
widget=forms.PasswordInput
)

def clean(self):
try:
username = self.cleaned_data['username']
except KeyError as err:
# e.g.: username field validator has cleaned the value
# log.debug("No 'username' - Form errors: %r", self.errors)
return

# self.user set in UsernameForm.clean_username()
assert isinstance(self.user, get_user_model())

if not self.user.is_active==True:
raise self._raise_validate_error("User %r is not active!" % self.user)
username = self.cleaned_data.get('username')
password = self.cleaned_data.get('password')

assert self.request is not None
try:
pbkdf2_hash, second_pbkdf2_part, cnonce = self.cleaned_data['password']
except KeyError as err:
# e.g.: password field validator has cleaned the value
log.debug("No 'password' - Form errors: %r", self.errors)
return
except TypeError as err:
self._raise_validate_error("Wrong password data: %s" % err)

self.cleaned_data["password"] = "" # Don't send password back

server_challenge = self.request.old_server_challenge
if not server_challenge:
self._raise_validate_error("request.old_server_challenge not set.")
server_challenge = self.request.old_server_challenge
except AttributeError as err:
self._raise_validate_error("request.old_server_challenge not set: %s" % err)
# log.debug("Challenge from session: %r", server_challenge)

# Simple check if 'nonce' from client used in the past.
# Limitations:
# - Works only when run in a long-term server process, so not in CGI ;)
# - dict vary if more than one server process runs (one dict in one process)
if cnonce in CNONCE_CACHE:
self._raise_validate_error("Client-nonce %r used in the past!" % cnonce)

CNONCE_CACHE[cnonce] = None

self.user.previous_login = self.user.last_login # Save for: secure_js_login.views.display_login_info()

kwargs = {
"username":username,
"user": self.user,
"encrypted_part": self.user_profile.encrypted_part,
"server_challenge":server_challenge,
"pbkdf2_hash":pbkdf2_hash,
"second_pbkdf2_part":second_pbkdf2_part,
"cnonce":cnonce,
}
# log.info("Call authenticate with: %s", repr(kwargs))

try:
user = authenticate(**kwargs)
except crypt.CryptError as err:
self._raise_validate_error("crypt.check_secure_js_login error: %s" % err)

if not user:
self._raise_validate_error("crypt.check_secure_js_login failed!")

return user

def clean_password(self):
password = self.cleaned_data["password"]
if password.count("$") != 2:
log.error(
"No two $ (found: %i) in password found in: %r" % (
password.count("$"),password
)
)
return

pbkdf2_hash, second_pbkdf2_part, cnonce = password.split("$")

try:
PBKDF2_HASH_Validator.validate(pbkdf2_hash)
SECOND_PBKDF2_PART_Validator.validate(second_pbkdf2_part)
CLIENT_NONCE_HEX_Validator.validate(cnonce)
except ValidationError as err:
log.error("password value error: %s" % err)
return

# log.debug("Password data is valid.")
return (pbkdf2_hash, second_pbkdf2_part, cnonce)

def get_user(self):
# API for auth.views.login()
return self.user


# class JSPasswordChangeForm(Sha1BaseForm):
# """
# Form for changing the password with Client side JS encryption.
#
# inherited form Sha1BaseForm() this form fields:
# pbkdf2_hash
# second_pbkdf2_part
# cnonce
# for pre-verification with old password "JS-SHA1" values
# """
# # new password as salted SHA1 hash:
# salt = forms.CharField(min_length=crypt.SALT_LEN, max_length=crypt.SALT_LEN) # length see: hashers.SHA1PasswordHasher() and django.utils.crypto.get_random_string()
# sha1hash = forms.CharField(min_length=crypt.CLIENT_DATA_LEN, max_length=crypt.CLIENT_DATA_LEN)
# def clean_salt(self):
# return self._validate_filled_sha1_by_key("salt")
# def clean_sha1(self):
# return self._validate_sha1_by_key("sha1hash")

if username and password:
self.user_cache = authenticate(username=username, password=password, server_challenge=server_challenge)
# log.debug("Get %r back from authenticate()", self.user_cache)
if self.user_cache is None:
self._raise_validate_error("authenticate() check failed.")
else:
# log.debug("confirm_login_allowed()")
self.confirm_login_allowed(self.user_cache)
# log.debug("confirm_login_allowed() - OK")

return self.cleaned_data
4 changes: 3 additions & 1 deletion secure_js_login/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,9 @@ def set_secure_login_data(self, password):
Create a XOR encrypted PBKDF2 salted checksum from a plaintext password.
"""
init_pbkdf2_salt, encrypted_part = crypt.salt_hash_from_plaintext(password)
# log.debug("set init_pbkdf2_salt=%r and encrypted_part=%r", init_pbkdf2_salt, encrypted_part)
# log.debug("set_secure_login_data(%r): set init_pbkdf2_salt=%r and encrypted_part=%r",
# password, init_pbkdf2_salt, encrypted_part
# )
self.init_pbkdf2_salt = init_pbkdf2_salt
self.encrypted_part = encrypted_part
# log.info("Secure login data saved for user '%s'.", self.user)
Expand Down
4 changes: 4 additions & 0 deletions secure_js_login/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,7 @@

# use 'User.set_password' monkey-patch in models.py for create password hashes
AUTO_CREATE_PASSWORD_HASH = getattr(settings, "AUTO_CREATE_PASSWORD_HASH", False)

# Used cache backend for:
# * used cnonce
CACHE_NAME = getattr(settings, "CACHE_NAME", "default")
Loading

0 comments on commit f69a488

Please sign in to comment.