| """ |
| Encryption utilities - Fernet envelope encryption for user API keys. |
| Uses ENCRYPTION_KEY (separate from JWT SECRET_KEY) when available. |
| """ |
| import base64 |
|
|
| from cryptography.fernet import Fernet, InvalidToken |
| from cryptography.hazmat.primitives import hashes |
| from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC |
|
|
| from app.core.config import settings |
|
|
|
|
| def _get_encryption_source() -> str: |
| """Get the key source — ENCRYPTION_KEY if set, else SECRET_KEY.""" |
| return settings.ENCRYPTION_KEY or settings.SECRET_KEY |
|
|
|
|
| def _derive_fernet_key() -> bytes: |
| """Derive Fernet key via PBKDF2.""" |
| kdf = PBKDF2HMAC( |
| algorithm=hashes.SHA256(), |
| length=32, |
| salt=b"jobportal-api-key-encryption-v2", |
| iterations=100_000, |
| ) |
| return base64.urlsafe_b64encode(kdf.derive(_get_encryption_source().encode())) |
|
|
|
|
| def encrypt_api_key(plaintext: str) -> str: |
| """Encrypt an API key for database storage.""" |
| return Fernet(_derive_fernet_key()).encrypt(plaintext.encode()).decode() |
|
|
|
|
| def decrypt_api_key(ciphertext: str) -> str | None: |
| """Decrypt an API key. Returns None on failure.""" |
| try: |
| return Fernet(_derive_fernet_key()).decrypt(ciphertext.encode()).decode() |
| except (InvalidToken, Exception): |
| return None |
|
|
|
|
| def get_key_last4(key: str) -> str: |
| """Last 4 chars for display without decryption.""" |
| return key[-4:] if len(key) >= 4 else "****" |
|
|
|
|
| def get_key_prefix(key: str) -> str: |
| """First 4 chars for display.""" |
| return key[:4] if len(key) >= 4 else "" |
|
|
|
|
| def mask_api_key(key: str) -> str: |
| """Masked display: sk-...ab4f""" |
| if len(key) <= 8: |
| return "****" |
| return f"{key[:4]}...{key[-4:]}" |
|
|