Skip to content

Features/lockdown#4703

Open
niccellular wants to merge 8 commits intomainfrom
features/lockdown
Open

Features/lockdown#4703
niccellular wants to merge 8 commits intomainfrom
features/lockdown

Conversation

@niccellular
Copy link
Member

This PR presents one way to accomplish the "lockdown" mode I'm trying to get into firmware (meshtastic/firmware#9771) The basics of it is that with "lockdown" mode enabled on a node, physical access to an unattended node doesn't get you much. All the logging is disabled, a client connecting to the node will be required to enter a password before it can view/edit the configs, the .protos on the filesystem in flash are encrypted.

This PR plumbs in a auth mechanisms into the App such that it can talk to a locked down node. A node with lockdown mode enabled will not send complete protobufs (just defaults) to a client over BLE/USB unless they send the correct passphrase.

With these changes implemented, the App connecting to a node (BLE or USB) would be prompted to enter or set a passphrase. Without doing that the locked down node will just send you empty protobufs (defaults) not the actual contents. When sending the auth the App allows the user to specify a number of reboots and hours to allow until it expires. When it expires it wipes RAM and reboots. Which would default the node to a basic region unset, client, long_fast AQ== etc...

The App caches the passphrase so future reconnections don't prompt the user. I'm not sure if this is good or bad idea yet.

There is a new UI button created in Settings -> Security. A "Lock Now" button when pressed sends a protobuf to the node and tells it to erase the token, wipe RAM, and reboot.

jamesarich and others added 8 commits February 7, 2026 15:10
This commit introduces several enhancements to the service broadcasts and data handling:

-   **Disconnect Broadcast**: Adds and triggers a new `ACTION_MESH_DISCONNECTED` broadcast when the mesh connection state changes to `Disconnected`. This provides a more specific intent for apps to listen for disconnection events.

-   **Expanded App Port Handling**:
    -   Adds explicit broadcast actions for various app port numbers (e.g., `ATAK_PLUGIN`, `PRIVATE_APP`, `DETECTION_SENSOR_APP`).
    -   Ensures that packets for `ATAK`, `PRIVATE_APP`, and `DETECTION_SENSOR_APP` are now correctly broadcast to external applications.
    -   Implements a default behavior to broadcast any unrecognized port numbers, allowing for future extensibility and support for third-party apps.

-   **Backward Compatibility**: When broadcasting received data, a secondary broadcast with the numeric port number is also sent to maintain compatibility with older applications that may rely on it.

Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
This commit modifies the `onConnectionChanged` function to ensure that all connection state changes are reported, including transitions to the same state. Previously, redundant notifications were suppressed unless the state was `Connected`. This change allows for consistent handling of all connection events.

Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
Keeps the connection state as 'Connecting' if it was not 'Disconnected' when `handleConnected` is called.

This prevents the UI from briefly showing a disconnected state during a reconnection attempt, such as when the device is rebooting.

Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
This commit centralizes all Android Intent constants into a new `MeshtasticIntent` object within the `core/api` module.

This refactoring makes the constants accessible to external applications and removes the duplicated definitions from the main application. The app and the service example have been updated to use these new centralized constants.

Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
The `drainPacketQueueAndDispatch` test is being ignored because it is flaky and causes intermittent CI failures. This appears to be due to timing issues within the Nordic BLE mock library.

Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
This commit updates the README files for the `core:api`, `core:model`, and `core:proto` modules to improve documentation and reflect current usage.

