feat(vault): 凭据库(密码+2FA 动态码)与 MASTER_KEY 密钥托管

credentials 表为登录凭据唯一事实源(站点×登录方式,含 oauth/2FA);账号密码读写重定向凭据层并幂等迁移历史数据;TOTP 按 RFC6238 零依赖自实现,绑定需当前动态码校验;Key Escrow 防 MASTER_KEY 遗失;前端新增凭据库视图与账号 2FA 联动。64 pytest + 16 浏览器端到端验证通过。
This commit is contained in:
gouki
2026-09-05 15:54:41 +00:00
parent 2fbb658126
commit d711ad5827
26 changed files with 2529 additions and 35 deletions
+39
View File
@@ -2,6 +2,10 @@
用于加密存储 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
@@ -40,3 +44,38 @@ def decrypt(token: Optional[str]) -> Optional[str]:
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