Files
Aether/_deprecated_py_src/core/crypto.py
fawney19 1d9c77522a refactor: 移除 Python 后端源码,全面迁移至 Rust gateway 架构
- 删除全部 Python 源码 (src/) 及 Alembic 迁移脚本,归档至 _deprecated_py_src/
- 重构 Rust gateway ai_pipeline: 拆分 planner/finalize 模块,新增 contracts/adaptation 层
- 重组 handlers 模块为 admin/public/proxy/internal/shared 子模块结构
- 新增 executor 模块,引入 Rust 原生数据库迁移 (aether-data/migrations)
- 简化 CI/Docker 构建流程,移除 base image 二级构建,统一为单一 app image
- 移除 Python 相关基础设施文件 (entrypoint.sh, gunicorn_conf.py, Dockerfile.base)
2026-04-03 16:26:16 +08:00

264 lines
9.0 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
加密工具模块
提供API密钥的加密和解密功能
安全说明:
- 生产环境必须设置独立的 ENCRYPTION_KEY
- 加密密钥应独立于 JWT_SECRET_KEY避免密钥轮换问题
- 使用 PBKDF2 派生密钥时会使用应用级 salt
"""
from __future__ import annotations
import base64
import hashlib
import threading
import time
from collections import OrderedDict
from typing import TYPE_CHECKING, Any, cast
from src.core.logger import logger
from src.utils.perf import PerfRecorder
from ..config import config
from ..core.exceptions import DecryptionException
if TYPE_CHECKING:
from cryptography.fernet import Fernet
class CryptoService:
"""
加密服务
提供对称加密功能,用于保护 Provider API Key 等敏感数据。
使用 FernetAES-128-CBC + HMAC-SHA256确保数据机密性和完整性。
"""
_instance: CryptoService | None = None
_instance_lock = threading.Lock()
_cipher: Fernet | None = None
_key_source: str = "unknown" # 记录密钥来源,用于调试
# 应用级 salt基于应用名称生成比硬编码更安全
# 注意:更改此值会导致所有已加密数据无法解密
APP_SALT = hashlib.sha256(b"aether-v1").digest()[:16]
def __new__(cls) -> CryptoService:
if cls._instance is None:
with cls._instance_lock:
if cls._instance is None:
inst = super().__new__(cls)
inst._initialize()
cls._instance = inst
return cls._instance
def _initialize(self) -> None:
"""初始化加密服务"""
from cryptography.fernet import Fernet
logger.info("初始化加密服务")
encryption_key = config.encryption_key
if not encryption_key:
if config.environment == "production":
raise ValueError(
"ENCRYPTION_KEY must be set in production! "
"Use 'python generate_keys.py' to generate a secure key."
)
# 开发环境:使用固定的开发密钥
logger.warning("[DEV] 未设置 ENCRYPTION_KEY使用开发环境默认密钥。")
encryption_key = "dev-encryption-key-do-not-use-in-production"
self._key_source = "development_default"
else:
self._key_source = "environment_variable"
# 派生 Fernet 密钥
key = self._derive_fernet_key(encryption_key)
self._cipher = Fernet(key)
logger.info(f"加密服务初始化成功 (key_source={self._key_source})")
# 解密缓存配置(使用实例变量,避免测试场景下缓存跨实例持久化)
self._decrypt_cache: OrderedDict[str, tuple[str, float]] = OrderedDict()
self._decrypt_cache_lock = threading.Lock()
self._decrypt_cache_enabled = bool(getattr(config, "crypto_decrypt_cache_enabled", False))
self._decrypt_cache_size = int(getattr(config, "crypto_decrypt_cache_size", 0) or 0)
self._decrypt_cache_ttl_seconds = float(
getattr(config, "crypto_decrypt_cache_ttl_seconds", 0.0) or 0.0
)
if self._decrypt_cache_enabled and self._decrypt_cache_size > 0:
logger.info(
"解密缓存已启用 (size={}, ttl={}s)",
self._decrypt_cache_size,
self._decrypt_cache_ttl_seconds,
)
def _derive_fernet_key(self, encryption_key: str) -> bytes:
"""
从密码/密钥派生 Fernet 兼容的密钥
Args:
encryption_key: 原始密钥字符串
Returns:
Fernet 兼容的 base64 编码密钥
"""
from cryptography.fernet import Fernet
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
# 首先尝试直接作为 Fernet 密钥使用
try:
key_bytes = (
encryption_key.encode() if isinstance(encryption_key, str) else encryption_key
)
# 验证是否为有效的 Fernet 密钥32 字节 base64 编码)
Fernet(key_bytes)
return key_bytes
except Exception:
pass
# 不是有效的 Fernet 密钥,使用 PBKDF2 派生
kdf = PBKDF2HMAC(
algorithm=hashes.SHA256(),
length=32,
salt=self.APP_SALT,
iterations=100000,
)
derived_key = kdf.derive(encryption_key.encode())
return base64.urlsafe_b64encode(derived_key)
def encrypt(self, plaintext: str) -> str:
"""
加密字符串
Args:
plaintext: 明文字符串
Returns:
加密后的字符串base64编码
"""
if not plaintext:
return plaintext
try:
encrypted = self._cipher.encrypt(plaintext.encode())
return base64.urlsafe_b64encode(encrypted).decode()
except Exception as e:
logger.error(f"Encryption failed: {e}")
raise ValueError("Failed to encrypt data")
def decrypt(self, ciphertext: str, silent: bool = False) -> str:
"""
解密字符串
Args:
ciphertext: 加密的字符串base64编码
silent: 是否静默模式(失败时不打印错误日志)
Returns:
解密后的明文字符串
Raises:
DecryptionException: 解密失败时抛出异常
"""
if not ciphertext:
return ciphertext
cached = self._get_cached_decrypt(ciphertext)
if cached is not None:
PerfRecorder.record_counter("crypto_decrypt_cache_hits_total", 1)
return cached
PerfRecorder.record_counter("crypto_decrypt_cache_misses_total", 1)
start = PerfRecorder.start()
try:
encrypted = base64.urlsafe_b64decode(ciphertext.encode())
decrypted = self._cipher.decrypt(encrypted)
plaintext = decrypted.decode()
self._set_cached_decrypt(ciphertext, plaintext)
PerfRecorder.stop(start, "crypto_decrypt")
return plaintext
except Exception as e:
PerfRecorder.stop(start, "crypto_decrypt")
if not silent:
logger.error(f"Decryption failed: {e}")
# 抛出自定义异常,方便在上层通过类型判断是否需要打印堆栈
raise DecryptionException(
message=f"解密失败: {str(e)}。可能原因: ENCRYPTION_KEY 已改变或数据已损坏。解决方案: 请在管理面板重新设置 Provider API Key。",
details={"original_error": str(e), "key_source": self._key_source},
)
def hash_api_key(self, api_key: str) -> str:
"""
对API密钥进行哈希用于查找
Args:
api_key: API密钥明文
Returns:
哈希后的值
"""
return hashlib.sha256(api_key.encode()).hexdigest()
def _cache_key(self, ciphertext: str) -> str:
"""生成缓存 key使用密文 hash避免内存中保留完整密文"""
return hashlib.sha256(ciphertext.encode()).hexdigest()[:32]
def _get_cached_decrypt(self, ciphertext: str) -> str | None:
if not self._decrypt_cache_enabled:
return None
if not ciphertext:
return None
if self._decrypt_cache_size <= 0:
return None
cache_key = self._cache_key(ciphertext)
with self._decrypt_cache_lock:
entry = self._decrypt_cache.get(cache_key)
if not entry:
return None
value, expires_at = entry
if expires_at <= time.time():
self._decrypt_cache.pop(cache_key, None)
return None
# 维护 LRU 顺序
self._decrypt_cache.move_to_end(cache_key)
return value
def _set_cached_decrypt(self, ciphertext: str, plaintext: str) -> None:
if not self._decrypt_cache_enabled:
return
if not ciphertext:
return
if self._decrypt_cache_size <= 0:
return
if self._decrypt_cache_ttl_seconds <= 0:
return
cache_key = self._cache_key(ciphertext)
expires_at = time.time() + self._decrypt_cache_ttl_seconds
with self._decrypt_cache_lock:
self._decrypt_cache[cache_key] = (plaintext, expires_at)
self._decrypt_cache.move_to_end(cache_key)
while len(self._decrypt_cache) > self._decrypt_cache_size:
self._decrypt_cache.popitem(last=False)
def get_crypto_service() -> CryptoService:
"""获取加密服务单例(首次使用时才会初始化)。"""
return CryptoService()
class _LazyCryptoServiceProxy:
"""延迟代理,避免 import 阶段触发 cryptography 重载。"""
def __getattr__(self, name: str) -> Any:
return getattr(get_crypto_service(), name)
if TYPE_CHECKING:
crypto_service = CryptoService()
else:
crypto_service = cast(CryptoService, _LazyCryptoServiceProxy())