Files
plm-lottery/scripts/encrypt_master_key.py
T
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

77 lines
3.0 KiB
Python

"""One-time ops bootstrap: encrypt an externally-generated master xprv and
write it to disk, using the exact same scheme generate_master_key.py uses
(Fernet, XPRV_ENCRYPTION_KEY). Use this instead of generate_master_key.py
when you already have an xprv from elsewhere (e.g. generated offline/air-
gapped) and want to bring your own instead of letting the server create one.
This is the single secret the entire custodial wallet derives from (every
user address, the pool address) — treat both the input and the resulting
encrypted file like a root password.
Usage:
python scripts/encrypt_master_key.py
python scripts/encrypt_master_key.py --overwrite # replace an existing key
Requires XPRV_ENCRYPTION_KEY to already be set (.env). Writes to
MASTER_KEY_PATH (.env) — falls back to ./data/keys/master.xprv.enc (the
docker-compose.yml bind-mount location) if that directory exists and .env's
default path doesn't, same fallback as decrypt_master_key.py.
"""
import argparse
import getpass
import os
from embit.bip32 import HDKey
from app.config import settings
from app.wallet.keystore import encrypt_xprv
_DOCKER_COMPOSE_DIR = "./data/keys"
def _resolve_key_path() -> str:
configured_dir = os.path.dirname(settings.master_key_path) or "."
if os.path.isdir(configured_dir):
return settings.master_key_path
if os.path.isdir(_DOCKER_COMPOSE_DIR):
path = os.path.join(_DOCKER_COMPOSE_DIR, os.path.basename(settings.master_key_path) or "master.xprv.enc")
print(f"{configured_dir}/ not found — using {path} instead.")
return path
return settings.master_key_path
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument("--overwrite", action="store_true", help="replace an existing encrypted key file")
args = parser.parse_args()
if not settings.xprv_encryption_key:
raise SystemExit("XPRV_ENCRYPTION_KEY is not set")
key_path = _resolve_key_path()
if os.path.exists(key_path) and not args.overwrite:
raise SystemExit(f"{key_path} already exists (pass --overwrite to replace it)")
print("Paste the xprv to encrypt. Input is hidden and not echoed to the terminal.")
xprv = getpass.getpass("xprv: ").strip()
try:
parsed = HDKey.from_base58(xprv)
except Exception as exc:
raise SystemExit(f"Not a valid extended key: {exc}") from exc
# Not checking parsed.version against PLM_MAINNET["xprv"]: PLM reuses Bitcoin
# mainnet's own xprv/xpub version bytes (see plm_network.py) — a Bitcoin (or
# any Bitcoin-derived altcoin) xprv passes this check too, so it can't catch
# "wrong network" mistakes. It only tells us this parses as *some* valid
# extended key.
if not parsed.is_private:
raise SystemExit("This is a public key (xpub), not a private key (xprv) — refusing to encrypt it as one.")
os.makedirs(os.path.dirname(key_path) or ".", exist_ok=True)
with open(key_path, "wb") as f:
f.write(encrypt_xprv(xprv))
print(f"Master key written (encrypted) to {key_path}")