Skip to content

feat(wifi): persist the setup hotspot password per device; let the clone warning print - #54

Merged
kapoorankush merged 1 commit into
masterfrom
fix/port-hotspot-password-and-clone-warning
Aug 16, 2026
Merged

feat(wifi): persist the setup hotspot password per device; let the clone warning print#54
kapoorankush merged 1 commit into
masterfrom
fix/port-hotspot-password-and-clone-warning

Conversation

@kapoorankush

Copy link
Copy Markdown
Owner

Ports litclock-dev#620/#639 and litclock-dev#649 from the private dev repo. First of two port PRs for v0.224.0. They ship together because both touch scripts/prepare-for-cloning.sh — splitting them would mean porting a conflict.

What changes

litclock-dev#620 — the setup network's password is now fixed per device. create_hotspot() minted a fresh 8-character password every provisioning cycle and persisted it nowhere, while the SSID stayed LitClock-Setup. A phone that had joined once held a saved entry whose credential no longer worked. It fires on ordinary paths: a WiFi reset after moving house, or a power cut part-way through setup.

The password is now read-or-created once from /var/lib/litclock/hotspot-password (0600, atomic, fsynced) and reused. Persistence is best-effort by design — an unreadable, malformed or unwritable file regenerates and logs, because provisioning must never be blocked by its own bookkeeping. O_NOFOLLOW|O_NONBLOCK on read. It survives a plain factory reset and a WiFi reset deliberately; gift mode and prepare-for-cloning.sh rotate it, both failing closed.

A related leak closes with it: nmcli takes credentials as space-separated argv tokens, which the support-bundle redaction filter never matched (it required PASSWORD=), so sudo's command-audit line carried the PSK into persistent journald and out through the diagnostics bundle. Harmless while the value expired each cycle; not harmless once it is permanent.

litclock-dev#649 — the "do NOT clone this card" warning can now actually print. Under the script's set -e, a genuinely failing rm terminated prepare-for-cloning.sh on the rm line, so the check below it never ran and none of its three red lines printed. The warning written to stop a key-reuse mistake was unreachable in exactly the situation it was written for.

Port notes

  • scripts/reset-setup.sh was hand-merged, not copied. 84 lines of the litclock-dev#528 SSH-off gate exist only on this repo; a wholesale copy would destroy them. Verified after merging: the gate body is byte-identical to master and all four call sites survive.
  • The CHANGELOG entry carries litclock-dev#648's corrected Android measurement directly. Porting the original text and then the correction would have this repo publish a claim it never made and then retract it in the same release.
  • 13 bare dev#NNN references requalified to litclock-dev#NNN.
  • Verified nothing was dropped: with the ref namespace normalised on both sides, three of the eight files are byte-identical to dev and the rest differ only by this repo's existing forward divergence. Of every line the two upstream commits added, the three not present here were each superseded by litclock-dev#649's own later edits or are a formatter quote-style normalisation.

One ordering decision

litclock-dev#620 and litclock-dev#528 never constrained each other — they landed on separate repos and meet for the first time here. In the gift block the hotspot-password rotation now runs before disable_ssh_for_handoff. The rotation fails closed and can exit 1, which leaves the device with its current owner; disabling SSH first would strip that owner's remote access on the exact path where they still need it to diagnose a read-only card. SSH-off is the last thing before poweroff.

This is guarded by a new test, mutation-verified: with the order inverted, all 63 pre-existing tests still passed.

Testing

  • ruff check src/ image-gen/ tests/ scripts/ — clean
  • shellcheck --severity=warning on both changed scripts — clean
  • pytest — 3252 passed, 62 skipped (all skips pre-existing, in files this PR does not touch; zero skips in the four test files it does)
  • npm run test:js — 191 passed (no JS in this PR)

Follow-ups filed, not fixed here

Review surfaced several issues that are present identically on dev, so they are being fixed upstream and ported rather than patched here — patching only this repo would add to the forward divergence that already exists. The handoff-path key-rotation gap among them is scheduled to land before the v0.224.0 RC is built.

…one warning print

Ports litclock-dev#620/#639 and litclock-dev#649 together — both touch
scripts/prepare-for-cloning.sh, so splitting them would mean porting a
conflict.

litclock-dev#620 — create_hotspot() minted a fresh 8-char password every
provisioning cycle and persisted it nowhere while the SSID stayed
LitClock-Setup, so a phone that had joined once held a saved entry whose
credential no longer worked. Fires on ordinary paths: a WiFi reset after
moving house, or a power cut part-way through setup. The password is now
read-or-created once from /var/lib/litclock/hotspot-password (0600,
atomic, fsynced) and reused. Persistence is best-effort — an unreadable,
malformed or unwritable file regenerates and logs, because provisioning
must never be blocked by its own bookkeeping. O_NOFOLLOW|O_NONBLOCK on
read. It survives a plain factory reset and a WiFi reset deliberately;
gift mode and prepare-for-cloning.sh rotate it, both failing closed.

