Some checks failed
Backend Tests / pytest (Python 3.12) (push) Failing after 46s
Critical security and robustness fixes from a code audit: - WS terminal/VNC now require a JWT (?token=), validate the session, vms.console permission and VM ownership before bridging to Proxmox (previously the LXC terminal gave unauthenticated root shell access). Frontend ConsolePage passes the token in both WS URLs. - Fix SSH fallback for `pvecm delnode`: SSHClient now supports password auth (it previously crashed with TypeError); ProxmoxClient stores the password for SSH fallbacks and the host port suffix is stripped. - Add production startup warnings for default ADMIN_PASSWORD and wildcard CORS_ORIGINS. - dashboard: use tz-aware utcnow() instead of naive datetime.utcnow(). - crypto: narrow decrypt_value except to InvalidToken and log failures. - ssh_client: guard the shared connection cache with a lock; make unknown-host-key rejection opt-in via SSH_REJECT_UNKNOWN_HOSTS. - logging: send loguru console output to stderr instead of stdout. - Sync version fallback to 1.5.1; remove one-off refactor scripts. - Add CODE_REVIEW.md audit report. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
92 lines
2.8 KiB
Python
92 lines
2.8 KiB
Python
"""
|
|
Field-level encryption for sensitive database columns.
|
|
|
|
Uses Fernet symmetric encryption (AES-128-CBC + HMAC-SHA256).
|
|
Configure via FERNET_KEY environment variable.
|
|
|
|
Key generation:
|
|
python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"
|
|
"""
|
|
|
|
import logging
|
|
from cryptography.fernet import Fernet, InvalidToken
|
|
from sqlalchemy import TypeDecorator, String
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
# Warn once if FERNET_KEY is not configured
|
|
_warned_no_key = False
|
|
|
|
|
|
def _get_fernet():
|
|
global _warned_no_key
|
|
from .config import settings
|
|
key = settings.FERNET_KEY
|
|
if not key:
|
|
if not _warned_no_key:
|
|
logger.warning(
|
|
"FERNET_KEY is not set — sensitive fields (passwords) are stored "
|
|
"as plaintext. Set FERNET_KEY in your environment for production."
|
|
)
|
|
_warned_no_key = True
|
|
return None
|
|
return Fernet(key.encode() if isinstance(key, str) else key)
|
|
|
|
|
|
def encrypt_value(value: str) -> str:
|
|
"""Encrypt a string value. Returns plaintext if FERNET_KEY is not configured."""
|
|
if not value:
|
|
return value
|
|
f = _get_fernet()
|
|
if f is None:
|
|
return value
|
|
return f.encrypt(value.encode()).decode()
|
|
|
|
|
|
def decrypt_value(value: str) -> str:
|
|
"""Decrypt a Fernet-encrypted string.
|
|
|
|
Falls back to returning the value as-is if decryption fails
|
|
(handles legacy plaintext rows during migration).
|
|
"""
|
|
if not value:
|
|
return value
|
|
f = _get_fernet()
|
|
if f is None:
|
|
return value
|
|
try:
|
|
return f.decrypt(value.encode()).decode()
|
|
except InvalidToken:
|
|
# Value is likely plaintext (pre-encryption legacy row) or was
|
|
# encrypted with a different FERNET_KEY. Return as-is for backward
|
|
# compatibility, but warn so key-rotation mistakes are visible.
|
|
logger.warning(
|
|
"Fernet decryption failed (InvalidToken). Value returned as-is — "
|
|
"this is expected for legacy plaintext rows, but check FERNET_KEY "
|
|
"if previously-encrypted data is now unreadable."
|
|
)
|
|
return value
|
|
|
|
|
|
class EncryptedString(TypeDecorator):
|
|
"""SQLAlchemy column type that transparently encrypts/decrypts values via Fernet.
|
|
|
|
Usage::
|
|
|
|
from .crypto import EncryptedString
|
|
password = Column(EncryptedString(255), nullable=True)
|
|
|
|
The underlying storage type is String, so no schema change is required.
|
|
"""
|
|
|
|
impl = String
|
|
cache_ok = True
|
|
|
|
def process_bind_param(self, value, dialect):
|
|
"""Called when writing to the database."""
|
|
return encrypt_value(value) if value else value
|
|
|
|
def process_result_value(self, value, dialect):
|
|
"""Called when reading from the database."""
|
|
return decrypt_value(value) if value else value
|