58 lines
1.7 KiB
Python
58 lines
1.7 KiB
Python
import secrets
|
|
from typing import Optional
|
|
|
|
import bcrypt
|
|
from cryptography.fernet import Fernet, InvalidToken
|
|
|
|
from app.core.config import settings
|
|
|
|
|
|
def generate_api_key(prefix_len: int = 8, token_bytes: int = 32) -> str:
|
|
prefix = secrets.token_urlsafe(prefix_len)[:prefix_len]
|
|
token = secrets.token_urlsafe(token_bytes)
|
|
return f"{prefix}.{token}"
|
|
|
|
|
|
def get_prefix(api_key: str) -> str:
|
|
return api_key.split(".", 1)[0]
|
|
|
|
|
|
def hash_api_key(api_key: str) -> str:
|
|
hashed = bcrypt.hashpw(api_key.encode("utf-8"), bcrypt.gensalt())
|
|
return hashed.decode("utf-8")
|
|
|
|
|
|
def verify_api_key(api_key: str, api_key_hash: str) -> bool:
|
|
return bcrypt.checkpw(api_key.encode("utf-8"), api_key_hash.encode("utf-8"))
|
|
|
|
|
|
def _get_fernet() -> Fernet:
|
|
if not settings.API_KEY_ENC_SECRET:
|
|
raise ValueError("API_KEY_ENC_SECRET is not configured")
|
|
# Expect a base64 urlsafe key; if plaintext is provided, derive a Fernet key
|
|
secret = settings.API_KEY_ENC_SECRET
|
|
# If length is 44 and endswith '=', assume already a Fernet key
|
|
if len(secret) == 44 and secret.endswith("="):
|
|
key = secret.encode("utf-8")
|
|
else:
|
|
# Derive deterministic Fernet key from secret (simple approach)
|
|
import base64
|
|
import hashlib
|
|
|
|
digest = hashlib.sha256(secret.encode("utf-8")).digest()
|
|
key = base64.urlsafe_b64encode(digest)
|
|
return Fernet(key)
|
|
|
|
|
|
def encrypt_api_key(plain_key: str) -> str:
|
|
f = _get_fernet()
|
|
return f.encrypt(plain_key.encode("utf-8")).decode("utf-8")
|
|
|
|
|
|
def decrypt_api_key(cipher_text: str) -> Optional[str]:
|
|
try:
|
|
f = _get_fernet()
|
|
return f.decrypt(cipher_text.encode("utf-8")).decode("utf-8")
|
|
except (InvalidToken, ValueError):
|
|
return None
|