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
+92
View File
@@ -0,0 +1,92 @@
"""MASTER_KEY 丢失恢复(一次性脚本,日常运行不接触)
用法:
RESTORE_KEY=<钥匙> .venv/bin/python scripts/recover_master_key.py # 仅打印找回的 MASTER_KEY
RESTORE_KEY=<钥匙> .venv/bin/python scripts/recover_master_key.py --write # 备份 .env 后回写 MASTER_KEY
也支持 --key 传参(会残留 shell 历史,建议用环境变量)。
恢复后重启服务即可:全部密文零迁移、照常解密。
"""
import argparse
import os
import re
import shutil
import sys
from pathlib import Path
# 确保能导入 app 包(脚本位于 scripts/ 子目录)
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
from app.core import crypto
BASE_DIR = Path(__file__).resolve().parent.parent
DEFAULT_ESCROW = BASE_DIR / "data" / "master_key.escrow"
DEFAULT_ENV = BASE_DIR / ".env"
def _write_env(master_key: str, env_path: Path) -> Path | None:
"""备份 .env 后回写 MASTER_KEY 行(不存在则追加),返回备份路径"""
backup: Path | None = None
if env_path.exists():
backup = env_path.parent / (env_path.name + ".bak-pre-recover")
shutil.copy2(env_path, backup)
text = env_path.read_text(encoding="utf-8")
if re.search(r"^MASTER_KEY=.*$", text, flags=re.MULTILINE):
text = re.sub(
r"^MASTER_KEY=.*$", f"MASTER_KEY={master_key}", text, count=1, flags=re.MULTILINE
)
else:
text = text.rstrip("\n") + f"\nMASTER_KEY={master_key}\n"
env_path.write_text(text, encoding="utf-8")
else:
env_path.write_text(f"MASTER_KEY={master_key}\n", encoding="utf-8")
return backup
def main() -> int:
parser = argparse.ArgumentParser(description="凭 RESTORE_KEY 找回 MASTER_KEY")
parser.add_argument(
"--key", help="RESTORE_KEY(建议改用环境变量 RESTORE_KEY,避免 shell 历史残留)"
)
parser.add_argument(
"--write", action="store_true", help="找回后直接回写 .env(先备份为 .env.bak-pre-recover"
)
parser.add_argument(
"--escrow", default=str(DEFAULT_ESCROW),
help="escrow 文件路径(默认 data/master_key.escrow",
)
parser.add_argument("--env", default=str(DEFAULT_ENV), help=".env 路径(默认项目根 .env")
args = parser.parse_args()
restore_key = args.key or os.environ.get("RESTORE_KEY")
if not restore_key:
print("[恢复] 缺少 RESTORE_KEY--key 传入或设置环境变量 RESTORE_KEY", file=sys.stderr)
return 2
escrow_path = Path(args.escrow)
if not escrow_path.exists():
print(f"[恢复] 未找到托管文件 {escrow_path}(尚未建立托管?)", file=sys.stderr)
return 2
try:
master_key = crypto.recover_master_key(
restore_key.strip(), escrow_path.read_text(encoding="utf-8").strip()
)
except ValueError as e:
print(f"[恢复] 失败:{e}", file=sys.stderr)
return 1
if args.write:
backup = _write_env(master_key, Path(args.env))
where = f"(原文件已备份至 {backup.name}" if backup else ""
print(f"[恢复] MASTER_KEY 已找回并回写 {args.env}{where}")
print("[恢复] 请重启服务使 key 生效:sudo systemctl restart vps-manager")
else:
print("[恢复] MASTER_KEY 已找回(未回写 .env):")
print(f" MASTER_KEY={master_key}")
print("[恢复] 确认无误后加 --write 自动回写 .env,或手动粘贴到 .env 的 MASTER_KEY= 行")
return 0
if __name__ == "__main__":
sys.exit(main())