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

1"""Encryption utilities for sensitive data at rest.""" 

2 

3import base64 

4import hashlib 

5 

6from django.conf import settings 

7 

8try: 

9 from cryptography.fernet import Fernet, InvalidToken 

10 

11 CRYPTOGRAPHY_AVAILABLE = True 

12except ImportError: 

13 CRYPTOGRAPHY_AVAILABLE = False 

14 Fernet = None 

15 InvalidToken = Exception 

16 

17 

18def _get_encryption_key() -> bytes: 

19 """Derive a Fernet key from Django's SECRET_KEY. 

20 

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) 

27 

28 

29def encrypt_value(plaintext: str) -> str: 

30 """Encrypt a string value. 

31 

32 Args: 

33 plaintext: The string to encrypt. 

34 

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 "" 

41 

42 if not CRYPTOGRAPHY_AVAILABLE: 

43 # Fallback: store as-is if cryptography not installed 

44 return plaintext 

45 

46 key = _get_encryption_key() 

47 fernet = Fernet(key) 

48 encrypted = fernet.encrypt(plaintext.encode()) 

49 return f"enc:{encrypted.decode()}" 

50 

51 

52def decrypt_value(ciphertext: str) -> str: 

53 """Decrypt an encrypted string value. 

54 

55 Args: 

56 ciphertext: The encrypted string (with 'enc:' prefix). 

57 

58 Returns: 

59 The decrypted plaintext string. 

60 Returns original value if not encrypted or decryption fails. 

61 """ 

62 if not ciphertext: 

63 return "" 

64 

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 

69 

70 if not CRYPTOGRAPHY_AVAILABLE: 

71 # Can't decrypt without cryptography - return empty for safety 

72 return "" 

73 

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 "" 

83 

84 

85def is_encrypted(value: str) -> bool: 

86 """Check if a value is already encrypted.""" 

87 return value.startswith("enc:") if value else False 

← Back to Dashboard