Skip to content

feat!: auto-discovered devices start disabled (opt-in) - #25

Merged
odtgit merged 2 commits into
masterfrom
feat/opt-in-devices
Sep 28, 2026
Merged

odtgit merged 2 commits into
masterfrom
feat/opt-in-devices

Conversation

@odtgit

@odtgit odtgit commented Sep 28, 2026

Copy link
Copy Markdown
Owner

Summary

Security fix (adversarial review finding, opt-in chosen by the owner): since 2.1.0, an empty enabled_devices meant "enable all", so every auto-discovered keyboard-like device was enabled and publishing its keys to MQTT by default. On any host with a local keyboard attached, that's keylogging to the broker out of the box, since auto-discovered devices are read but not grabbed.

What changes for users

  • A device listed in devices or enabled_devices (by id, path, symlink or name) starts enabled, as before.
  • Every other device, including every auto-discovered one, now starts disabled: it still gets Home Assistant discovery entities, but nothing is read into events, nothing is published, and it is not grabbed until a person enables it via the HA switch (or lists it in config).
  • Once a device's enable state has been set (by config or by the switch), it's persisted in the state file and wins on every future start, same as today.

State file migration

The state file gets a schema version (2). 2.0/2.1 state files were seeded "enabled": true for every device because of the old all-enabled default, so an old file can't tell a real user choice apart from that default. On loading an old or unversioned file:

  • "enabled": true is kept only for devices that still match devices/enabled_devices
  • every other device is reset to "enabled": false
  • one clear WARNING names every device that was disabled and how to re-enable it
  • the file is rewritten at the new schema version

A schema-2+ file is trusted as-is (no re-migration). Migration is unit-tested in tests/test_state.py (unversioned file, explicit version: 1, id/path/name matching, already-off devices, no-op when nothing needs disabling).

Reader / grab behaviour

DeviceReader already gates both grab and event emission on the enabled flag, and keeps draining the device fd while disabled (only skips constructing/publishing the event) rather than pausing reads — so there's no stale-kernel-buffer replay when a device is enabled later. Added scenario coverage for this at the gateway level (tests/test_gateway.py) and against a real kernel device via uinput (tests/integration/test_uinput.py, CI-only tier).

Docs

README: config table, "Device selection", "Enable, grab and persistence" (states the privacy rationale), and a new "Upgrading to 3.0" section. translations/en.yaml option descriptions updated. No DOCS.md exists on master.

Test plan

  • pytest -m "not broker and not uinput" — 212 passed
  • ruff check / ruff format --check / mypy src — clean
  • New scenario tests: auto-discovered device (entities published, switch OFF, key press publishes nothing, then enable via switch publishes and persists across restart), listed device starts enabled, migration from an old state file (unit tests)
  • uinput tier updated (test_disabled_at_start_is_not_grabbed now seeds a pre-2 state file) — not run locally per instructions (keystrokes leak to the desktop); CI runs it
  • broker tier — mosquitto not available in this environment; existing broker roundtrip tests (test_switch_roundtrip_and_events, test_persistence_roundtrip) already use a listed device so are unaffected by the default change; CI runs them

https://claude.ai/code/session_01TJskTZQqRHiHQBc8kE5Lr2

Since 2.1.0 an empty enabled_devices meant "enable all", so every
auto-discovered keyboard-like device was enabled and publishing keys
by default. On a host with a local keyboard that is keylogging to the
broker out of the box. Found by an adversarial security review.

Only devices listed in devices or enabled_devices now start enabled;
everything else, including every auto-discovered device, starts
disabled and must be opted in via the HA switch or the config.

State file gets a schema version (2). A 2.0/2.1 file was seeded "on"
for all devices under the old default, so "on" can't be told apart
from a real choice; on load, such a file is migrated: "on" is kept
only for devices matching devices/enabled_devices, everything else is
disabled, one WARNING names what was disabled and how to re-enable it,
then the file is rewritten at the new schema. A version-2+ file is
trusted as-is.

Reader behaviour already gated publish/grab on the enabled flag and
kept draining the fd while disabled, so a disabled device was already
safe against a stale-buffer replay on enable; added scenario coverage
for it at the gateway and real-kernel-device (uinput) tiers.

Docs: README (config table, device selection, enable/grab/persistence,
new "Upgrading to 3.0" section) and translations/en.yaml updated.
@odtgit
odtgit merged commit 4f5a20f into master Sep 28, 2026
16 checks passed
@odtgit
odtgit deleted the feat/opt-in-devices branch September 28, 2026 17:55
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