Skip to content

Panel: manage Phrases from the panel's web page #98

Open

Description

@Skeyelab

Part of #68. Requested by Eric on 2026-10-25.

The panel serves a web page on the home network for adding, editing, removing and ordering its Buttons and Phrases.

Decisions (Eric):

  • Access: HTTPS plus an admin password. The panel serves HTTPS with its own self-signed cert, so the browser warns once. The admin password is set on the touchscreen at first setup and stored hashed (salted PBKDF2 or similar) in NVS. Phrases are typed into computers, so editing them is as sensitive as running commands. There's no unauthenticated write path, and sessions time out.
  • Phrases: the panel's own list. It's stored on the panel (NVS/LittleFS) and isn't synced with the iPhone Deck.

Acceptance:

  • The first-setup screen on the touchscreen sets the admin password.
  • The HTTPS server runs in its own task (not the LVGL loop), and the screen stays responsive during requests.
  • The page lists Buttons and can add, edit, delete and reorder them. Changes show on the panel without a reboot.
  • Phrases persist across reboots and survive a firmware update.
  • Bad passwords are rate-limited. Every write needs the panel's own Origin (web::originAllowed, checked in web_server.cpp) and a __Host- session cookie with SameSite=Strict (Eric chose this over a CSRF token on 2026-10-27). Phrase length and count are capped.
  • Host tests cover the phrase-store logic and the password check.
  • Factory reset (holding a corner of the touchscreen at boot) clears the password and phrases.

Blocked by: Wi-Fi working on the panel (slice 1b-2).

