feat(vault): 凭据库(密码+2FA 动态码)与 MASTER_KEY 密钥托管
credentials 表为登录凭据唯一事实源(站点×登录方式,含 oauth/2FA);账号密码读写重定向凭据层并幂等迁移历史数据;TOTP 按 RFC6238 零依赖自实现,绑定需当前动态码校验;Key Escrow 防 MASTER_KEY 遗失;前端新增凭据库视图与账号 2FA 联动。64 pytest + 16 浏览器端到端验证通过。
This commit is contained in:
@@ -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())
|
||||
Reference in New Issue
Block a user