import string import secrets import hashlib import base64 from django.db import models from django.conf import settings def _generate_api_key(length: int = 50) -> str: characters = string.ascii_letters + string.digits api_key = "".join(secrets.choice(characters) for _ in range(length)) return "labs-" + api_key def _encrypt_api_key(api_key: str) -> str: # An empty salt is used because we need to look tokens up solely by # their hashed value. Additionally, tokens are always cryptographically # pseudo-random and unique, therefore salting provides no # additional security. algorithm = "pbkdf2_sha512" api_key = api_key.encode("utf-8") salt = b"" iterations = 20_000 hash = hashlib.pbkdf2_hmac("sha512", api_key, salt, iterations) hash = base64.b64encode(hash).decode("ascii").strip() return "%s$%d$%s" % (algorithm, iterations, hash) class APIKeyManager(models.Manager): def create_api_key(self, user: object, description: str) -> str: raw_api_key = _generate_api_key() hashed_api_key = _encrypt_api_key(raw_api_key) self.create( user=user, description=description, api_key_display=raw_api_key[:7] + "..." + raw_api_key[-3:], hashed_api_key=hashed_api_key, ) return raw_api_key def get_by_raw_key(self, raw_api_key: str) -> "APIKey": hashed_api_key = _encrypt_api_key(raw_api_key) return self.select_related("user").get(hashed_api_key=hashed_api_key) class APIKey(models.Model): description = models.CharField(max_length=100, null=True, blank=True) api_key_display = models.CharField(max_length=255) hashed_api_key = models.CharField(max_length=255, unique=True) user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE) created_at = models.DateTimeField(auto_now_add=True) objects = APIKeyManager() class Meta: db_table = "api_keys" def __str__(self): return self.api_key_display