feat: Phase 3 - AES encryption, SQL audit engine, audit logs (85 tests)

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
This commit is contained in:
logan committed 2026-05-22 13:08:55 +08:00
1 parent 27989274f5
commit 66b09e4d85
16 files changed
+1146 -6

No files matched your search

+45
View File
@@ -0,0 +1,45 @@
"""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