Files
vps-manager/scripts/setup_key_escrow.py
T
gouki d711ad5827 feat(vault): 凭据库(密码+2FA 动态码)与 MASTER_KEY 密钥托管
credentials 表为登录凭据唯一事实源(站点×登录方式,含 oauth/2FA);账号密码读写重定向凭据层并幂等迁移历史数据;TOTP 按 RFC6238 零依赖自实现,绑定需当前动态码校验;Key Escrow 防 MASTER_KEY 遗失;前端新增凭据库视图与账号 2FA 联动。64 pytest + 16 浏览器端到端验证通过。
2026-09-05 15:54:41 +00:00

121 lines
5.2 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.
"""建立 MASTER_KEY 密钥托管(Key Escrow,防 key 遗失)
用法:
.venv/bin/python scripts/setup_key_escrow.py # 生成托管,终端显示 RESTORE_KEY
.venv/bin/python scripts/setup_key_escrow.py --key <钥匙> # 回验已有托管是否可解
.venv/bin/python scripts/setup_key_escrow.py --force # 重建(旧钥匙抄本随之作废)
- 生成随机 RESTORE_KEYFernet 格式),加密当前 MASTER_KEY 写入 data/master_key.escrow
- RESTORE_KEY 仅在终端显示一次,请立即抄写到 2~3 个离线位置(密码管理器/纸质/可信家人)
- escrow 随 data/ 备份流转(内容被离线钥匙加密,库泄露也无法还原),建议另复制一份到离线介质
"""
import argparse
import sys
from pathlib import Path
# 确保能导入 app 包(脚本位于 scripts/ 子目录)
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
from cryptography.fernet import Fernet
from app.core import crypto
from app.core.config import settings
BASE_DIR = Path(__file__).resolve().parent.parent
DEFAULT_ESCROW = BASE_DIR / "data" / "master_key.escrow"
README_PATH = BASE_DIR / "data" / "master_key.escrow.README"
README_TEXT = """MASTER_KEY 密钥托管说明(Key Escrow
=====================================
data/master_key.escrow 是用 RESTORE_KEY 加密的 MASTER_KEY 副本,
随 data/ 一起备份。其内容只有离线保管的 RESTORE_KEY 能解开。
MASTER_KEY 遗失时的恢复步骤:
1. 找到离线保存的 RESTORE_KEY(44 字符,建立托管时终端显示过一次)
2. 在 vps-manager 目录执行(仅打印找回结果,先人工确认):
RESTORE_KEY=<你的钥匙> .venv/bin/python scripts/recover_master_key.py
3. 确认无误后回写 .env(自动备份为 .env.bak-pre-recover):
RESTORE_KEY=<你的钥匙> .venv/bin/python scripts/recover_master_key.py --write
4. 重启服务:sudo systemctl restart vps-manager
全部加密数据(密码/API Key/2FA secret)即可正常解密,零迁移。
注意:
- RESTORE_KEY 只离线保存,绝不写入本文件、.env 或数据库
- 重建托管(setup_key_escrow.py --force)后,旧 RESTORE_KEY 抄本即作废,需重新抄写
- 建议把 escrow 文件另复制一份到离线介质,与 .env 分开存放
"""
def _write_readme() -> None:
"""写恢复步骤说明(不含任何钥匙材料)"""
README_PATH.parent.mkdir(parents=True, exist_ok=True)
README_PATH.write_text(README_TEXT, encoding="utf-8")
def main() -> int:
parser = argparse.ArgumentParser(description="建立 MASTER_KEY 密钥托管(Key Escrow")
parser.add_argument("--key", help="已有 RESTORE_KEY:仅回验托管是否可解,不重建")
parser.add_argument("--force", action="store_true", help="强制重建托管(旧 RESTORE_KEY 抄本随之作废)")
parser.add_argument(
"--escrow", default=str(DEFAULT_ESCROW),
help="escrow 文件路径(默认 data/master_key.escrow",
)
args = parser.parse_args()
escrow_path = Path(args.escrow)
# 回验模式:--key 提供时只校验已有托管,不重建
if args.key:
if not escrow_path.exists():
print(f"[托管] 未找到托管文件 {escrow_path}", file=sys.stderr)
return 2
try:
recovered = crypto.recover_master_key(
args.key.strip(), escrow_path.read_text(encoding="utf-8").strip()
)
except ValueError as e:
print(f"[托管] 回验失败:{e}", file=sys.stderr)
return 1
ok = recovered == settings.MASTER_KEY
print(
f"[托管] 回验{'通过' if ok else '不通过'}escrow 可解开,"
f"还原的 MASTER_KEY 与 .env {'一致' if ok else '不一致(.env 已更换 key?)'}"
)
return 0 if ok else 1
if escrow_path.exists() and not args.force:
print(
f"[托管] {escrow_path} 已存在,未重建(重建加 --force;回验已有托管用 --key",
file=sys.stderr,
)
return 2
restore_key = Fernet.generate_key().decode()
escrow = crypto.build_escrow(restore_key)
# 自检:写盘前立即回验,确保托管文件可用(避免生成废档)
if crypto.recover_master_key(restore_key, escrow) != settings.MASTER_KEY:
print("[托管] 自检失败,未写入文件", file=sys.stderr)
return 1
escrow_path.parent.mkdir(parents=True, exist_ok=True)
escrow_path.write_text(escrow + "\n", encoding="utf-8")
escrow_path.chmod(0o600)
if escrow_path == DEFAULT_ESCROW:
_write_readme()
print("[托管] 已建立 MASTER_KEY 密钥托管:")
print(f" escrow 文件:{escrow_path}(权限 600,随 data/ 备份流转)")
print()
print(" RESTORE_KEY(仅此一次显示,请立即抄写保存到 2~3 个离线位置):")
print(f" {restore_key}")
print()
print(" 建议保存位置:密码管理器 / 纸质抄件(防火防潮)/ 可信家人")
print(" 另建议把 escrow 文件复制一份到离线介质(与 .env 分开存放)")
print(" 恢复命令:RESTORE_KEY=<钥匙> .venv/bin/python scripts/recover_master_key.py")
return 0
if __name__ == "__main__":
sys.exit(main())