# Tasks: Lockdown Mode

**Input**: Design documents from `specs/20260513-075218-lockdown-mode/`  
**Prerequisites**: plan.md ✅, spec.md ✅, research.md ✅, data-model.md ✅, contracts/ ✅, quickstart.md ✅  
**Base**: Building on Nick's PR #5439 (`features/lockdown-v2` branch, 785+ additions)

## Phase 0: Cherry-pick PR #5439

**Purpose**: Establish baseline from Nick's working proof-of-concept before refactoring

- [X] T000a Fetch Nick's `features/lockdown-v2` branch and use it as the working baseline against current `origin/main`
- [X] T000b Verify cherry-picked code compiles: `./gradlew assembleDebug` (expect lint/detekt issues — fix in later phases)
- [X] T000c Inventory PR files for subsequent refactoring: identify which files stay as-is, which move modules, which need interface extraction

---

## Phase 1: Setup

**Purpose**: Establish module structure and dependencies for lockdown feature

- [X] T001 Create `core/model/src/commonMain/kotlin/org/meshtastic/core/model/service/` lockdown state model file
- [X] T002 [P] Verify `androidx.security:security-crypto` dependency exists in `core/service/build.gradle.kts` (correct module for Android encrypted storage)
- [X] T003 [P] Verify proto submodule contains `LockdownAuth` and `LockdownStatus` generated classes in `core/proto/build/generated/source/wire/org/meshtastic/proto/`

---

## Phase 2: Foundational (Blocking Prerequisites)

**Purpose**: Extract and refactor PR #5439 code into proper KMP architecture

**⚠️ CRITICAL**: No user story work can begin until this phase is complete

**Note**: Nick's PR contains working implementations for most of these. Tasks below specify what to **port/refactor** from the PR rather than creating from scratch.

