use 54 bytes keys and sha512 hashing

This commit is contained in:
imperosol 2025-05-25 15:34:00 +02:00
parent 2dd28a37ab
commit e765fcc96e
4 changed files with 13 additions and 14 deletions

View File

@ -4,14 +4,12 @@ from ninja.security import APIKeyHeader
from apikey.hashers import get_hasher
from apikey.models import ApiClient, ApiKey
_UUID_LENGTH = 36
class ApiKeyAuth(APIKeyHeader):
param_name = "X-APIKey"
def authenticate(self, request: HttpRequest, key: str | None) -> ApiClient | None:
if not key or len(key) != _UUID_LENGTH:
if not key or len(key) != ApiKey.KEY_LENGTH:
return None
hasher = get_hasher()
hashed_key = hasher.encode(key)

View File

@ -1,12 +1,12 @@
import functools
import hashlib
import uuid
import secrets
from django.contrib.auth.hashers import BasePasswordHasher
from django.utils.crypto import constant_time_compare
class Sha256ApiKeyHasher(BasePasswordHasher):
class Sha512ApiKeyHasher(BasePasswordHasher):
"""
An API key hasher using the sha256 algorithm.
@ -15,14 +15,14 @@ class Sha256ApiKeyHasher(BasePasswordHasher):
high entropy, randomly generated API keys.
"""
algorithm = "sha256"
algorithm = "sha512"
def salt(self) -> str:
# No need for a salt on a high entropy key.
return ""
def encode(self, password: str, salt: str = "") -> str:
hashed = hashlib.sha256(password.encode()).hexdigest()
hashed = hashlib.sha512(password.encode()).hexdigest()
return f"{self.algorithm}$${hashed}"
def verify(self, password: str, encoded: str) -> bool:
@ -32,11 +32,12 @@ class Sha256ApiKeyHasher(BasePasswordHasher):
@functools.cache
def get_hasher():
return Sha256ApiKeyHasher()
return Sha512ApiKeyHasher()
def generate_key() -> tuple[str, str]:
"""Generate a [key, hash] couple."""
key = str(uuid.uuid4())
# this will result in key with a length of 72
key = str(secrets.token_urlsafe(54))
hasher = get_hasher()
return key, hasher.encode(key)

View File

@ -88,7 +88,7 @@ class Migration(migrations.Migration):
models.CharField(
db_index=True,
editable=False,
max_length=150,
max_length=136,
verbose_name="hashed key",
),
),

View File

@ -17,9 +17,7 @@ class ApiClient(models.Model):
on_delete=models.CASCADE,
)
groups = models.ManyToManyField(
Group,
verbose_name=_("groups"),
related_name="api_clients",
Group, verbose_name=_("groups"), related_name="api_clients", blank=True
)
client_permissions = models.ManyToManyField(
Permission,
@ -70,11 +68,13 @@ class ApiClient(models.Model):
class ApiKey(models.Model):
PREFIX_LENGTH = 5
KEY_LENGTH = 72
HASHED_KEY_LENGTH = 136
name = models.CharField(_("name"), blank=True, default="")
prefix = models.CharField(_("prefix"), max_length=PREFIX_LENGTH, editable=False)
hashed_key = models.CharField(
_("hashed key"), max_length=150, db_index=True, editable=False
_("hashed key"), max_length=HASHED_KEY_LENGTH, db_index=True, editable=False
)
client = models.ForeignKey(
ApiClient,