Plings Keygen Menu (plings_keygen.py) — Design Spec

Created: Sat 13 Jun 2026 10:40:40 CEST Document Version: 1.0 - Initial design spec Security Classification: Internal Technical Documentation Target Audience: Backend Developers Author: Paul Wisén

Problem

Generating keys/tags today requires long, error-prone CLI invocations, e.g.:

poetry run python scripts/derive_path_keys.py \
  --path "3.1.2.1" --decimal-path --quantity 100 \
  --master-key keys/dev/dev_key.json --csv-output keys/dev/pauls_1-100st.csv

The author wants to stop typing commands. This spec defines an interactive, menu-driven front-end that wraps the existing derivation logic and the master-key scripts, plus a URL-validation helper.

Scope

In scope (this iteration):

  1. Generate keys/tags — friendly wrapper around HDPathKeyDerivation (single + batch + batch-from-instance), CSV output.
  2. Master-key management — create new / import existing, wrapping the existing scripts.
  3. Validate a URL — paste a complete s.plings.io URL and verify it against a selected master key.

Out of scope (YAGNI for now):

  • Editing/deleting existing batches.
  • Any database writes (no Supabase/Neo4j integration — this is pure local key derivation).
  • Anything beyond wrapping the existing master-key scripts.

Architecture & file layout

  • New entrypoint: scripts/plings_keygen.py, run with poetry run python scripts/plings_keygen.py.
  • Reuse, don’t duplicate: imports HDPathKeyDerivation and load_master_key from scripts/derive_path_keys.py, and PlingsPathUtils / HDPathUtils / CryptoUtils from scripts/utils/. No cryptographic logic is reimplemented.
  • Shared CSV writer: the CSV-writing block currently inline in derive_path_keys.main() is extracted into scripts/utils/output_utils.py (write_keys_csv(keys, path)), so both the CLI and the menu use one implementation. derive_path_keys.py is updated to call it (behaviour-preserving — same columns: shortcode, decimal_path, base58_path, url).
  • TUI library: questionary for prompts (select / text / confirm), rich for summary tables and coloured output.

Dependencies

  • Add questionary and rich to a dev-only group in pyproject.toml ([tool.poetry.group.dev.dependencies]).
  • Do NOT add them to requirements.txt — Vercel’s serverless build reads requirements.txt, and this is a local-only tool that never runs on Vercel. Keeping the deploy manifest clean is a requirement.

A questionary select loop:

1) Generera nycklar / taggar
2) Master-nycklar   →  (Skapa ny  /  Importera befintlig)
3) Validera en URL
4) Avsluta

Flow: Generate keys/tags

  1. Select master key — auto-discover keys/**/*.json, present a pick-list showing relative path + wallet_version. (Master-key files are recognised by the presence of private_key + chain_code keys; the history file and CSVs are excluded.)
  2. What to generate:
    • From history — pick a remembered entry (label + decimal path); the menu pre-fills start instance = last_instance + 1.
    • New — enter a label and a decimal path (e.g. 3.1.2.1). Path is validated via PlingsPathUtils; on error, re-prompt.
  3. Quantity and start instance (start defaults to the history-suggested value, else 1).
  4. Class pointer — optional; default none (matches existing batches whose URLs have no cp=).
  5. Confirm summary — show selected master key, decimal + base58 path, instance range, and target file; questionary.confirm before deriving.
  6. Derive → validate → write CSV using HDPathKeyDerivation + write_keys_csv. Output path: keys/<env>/<label>_<start>-<end>st.csv, where <env> is the directory family of the chosen master key (e.g. keys/dev/). Never overwrite an existing file without an explicit confirm (offer overwrite or auto-suffix).
  7. Update history (label → {path_decimal, last_instance=end, master_key, updated_at}).
  8. Show summaryrich table: count, output file, first few rows.

Flow: Master keys

Thin wrappers reusing the existing scripts’ classes directly (both are importable, not CLI-only):

  • Create new — prompt wallet_version, dev/production, output filename under the chosen keys/<env>/; uses MasterKeyGenerator (generate_master_key.py): MasterKeyGenerator(production).generate_master_key(wallet_version) + save_master_key().
  • Import existing — uses MasterKeyImporter (import_master_key.py) to import a pasted base58/JSON key and save it under keys/<env>/.

Flow: Validate a URL

  1. Paste a complete URL, e.g. https://s.plings.io?t=q&i=<instance_key>&p=<base58_path>&cp=<cp>.
  2. Parse query params: i (instance_key), p (path), cp (optional class pointer), t (transport — informational only).
  3. Select a master key.
  4. Re-derive the key for path p via HDPathKeyDerivation.derive_path_key.
  5. Compare the derived instance_key to the URL’s i=. Report:
    • ✅/❌ instance-key match
    • decoded decimal path, wallet version, short_code
    • whether cp matches the supplied class pointer (if any)

This verifies a physical tag’s URL truly derives from the selected master key (tamper / wrong-key detection).

History file

  • Location: keys/.keygen_history.json (added to .gitignore).
  • Format:
    {
      "pauls": { "path_decimal": "3.1.2.1", "last_instance": 100, "master_key": "keys/dev/dev_key.json", "updated_at": "2026-06-13T08:00:00+00:00" }
    }
    
  • Read at startup; written after each successful generation. Corrupt/missing file → treated as empty, never fatal.

Error handling & safety

  • Invalid path / URL / master-key JSON → clear message + re-prompt; no stack-trace crash.
  • Ctrl-C / EOF → graceful exit.
  • Output CSVs contain only public data (shortcode, path, URL, instance_key = public Solana address) — no private keys — so default file perms are acceptable. Master-key files remain chmod 600 (unchanged).
  • Generation is deterministic (BIP32): same master key + path + instance ⇒ identical output (already verified: regenerating keys/dev/pauls_1-100st.csv reproduces it byte-for-byte).

Testing strategy

The interactive TUI layer is thin and verified manually. The non-interactive core is factored into testable functions and covered in tests/:

  • Filename builder(label, start, end) → "<label>_<start>-<end>st.csv".
  • History round-trip — write then read returns the same structure; corrupt file → empty.
  • URL validator — a known-good URL (from pauls_1-100st.csv) validates as ✅; a tampered i= validates as ❌.
  • Determinism regression — regenerating pauls_1-100st.csv from dev_key.json matches the committed file.

Resolved details

  • Reuse is confirmed: HDPathKeyDerivation + load_master_key (derive_path_keys.py), MasterKeyGenerator (generate_master_key.py), and MasterKeyImporter (import_master_key.py) are all importable classes/functions — no subprocess wrapping needed.
  • <env> inference: use the immediate parent directory of the chosen master-key file (e.g. keys/dev/dev_key.jsonkeys/dev/). Output CSVs and any new master keys are written into that same directory. If a master key sits directly under keys/, fall back to writing alongside it.