"""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, the key in XPRV_ENCRYPTION_KEY_FILE). 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_FILE (.env) to point at the key file. 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, write_secret_file from app.wallet.verify import account_key, load_sample, sample_problems _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_FILE is not set, or its file is missing or empty") 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.") # SEC-05/SEC-50: the version bytes cannot tell a PLM key from a Bitcoin one, the stored # addresses can. A mismatch is refused even with --overwrite. problems = sample_problems(account_key(xprv), load_sample()) if problems: raise SystemExit("this xprv does not match the database — refusing to write it: " + "; ".join(problems)) os.makedirs(os.path.dirname(key_path) or ".", exist_ok=True) write_secret_file(key_path, encrypt_xprv(xprv)) print(f"Master key written (encrypted) to {key_path}")