120 lines
5.2 KiB
Python
Executable File
120 lines
5.2 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Fill in any empty service tokens in CoreSystemConfig.json, and write tokens.txt.
|
|
|
|
Runs before validation on every build (see lib/coreconfig.sh), so a fresh config only
|
|
needs the interesting fields filled in — addresses, rooms, which services you want —
|
|
and the random secrets appear on their own.
|
|
|
|
THE CRITICAL PART IS THAT GENERATED TOKENS ARE WRITTEN BACK TO THE CONFIG. A token is
|
|
only useful because two machines agree on it: the container host runs `identity` with
|
|
IDENTITY_TOKEN, and every kiosk image is built with the same value baked into its URLs.
|
|
Generating fresh randomness per build would produce a door panel that cannot talk to
|
|
the service it was built for — so this fills empties ONCE, persists them, and never
|
|
touches a value that is already set.
|
|
|
|
tokens.txt is a convenience copy for the human: same values, plus what each one is for
|
|
and which of them this script deliberately cannot generate.
|
|
|
|
Usage: generate-tokens.py [config.json]
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import secrets
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
# name -> (enable flag that makes it required, what it's for)
|
|
GENERATABLE = {
|
|
"identity_token": ("identity", "identity's API — also baked into the door panel and kitchen display"),
|
|
"pantry_vision_token": ("pantry_vision", "pantry-vision's API — also baked into the kitchen display"),
|
|
"transit_token": ("transit", "transit's API — called by Home Assistant for voice departures"),
|
|
}
|
|
|
|
# Deliberately NOT generated, with the reason. Inventing a value for any of these would
|
|
# produce something that looks configured and cannot work.
|
|
NOT_GENERATABLE = {
|
|
"ha_token": "a Long-Lived Access Token from Home Assistant's own UI (profile -> Security). "
|
|
"It doesn't exist until HA is running and an account exists, and only HA can mint it.",
|
|
"mqtt_password": "must match what Mosquitto is configured to accept. Generating one here "
|
|
"would just mean nothing can connect.",
|
|
"admin_password_hash": "a crypt(3) hash for the installer — generate with: mkpasswd -m sha-512",
|
|
"kiosk_password": "only needed if a kiosk account should have a password at all.",
|
|
"ssh_authorized_key": "your existing PUBLIC key (~/.ssh/id_ed25519.pub) — a generated one "
|
|
"would have no private half you hold.",
|
|
}
|
|
|
|
|
|
def main(argv: list[str]) -> int:
|
|
path = Path(argv[1]) if len(argv) > 1 else Path(__file__).resolve().parent.parent / "CoreSystemConfig.json"
|
|
if not path.exists():
|
|
print(f"error: {path} not found", file=sys.stderr)
|
|
return 2
|
|
|
|
try:
|
|
cfg = json.loads(path.read_text())
|
|
except json.JSONDecodeError as exc:
|
|
print(f"error: {path} is not valid JSON — line {exc.lineno}: {exc.msg}", file=sys.stderr)
|
|
return 2
|
|
|
|
secrets_block = cfg.setdefault("secrets", {})
|
|
enable = (cfg.get("container_host", {}) or {}).get("enable", {}) or {}
|
|
|
|
generated = []
|
|
for name, (flag, _purpose) in GENERATABLE.items():
|
|
# A token for a service you haven't enabled isn't needed, and filling it in
|
|
# anyway would put a live-looking credential in a file for no reason.
|
|
if not enable.get(flag):
|
|
continue
|
|
if (secrets_block.get(name) or "").strip():
|
|
continue
|
|
secrets_block[name] = secrets.token_hex(32)
|
|
generated.append(name)
|
|
|
|
if generated:
|
|
# Written back with the same 2-space indent the template uses, so the diff is
|
|
# just the token lines rather than the whole file reformatting.
|
|
path.write_text(json.dumps(cfg, indent=2) + "\n")
|
|
print(f" generated {len(generated)} token(s) into {path.name}: {', '.join(generated)}")
|
|
|
|
_write_tokens_txt(path.parent / "tokens.txt", cfg, enable)
|
|
return 0
|
|
|
|
|
|
def _write_tokens_txt(dest: Path, cfg: dict, enable: dict) -> None:
|
|
secrets_block = cfg.get("secrets", {}) or {}
|
|
lines = [
|
|
"SmartestHome — service tokens",
|
|
"=" * 60,
|
|
"",
|
|
"GENERATED by tools/generate-tokens.py from CoreSystemConfig.json.",
|
|
"This is a convenience copy for you, not a source of truth — the config is.",
|
|
"Regenerated on every build; edit the config, not this file.",
|
|
"",
|
|
"TREAT THIS FILE AS A CREDENTIAL. It is gitignored, but it is plain text on",
|
|
"disk: every token below grants full API access to the service named, and",
|
|
"identity's in particular can grant a person the right to open a smart lock.",
|
|
"",
|
|
]
|
|
|
|
for name, (flag, purpose) in GENERATABLE.items():
|
|
if not enable.get(flag):
|
|
lines += [f"{name}:", " (not set — container_host.enable." + flag + " is false)", ""]
|
|
continue
|
|
value = (secrets_block.get(name) or "").strip() or "(EMPTY — run a build to generate)"
|
|
lines += [f"{name}:", f" {value}", f" for: {purpose}", ""]
|
|
|
|
lines += ["", "Not generated here, and why:", ""]
|
|
for name, why in NOT_GENERATABLE.items():
|
|
value = (secrets_block.get(name) or "").strip()
|
|
state = "set" if value else "EMPTY"
|
|
lines += [f"{name} [{state}]", f" {why}", ""]
|
|
|
|
dest.write_text("\n".join(lines))
|
|
print(f" wrote {dest.name}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main(sys.argv))
|