Phase 3.1: Instance password AES-256-GCM encryption Phase 3.2: SQL audit rule engine (10 rules) Phase 3.3: Auto-audit on ticket creation Phase 3.4: Operation audit logs + GET /audit/logs Tests: 44 -> 85 (41 new), Frontend: AuditLogsPage
46 lines
1.5 KiB
Python
46 lines
1.5 KiB
Python
"""AES-256-GCM encryption for instance passwords."""
|
|
import base64
|
|
import os
|
|
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
|
|
|
|
from app.core.config import get_settings
|
|
|
|
_NONCE_SIZE = 12 # 96-bit nonce for AES-GCM
|
|
|
|
|
|
def _get_key() -> bytes:
|
|
"""Derive a 32-byte AES key from settings.SECRET_KEY."""
|
|
settings = get_settings()
|
|
# Use first 32 bytes of utf-8 encoded secret key (padded if needed)
|
|
raw = settings.SECRET_KEY.encode("utf-8")
|
|
if len(raw) >= 32:
|
|
return raw[:32]
|
|
return raw.ljust(32, b"\x00")
|
|
|
|
|
|
def encrypt_password(plain: str) -> str:
|
|
"""Encrypt a password string, return base64-encoded ciphertext (nonce + ciphertext)."""
|
|
key = _get_key()
|
|
nonce = os.urandom(_NONCE_SIZE)
|
|
aesgcm = AESGCM(key)
|
|
ct = aesgcm.encrypt(nonce, plain.encode("utf-8"), None)
|
|
return base64.b64encode(nonce + ct).decode("ascii")
|
|
|
|
|
|
def decrypt_password(encrypted: str) -> str:
|
|
"""Decrypt a base64-encoded encrypted password back to plaintext.
|
|
If the value is not valid base64 (e.g., legacy plaintext), return as-is.
|
|
"""
|
|
try:
|
|
key = _get_key()
|
|
raw = base64.b64decode(encrypted, validate=True)
|
|
if len(raw) <= _NONCE_SIZE:
|
|
return encrypted # too short to be encrypted
|
|
nonce = raw[:_NONCE_SIZE]
|
|
ct = raw[_NONCE_SIZE:]
|
|
aesgcm = AESGCM(key)
|
|
return aesgcm.decrypt(nonce, ct, None).decode("utf-8")
|
|
except Exception:
|
|
# Not encrypted (legacy plaintext password) — return as-is
|
|
return encrypted
|