"""凭证加密模块(Fernet 对称加密) 用于加密存储 SSH 密钥、密码、API Key、平台 API 配置等敏感信息。 MASTER_KEY 从 .env 读取,不入库。 密钥托管(Key Escrow):MASTER_KEY 遗失时全量密文不可解,故提供 build_escrow/recover_master_key——用离线保管的 RESTORE_KEY 加密 MASTER_KEY 本身生成 escrow 文件(随 data/ 备份流转),丢失时凭 RESTORE_KEY 找回。 """ from functools import lru_cache from typing import Optional from cryptography.fernet import Fernet, InvalidToken from app.core.config import settings @lru_cache(maxsize=1) def _get_fernet() -> Fernet: """获取 Fernet 实例(进程内缓存,避免每次加解密重建;MASTER_KEY 未配置时抛错)""" key = settings.MASTER_KEY if not key: raise RuntimeError( "MASTER_KEY 未配置,无法加解密凭证。请在 .env 设置 MASTER_KEY," "生成命令:python -c 'from cryptography.fernet import Fernet; " "print(Fernet.generate_key().decode())'" ) return Fernet(key.encode() if isinstance(key, str) else key) def encrypt(plain: Optional[str]) -> Optional[str]: """加密明文,返回 token 字符串;空值原样返回 None""" if plain is None or plain == "": return None return _get_fernet().encrypt(plain.encode()).decode() def decrypt(token: Optional[str]) -> Optional[str]: """解密 token,返回明文;空值或解密失败返回 None""" if not token: return None try: return _get_fernet().decrypt(token.encode()).decode() except (InvalidToken, ValueError): return None # ---------------- 密钥托管(Key Escrow) ---------------- # ESCROW_PREFIX = "v1:" def _fernet_from_key(key: str) -> Fernet: """按给定密钥构造独立 Fernet 实例(不读 settings、不走进程缓存)""" return Fernet(key.encode() if isinstance(key, str) else key) def build_escrow(restore_key: str) -> str: """用 RESTORE_KEY 加密当前 MASTER_KEY,生成 escrow token('v1:' + Fernet token) escrow 文件内容只有离线保管的 RESTORE_KEY 能解开:数据库/备份泄露也无法 还原 MASTER_KEY。恢复时凭 escrow 找回 MASTER_KEY,全量密文零迁移。 """ master_key = settings.MASTER_KEY if not master_key: raise RuntimeError("MASTER_KEY 未配置,无法建立托管(请先在 .env 配置 MASTER_KEY)") return ESCROW_PREFIX + _fernet_from_key(restore_key).encrypt(master_key.encode()).decode() def recover_master_key(restore_key: str, escrow: str) -> str: """用 RESTORE_KEY 解密 escrow token,还原 MASTER_KEY 明文 钥匙错误、前缀缺失或文件损坏时抛 ValueError。 """ if not escrow.startswith(ESCROW_PREFIX): raise ValueError("escrow 格式无效(缺少 v1: 前缀)") try: return _fernet_from_key(restore_key).decrypt(escrow[len(ESCROW_PREFIX):].encode()).decode() except (InvalidToken, ValueError) as e: raise ValueError("恢复钥匙错误或 escrow 已损坏") from e