- app runs as uid 1000; a one-shot data-permissions service chowns ./data to it and closes it to others (works on an older root-owned install with no host sudo); files are created 0600 (umask 077) - python:3.12-slim and caddy:2-alpine pinned by digest; dependencies installed from requirements.lock with --require-hashes - the Fernet key is XPRV_ENCRYPTION_KEY_FILE (./secrets/xprv_encryption.key, a compose secret), out of every environment variable and of a ./data backup - setup.md no longer presents a second way to run the server (B-72) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
83 lines
3.5 KiB
Python
83 lines
3.5 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, 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}")
|