Coverage for apps / core / encryption.py: 30%
40 statements
« prev ^ index » next coverage.py v7.13.4, created at 2026-02-14 19:13 +0000
« prev ^ index » next coverage.py v7.13.4, created at 2026-02-14 19:13 +0000
1"""Encryption utilities for sensitive data at rest."""
3import base64
4import hashlib
6from django.conf import settings
8try:
9 from cryptography.fernet import Fernet, InvalidToken
11 CRYPTOGRAPHY_AVAILABLE = True
12except ImportError:
13 CRYPTOGRAPHY_AVAILABLE = False
14 Fernet = None
15 InvalidToken = Exception
18def _get_encryption_key() -> bytes:
19 """Derive a Fernet key from Django's SECRET_KEY.
21 Uses SHA-256 to create a consistent 32-byte key from the secret,
22 then base64 encodes it for Fernet compatibility.
23 """
24 secret = settings.SECRET_KEY.encode()
25 key = hashlib.sha256(secret).digest()
26 return base64.urlsafe_b64encode(key)
29def encrypt_value(plaintext: str) -> str:
30 """Encrypt a string value.
32 Args:
33 plaintext: The string to encrypt.
35 Returns:
36 Base64-encoded encrypted string, prefixed with 'enc:'.
37 Returns plaintext if cryptography is not available.
38 """
39 if not plaintext:
40 return ""
42 if not CRYPTOGRAPHY_AVAILABLE:
43 # Fallback: store as-is if cryptography not installed
44 return plaintext
46 key = _get_encryption_key()
47 fernet = Fernet(key)
48 encrypted = fernet.encrypt(plaintext.encode())
49 return f"enc:{encrypted.decode()}"
52def decrypt_value(ciphertext: str) -> str:
53 """Decrypt an encrypted string value.
55 Args:
56 ciphertext: The encrypted string (with 'enc:' prefix).
58 Returns:
59 The decrypted plaintext string.
60 Returns original value if not encrypted or decryption fails.
61 """
62 if not ciphertext:
63 return ""
65 # Check if value is encrypted (has our prefix)
66 if not ciphertext.startswith("enc:"):
67 # Return as-is (legacy unencrypted value)
68 return ciphertext
70 if not CRYPTOGRAPHY_AVAILABLE:
71 # Can't decrypt without cryptography - return empty for safety
72 return ""
74 try:
75 key = _get_encryption_key()
76 fernet = Fernet(key)
77 encrypted_data = ciphertext[4:].encode() # Remove 'enc:' prefix
78 decrypted = fernet.decrypt(encrypted_data)
79 return decrypted.decode()
80 except (InvalidToken, ValueError):
81 # If decryption fails, return empty string for safety
82 return ""
85def is_encrypted(value: str) -> bool:
86 """Check if a value is already encrypted."""
87 return value.startswith("enc:") if value else False