Key changes include:
- Bumping the library version to `v2.7.13` in usage examples.
- Adding a detailed README for the `core:proto` module.
- Updating the `core:api` README to use `MeshtasticIntent` constants instead of hardcoded strings.

Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
  Implement the client-side TAK passphrase authentication flow for
  devices running TAK-locked firmware.

  Key components:
  - TakPassphraseStore: per-device passphrase persistence using
    EncryptedSharedPreferences (Android Keystore AES-256-GCM), with
    boot and hour TTL fields stored alongside the passphrase
  - TakLockHandler: orchestrates the full lock/unlock lifecycle —
    auto-unlock on reconnect using stored credentials, passphrase
    submission, token info parsing, and backoff/failure handling
  - MeshCommandSender: sendTakPassphrase() and sendTakLockNow() build
    plain local packets that bypass PKC signing and session_passkey;
    hour TTL is encoded as an absolute Unix epoch as required by firmware
  - ServiceRepository: TakLockState sealed class (None, Locked,
    NeedsProvision, Unlocked, LockNowAcknowledged, UnlockFailed,
    UnlockBackoff), TakTokenInfo (boots remaining + expiry epoch), and
    sessionAuthorized flag
  - TakUnlockDialog: Compose dialog for passphrase entry, shown on
    Locked and NeedsProvision states; onDismissRequest is a no-op to
    prevent race conditions with firmware response timing; cancel
    disconnects the user and navigates to the Connections tab
  - Lock Now (Security settings): immediately disconnects the client
    after informing firmware, purges cached config, navigates away
    without showing a passphrase dialog
  - ConnectionsScreen: suppress "region unset" prompt while the device
    is TAK-locked, since pre-auth config is zeroed/redacted and would
    lead the user to a blank LoRa settings screen
  - AIDL: sendTakUnlock() and sendTakLockNow() wired through
    MeshService → MeshActionHandler → TakLockHandler
  - Security settings: "Lock Now (TAK)" button and token info display
    showing boots remaining and expiry date
  3 files renamed (via git mv, history preserved):
  - TakLockHandler.kt → LockdownHandler.kt
  - TakPassphraseStore.kt → LockdownPassphraseStore.kt
  - TakUnlockDialog.kt → LockdownUnlockDialog.kt

  16 files updated with consistent renames across the entire codebase. No stray TAK-named symbols remain in any .kt or .aidl source file.

  What stayed the same (wire protocol / firmware-defined):
  - The firmware notification strings: "TAK_LOCKED", "TAK_NEEDS_PROVISION", "TAK_UNLOCKED", "TAK_UNLOCK_FAILED" — still matched as string literals in LockdownHandler.kt
  - Config.DeviceConfig.Role.TAK / TAK_TRACKER proto enum values
  - The SharedPrefs key changed from "tak_passphrase_store" → "lockdown_passphrase_store" (existing stored passphrases won't migrate automatically — users will need to re-enter on first launch of the updated app)
@github-actions github-actions bot added the enhancement New feature or request label Mar 3, 2026
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds “lockdown mode” support to the Android app/service so clients must authenticate to read/write real config on locked nodes, plus a “Lock Now” action and intent constant cleanup for external consumers.

Changes:

  • Introduces lockdown state/token flows and UI (unlock dialog, token display, “Lock Now” button) wired through repository/viewmodels to the service.
  • Extends AIDL/service plumbing with sendLockdownUnlock() and sendLockNow(); adds encrypted local storage for cached passphrases.
  • Centralizes broadcast Intent action constants under MeshtasticIntent and adjusts broadcast behavior/compat.

Reviewed changes

Copilot reviewed 35 out of 35 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
mesh_service_example/src/main/kotlin/com/meshtastic/android/meshserviceexample/MainActivity.kt Uses shared MeshtasticIntent action constants instead of hardcoded strings.
gradle/libs.versions.toml Adds AndroidX Security Crypto dependency for encrypted passphrase storage.
feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/SecurityConfigItemList.kt Adds “Lock Now” action and displays lockdown token expiry/boots remaining.
feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/LoRaConfigItemList.kt Avoids early-return when channel list is empty (supports locked nodes returning defaults).
feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/channel/ChannelConfigScreen.kt Avoids early-return when primary channel is missing (supports locked nodes returning defaults).
feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/RadioConfigViewModel.kt Exposes Lock Now command and lockdown token info flow.
feature/settings/src/main/kotlin/org/meshtastic/feature/settings/SettingsViewModel.kt Exposes per-connection authorization state to settings UI.
feature/settings/src/main/kotlin/org/meshtastic/feature/settings/SettingsScreen.kt Adjusts “managed” gating based on session authorization state.
core/service/src/test/kotlin/org/meshtastic/core/service/FakeIMeshService.kt Updates test stub with new lockdown AIDL methods.
core/service/src/main/kotlin/org/meshtastic/core/service/testing/FakeIMeshService.kt Updates runtime/testing stub with new lockdown AIDL methods.
core/service/src/main/kotlin/org/meshtastic/core/service/ServiceRepository.kt Adds lockdown state/token/auth flows for UI/service coordination.
core/proto/src/main/proto Updates proto submodule pointer.
core/proto/README.md Adds module documentation and usage example.
core/model/README.md Updates dependency version in docs.
core/api/src/main/kotlin/org/meshtastic/core/api/MeshtasticIntent.kt Introduces shared broadcast action/extra constants in API module.
core/api/src/main/aidl/org/meshtastic/core/service/IMeshService.aidl Adds AIDL APIs for lockdown unlock + lock-now.
core/api/README.md Documents use of MeshtasticIntent constants.
app/src/test/java/com/geeksville/mesh/repository/radio/NordicBleInterfaceDrainTest.kt Disables a flaky BLE mock timing test.
app/src/main/java/com/geeksville/mesh/ui/connections/ConnectionsViewModel.kt Exposes lockdown state to connections UI.
app/src/main/java/com/geeksville/mesh/ui/connections/ConnectionsScreen.kt Suppresses region-unset prompt until authorized on locked nodes.
app/src/main/java/com/geeksville/mesh/ui/Main.kt Adds unlock dialog + Lock Now disconnect handling; filters TAK_* notifications from generic UI toast flow.
app/src/main/java/com/geeksville/mesh/ui/LockdownUnlockDialog.kt New UI for passphrase entry/provisioning and TTL selection.
app/src/main/java/com/geeksville/mesh/service/MeshServiceBroadcasts.kt Adds compat broadcast behavior and emits explicit disconnect broadcast.
app/src/main/java/com/geeksville/mesh/service/MeshService.kt Wires new AIDL methods to action handler.
app/src/main/java/com/geeksville/mesh/service/MeshRouter.kt Adds LockdownHandler to service router.
app/src/main/java/com/geeksville/mesh/service/MeshDataHandler.kt Adjusts broadcast logic for additional port types and unknown packets.
app/src/main/java/com/geeksville/mesh/service/MeshConnectionManager.kt Hooks lockdown lifecycle on connect/disconnect; adds local config clearing helper.
app/src/main/java/com/geeksville/mesh/service/MeshCommandSender.kt Implements commands to submit passphrase/TTL and send Lock Now.
app/src/main/java/com/geeksville/mesh/service/MeshActionHandler.kt Adds handlers to forward AIDL calls to LockdownHandler.
app/src/main/java/com/geeksville/mesh/service/LockdownPassphraseStore.kt New encrypted storage for cached passphrases per device address.
app/src/main/java/com/geeksville/mesh/service/LockdownHandler.kt New core lockdown state machine (auto-unlock, backoff, lock-now handling).
app/src/main/java/com/geeksville/mesh/service/FromRadioPacketHandler.kt Routes TAK_* client notifications to lockdown handler (instead of generic notifications).
app/src/main/java/com/geeksville/mesh/service/Constants.kt Switches app constants to MeshtasticIntent and adds missing action constants.
app/src/main/java/com/geeksville/mesh/model/UIState.kt Exposes lockdown state/token and forwards unlock/lock-now calls to service.
app/build.gradle.kts Adds encrypted shared prefs dependency to the app.
Comments suppressed due to low confidence (7)

core/api/src/main/kotlin/org/meshtastic/core/api/MeshtasticIntent.kt:22

  • These self-imports (importing members of MeshtasticIntent from within the same file that defines MeshtasticIntent) are unnecessary and can be confusing; they may also trigger lint/formatting issues as unused imports. Remove them and reference the constants directly in KDoc (or keep KDoc references unqualified—Kotlin resolves them within the object).
import org.meshtastic.core.api.MeshtasticIntent.EXTRA_CONNECTED
import org.meshtastic.core.api.MeshtasticIntent.EXTRA_NODEINFO
import org.meshtastic.core.api.MeshtasticIntent.EXTRA_PACKET_ID
import org.meshtastic.core.api.MeshtasticIntent.EXTRA_STATUS

app/src/main/java/com/geeksville/mesh/service/MeshServiceBroadcasts.kt:56

  • The comment says this is “numeric port number” compatibility, but payload.dataType.toString() is not guaranteed to be numeric (it may be an enum name). If the intent is to broadcast RECEIVED.<portNumInt>, use the actual numeric port value (e.g., an explicit int field/property) rather than toString(), so compatibility broadcasts reliably match what older consumers expect.
        // Also broadcast with the numeric port number for backwards compatibility with some apps
        val numericAction = actionReceived(payload.dataType.toString())
        if (numericAction != action) {

feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/SecurityConfigItemList.kt:1

  • Newly added user-visible strings are hardcoded, which bypasses localization and makes consistency harder to maintain. Move these strings to resources (including pluralization for “boots remaining”) and use stringResource(...) so they can be translated and format-safe.
    feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/SecurityConfigItemList.kt:1
  • Newly added user-visible strings are hardcoded, which bypasses localization and makes consistency harder to maintain. Move these strings to resources (including pluralization for “boots remaining”) and use stringResource(...) so they can be translated and format-safe.
    feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/SecurityConfigItemList.kt:1
  • Newly added user-visible strings are hardcoded, which bypasses localization and makes consistency harder to maintain. Move these strings to resources (including pluralization for “boots remaining”) and use stringResource(...) so they can be translated and format-safe.
    feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/SecurityConfigItemList.kt:1
  • Creating a new SimpleDateFormat inside composition can cause unnecessary allocations on recomposition and SimpleDateFormat is generally error-prone. Prefer java.time formatting (where available) or at least wrap the formatter creation in remember(...) so it isn’t re-created every recomposition.
    app/src/test/java/com/geeksville/mesh/repository/radio/NordicBleInterfaceDrainTest.kt:55
  • Ignoring this test removes coverage for packet queue draining behavior. If CI flakiness is due to timing, consider refactoring the test to be deterministic (e.g., controlling the scheduler/virtual time, removing real delays, or asserting via mocked callbacks/events) rather than disabling it entirely.
    @Ignore("Flaky: relies on timing in the Nordic BLE mock library which causes intermittent CI failures")
    @Test
    fun `drainPacketQueueAndDispatch reads multiple packets until empty`() = runTest(testDispatcher) {

serviceRepository.setClientNotification(clientNotification)
serviceNotifications.showClientNotification(clientNotification)
val msg = clientNotification.message
if (msg.startsWith("TAK_")) {
Copy link

Copilot AI Mar 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

clientNotification.message appears to be nullable, but msg.startsWith("TAK_") will throw if msg is null. Use a safe-call check (or compare msg?.startsWith(...) == true) and only forward to handleLockdownNotification when non-null; otherwise fall back to existing notification handling.

Suggested change
if (msg.startsWith("TAK_")) {
if (msg?.startsWith("TAK_") == true) {

Copilot uses AI. Check for mistakes.
@niccellular niccellular requested a review from thebentern March 4, 2026 00:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants