Files
plm-lottery/scripts/encrypt_master_key.py
T
davideandClaude Sonnet 5.5 55d852fe08 Verify the master key against the database at startup (B-89, SEC-49, SEC-50, SEC-09)
The lifespan decrypts the key and compares a fixed sample (reserve/round pool
addresses, the 10 lowest and highest user addresses, unrevealed round secrets)
with the database, and refuses to serve on any mismatch without printing key
material. The key scripts refuse a new or imported key that would not reproduce
the stored addresses, and write 0600.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-18 00:38:26 +02:00

83 lines
3.4 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, 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 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.")
# 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}")