- [X] T004 Port `LockdownState` to `core/model/src/commonMain/kotlin/org/meshtastic/core/model/service/LockdownState.kt` using the shipped variants: `None`, `NeedsProvision`, `Locked(lockReason: String)`, `Unlocked`, `UnlockFailed`, `UnlockBackoff`, `LockNowAcknowledged`, plus `LockdownTokenInfo`
- [X] T005 [P] Extract `LockdownCoordinator` interface to `core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/LockdownCoordinator.kt` with `onConnect()`, `onConfigComplete()`, `onDisconnect()`, `handleLockdownStatus()`, `submitPassphrase()`, and `lockNow()`
- [X] T006 [P] Extract `LockdownPassphraseStore` interface to `core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/LockdownPassphraseStore.kt` with `getPassphrase(deviceAddress)`, `savePassphrase(...)`, and `clearPassphrase(deviceAddress)`
- [X] T007 Keep Android `LockdownPassphraseStoreImpl` in `core/service/src/androidMain/kotlin/org/meshtastic/core/service/LockdownPassphraseStoreImpl.kt` using EncryptedSharedPreferences
- [X] T008 [P] Implement `LockdownPassphraseStoreImpl` for JVM in `core/service/src/jvmMain/kotlin/org/meshtastic/core/service/LockdownPassphraseStoreImpl.kt` — PKCS12 KeyStore + AES-256-GCM file-backed store under `$MESHTASTIC_DATA_DIR/lockdown/` (default `~/.meshtastic/lockdown/`)
- [X] T009 [P] No iOS implementation in this branch; limit platform support to Android + JVM/Desktop
- [X] T010 Extract state machine logic from PR's `LockdownHandlerImpl` (currently in `core/service/src/androidMain/`) to `core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/LockdownCoordinatorImpl.kt` — keep auto-replay, wasLockNow flag, pending passphrase tracking. Remove Android/AIDL dependencies so it compiles in commonMain.
- [X] T010b Keep thin AIDL adapter in `core/service/src/androidMain/` that delegates to `LockdownCoordinatorImpl` for `MeshService` IPC calls (`sendLockdownPassphrase`, `sendLockNow`)
- [X] T011 Verify PR's `FromRadioPacketHandlerImpl` `lockdown_status` dispatch is intact; add `coordinator.onConfigComplete()` call from config completion handler if not already present
- [X] T012 Verify PR's `CommandSenderImpl` extensions (`sendLockdownPassphrase`/`sendLockNow`) are intact; adapt method signatures if coordinator interface changed
- [X] T012b Wire `LockdownCoordinator.onConnect()` / `onDisconnect()` into `MeshConnectionManagerImpl` connection lifecycle callbacks
- [X] T012c Expose `lockdownState: StateFlow<LockdownState>` and `sessionAuthorized: StateFlow<Boolean>` via `ServiceRepository` (port from PR's existing exposure)
- [X] T013 Register `LockdownCoordinator` and `LockdownPassphraseStore` bindings in Koin DI — use `@Single` annotation on impl classes (`LockdownCoordinatorImpl`, `LockdownPassphraseStoreImpl`) and `@Module` on containing Koin module per project convention

**Checkpoint**: Foundation ready — coordinator processes lockdown status, sends auth, manages state. AIDL layer delegates to coordinator. User story UI can begin.

---

## Phase 3: User Story 1 — Unlock a Locked Node (Priority: P1) 🎯 MVP

**Goal**: User connects to a locked node, enters passphrase, node unlocks, full config accessible.

**Independent Test**: Connect to a locked node → enter correct passphrase → verify UNLOCKED state and config access.

### Implementation for User Story 1

- [X] T014 [US1] Move and refactor Nick's `LockdownUnlockDialog` from `app/src/main/.../ui/` to `feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/lockdown/LockdownDialog.kt` — adapt to a non-dismissable AlertDialog with passphrase fields, submit button, error display, and disconnect option (`onDismissRequest = {}`)
- [X] T015 [US1] Implement unlock flow in `LockdownDialog`: passphrase entry → call `coordinator.submitPassphrase()` → show loading state → handle UNLOCKED/UNLOCK_FAILED transitions
- [X] T016 [US1] Implement backoff enforcement in `LockdownDialog`: when `UnlockFailed(backoffSeconds > 0)`, show countdown timer and disable Submit button until backoff expires
- [X] T017 [US1] Integrate `LockdownDialog` in the app shell via ViewModel-exposed `lockdownState`; show it when state is `Locked`, `NeedsProvision`, `UnlockFailed`, or `UnlockBackoff`, hide it for `None`, `Unlocked`, and `LockNowAcknowledged`
- [X] T018 [US1] Add string resources for lockdown UI: "Unlock Device", "Enter passphrase", "Incorrect passphrase", "Retry in %d seconds", "Disconnect" in `core/resources/src/commonMain/composeResources/values/strings.xml`
- [X] T019 [US1] Run `python3 scripts/sort-strings.py` after adding string resources

**Checkpoint**: User Story 1 complete — locked nodes can be unlocked via non-dismissable dialog.

---

## Phase 4: User Story 2 — Provision a New Lockdown Passphrase (Priority: P1)

**Goal**: User connects to an unprovisioned node, creates a passphrase with optional TTL, node provisions DEK and unlocks.

**Independent Test**: Connect to unprovisioned node → set passphrase → verify UNLOCKED with session info.

### Implementation for User Story 2

- [X] T020 [US2] Add provision mode to `LockdownDialog`: when state is `NeedsProvision`, show "Set Passphrase" title, passphrase + confirm fields, optional "Boots remaining" and "Hours until expiry" number inputs
- [X] T021 [US2] Implement passphrase validation: non-empty, 1-64 UTF-8 bytes, confirm field matches, provisioning TTL fields use integer `boots` / `hours`
- [X] T022 [US2] Convert "hours until expiry" user input to `valid_until_epoch` (current Unix time + hours * 3600) before sending to coordinator
- [X] T023 [US2] Add string resources for provision mode: "Set Passphrase", "Confirm passphrase", "Passphrases do not match", "Boots remaining (optional)", "Hours until expiry (optional)" in `core/resources/src/commonMain/composeResources/values/strings.xml`
- [X] T024 [US2] Run `python3 scripts/sort-strings.py` after adding string resources

**Checkpoint**: User Story 2 complete — unprovisioned nodes can be set up with a passphrase.

---

## Phase 5: User Story 3 — Lock Now (Priority: P2)

**Goal**: User presses "Lock Now" in Security settings, device re-locks and reboots, app disconnects gracefully.

**Independent Test**: Unlock node → press Lock Now → verify device disconnects and next connection requires auth.

### Implementation for User Story 3

- [X] T025 [US3] Integrate a Lock Now action directly into `feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/SecurityConfigScreen.kt`; enable only when `sessionAuthorized == true`
- [X] T026 [US3] Wire the Lock Now action through `RadioConfigViewModel.sendLockNow()`
- [X] T027 [US3] Implement explicit disconnect in `LockdownCoordinatorImpl` after `LockNowAcknowledged` state: delay 500ms → call connection manager disconnect
- [X] T028 [US3] Handle `LockNowAcknowledged` without flashing the unlock dialog; reset state after the disconnect path completes
- [X] T029 [US3] Add string resources: "Lock Now", "Locking device...", "Device locked", "Device is locked" in `core/resources/src/commonMain/composeResources/values/strings.xml`
- [X] T030 [US3] Run `python3 scripts/sort-strings.py` after adding string resources

**Checkpoint**: User Story 3 complete — users can actively re-lock devices.

---

## Phase 6: User Story 4 — Cached Passphrase Auto-Reconnect (Priority: P2)

**Goal**: Returning users reconnect without re-entering passphrase; auto-replay handles it transparently.

**Independent Test**: Authenticate → disconnect → reconnect → verify no passphrase prompt appears.

### Implementation for User Story 4

- [X] T031 [US4] Implement auto-replay in `LockdownCoordinatorImpl`: on `Locked`, check `passphraseStore.getPassphrase(deviceAddress)` and automatically send the cached passphrase when present
- [X] T032 [US4] Implement cache-on-success in `LockdownCoordinatorImpl`: on `Unlocked` after manual submit, call `passphraseStore.savePassphrase(deviceAddress, passphrase, boots, hours)`
- [X] T033 [US4] Implement cache-clear-on-failure: on auto-replay `UnlockFailed` with no backoff, call `passphraseStore.clearPassphrase(deviceAddress)` and return to `Locked`
- [X] T034 [US4] Add visual indicator in `LockdownDialog` for auto-replay in progress: show "Authenticating..." with spinner instead of passphrase fields while auto-replay is attempted

**Checkpoint**: User Story 4 complete — reconnections are seamless for cached passphrases.

---

## Phase 7: User Story 5 — View Session Token Status (Priority: P3)

**Goal**: Users see remaining session lifetime (boots, expiry) in Security settings.

**Independent Test**: Unlock node → view Security settings → verify boots remaining and expiry displayed.

### Implementation for User Story 5

- [X] T035 [US5] Create `LockdownSessionStatus` composable in `feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/lockdown/LockdownSessionStatus.kt` displaying boots remaining and formatted expiry time
- [X] T036 [US5] Wire `LockdownSessionStatus` into `SecurityConfigScreen` above the Lock Now action — visible only when `sessionAuthorized == true`
- [X] T037 [US5] Add string resources: "Session: %d reboots remaining", "expires %s", "no time limit", "no expiry configured" in `core/resources/src/commonMain/composeResources/values/strings.xml`
- [X] T038 [US5] Run `python3 scripts/sort-strings.py` after adding string resources

**Checkpoint**: User Story 5 complete — session TTL info visible in settings.

---

## Phase 8: Polish & Cross-Cutting Concerns

**Purpose**: Banner gating, privacy audit, lint, and final validation

- [X] T039 [P] Gate lockdown-sensitive actions on `sessionAuthorized` / `lockdownState` from `ServiceRepository`
- [X] T040 [P] Audit `LockdownCoordinatorImpl` and `LockdownPassphraseStoreImpl` logs: ensure no passphrase content is logged and avoid logging full device addresses
- [X] T041 [P] Review lockdown UI against Meshtastic design standards: M3 components, accessibility (TalkBack semantics, touch targets), typography hierarchy
- [X] T042 [P] Confirm no logs, telemetry, or config changes expose PII, location data, secrets, or modify `core/proto`
- [X] T043 [P] Verify `LockdownCoordinator.onDisconnect()` is called on connection disconnect (already wired in T012b) to ensure clean state for next connection
- [X] T043b [P] Write unit tests for `LockdownCoordinatorImpl` state machine: cover all 8 state transitions, auto-replay success/failure, lock-now flow with wasLockNow flag, onDisconnect reset, and backoff enforcement
- [X] T044 Run `./gradlew spotlessApply spotlessCheck detekt` for all touched modules
- [X] T045 Run `./gradlew assembleDebug test allTests` to verify compilation and tests pass
- [X] T046 Verify build with `./gradlew :core:model:allTests :core:repository:allTests :core:data:allTests :core:datastore:allTests :feature:settings:allTests`

---

## Dependencies & Execution Order

### Phase Dependencies

- **Phase 0 (Cherry-pick)**: No dependencies — must complete first to establish baseline code
- **Phase 1 (Setup)**: Depends on Phase 0 — verify module structure after cherry-pick
- **Phase 2 (Foundational)**: Depends on Phase 1 — refactors PR code into KMP architecture. BLOCKS all user stories
- **Phases 3-4 (US1, US2)**: Both depend on Phase 2; can run in parallel (US1 and US2 share the same `LockdownDialog` composable but address different states)
- **Phase 5 (US3)**: Depends on Phase 2; independent of US1/US2
- **Phase 6 (US4)**: Depends on Phase 2 + Phase 3 (auto-replay triggers from the same Locked state as US1)
- **Phase 7 (US5)**: Depends on Phase 5 (session status displayed near Lock Now button)
- **Phase 8 (Polish)**: Depends on all desired user stories being complete

### User Story Dependencies

- **US1 (Unlock)**: Phase 2 only — independently testable
- **US2 (Provision)**: Phase 2 only — independently testable (shares LockdownDialog with US1)
- **US3 (Lock Now)**: Phase 2 only — independently testable
- **US4 (Auto-Reconnect)**: Phase 2 + US1 (needs unlock flow to cache passphrase first)
- **US5 (Session Status)**: Phase 2 + US3 (displayed alongside Lock Now button)

### Parallel Opportunities

Within Phase 2:
- T005, T006 can run in parallel (independent interface extractions)
- T007, T008, T009 can run in parallel (platform impls of same interface)
- T010 and T010b can be done together (split coordinator from AIDL adapter)

Within user stories:
- US1 and US2 can be developed together (same screen, different states)
- US3 is fully independent
- All string resource tasks are parallelizable

---

## Implementation Strategy

### MVP First (User Stories 1 + 2)

1. Complete Phase 0: Cherry-pick PR #5439 (baseline)
2. Complete Phase 1: Verify setup
3. Complete Phase 2: Refactor into KMP architecture (extract interfaces, move modules, split commonMain/androidMain)
4. Complete Phase 3 + 4: US1 + US2 together (they share `LockdownDialog`)
5. **STOP and VALIDATE**: Test unlock and provision flows
6. This delivers a functional lockdown client for day-one firmware support

### Incremental Delivery

1. Cherry-pick + Setup → Compilable baseline from PR
2. Foundational refactor → KMP-proper state machine
3. US1 + US2 → Unlock and provision functional (MVP!)
3. US3 → Lock Now button in Security settings
4. US4 → Auto-reconnect for returning users
5. US5 → Session info display
6. Polish → Banner gating, audit, lint