Activity

  1. added this to the 3. Deepening milestone on Oct 26, 2026
  2. Skeyelab commented on Oct 26, 2026

    @Skeyelab
    MemberAuthor

    Design review for the web phrase manager (Application Security Engineer subagent, read-only)

    The proposed design is PBKDF2, a lockout, one session with an HttpOnly/Secure/SameSite=Strict cookie and idle and absolute timeouts, a CSRF token plus an Origin check, an on-device ECDSA P-256 cert, esp_https_server on its own task, and LittleFS writes as temp-then-rename. The review adds the points below; I'm building them into #98.

    Building in:

    • First-use trust: the panel shows its cert's SHA-256 so the first browser accept can be compared. The cert names both the IP and the .local name. A factory reset makes a new key.
    • XSS: Phrases are served as JSON and rendered only with textContent.
      • CSP default-src 'self'; frame-ancestors 'none', with no inline script.
      • X-Frame-Options: DENY, Cache-Control: no-store, nosniff, Referrer-Policy: no-referrer.
    • DNS rebinding: reject any Host header that isn't the panel's IP or .local name.
    • Lockout:
      • The failure count persists in NVS and clears on success, so a crash can't reset it.
      • The touchscreen can clear a lockout, so a LAN attacker can't lock the owner out for good.
      • Failed logins show on screen.
    • Password and reset: changing the password needs the current one. Factory reset wipes the password, Phrases, cert key and sessions together.
    • Writes and login:
      • Every POST, /login included, needs a matching Origin; a POST with no Origin is refused.
      • Writes also need Content-Type: application/json.
      • SameSite and the CSRF token stay too.
    • PBKDF2: at least 10,000 iterations, even if a login takes about 1 s; measured on the S3 with esp_timer. The expert's estimate is 2k–10k iterations per 300 ms; not measured yet.
    • esp_https_server:
      • CONFIG_HTTPD_MAX_REQ_HDR_LEN 2048 (Chrome headers plus the cookie go over 512).
      • A truncated cookie counts as no session.
      • A body over the cap gets 413 before it's read.
      • 3–4 sockets with lru_purge_enable, and short recv/send timeouts.
      • CONFIG_MBEDTLS_DYNAMIC_BUFFER, and a 10 KB+ httpd stack.
      • httpd runs at a lower priority than LVGL, never calls lv_*, and posts to a queue.
    • Flash cache: LittleFS writes turn off the flash cache, which can glitch the RGB panel. The panel already uses bounce buffers; watch for it on the board.

    Needs Eric (product calls):

    1. A Phrase with Enter or control keys acts as a command in a terminal. Should the panel send an edit counter, so the Helper shows a notice the first time it types a changed Phrase? Separately, should newlines in panel Phrases be capped or flagged?
    2. Firmware updates stay USB only for now. Any over-the-air path would need signed images.
  3. Skeyelab commented on Oct 26, 2026

    @Skeyelab
    MemberAuthor

    Eric's decisions (2026-10-25, via main):

    1. Notice on first send. Every panel Phrase carries an edit counter, which goes up on each edit. The panel sends it with /type. The Helper shows a one-time notice the first time it types a Phrase whose counter it hasn't seen yet. The Helper side is its own small PR; agent1 reviews it. On the web editor, a Phrase containing Enter shows a visible ↵.
    2. USB-only updates for now; no over-the-air updates. The partition table keeps its two app slots (app0/app1), so OTA can be added later with signed images.
  4. Skeyelab commented on Oct 26, 2026

    @Skeyelab
    MemberAuthor

    Eric's decisions (2026-10-25, via main); they replace the 80-character preview:

    1. Confirm on the panel first. The first tap on a Phrase that was changed on the panel shows its text and asks "Send?". Nothing is sent until the user confirms. Later taps on that Phrase send right away.
    2. The Helper notice shows no Phrase text. It reads only "Typed a Phrase that changed on a panel".
  5. Skeyelab commented on Oct 26, 2026

    @Skeyelab
    MemberAuthor

    Panel setup checklist (Eric)

    This covers a panel on current main (#140 through #154). Every step happens at the panel, except where it says otherwise.

    Before you start

    1. Have main confirm the panel has the current main build and is in Wi-Fi Mode.
    2. Have main confirm your Helper runs with -lan (for example -lan 192.168.1.0/24). The panel reaches it only over the Home network route.

    First start: admin password (Step 1 of 3)

    1. Turn the panel on. A new or erased panel opens "Step 1 of 3: set an admin password".
    2. Type a password of at least 8 characters, twice, and tap OK. It guards Settings and the web editor.
      • If it fails: "The two don't match" means retype both. "Couldn't save it" means try again; if it repeats, tell main.
      • Lockout: after 5 wrong passwords, each new try waits 30 s, then longer, up to 15 min. A restart doesn't reset the count.

    Pair with your Helper (Step 2 of 3, Wi-Fi Mode)

    1. main runs phrased pair on your Computer. It prints a Pairing code and a line starting Pin for a panel:.
    2. On the panel's Pair screen, pick your Computer from the Nearby list. Enter the Pairing code, then the Pin (16 hex characters), and tap Pair.
    3. Expected: "Paired with ".
      • Your Computer isn't in the Nearby list: check that the Helper is running with -lan and that the panel is on the same Wi-Fi, then reopen the Pair screen.
      • "Pin doesn't match": don't retry with the same Pin. The panel refused a certificate that isn't your Helper's. Have main run phrased pair again and use the new code and Pin. If it still doesn't match, stop and tell main.
      • Any other error: have main run phrased pair again; each code works once and only for a short time.
    4. Step 3 of 3 shows the web editor's address. Open it later; there's nothing to do here now.
    5. Tap a Button. Expected: the Phrase is typed on your Computer and the panel says "Typed".

    Bluetooth Mode with your Mac (optional)

    1. Gear > Mode > Switch to Bluetooth keyboard. The panel restarts.
    2. Gear > Computer > Pair a Computer. The panel opens pairing for 2 minutes and shows where the passkey will appear.
    3. On the Mac: System Settings > Bluetooth, find Phrasedeck panel, and click Connect.
    4. When the Mac asks, type the 6-digit passkey shown on the panel. It's new for every pairing.
      • If macOS opens the Keyboard Setup Assistant, close it, or choose ANSI (US). The panel types on a US layout.
    5. Expected: Settings > Computer says "Bluetooth keyboard: Connected". Tap a Button with a text editor focused on the Mac. The panel says "Sent as keystrokes".
      • No passkey appears / the Mac says pairing failed: pairing may have closed (2 minutes). Tap Pair a Computer again. On the Mac, choose Forget This Device for the panel, then connect again.
      • "Send? No Computer is connected": the Mac isn't connected. Check Bluetooth on the Mac, or pair again.
    6. Restart check: turn the panel off and on, wait for it to start, then tap a Button.
    7. To go back: Gear > Mode > Switch to Wi-Fi. The Helper pairing from step 7 is kept.

    Factory reset (erases everything)

    1. With the panel off, put a finger on the top-left corner of the screen and keep it there. Turn the panel on and keep holding. The screen counts down "Keep holding to erase this panel: 10 s".
    2. After 10 s it shows "Erased. Lift your finger." Everything is gone: the admin password, the Helper pairing, the Mode, the Buttons and the Bluetooth bonds. The panel starts again at step 3.
      • To cancel, lift your finger before the countdown ends. Nothing is erased.
      • "Erase FAILED": tell main. Don't keep using the panel.
      • On a Mac that was paired over Bluetooth, also choose Forget This Device.

    If anything else fails

    1. Note the step number and the exact words on the screen, and send them to main. Don't keep power-cycling it in the middle of a step.
  6. Skeyelab commented on Oct 27, 2026

    @Skeyelab
    MemberAuthor

    Acceptance check against main (no panel)

    # Item Where it's done Test / evidence Status
    1 The first-setup screen sets the admin password #110 (lib/admin_password), #126 (the wizard: "Step 1 of 3: set an admin password") test_admin_password: test_short_passwords_and_low_iteration_counts_are_refused, test_only_the_right_password_checks_out. Eric went through this screen on 2026-10-26. Done
    2 HTTPS in its own task, and the screen stays responsive #133: web_server.cpp:367 xTaskCreate(run, "web", ...), separate from the LVGL loop and from the net task No host test (task wiring). No PR records a hardware check that the screen stays responsive during a request. Code done; responsiveness not recorded
    3 List, add, edit, delete and reorder; the panel updates without a reboot #137 test_web_editor: test_buttons_can_be_added_edited_moved_and_deleted, test_edits_apply_to_the_button_they_name, test_a_stale_edit_changes_nothing. #137 hardware run: the grid rebuilds with no reboot. Done
    4 Phrases persist across reboots and survive a firmware update #113: /phrases.json in LittleFS on the spiffs partition, written to .tmp then renamed (main.cpp:363-368) test_phrases: test_the_list_round_trips_through_json, test_a_bad_file_is_refused_and_the_list_kept. #113 hardware: the list survives a reboot. A firmware update rewrites only the app (the partition table hasn't changed since #102), but no PR records a reflash check. Reboot done; firmware update by design, not recorded
    5a Bad passwords are rate-limited #110 lib/lockout, used by the web login in #133 (admin::check, 429) test_lockout (3 tests). #133 hardware: the 6th wrong try returns 429. #137: the lockout is still on after a reboot. Done
    5b A CSRF token protects writes #133 + #156 use a different defense: every POST needs Origin: https://<panel ip> (else 403), and the session cookie is __Host-s; HttpOnly; Secure; SameSite=Strict test_web: test_a_post_needs_the_panels_own_origin, test_only_the_panels_own_address_is_an_allowed_host There's no CSRF token. The Origin check plus a SameSite=Strict cookie covers the same attack. Decision needed: accept this and amend the item, or add a token
    5c Phrase length and count are capped phrases.h: 48 Buttons, a 40-byte label, a 2,000-byte Phrase; kMaxEditBody on the request test_count_and_lengths_are_capped, test_control_characters_are_refused Done
    6 Host tests cover the phrase store and the password check test_phrases, test_web_editor, test_admin_password, test_lockout Run by the firmware check (pio test -e native) Done
    7 Factory reset clears the password and the Phrases #138: holding the top-left corner for 10 s erases the panel NVS and the whole Buttons partition (main.cpp:977) test_factory_reset (the hold logic). #138 hardware run. Done

    Gaps

    1. 5b, CSRF token. The code uses Origin plus SameSite=Strict instead. I recommend accepting that and rewording the item, since a token adds code and no protection against an attacker who can't send our Origin. If a token is wanted anyway, it's a small PR.
    2. 2 and 4, hardware checks not recorded. "The screen stays responsive during requests" and "Phrases survive a firmware update" have never been written down as hardware checks. Each is about 2 minutes when someone has the panel: open the web page while tapping, and reflash then look at the list.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions