113 lines
3.2 KiB
Python
113 lines
3.2 KiB
Python
"""Encryption utilities for sensitive data."""
|
|
import base64
|
|
import os
|
|
from cryptography.fernet import Fernet, InvalidToken
|
|
from cryptography.hazmat.primitives import hashes
|
|
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
|
|
|
|
|
|
# Encryption key derivation settings
|
|
SALT_LENGTH = 16
|
|
KEY_ITERATIONS = 480000
|
|
|
|
|
|
def _get_fernet_key(secret_key: str, salt: bytes = None) -> bytes:
|
|
"""
|
|
Derive a Fernet key from a secret key using PBKDF2.
|
|
|
|
Args:
|
|
secret_key: The master secret key
|
|
salt: Optional salt bytes (will be generated if not provided)
|
|
|
|
Returns:
|
|
32-byte key suitable for Fernet encryption
|
|
"""
|
|
if salt is None:
|
|
salt = os.urandom(SALT_LENGTH)
|
|
|
|
kdf = PBKDF2HMAC(
|
|
algorithm=hashes.SHA256(),
|
|
length=32,
|
|
salt=salt,
|
|
iterations=KEY_ITERATIONS,
|
|
)
|
|
key = base64.urlsafe_b64encode(kdf.derive(secret_key.encode()))
|
|
return key
|
|
|
|
|
|
def encrypt(plaintext: str, secret_key: str = None) -> str:
|
|
"""
|
|
Encrypt a string using Fernet symmetric encryption.
|
|
|
|
Args:
|
|
plaintext: The string to encrypt
|
|
secret_key: The encryption key (uses app config if not provided)
|
|
|
|
Returns:
|
|
Base64-encoded encrypted string with salt prepended
|
|
"""
|
|
from flask import current_app
|
|
|
|
if not plaintext:
|
|
return ""
|
|
|
|
# Get secret key from app config or use provided key
|
|
if secret_key is None:
|
|
secret_key = current_app.config.get("ENCRYPTION_KEY", "")
|
|
|
|
if not secret_key:
|
|
raise ValueError("Encryption key not configured")
|
|
|
|
# Generate a random salt for this encryption
|
|
salt = os.urandom(SALT_LENGTH)
|
|
fernet_key = _get_fernet_key(secret_key, salt)
|
|
fernet = Fernet(fernet_key)
|
|
|
|
# Encrypt the plaintext
|
|
encrypted_bytes = fernet.encrypt(plaintext.encode())
|
|
|
|
# Combine salt + encrypted data and base64 encode
|
|
combined = salt + encrypted_bytes
|
|
return base64.urlsafe_b64encode(combined).decode()
|
|
|
|
|
|
def decrypt(encrypted_data: str, secret_key: str = None) -> str:
|
|
"""
|
|
Decrypt a string that was encrypted with the encrypt function.
|
|
|
|
Args:
|
|
encrypted_data: Base64-encoded encrypted string with salt prepended
|
|
secret_key: The encryption key (uses app config if not provided)
|
|
|
|
Returns:
|
|
The original plaintext string
|
|
"""
|
|
from flask import current_app
|
|
|
|
if not encrypted_data:
|
|
return ""
|
|
|
|
# Get secret key from app config or use provided key
|
|
if secret_key is None:
|
|
secret_key = current_app.config.get("ENCRYPTION_KEY", "")
|
|
|
|
if not secret_key:
|
|
raise ValueError("Encryption key not configured")
|
|
|
|
try:
|
|
# Decode from base64
|
|
combined = base64.urlsafe_b64decode(encrypted_data.encode())
|
|
|
|
# Extract salt and encrypted data
|
|
salt = combined[:SALT_LENGTH]
|
|
encrypted_bytes = combined[SALT_LENGTH:]
|
|
|
|
# Derive the key and decrypt
|
|
fernet_key = _get_fernet_key(secret_key, salt)
|
|
fernet = Fernet(fernet_key)
|
|
plaintext = fernet.decrypt(encrypted_bytes)
|
|
|
|
return plaintext.decode()
|
|
except (InvalidToken, ValueError):
|
|
raise ValueError("Failed to decrypt data - invalid key or corrupted data")
|