Also closes the related leak: nmcli takes credentials as space-separated
argv tokens, which the redaction filter never matched (it required
PASSWORD=), so sudo's command-audit line carried the PSK into persistent
journald and out through the diagnostics bundle.

litclock-dev#649 — under the script's `set -e`, a genuinely failing `rm`
terminated prepare-for-cloning.sh ON the rm line, so the check below it
never ran and the three red "Do NOT clone this card" lines never printed.
The warning written to stop a key-reuse mistake was unreachable in exactly
the situation it was written for.

Port notes:

- scripts/reset-setup.sh was hand-merged, not copied. 84 lines of the
  litclock-dev#528 SSH-off gate exist only on this repo and a wholesale
  copy would destroy them.
- The CHANGELOG entry carries litclock-dev#648's corrected Android
  measurement directly. Porting the original text and then the correction
  would have this repo publish a claim it never made and then retract it.
- 13 bare dev#NNN references requalified to litclock-dev#NNN.

One ordering decision the two upstream changes did not settle between
them, because they landed on separate repos and meet for the first time
here: in the gift block the hotspot-password rotation now runs BEFORE
disable_ssh_for_handoff. The rotation fails closed and can exit 1, which
leaves the device with its CURRENT owner; disabling SSH first would strip
that owner's remote access on the exact path where they still need it to
diagnose a read-only card. Guarded by a new test, verified to fail when
the order is inverted — all 63 pre-existing tests passed with it inverted.
@kapoorankush
kapoorankush merged commit 51d96aa into master Aug 16, 2026
5 checks passed
kapoorankush added a commit that referenced this pull request Aug 16, 2026
…fixes (#56)

Ports the two pre-RC blockers fixed upstream in litclock-dev#668 and #669.
Both were found by review of the port PRs that brought #620 and #645 here, so
this closes the loop on that train.

litclock-dev#660 — the PWA "Factory reset" (litclock-reset.service runs
`--wipe-wifi --strict-env-wipe --poweroff --yes`) did not rotate the setup-WiFi
key that #620 had just made permanent. Because that path wipes WiFi, the next
power-on raises the setup hotspot broadcasting LitClock-Setup with the PREVIOUS
owner's key, permanently, surviving every reset the new owner later performs —
while the card promises "Erases everything". v0.223.0 had no such leak, so this
was a regression #620 introduced. The discriminator is `--wipe-wifi` AND the
power-off, not either alone: a hand-run `--poweroff` never raises a hotspot, and
`--wipe-wifi --reboot` is #620's same-owner WiFi reset, which must preserve.

litclock-dev#667 — #645's link-local blanking removed the address from the
same-IP short-circuit's coverage, so every dhcp4-change on a clock that cannot
get a lease fell through to a full e-ink render (3 renders measured for 3
identical events). Review found the same hole for a genuinely empty address,
which NM's defaults make at least as likely. Coalescing now keys on the observed
address in its own tmpfs marker, while the blanked value stays the only thing
diagnostics sees.

Port notes:

- `scripts/reset-setup.sh` HAND-MERGED. This repo has the litclock-dev#528/#636
  SSH-off gate, which does not exist upstream at all — it was authored here in
  #52/#53 — so upstream's `--poweroff` arm has nothing to order against. The
  rotation call is placed BEFORE `disable_ssh_for_handoff`, matching the gift
  arm and for the same reason: the rotation fails closed and can exit 1, leaving
  the device with its current owner, who then still needs SSH to diagnose a
  read-only card. Verified after merging: the gate body is byte-identical to
  master and all four call sites survive.
- `CLAUDE.md` deliberately NOT ported. The upstream half rewrites a #620 QA
  block this repo does not have (`grep -c 620 CLAUDE.md` → 0), so applying it
  would invent that block here. Same phantom-conflict trap as the #641/#648 half
  of the previous train.
- `tests/test_reset_setup_sh.py` hand-merged per hunk rather than wholesale:
  the conflict replaced the old `_extract_block` helper with the new harness,
  but a whole-file resolution would have discarded this repo's SSH-gate tests.
  All twelve survive.
- 4 bare dev#NNN references requalified.
- Verified nothing was dropped: of every line the two upstream commits added,
  none is missing here.

`test_gift_mode_rotates_hotspot_password_before_disabling_ssh` — added when the
two features first met in #54 — is now parametrised over BOTH handoff arms,
since #660 gave the `--poweroff` arm the same pairing and nothing covered it.
Its anchor is also fixed: it matched the function name in the COMMENT above the
call, so inverting the order in the new arm kept it green. Mutation-verified in
both arms after the fix.

Testing:
- ruff + shellcheck clean
- pytest: 3392 passed, 62 skipped
- npm run test:js: 191 passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant