c9a99f4fb3
代码修复: - web/agent/agent.py: datetime.utcnow() → datetime.now(timezone.utc) - requirements.txt: 移除 paramiko==3.5.0 (已无活跃引用) - 删除 server/infrastructure/ssh/pool.py (DEPRECATED, 无引用) 连接池三层对齐 (config.py/.env.example/session.py): - DB_POOL_SIZE 100→160, DB_MAX_OVERFLOW 100→120 - 基于 MySQL max_connections=400, install.php 公式 文档修复 (21项): - P0: 硬编码域名/IP替换为配置变量占位符 - P1: 10个过时设计文档加归档标注 (引用旧文件结构) - P2: step-3-webssh报告 paramiko引用修正 - 6份审查报告连接池参数勘误 100/100→160/120 - ECC安全报告 EC5 datetime.utcnow 标记已修复 - docs/README.md 文档索引重写 - docs/memory/mem_nexus_overview.md 移除硬编码凭证 - docs/project/tech-stack-inventory.md paramiko标记已移除 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
75 lines
2.3 KiB
Python
75 lines
2.3 KiB
Python
"""Nexus — Encryption Utilities (Fernet + AES)
|
|
Shared crypto module — used by SSH pool, DB credentials, password presets.
|
|
"""
|
|
|
|
import base64
|
|
import hashlib
|
|
import logging
|
|
from cryptography.fernet import Fernet
|
|
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
|
|
|
|
from server.config import settings
|
|
|
|
logger = logging.getLogger("nexus.crypto")
|
|
|
|
|
|
def _fernet() -> Fernet:
|
|
key = settings.ENCRYPTION_KEY
|
|
if not key:
|
|
raw = hashlib.sha256(settings.SECRET_KEY.encode()).digest()
|
|
key = base64.urlsafe_b64encode(raw).decode()
|
|
return Fernet(key.encode() if isinstance(key, str) else key)
|
|
|
|
|
|
_fernet_instance: Fernet | None = None
|
|
|
|
|
|
def get_fernet() -> Fernet:
|
|
global _fernet_instance
|
|
if _fernet_instance is None:
|
|
_fernet_instance = _fernet()
|
|
return _fernet_instance
|
|
|
|
|
|
def _derive_aes_key() -> bytes:
|
|
"""Unified key derivation: must match PHP _encryptPw()
|
|
PHP uses hash('sha256', API_KEY, true).
|
|
This uses API_KEY first, falls back to SECRET_KEY."""
|
|
key = settings.API_KEY or settings.SECRET_KEY
|
|
return hashlib.sha256(key.encode()).digest()
|
|
|
|
|
|
def encrypt_value(plaintext: str) -> str:
|
|
"""Encrypt a string using Fernet"""
|
|
if not plaintext:
|
|
return plaintext
|
|
return get_fernet().encrypt(plaintext.encode()).decode()
|
|
|
|
|
|
def decrypt_value(ciphertext: str) -> str:
|
|
"""Decrypt a string — supports both Fernet and PHP AES format"""
|
|
if not ciphertext:
|
|
return ciphertext
|
|
# PHP-compatible AES format
|
|
if ciphertext.startswith("aes:"):
|
|
try:
|
|
key = _derive_aes_key()
|
|
data = base64.b64decode(ciphertext[4:])
|
|
iv = data[:16]
|
|
enc = data[16:]
|
|
cipher = Cipher(algorithms.AES(key), modes.CBC(iv))
|
|
decryptor = cipher.decryptor()
|
|
plain = decryptor.update(enc) + decryptor.finalize()
|
|
pad_len = plain[-1]
|
|
if 1 <= pad_len <= 16:
|
|
plain = plain[:-pad_len]
|
|
return plain.decode()
|
|
except Exception as e:
|
|
logger.warning(f"AES decryption failed (key mismatch?): {e}")
|
|
return ciphertext
|
|
# Fernet format
|
|
try:
|
|
return get_fernet().decrypt(ciphertext.encode()).decode()
|
|
except Exception as e:
|
|
logger.warning(f"Fernet decryption failed (key mismatch?): {e}")
|
|
return ciphertext |