| """ |
| Encryption service — column-level encryption for sensitive fields. |
| |
| Used by Phase 2+ for MFA secrets, API keys, and other sensitive data. |
| Uses Django's signing framework with Fernet-style encryption. |
| """ |
| import base64 |
| import hashlib |
| import hmac |
|
|
| from django.conf import settings |
| from django.utils.crypto import get_random_string |
|
|
|
|
| def _get_key() -> bytes: |
| """Derive a 32-byte encryption key from Django SECRET_KEY.""" |
| return hashlib.sha256(settings.SECRET_KEY.encode()).digest() |
|
|
|
|
| def encrypt_field(plaintext: str) -> str: |
| """ |
| Encrypt a string value for safe database storage. |
| |
| Uses HMAC-based encryption with the Django SECRET_KEY. |
| Returns a base64-encoded ciphertext string. |
| """ |
| key = _get_key() |
| nonce = get_random_string(16).encode() |
| mac = hmac.new(key, nonce + plaintext.encode(), hashlib.sha256).digest() |
| payload = nonce + mac + plaintext.encode() |
| return base64.urlsafe_b64encode(payload).decode() |
|
|
|
|
| def decrypt_field(ciphertext: str) -> str: |
| """ |
| Decrypt a previously encrypted field value. |
| |
| Raises ValueError if the ciphertext is tampered with. |
| """ |
| key = _get_key() |
| payload = base64.urlsafe_b64decode(ciphertext.encode()) |
| nonce = payload[:16] |
| stored_mac = payload[16:48] |
| plaintext_bytes = payload[48:] |
| expected_mac = hmac.new(key, nonce + plaintext_bytes, hashlib.sha256).digest() |
| if not hmac.compare_digest(stored_mac, expected_mac): |
| raise ValueError("Decryption failed: ciphertext has been tampered with.") |
| return plaintext_bytes.decode() |
|
|
|
|
| def hash_token(token: str) -> str: |
| """One-way hash for tokens (API keys, device IDs, etc.).""" |
| return hashlib.sha256(token.encode()).hexdigest() |
|
|