Files
plm-lottery/scripts/decrypt_master_key.py
davideandClaude Sonnet 5 1941f30b10 Add ops scripts to decrypt/re-encrypt the master xprv, and document them
decrypt_master_key.py: prints the existing master xprv after an explicit
confirmation prompt, for disaster-recovery backups. Falls back to
./data/keys/master.xprv.enc (the docker-compose.yml bind-mount path) when
.env's configured MASTER_KEY_PATH doesn't exist locally.

encrypt_master_key.py: the reverse direction — takes an externally-generated
xprv (e.g. created offline/air-gapped) via a hidden getpass prompt, validates
it parses as a private extended key, and encrypts it with the same Fernet
scheme generate_master_key.py uses. Refuses to overwrite an existing key file
unless --overwrite is passed.

Neither script is reachable via any API endpoint or the admin panel, by
design — this is the one secret the entire custodial wallet derives from.
Documented in docs/setup.md (new "Recuperare o portare una xprv esistente"
section) and CLAUDE.md's Commands block.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-22 16:20:03 +02:00

52 lines
1.9 KiB
Python

"""One-time ops recovery: decrypt and print the server's master xprv.
This is the single secret the entire custodial wallet derives from (every
user address, the pool address) — treat the output like a root password.
Not exposed via any API endpoint or the admin panel, by design; run manually,
locally, only when you actually need it (e.g. disaster-recovery backup).
Usage:
python scripts/decrypt_master_key.py
Requires XPRV_ENCRYPTION_KEY to already be set (.env), same as the running
server. Looks for the encrypted key at MASTER_KEY_PATH (.env) — if that path
doesn't exist, falls back to ./data/keys/master.xprv.enc, the location
docker-compose.yml bind-mounts it to, since .env's default (./master.xprv.enc)
often doesn't match wherever the running server actually reads it from.
"""
import os
from app.config import settings
from app.wallet.keystore import decrypt_xprv
_DOCKER_COMPOSE_PATH = "./data/keys/master.xprv.enc"
def _resolve_key_path() -> str:
if os.path.exists(settings.master_key_path):
return settings.master_key_path
if os.path.exists(_DOCKER_COMPOSE_PATH):
print(f"{settings.master_key_path} not found — using {_DOCKER_COMPOSE_PATH} instead.")
return _DOCKER_COMPOSE_PATH
raise SystemExit(f"No master key found at {settings.master_key_path} or {_DOCKER_COMPOSE_PATH}")
if __name__ == "__main__":
if not settings.xprv_encryption_key:
raise SystemExit("XPRV_ENCRYPTION_KEY is not set")
key_path = _resolve_key_path()
print(f"About to decrypt and print the master xprv from {key_path}.")
print("This key controls every user's funds and the pool. Make sure this")
print("terminal isn't logged/recorded and no one is watching your screen.")
confirm = input("Type 'yes' to continue: ")
if confirm.strip().lower() != "yes":
raise SystemExit("Aborted.")
with open(key_path, "rb") as f:
token = f.read()
print(decrypt_xprv(token))