53 lines
1.6 KiB
Python
53 lines
1.6 KiB
Python
"""Password hashing, API key generation, and Fernet encryption for credentials."""
|
|
from __future__ import annotations
|
|
import secrets
|
|
from passlib.context import CryptContext
|
|
from cryptography.fernet import Fernet
|
|
from ..config import settings
|
|
|
|
_pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
|
|
|
|
|
def hash_password(plain: str) -> str:
|
|
return _pwd_context.hash(plain)
|
|
|
|
|
|
def verify_password(plain: str, hashed: str) -> bool:
|
|
return _pwd_context.verify(plain, hashed)
|
|
|
|
|
|
def generate_api_key() -> tuple[str, str, str]:
|
|
"""Returns (full_key, key_hash, key_prefix).
|
|
|
|
full_key is shown to the user exactly once.
|
|
key_hash is stored in the DB.
|
|
key_prefix (first 8 chars) is stored for display.
|
|
"""
|
|
raw = secrets.token_hex(32)
|
|
full_key = f"mtom_{raw}"
|
|
key_hash = _pwd_context.hash(full_key)
|
|
key_prefix = full_key[:12]
|
|
return full_key, key_hash, key_prefix
|
|
|
|
|
|
def verify_api_key(plain: str, hashed: str) -> bool:
|
|
return _pwd_context.verify(plain, hashed)
|
|
|
|
|
|
def _fernet() -> Fernet:
|
|
key = settings.encryption_key
|
|
if key == "change-me":
|
|
raise RuntimeError("ENCRYPTION_KEY is not set. Generate one with: python -c \"from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())\"")
|
|
try:
|
|
return Fernet(key.encode())
|
|
except Exception:
|
|
raise RuntimeError("ENCRYPTION_KEY is not a valid Fernet key.")
|
|
|
|
|
|
def encrypt_credential(plain: str) -> str:
|
|
return _fernet().encrypt(plain.encode()).decode()
|
|
|
|
|
|
def decrypt_credential(enc: str) -> str:
|
|
return _fernet().decrypt(enc.encode()).decode()
|