mirror of
https://github.com/suitenumerique/meet.git
synced 2026-09-25 04:12:31 +00:00
fixup! ⚡️(backend) hash application secrets with SHA-256
This commit is contained in:
+18
-14
@@ -7,27 +7,33 @@ import hashlib
|
||||
import re
|
||||
|
||||
from django.contrib.auth.hashers import check_password
|
||||
from django.utils.crypto import constant_time_compare, get_random_string
|
||||
from django.utils.crypto import constant_time_compare
|
||||
from django.utils.encoding import force_bytes
|
||||
|
||||
CLIENT_SECRET_HASH_PREFIX = "sha256$" # noqa: S105 - format identifier, not a secret
|
||||
|
||||
# Accept only the salted format: sha256$<salt>$<digest>.
|
||||
CLIENT_SECRET_HASH_PATTERN = re.compile(
|
||||
rf"{re.escape(CLIENT_SECRET_HASH_PREFIX)}"
|
||||
r"(?P<salt>[a-zA-Z0-9]{22})\$(?P<digest>[0-9a-f]{64})"
|
||||
CLIENT_SECRET_HASH_ALGORITHM = "sha256"
|
||||
CLIENT_SECRET_HASH_VERSION = "v0"
|
||||
CLIENT_SECRET_HASH_PREFIX = ( # noqa: S105 - format identifier, not a secret
|
||||
f"{CLIENT_SECRET_HASH_ALGORITHM}${CLIENT_SECRET_HASH_VERSION}$"
|
||||
)
|
||||
|
||||
# Accept only the versioned format: sha256$v0$<digest>.
|
||||
CLIENT_SECRET_HASH_PATTERN = re.compile(
|
||||
rf"{re.escape(CLIENT_SECRET_HASH_PREFIX)}(?P<digest>[0-9a-f]{{64}})"
|
||||
)
|
||||
|
||||
|
||||
def _digest(raw_secret):
|
||||
"""Return the hex SHA-256 digest of a raw secret."""
|
||||
return hashlib.sha256(force_bytes(raw_secret)).hexdigest()
|
||||
|
||||
|
||||
def hash_client_secret(raw_secret):
|
||||
"""Hash a machine-generated application secret without key stretching."""
|
||||
salt = get_random_string(22)
|
||||
digest = hashlib.sha256(salt.encode() + force_bytes(raw_secret)).hexdigest()
|
||||
return f"{CLIENT_SECRET_HASH_PREFIX}{salt}${digest}"
|
||||
return f"{CLIENT_SECRET_HASH_PREFIX}{_digest(raw_secret)}"
|
||||
|
||||
|
||||
def verify_client_secret(raw_secret, encoded):
|
||||
"""Verify the salted application format or a legacy Django password hash."""
|
||||
"""Verify the versioned application format or a legacy Django password hash."""
|
||||
if raw_secret is None:
|
||||
return False
|
||||
|
||||
@@ -37,6 +43,4 @@ def verify_client_secret(raw_secret, encoded):
|
||||
if not match:
|
||||
return check_password(raw_secret, encoded)
|
||||
|
||||
salt = match["salt"]
|
||||
digest = hashlib.sha256(salt.encode() + force_bytes(raw_secret)).hexdigest()
|
||||
return constant_time_compare(match["digest"], digest)
|
||||
return constant_time_compare(match["digest"], _digest(raw_secret))
|
||||
|
||||
@@ -23,15 +23,16 @@ def test_application_hash(secret):
|
||||
"""Application hashes verify correctly but are not accepted for user passwords."""
|
||||
encoded = hashers.hash_client_secret(secret)
|
||||
raw = secret.encode() if isinstance(secret, str) else secret
|
||||
algorithm, salt, digest = encoded.split("$")
|
||||
algorithm, version, digest = encoded.split("$")
|
||||
assert algorithm == "sha256"
|
||||
assert len(salt) == 22
|
||||
assert digest == hashlib.sha256(salt.encode() + raw).hexdigest()
|
||||
assert hashers.hash_client_secret(secret) != encoded
|
||||
assert version == "v0"
|
||||
assert digest == hashlib.sha256(raw).hexdigest()
|
||||
assert hashers.hash_client_secret(secret) == encoded
|
||||
assert hashers.verify_client_secret(secret, encoded)
|
||||
assert not hashers.verify_client_secret("wrong", encoded)
|
||||
assert not hashers.verify_client_secret(None, encoded)
|
||||
assert not hashers.verify_client_secret(secret, "sha256$invalid")
|
||||
assert not hashers.verify_client_secret(secret, "sha256$v1$" + digest)
|
||||
assert not check_password(secret, encoded)
|
||||
with pytest.raises(ValueError):
|
||||
identify_hasher(encoded)
|
||||
@@ -63,7 +64,7 @@ def test_token_migrates_legacy_secret_once(algorithm):
|
||||
app.refresh_from_db()
|
||||
migrated = app.client_secret_sha256
|
||||
assert check_password(secret, app.client_secret)
|
||||
assert hashers.CLIENT_SECRET_HASH_PATTERN.fullmatch(migrated)["salt"]
|
||||
assert hashers.CLIENT_SECRET_HASH_PATTERN.fullmatch(migrated)["digest"]
|
||||
assert hashers.verify_client_secret(secret, migrated)
|
||||
with CaptureQueriesContext(connection) as queries:
|
||||
response = client.post(
|
||||
@@ -167,6 +168,10 @@ def test_migration_preserves_concurrent_deletion():
|
||||
"sha256$$" + "a" * 64,
|
||||
"sha256$short$" + "a" * 64,
|
||||
"sha256$" + "b" * 22 + "$" + "g" * 64,
|
||||
"sha256$" + "b" * 22 + "$" + "a" * 64,
|
||||
"sha256$v0$" + "g" * 64,
|
||||
"sha256$v0$" + "a" * 63,
|
||||
"sha256$v1$" + "a" * 64,
|
||||
],
|
||||
)
|
||||
def test_prefixed_plaintext_is_hashed(secret):
|
||||
@@ -176,7 +181,7 @@ def test_prefixed_plaintext_is_hashed(secret):
|
||||
app.refresh_from_db()
|
||||
encoded = app.client_secret_sha256
|
||||
assert encoded != secret
|
||||
assert hashers.CLIENT_SECRET_HASH_PATTERN.fullmatch(encoded)["salt"]
|
||||
assert hashers.CLIENT_SECRET_HASH_PATTERN.fullmatch(encoded)["digest"]
|
||||
assert app.check_client_secret(secret)
|
||||
app.name = "Updated application"
|
||||
app.save()
|
||||
|
||||
Reference in New Issue
Block a user