Skip to content

Commit

Permalink
feat: add SHA256 password hashers (#14)
Browse files Browse the repository at this point in the history
* refactor: reorganize tests

* feat: add sha256 hasher

* style: fix ruff config

* 1.0.0
  • Loading branch information
lucasrcezimbra authored May 9, 2024
1 parent 2a74a1c commit 4ee912c
Show file tree
Hide file tree
Showing 5 changed files with 108 additions and 7 deletions.
4 changes: 2 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@ fmt format: # Run code formatters
black .

test: # Run tests
pytest --ds=sample_project.settings -v sample_project ninja_apikey/tests.py
pytest --ds=sample_project.settings -v sample_project ninja_apikey/tests

cov test-cov: # Run tests with coverage
pytest --ds=sample_project.settings --cov=ninja_apikey --cov-report=term-missing --cov-report=xml -v sample_project ninja_apikey/tests.py
pytest --ds=sample_project.settings --cov=ninja_apikey --cov-report=term-missing --cov-report=xml -v sample_project ninja_apikey/tests

build: # Build project
make install
Expand Down
48 changes: 48 additions & 0 deletions ninja_apikey/hashers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import hashlib

from django.contrib.auth.hashers import BasePasswordHasher, mask_hash, must_update_salt
from django.utils.crypto import constant_time_compare
from django.utils.translation import gettext_noop as _


class SHA256PasswordHasher(BasePasswordHasher):
"""
This was based on the Django's SHA1PasswordHasher from Django 5.0.6:
https://github.com/django/django/blob/5.0.6/django/contrib/auth/hashers.py#L645
"""

algorithm = "sha256"

def encode(self, password, salt):
self._check_encode_args(password, salt)
hash = hashlib.sha256((salt + password).encode()).hexdigest()
return "%s$%s$%s" % (self.algorithm, salt, hash)

def decode(self, encoded):
algorithm, salt, hash = encoded.split("$", 2)
assert algorithm == self.algorithm
return {
"algorithm": algorithm,
"hash": hash,
"salt": salt,
}

def verify(self, password, encoded):
decoded = self.decode(encoded)
encoded_2 = self.encode(password, decoded["salt"])
return constant_time_compare(encoded, encoded_2)

def safe_summary(self, encoded):
decoded = self.decode(encoded)
return {
_("algorithm"): decoded["algorithm"],
_("salt"): mask_hash(decoded["salt"], show=2),
_("hash"): mask_hash(decoded["hash"]),
}

def must_update(self, encoded):
decoded = self.decode(encoded)
return must_update_salt(decoded["salt"], self.salt_entropy)

def harden_runtime(self, password, encoded):
pass
49 changes: 49 additions & 0 deletions ninja_apikey/tests/test_hashers_sha256.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
from django.contrib.auth.hashers import (
check_password,
identify_hasher,
is_password_usable,
make_password,
)
from django.test.utils import override_settings

from ninja_apikey.hashers import SHA256PasswordHasher


@override_settings(PASSWORD_HASHERS=["ninja_apikey.hashers.SHA256PasswordHasher"])
def test_sha256():
"""
Based on https://github.com/django/django/blob/5.0.6/tests/auth_tests/test_hashers.py#L110
"""
encoded = make_password("lètmein", "seasalt", hasher="sha256")
assert (
encoded
== "sha256$seasalt$e0327e0c88846ec7f85601380e86c72a5242e3455a1ae0f736f349858f126eb9"
)
assert is_password_usable(encoded) is True
assert check_password("lètmein", encoded) is True
assert check_password("lètmeinz", encoded) is False
assert identify_hasher(encoded).algorithm == "sha256"


@override_settings(PASSWORD_HASHERS=["ninja_apikey.hashers.SHA256PasswordHasher"])
def test_blank_password():
"""
Based on https://github.com/django/django/blob/5.0.6/tests/auth_tests/test_hashers.py#L110
"""
blank_encoded = make_password("", "seasalt", "sha256")
assert blank_encoded.startswith("sha256$")
assert is_password_usable(blank_encoded) is True
assert check_password("", blank_encoded) is True
assert check_password(" ", blank_encoded) is False


@override_settings(PASSWORD_HASHERS=["ninja_apikey.hashers.SHA256PasswordHasher"])
def test_entropy_check():
"""
Based on https://github.com/django/django/blob/5.0.6/tests/auth_tests/test_hashers.py#L110
"""
hasher = SHA256PasswordHasher()
encoded_weak_salt = make_password("lètmein", "iodizedsalt")
encoded_strong_salt = make_password("lètmein", hasher.salt())
assert hasher.must_update(encoded_weak_salt) is True
assert hasher.must_update(encoded_strong_salt) is False
6 changes: 3 additions & 3 deletions ninja_apikey/tests.py → ninja_apikey/tests/test_security.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@
from django.utils import timezone
from django.utils.crypto import get_random_string

from .admin import APIKeyAdmin
from .models import APIKey
from .security import check_apikey, generate_key
from ninja_apikey.admin import APIKeyAdmin
from ninja_apikey.models import APIKey
from ninja_apikey.security import check_apikey, generate_key


def test_apikey_validation():
Expand Down
8 changes: 6 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@ requires = ["flit_core >=3.2,<4"]
build-backend = "flit_core.buildapi"



[project]
name = "ninja-api-key"
description = "Django Ninja API Key Authentication"
version = "0.2.2"
version = "1.0.0"
authors = [
{name = "Lucas Rangel Cezimbra", email="[email protected]"},
{name = "Maximilian Wassink", email="[email protected]"},
Expand Down Expand Up @@ -55,11 +56,14 @@ test = [
]



[tool.flit.module]
name = "ninja_apikey"


[tool.ruff]
line-length = 88

[tool.ruff.lint]
select = ["E", "F", "I"]
ignore = ["E501"]
line-length = 88

0 comments on commit 4ee912c

Please sign in to comment.