From f93a4ac98b6e6e6d61d3430351712ee0ad6c4800 Mon Sep 17 00:00:00 2001
From: imperosol <thgirod@hotmail.com>
Date: Sun, 25 May 2025 15:34:00 +0200
Subject: [PATCH] use 54 bytes keys and sha512 hashing

---
 apikey/auth.py                    |  4 +---
 apikey/hashers.py                 | 13 +++++++------
 apikey/migrations/0001_initial.py |  2 +-
 apikey/models.py                  |  8 ++++----
 4 files changed, 13 insertions(+), 14 deletions(-)

diff --git a/apikey/auth.py b/apikey/auth.py
index 3d45a4eb..00212c1b 100644
--- a/apikey/auth.py
+++ b/apikey/auth.py
@@ -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)
diff --git a/apikey/hashers.py b/apikey/hashers.py
index 3a623177..95c16673 100644
--- a/apikey/hashers.py
+++ b/apikey/hashers.py
@@ -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)
diff --git a/apikey/migrations/0001_initial.py b/apikey/migrations/0001_initial.py
index cda9e9f6..b416b749 100644
--- a/apikey/migrations/0001_initial.py
+++ b/apikey/migrations/0001_initial.py
@@ -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",
                     ),
                 ),
diff --git a/apikey/models.py b/apikey/models.py
index b172bb7a..36e20287 100644
--- a/apikey/models.py
+++ b/apikey/models.py
@@ -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,