[READ-ONLY] Mirror of https://github.com/jmrplens/kleidos. Kleidos — Hardware BLE password manager for ESP32-S3 (M5StickC Plus2, StickS3, M5Stack Gray, T-Deck, Cardputer) jmrplens.github.io/kleidos/
0

Configure Feed

Select the types of activity you want to include in your feed.

Group AppContext fields into cohesive sub-structs (S1820)

Reorganize the AppContext shared-context struct from ~29 flat data members into 13:
keep the 7 injected hardware references and the appState/prevState FSM cursor at top
level, and group the session/config state into four cohesive nested sub-structs —
display (activity + dim/off timing), vault (masterKey / unlock / autolock /
bleCredential), menu (credential + TOTP lists + selection), and boot (bootCount /
rtcAvailable). Re-path every ctx access across main.cpp, the state handlers, the
extracted controllers/views, and the debug console accordingly.

Pure data reorganization, behavior-preserving: no logic, timing, or FSM change; the
security-critical goToSleep wipe (secureWipe masterKey -> bleCredential.wipe ->
vaultUnlocked=false -> credListLoaded=false) is byte-identical, only re-pathed. Drops
AppContext under the Sonar S1820 (<20 fields) threshold.

Verified: native 1168/1168, pio check clean, guards, full fleet -Werror (9 variants +
debug).

Claude-Session: https://claude.ai/code/session_01Y7Cy1HetRp6TZAUAeekN8X

José M. Requena Plens (Jul 2, 2026, 7:53 PM +0200) 4bf58c94 85f23311

+419 -390
+26 -23
src/main.cpp
··· 452 452 appCtx.resetActivity(); 453 453 } 454 454 VaultStore::end(); 455 - appCtx.credListLoaded = false; 455 + appCtx.menu.credListLoaded = false; 456 456 appCtx.resetActivity(); 457 457 return added; 458 458 } ··· 762 762 { 763 763 NvsStore prefs; 764 764 if ( prefs.open( "kleidos", NvsStore::OpenMode::ReadWrite ) ) { 765 - ctx().bootCount = prefs.getU32( "boots", 0 ) + 1; 766 - prefs.setU32( "boots", ctx().bootCount ); 765 + ctx().boot.bootCount = prefs.getU32( "boots", 0 ) + 1; 766 + prefs.setU32( "boots", ctx().boot.bootCount ); 767 767 768 768 if ( const uint8_t alm = prefs.getU8( "auto_lock", 0 ); 769 769 alm <= std::to_underlying( AutoLockMode::TIMEOUT ) ) { 770 - ctx().autoLockMode = static_cast<AutoLockMode>( alm ); 770 + ctx().vault.autoLockMode = static_cast<AutoLockMode>( alm ); 771 771 } 772 772 773 - ctx().displayDimMs = prefs.getU32( "disp_dim_ms", 5000 ); 774 - ctx().displayOffMs = prefs.getU32( "disp_off_ms", 10000 ); 775 - ctx().idleSleepMs = prefs.getU32( "idle_slp_ms", 60000 ); 773 + ctx().display.displayDimMs = prefs.getU32( "disp_dim_ms", 5000 ); 774 + ctx().display.displayOffMs = prefs.getU32( "disp_off_ms", 10000 ); 775 + ctx().display.idleSleepMs = prefs.getU32( "idle_slp_ms", 60000 ); 776 776 777 777 // PIN-arc sweep duration: clamp the persisted value to the viable 778 778 // window so a stale / out-of-range value can never brick the dial. 779 - ctx().pinArcSweepMs = 779 + ctx().display.pinArcSweepMs = 780 780 pin_arc::clampSweepMs( prefs.getU32( "pin_arc_ms", pin_arc::kDefaultSweepMs ) ); 781 781 782 782 #ifdef DEBUG_SERIAL_BUTTONS ··· 785 785 // timeouts so QA capture sessions are not interrupted. Persisted in 786 786 // NVS; re-applied here on every boot. Debug builds only. 787 787 if ( prefs.getU8( "disp_stayon", 0 ) != 0 ) { 788 - ctx().displayDimMs = UINT32_MAX; 789 - ctx().displayOffMs = UINT32_MAX; 790 - ctx().idleSleepMs = UINT32_MAX; 788 + ctx().display.displayDimMs = UINT32_MAX; 789 + ctx().display.displayOffMs = UINT32_MAX; 790 + ctx().display.idleSleepMs = UINT32_MAX; 791 791 KLEIDOS_LOGW( kTag, "DISPKEEP: display stay-on enabled from NVS" ); 792 792 } 793 793 #endif // DEBUG_SERIAL_BUTTONS 794 794 } else { 795 - ctx().bootCount = 1; 795 + ctx().boot.bootCount = 1; 796 796 } 797 797 } 798 798 ··· 800 800 // Debug-only: keep the device awake indefinitely so the screenshot 801 801 // capture scripts can navigate the UI without the firmware deep-sleeping 802 802 // mid-session. Never enable in production builds. 803 - ctx().displayDimMs = UINT32_MAX; 804 - ctx().displayOffMs = UINT32_MAX; 805 - ctx().idleSleepMs = UINT32_MAX; 803 + ctx().display.displayDimMs = UINT32_MAX; 804 + ctx().display.displayOffMs = UINT32_MAX; 805 + ctx().display.idleSleepMs = UINT32_MAX; 806 806 KLEIDOS_LOGW( kTag, "DEBUG: auto-lock and display timeouts disabled" ); 807 807 #endif // DISABLE_AUTO_LOCK_DEBUG 808 808 809 - ctx().rtcAvailable = rtc::isAvailable(); 810 - if ( ctx().rtcAvailable && ctx().bootCount == 1 ) { 809 + ctx().boot.rtcAvailable = rtc::isAvailable(); 810 + if ( ctx().boot.rtcAvailable && ctx().boot.bootCount == 1 ) { 811 811 rtc::setTime( { 12, 0, 0 } ); 812 812 rtc::setDate( { 2026, 4, 20 } ); 813 813 KLEIDOS_LOGI( kTag, "RTC initialized to 2026-04-20 12:00:00" ); ··· 816 816 // Serial banner 817 817 KLEIDOS_LOGI( kTag, "========================================" ); 818 818 KLEIDOS_LOGI( kTag, "Kleidos v%s — %s", KLEIDOS_VERSION, brd.deviceName ); 819 - KLEIDOS_LOGI( kTag, "Boot #%u | Wake: %s", ctx().bootCount, brd.power->getWakeReasonString() ); 819 + KLEIDOS_LOGI( kTag, "Boot #%u | Wake: %s", ctx().boot.bootCount, 820 + brd.power->getWakeReasonString() ); 820 821 brd.power->logBatteryStatus(); 821 822 822 823 uint8_t attempts = BruteForceGuard::getAttempts(); 823 - KLEIDOS_LOGI( kTag, "PIN attempts: %u | RTC: %s", attempts, ctx().rtcAvailable ? "OK" : "N/A" ); 824 + KLEIDOS_LOGI( kTag, "PIN attempts: %u | RTC: %s", attempts, 825 + ctx().boot.rtcAvailable ? "OK" : "N/A" ); 824 826 (void)attempts; 825 827 KLEIDOS_LOGI( kTag, "========================================" ); 826 828 ··· 1096 1098 case PinEntry: 1097 1099 case VaultMenu: 1098 1100 case BleHidTyping: { 1099 - uint32_t idle = nowMs - ctx().lastActivity; 1100 - uint32_t remaining = ( idle >= ctx().idleSleepMs ) ? 0 : ( ctx().idleSleepMs - idle ); 1101 + uint32_t idle = nowMs - ctx().display.lastActivity; 1102 + uint32_t remaining = 1103 + ( idle >= ctx().display.idleSleepMs ) ? 0 : ( ctx().display.idleSleepMs - idle ); 1101 1104 // "activity reset" arms the toast again for the next idle window. 1102 1105 static uint32_t s_lastSeenActivity = 0; 1103 - bool justReset = ( ctx().lastActivity != s_lastSeenActivity ); 1104 - s_lastSeenActivity = ctx().lastActivity; 1106 + bool justReset = ( ctx().display.lastActivity != s_lastSeenActivity ); 1107 + s_lastSeenActivity = ctx().display.lastActivity; 1105 1108 status_notifier::notifyAutoLockCountdown( remaining, justReset ); 1106 1109 break; 1107 1110 }
+3 -3
src/states/admin_state.cpp
··· 56 56 // platformSystem::restart() is [[noreturn]]; nothing after it runs. 57 57 platformSystem::restart(); 58 58 } 59 - AdminPortal::begin( ctx_.masterKey.data(), ctx_.power ); 59 + AdminPortal::begin( ctx_.vault.masterKey.data(), ctx_.power ); 60 60 } 61 61 KLEIDOS_LOGI( kTag, "Entering admin mode" ); 62 62 } ··· 153 153 drawAdminScreen(); 154 154 rtos::delayMs( 2000 ); 155 155 AdminPortal::end(); 156 - VaultCrypto::secureWipe( ctx_.masterKey.data(), sizeof( ctx_.masterKey ) ); 157 - ctx_.bleCredential.wipe(); 156 + VaultCrypto::secureWipe( ctx_.vault.masterKey.data(), sizeof( ctx_.vault.masterKey ) ); 157 + ctx_.vault.bleCredential.wipe(); 158 158 platformSystem::restart(); 159 159 } 160 160
+1 -1
src/states/admin_state.h
··· 20 20 * (Hold-A to stop, BtnB toggle QR/info) 21 21 * and 3-button (BtnA/BtnC toggle, BtnB exit) layouts are supported. 22 22 * 23 - * @warning SECURITY: the master key lives in @c ctx_.masterKey for the 23 + * @warning SECURITY: the master key lives in @c ctx_.vault.masterKey for the 24 24 * entire session. The state zeroizes it before reboot and on 25 25 * @c exit(); never read or log it from this state. The portal 26 26 * password is derived from this key — leaving the state without
+76 -61
src/states/app_context.h
··· 165 165 AppState appState = AppState::BootCheck; 166 166 AppState prevState = AppState::BootCheck; 167 167 168 - // Timing and display 169 - uint32_t lastActivity = 0; 170 - bool displayDimmed = false; 171 - bool fullRedraw = true; 168 + /// Timing, display-dimming state, and the NVS-configurable idle timeouts. 169 + struct Display { 170 + uint32_t lastActivity = 0; 171 + bool displayDimmed = false; 172 + bool fullRedraw = true; 172 173 173 - // NVS-configurable timeouts 174 - uint32_t idleSleepMs = 60000; 175 - uint32_t displayDimMs = 20000; 176 - uint32_t displayOffMs = 30000; 174 + // NVS-configurable timeouts 175 + uint32_t idleSleepMs = 60000; 176 + uint32_t displayDimMs = 20000; 177 + uint32_t displayOffMs = 30000; 178 + 179 + // PIN-entry timing-arc full-sweep duration (0->9), NVS-configurable. 180 + // Clamped to a viable minimum at load + edit so a too-fast sweep can 181 + // never make the dial uncontrollable (see pin_arc::kMinSweepMs / 182 + // kMaxSweepMs). 183 + uint32_t pinArcSweepMs = pin_arc::kDefaultSweepMs; 184 + } display; 177 185 178 - // PIN-entry timing-arc full-sweep duration (0->9), NVS-configurable. Clamped 179 - // to a viable minimum at load + edit so a too-fast sweep can never make the 180 - // dial uncontrollable (see pin_arc::kMinSweepMs / kMaxSweepMs). 181 - uint32_t pinArcSweepMs = pin_arc::kDefaultSweepMs; 186 + /// Unlock session: master key, unlock flag, auto-lock policy, and the 187 + /// credential staged for BLE HID typing. Wiped on the deep-sleep path. 188 + struct VaultSession { 189 + std::array<uint8_t, kAesKeySize> masterKey = {}; 190 + bool vaultUnlocked = false; 191 + AutoLockMode autoLockMode = AutoLockMode::AfterSend; 182 192 183 - // Vault 184 - std::array<uint8_t, kAesKeySize> masterKey = {}; 185 - bool vaultUnlocked = false; 186 - AutoLockMode autoLockMode = AutoLockMode::AfterSend; 193 + // BLE credential (loaded for typing) 194 + Credential bleCredential = {}; 187 195 #ifdef DEBUG_SERIAL_BUTTONS 188 - bool debugMockVaultPreview = false; 196 + bool debugMockVaultPreview = false; 189 197 #endif // DEBUG_SERIAL_BUTTONS 198 + } vault; 190 199 191 - // Credential menu 192 - std::array<CredentialEntry, kMaxCredentials> credEntries = {}; 193 - uint8_t credCount = 0; 194 - bool credListLoaded = false; 200 + /// Cached credential / TOTP lists and the unified menu cursor. 201 + struct MenuModel { 202 + // Credential menu 203 + std::array<CredentialEntry, kMaxCredentials> credEntries = {}; 204 + uint8_t credCount = 0; 205 + bool credListLoaded = false; 195 206 196 - // TOTP menu 197 - std::array<CredentialEntry, kMaxTotp> totpEntries = {}; 198 - uint8_t totpCount = 0; 207 + // TOTP menu 208 + std::array<CredentialEntry, kMaxTotp> totpEntries = {}; 209 + uint8_t totpCount = 0; 199 210 200 - // Unified menu selection 201 - uint8_t menuSelected = 0; 211 + // Unified menu selection 212 + uint8_t menuSelected = 0; 213 + } menu; 202 214 203 - // BLE credential (loaded for typing) 204 - Credential bleCredential = {}; 205 - 206 - // Boot info 207 - uint32_t bootCount = 0; 208 - bool rtcAvailable = false; 215 + /// Boot counter and RTC availability captured at BootCheck. 216 + struct BootInfo { 217 + uint32_t bootCount = 0; 218 + bool rtcAvailable = false; 219 + } boot; 209 220 210 221 // ----------------------------------------------------------------------- 211 222 // Shared methods ··· 218 229 * call multiple times per tick. 219 230 */ 220 231 void resetActivity() { 221 - lastActivity = kleidos::platform::clock::millis32(); 222 - if ( displayDimmed ) { 223 - display::setBrightness( kBrightnessFull ); 224 - displayDimmed = false; 232 + display.lastActivity = kleidos::platform::clock::millis32(); 233 + if ( display.displayDimmed ) { 234 + ::display::setBrightness( kBrightnessFull ); 235 + display.displayDimmed = false; 225 236 } 226 237 } 227 238 ··· 273 284 KLEIDOS_LOGW( kAppTag, "Keyboard bring-up: sleep request ignored" ); 274 285 #else 275 286 KLEIDOS_LOGW( kAppTag, "DEBUG: sleep/deep-sleep request ignored" ); 276 - display::wakeup(); 277 - display::setBrightness( kBrightnessFull ); 278 - displayDimmed = false; 287 + ::display::wakeup(); 288 + ::display::setBrightness( kBrightnessFull ); 289 + display.displayDimmed = false; 279 290 #endif // HAS_KEYBOARD 280 291 resetActivity(); 281 292 } ··· 314 325 KLEIDOS_LOGW( kAppTag, "BLE name store flush deferred/failed" ); 315 326 } 316 327 317 - VaultCrypto::secureWipe( masterKey.data(), sizeof( masterKey ) ); 318 - bleCredential.wipe(); 319 - vaultUnlocked = false; 320 - credListLoaded = false; 328 + VaultCrypto::secureWipe( vault.masterKey.data(), sizeof( vault.masterKey ) ); 329 + vault.bleCredential.wipe(); 330 + vault.vaultUnlocked = false; 331 + menu.credListLoaded = false; 321 332 audio.end(); 322 333 323 334 // Deep sleep is a shutdown path: do not write NVS here. ··· 325 336 // app-side NVS access even after NimBLEDevice::deinit(true). 326 337 // The RTC itself survives sleep; diagnostic sleep snapshots are not 327 338 // worth risking the power-off path. 328 - if ( rtcAvailable ) { 339 + if ( boot.rtcAvailable ) { 329 340 const RtcDate d = rtc::getDate(); 330 341 const RtcTime t = rtc::getTime(); 331 342 KLEIDOS_LOGI( kAppTag, "RTC before sleep: %04d-%02d-%02d %02d:%02d:%02d", d.year, ··· 402 413 */ 403 414 void loadCredentialList() { 404 415 #ifdef DEBUG_SERIAL_BUTTONS 405 - if ( debugMockVaultPreview ) 416 + if ( vault.debugMockVaultPreview ) 406 417 return; 407 418 #endif // DEBUG_SERIAL_BUTTONS 408 - if ( credListLoaded ) { 419 + if ( menu.credListLoaded ) { 409 420 return; 410 421 } 411 422 if ( !VaultStore::begin() ) { ··· 413 424 return; 414 425 } 415 426 // G1: Use index cache for fast loading (falls back to legacy if missing) 416 - credCount = VaultStore::loadIndex( masterKey.data(), credEntries.data(), kMaxCredentials ); 417 - totpCount = VaultStore::loadTotpIndex( masterKey.data(), totpEntries.data(), kMaxTotp ); 427 + menu.credCount = VaultStore::loadIndex( vault.masterKey.data(), menu.credEntries.data(), 428 + kMaxCredentials ); 429 + menu.totpCount = 430 + VaultStore::loadTotpIndex( vault.masterKey.data(), menu.totpEntries.data(), kMaxTotp ); 418 431 VaultStore::end(); 419 - credListLoaded = true; 432 + menu.credListLoaded = true; 420 433 421 - if ( menuSelected >= credCount + totpCount && credCount + totpCount > 0 ) { 422 - menuSelected = 0; 434 + if ( menu.menuSelected >= menu.credCount + menu.totpCount 435 + && menu.credCount + menu.totpCount > 0 ) { 436 + menu.menuSelected = 0; 423 437 } 424 438 425 - KLEIDOS_LOGI( kAppTag, "Vault: %u credentials, %u TOTP entries", credCount, totpCount ); 439 + KLEIDOS_LOGI( kAppTag, "Vault: %u credentials, %u TOTP entries", menu.credCount, 440 + menu.totpCount ); 426 441 } 427 442 428 443 /** ··· 443 458 // Debug/UI-test builds keep the display fully awake for captures. 444 459 return; 445 460 #else 446 - uint32_t idle = kleidos::platform::clock::millis32() - lastActivity; 447 - if ( !displayDimmed && idle >= displayDimMs ) { 448 - display::setBrightness( kBrightnessDim ); 449 - displayDimmed = true; 461 + uint32_t idle = kleidos::platform::clock::millis32() - display.lastActivity; 462 + if ( !display.displayDimmed && idle >= display.displayDimMs ) { 463 + ::display::setBrightness( kBrightnessDim ); 464 + display.displayDimmed = true; 450 465 } 451 - if ( displayDimmed && idle >= displayOffMs ) { 466 + if ( display.displayDimmed && idle >= display.displayOffMs ) { 452 467 power.displayOff(); 453 468 } 454 469 #endif // HAS_KEYBOARD || defined( DISABLE_AUTO_LOCK_DEBUG ) ··· 461 476 * returns to full brightness the moment the user presses a button. 462 477 */ 463 478 void wakeDisplay() { 464 - if ( displayDimmed ) { 465 - display::wakeup(); 466 - display::setBrightness( kBrightnessFull ); 467 - displayDimmed = false; 479 + if ( display.displayDimmed ) { 480 + ::display::wakeup(); 481 + ::display::setBrightness( kBrightnessFull ); 482 + display.displayDimmed = false; 468 483 } 469 484 } 470 485 };
+19 -19
src/states/ble_state.cpp
··· 105 105 if ( !accept ) { 106 106 KLEIDOS_LOGW( kTag, "Pairing rejected by user" ); 107 107 kble::Facade::stop(); 108 - ctx_.bleCredential.wipe(); 108 + ctx_.vault.bleCredential.wipe(); 109 109 return AppState::VaultMenu; 110 110 } 111 111 } ··· 152 152 screenDrawn_ = false; // force redraw of underlying screen 153 153 if ( !accept ) { 154 154 kble::Facade::stop(); 155 - ctx_.bleCredential.wipe(); 155 + ctx_.vault.bleCredential.wipe(); 156 156 return AppState::VaultMenu; 157 157 } 158 158 } ··· 179 179 if ( ctx_.shakeDetector.checkShake() ) { 180 180 KLEIDOS_LOGI( kTag, "Shake detected — locking" ); 181 181 kble::Facade::stop(); 182 - ctx_.bleCredential.wipe(); 182 + ctx_.vault.bleCredential.wipe(); 183 183 ctx_.goToSleep(); 184 184 } 185 185 ··· 190 190 if ( !ctx_.inputC && kleidos::states::twobtn::backLong( ctx_.inputB ) ) { 191 191 KLEIDOS_LOGI( kTag, "BtnB long — cancelling BLE" ); 192 192 kble::Facade::stop(); 193 - ctx_.bleCredential.wipe(); 193 + ctx_.vault.bleCredential.wipe(); 194 194 return AppState::VaultMenu; 195 195 } 196 196 ··· 228 228 // Safety timeout (only when actively typing/connected and no pairing pending). 229 229 if ( bleState != kble::StackState::ADVERTISING && !kble::Facade::isNcPending() 230 230 && !kble::Facade::isRepairPending() && !kble::Facade::isEvictionPending() 231 - && platformClock::millis32() - ctx_.lastActivity > ctx_.idleSleepMs ) { 231 + && platformClock::millis32() - ctx_.display.lastActivity > ctx_.display.idleSleepMs ) { 232 232 KLEIDOS_LOGW( kTag, "BLE session timeout" ); 233 233 kble::Facade::stop(); 234 - ctx_.bleCredential.wipe(); 234 + ctx_.vault.bleCredential.wipe(); 235 235 ctx_.goToSleep(); 236 236 } 237 237 ··· 255 255 i18n::tr( i18n::StringId::PopToHost ), popup::Severity::SUCCESS, 2000 ); 256 256 rtos::delayMs( 1500 ); 257 257 kble::Facade::stop(); 258 - if ( ctx_.autoLockMode == AutoLockMode::AfterSend ) { 258 + if ( ctx_.vault.autoLockMode == AutoLockMode::AfterSend ) { 259 259 // Real builds: goToSleep() locks + deep-sleeps and never 260 260 // returns. Debug builds (DISABLE_AUTO_LOCK_DEBUG / HAS_KEYBOARD) 261 261 // make it a no-op, so we must fall through to VaultMenu instead ··· 283 283 if ( ctx_.inputC && ( ctx_.inputA.wasPressed() || ctx_.inputC->wasPressed() ) ) { 284 284 KLEIDOS_LOGI( kTag, "BLE cancelled by user (3btn)" ); 285 285 kble::Facade::stop(); 286 - ctx_.bleCredential.wipe(); 286 + ctx_.vault.bleCredential.wipe(); 287 287 return AppState::VaultMenu; 288 288 } 289 289 // 2-button: cancel is B-long, handled globally in update() ··· 309 309 drawBleScreen(); 310 310 rtos::delayMs( 2000 ); 311 311 kble::Facade::stop(); 312 - ctx_.bleCredential.wipe(); 312 + ctx_.vault.bleCredential.wipe(); 313 313 return AppState::VaultMenu; 314 314 315 315 case OFF: ··· 364 364 AppState BleStateHandler::beginTyping() { 365 365 ctx_.resetActivity(); 366 366 KLEIDOS_LOGI( kTag, "Typing: user=%u pass=%u chars", 367 - kleidos::platform::str::length( ctx_.bleCredential.user ), 368 - kleidos::platform::str::length( ctx_.bleCredential.pass ) ); 367 + kleidos::platform::str::length( ctx_.vault.bleCredential.user ), 368 + kleidos::platform::str::length( ctx_.vault.bleCredential.pass ) ); 369 369 drawBleTypingScreen(); 370 370 371 - if ( const size_t userLen = kleidos::platform::str::length( ctx_.bleCredential.user ); 371 + if ( const size_t userLen = kleidos::platform::str::length( ctx_.vault.bleCredential.user ); 372 372 userLen > 0 ) { 373 373 typingToken_ = kble::Facade::typeCredential( 374 - std::string_view{ ctx_.bleCredential.user.c_str(), userLen }, 375 - std::string_view{ ctx_.bleCredential.pass.c_str(), 376 - kleidos::platform::str::length( ctx_.bleCredential.pass ) } ); 374 + std::string_view{ ctx_.vault.bleCredential.user.c_str(), userLen }, 375 + std::string_view{ ctx_.vault.bleCredential.pass.c_str(), 376 + kleidos::platform::str::length( ctx_.vault.bleCredential.pass ) } ); 377 377 } else { 378 378 typingToken_ = kble::Facade::typePassword( 379 - std::string_view{ ctx_.bleCredential.pass.c_str(), 380 - kleidos::platform::str::length( ctx_.bleCredential.pass ) } ); 379 + std::string_view{ ctx_.vault.bleCredential.pass.c_str(), 380 + kleidos::platform::str::length( ctx_.vault.bleCredential.pass ) } ); 381 381 } 382 382 383 383 // Credential staged on Core 0; safe to wipe locally. 384 - ctx_.bleCredential.wipe(); 384 + ctx_.vault.bleCredential.wipe(); 385 385 386 386 if ( typingToken_ == 0 ) { 387 387 KLEIDOS_LOGW( kTag, "beginTyping returned 0 — falling back" ); ··· 393 393 394 394 void BleStateHandler::cancelBle() { 395 395 kble::Facade::stop(); 396 - ctx_.bleCredential.wipe(); 396 + ctx_.vault.bleCredential.wipe(); 397 397 } 398 398 399 399 // ---------------------------------------------------------------------------
+1 -1
src/states/ble_state.h
··· 18 18 * button-driven redraws and the per-state footer animations. 19 19 * 20 20 * @warning SECURITY: this state is the only consumer of the decrypted 21 - * BLE credential in @c ctx_.bleCredential. The credential is 21 + * BLE credential in @c ctx_.vault.bleCredential. The credential is 22 22 * always zeroized on @c exit() and on any error branch that 23 23 * returns to @c VaultMenu. The numeric-comparison passkey is 24 24 * shown only via the confirm popup and is never logged.
+1 -1
src/states/bond_manager_controller.cpp
··· 393 393 } 394 394 395 395 ctx.handleDisplayDimming(); 396 - if ( platformClock::millis32() - ctx.lastActivity >= ctx.idleSleepMs ) { 396 + if ( platformClock::millis32() - ctx.display.lastActivity >= ctx.display.idleSleepMs ) { 397 397 active_ = false; 398 398 host.onBondManagerIdleSleep(); 399 399 ctx.goToSleep();
+5 -4
src/states/change_pin_controller.cpp
··· 200 200 201 201 // Idle timeout 202 202 ctx.handleDisplayDimming(); 203 - if ( platformClock::millis32() - ctx.lastActivity >= ctx.idleSleepMs ) { 203 + if ( platformClock::millis32() - ctx.display.lastActivity >= ctx.display.idleSleepMs ) { 204 204 end(); 205 205 host.onChangePinClosed(); 206 206 host.onChangePinIdleSleep(); ··· 233 233 234 234 void ChangePinController::runReencrypt( AppContext& ctx, ChangePinHost& host ) { 235 235 popup::spinnerShow( "Updating vault\x{E2}\x{80}\x{A6}" ); 236 - bool ok = VaultStore::begin() 237 - && VaultStore::rekey( ctx.masterKey.data(), logic_.newPin(), logic_.newPinLen() ); 236 + bool ok = 237 + VaultStore::begin() 238 + && VaultStore::rekey( ctx.vault.masterKey.data(), logic_.newPin(), logic_.newPinLen() ); 238 239 VaultStore::end(); 239 240 popup::spinnerHide(); 240 241 241 242 // Update in-memory master key to new derivation so current 242 243 // session stays unlocked. 243 244 if ( ok && VaultStore::begin() ) { 244 - VaultStore::unlockVault( logic_.newPin(), logic_.newPinLen(), ctx.masterKey.data() ); 245 + VaultStore::unlockVault( logic_.newPin(), logic_.newPinLen(), ctx.vault.masterKey.data() ); 245 246 VaultStore::end(); 246 247 } 247 248 logic_.finishReencrypt( ok );
+1 -1
src/states/char_grid_editor_controller.cpp
··· 200 200 } 201 201 202 202 ctx.handleDisplayDimming(); 203 - if ( platformClock::millis32() - ctx.lastActivity >= ctx.idleSleepMs ) { 203 + if ( platformClock::millis32() - ctx.display.lastActivity >= ctx.display.idleSleepMs ) { 204 204 active_ = false; 205 205 host.onTextEditIdleSleep(); 206 206 ctx.goToSleep();
+1 -1
src/states/char_wheel_editor_controller.cpp
··· 251 251 } 252 252 253 253 ctx.handleDisplayDimming(); 254 - if ( platformClock::millis32() - ctx.lastActivity >= ctx.idleSleepMs ) { 254 + if ( platformClock::millis32() - ctx.display.lastActivity >= ctx.display.idleSleepMs ) { 255 255 active_ = false; 256 256 host.onTextEditIdleSleep(); 257 257 ctx.goToSleep();
+8 -8
src/states/cred_detail_controller.cpp
··· 286 286 bool CredDetailController::open( uint8_t credId, AppContext& ctx ) { 287 287 bool loaded = false; 288 288 #ifdef DEBUG_SERIAL_BUTTONS 289 - if ( ctx.debugMockVaultPreview ) { 290 - for ( uint8_t idx = 0; idx < ctx.credCount; ++idx ) { 291 - const CredentialEntry& entry = ctx.credEntries[idx]; 289 + if ( ctx.vault.debugMockVaultPreview ) { 290 + for ( uint8_t idx = 0; idx < ctx.menu.credCount; ++idx ) { 291 + const CredentialEntry& entry = ctx.menu.credEntries[idx]; 292 292 if ( entry.id == credId ) { 293 293 fillDebugPreviewCredential( entry, idx, buf_ ); 294 294 loaded = true; ··· 302 302 KLEIDOS_LOGE( kTag, "Vault open failed" ); 303 303 return false; 304 304 } 305 - loaded = VaultStore::loadCredential( ctx.masterKey.data(), credId, buf_ ); 305 + loaded = VaultStore::loadCredential( ctx.vault.masterKey.data(), credId, buf_ ); 306 306 VaultStore::end(); 307 307 } 308 308 if ( !loaded ) { ··· 895 895 896 896 // Display dimming and idle sleep 897 897 ctx.handleDisplayDimming(); 898 - if ( platformClock::millis32() - ctx.lastActivity >= ctx.idleSleepMs ) { 898 + if ( platformClock::millis32() - ctx.display.lastActivity >= ctx.display.idleSleepMs ) { 899 899 KLEIDOS_LOGI( kTag, "Detail idle timeout — sleeping" ); 900 900 close(); 901 901 ctx.goToSleep(); ··· 995 995 ctx.wakeDisplay(); 996 996 ctx.resetActivity(); 997 997 ctx.audio.beepClick(); 998 - memcpy( &ctx.bleCredential, &buf_, sizeof( ctx.bleCredential ) ); 998 + memcpy( &ctx.vault.bleCredential, &buf_, sizeof( ctx.vault.bleCredential ) ); 999 999 popup::spinnerShow( "Typing\x{e2}\x{80}\x{a6}" ); 1000 1000 close(); 1001 1001 popup::spinnerHide(); ··· 1022 1022 } 1023 1023 1024 1024 ctx.handleDisplayDimming(); 1025 - if ( platformClock::millis32() - ctx.lastActivity >= ctx.idleSleepMs ) { 1025 + if ( platformClock::millis32() - ctx.display.lastActivity >= ctx.display.idleSleepMs ) { 1026 1026 KLEIDOS_LOGI( kTag, "Detail idle timeout — sleeping" ); 1027 1027 close(); 1028 1028 ctx.goToSleep(); ··· 1043 1043 if ( const CredAction kind = 1044 1044 isGray ? kCredDetailActions[selIdx].kind : kCredDetailActionsStick[selIdx].kind; 1045 1045 kind == CredAction::TYPE ) { 1046 - memcpy( &ctx.bleCredential, &buf_, sizeof( ctx.bleCredential ) ); 1046 + memcpy( &ctx.vault.bleCredential, &buf_, sizeof( ctx.vault.bleCredential ) ); 1047 1047 popup::spinnerShow( "Typing\x{e2}\x{80}\x{a6}" ); 1048 1048 close(); 1049 1049 popup::spinnerHide();
+2 -2
src/states/cred_detail_controller.h
··· 56 56 enum class CredDetailOutcome : uint8_t { 57 57 Stay = 0, ///< Remain in the overlay (or sleep was requested in place). 58 58 Closed = 1, ///< Back to the vault list without delete; parent repaints the menu. 59 - TypeBle = 2, ///< Type action: credential staged into ctx.bleCredential; caller types it. 59 + TypeBle = 2, ///< Type action: credential staged into ctx.vault.bleCredential; caller types it. 60 60 Deleted = 3, ///< Credential deleted; parent reloads the list + repaints the menu. 61 61 }; 62 62 ··· 90 90 * 91 91 * Loads the credential into the controller-owned @c buf_ (via 92 92 * @c VaultStore::loadCredential, or the debug mock-preview path when 93 - * @c ctx.debugMockVaultPreview is set), opens the headless logic, resets the 93 + * @c ctx.vault.debugMockVaultPreview is set), opens the headless logic, resets the 94 94 * action cursor / hold timers, arms the redraw + first-frame input swallow. 95 95 * Mirrors the legacy @c openCredDetail() exactly. 96 96 *
+4 -3
src/states/cred_row_cache.cpp
··· 16 16 // just an AES-CBC decrypt (fast). 17 17 // --------------------------------------------------------------------------- 18 18 void CredRowCache::prefetch( uint8_t entryIdx, AppContext& ctx ) { 19 - if ( entryIdx >= ctx.credCount ) { 19 + if ( entryIdx >= ctx.menu.credCount ) { 20 20 return; 21 21 } 22 22 if ( entryIdx >= kMaxCredentials ) { ··· 27 27 } 28 28 29 29 #ifdef DEBUG_SERIAL_BUTTONS 30 - if ( ctx.debugMockVaultPreview ) { 30 + if ( ctx.vault.debugMockVaultPreview ) { 31 31 static constexpr const char* kPreviewUsers[] = { 32 32 "octo@kleidos", 33 33 "ana@example", ··· 49 49 state[entryIdx] = 2; // failure: don't retry 50 50 return; 51 51 } 52 - bool ok = VaultStore::loadCredential( ctx.masterKey.data(), ctx.credEntries[entryIdx].id, tmp ); 52 + bool ok = VaultStore::loadCredential( ctx.vault.masterKey.data(), 53 + ctx.menu.credEntries[entryIdx].id, tmp ); 53 54 VaultStore::end(); 54 55 if ( ok ) { 55 56 kleidos::platform::str::copy( users[entryIdx], tmp.user.c_str() );
+1 -1
src/states/cred_row_cache.h
··· 38 38 /** 39 39 * @brief Per-credential lazy cache of the decrypted username + origin domain. 40 40 * 41 - * Index = @c ctx.credEntries[idx].id position (row index). @ref state gates 41 + * Index = @c ctx.menu.credEntries[idx].id position (row index). @ref state gates 42 42 * whether a row was already attempted: 0 = not loaded, 1 = loaded, 2 = load 43 43 * failed (do not retry). 44 44 */
+4 -4
src/states/host_select_controller.cpp
··· 275 275 #if defined( DEBUG_SERIAL_BUTTONS ) 276 276 debugMock_ = false; 277 277 #endif // DEBUG_SERIAL_BUTTONS 278 - ctx.bleCredential.wipe(); 278 + ctx.vault.bleCredential.wipe(); 279 279 ctx.resetActivity(); 280 280 return HostSelectResult::BackToMenu; 281 281 } ··· 317 317 return HostSelectResult::TypeBle; 318 318 } 319 319 KLEIDOS_LOGE( kTag, "BLE init failed" ); 320 - ctx.bleCredential.wipe(); 320 + ctx.vault.bleCredential.wipe(); 321 321 return HostSelectResult::BackToMenu; 322 322 } 323 323 324 - if ( platformClock::millis32() - ctx.lastActivity > ctx.idleSleepMs ) { 325 - ctx.bleCredential.wipe(); 324 + if ( platformClock::millis32() - ctx.display.lastActivity > ctx.display.idleSleepMs ) { 325 + ctx.vault.bleCredential.wipe(); 326 326 ctx.goToSleep(); 327 327 } 328 328 return HostSelectResult::Stay;
+1 -1
src/states/host_select_controller.h
··· 61 61 * @brief Per-tick update: animated scroll pump + 2/3-button input. 62 62 * 63 63 * Replicates the legacy @c updateHostSelect() exactly. The idle-timeout path 64 - * wipes @c ctx.bleCredential and calls @c ctx.goToSleep() in place, returning 64 + * wipes @c ctx.vault.bleCredential and calls @c ctx.goToSleep() in place, returning 65 65 * @ref HostSelectResult::Stay. 66 66 * 67 67 * @param[in,out] ctx Application context (input, activity, BLE staging, sleep).
+9 -9
src/states/hw_test_state.cpp
··· 72 72 drawCurrent(); 73 73 } 74 74 75 - uint32_t idle = platformClock::millis32() - ctx_.lastActivity; 76 - if ( !ctx_.displayDimmed && idle >= ctx_.displayDimMs ) { 75 + uint32_t idle = platformClock::millis32() - ctx_.display.lastActivity; 76 + if ( !ctx_.display.displayDimmed && idle >= ctx_.display.displayDimMs ) { 77 77 display::setBrightness( kBrightnessDim ); 78 - ctx_.displayDimmed = true; 78 + ctx_.display.displayDimmed = true; 79 79 } 80 - if ( screen_ != TestScreen::ButtonTest && idle >= ctx_.idleSleepMs ) { 80 + if ( screen_ != TestScreen::ButtonTest && idle >= ctx_.display.idleSleepMs ) { 81 81 KLEIDOS_LOGI( kTag, "Test idle timeout — sleeping" ); 82 82 ctx_.goToSleep(); 83 83 } ··· 167 167 display::setCursor( 4, 42 ); 168 168 display::printf( "Kleidos v%s", KLEIDOS_VERSION ); 169 169 display::setCursor( 4, 56 ); 170 - display::printf( "Boot #%" PRIu32, ctx_.bootCount ); 170 + display::printf( "Boot #%" PRIu32, ctx_.boot.bootCount ); 171 171 display::setCursor( 4, 70 ); 172 172 display::printf( "Wake: %s", ctx_.power.getWakeReasonString() ); 173 173 int pct = ctx_.power.getBatteryPercent(); ··· 178 178 auto* imu = board::get().imu; 179 179 display::printf( "IMU: %s", ( imu && imu->isAvailable() ) ? "OK" : "N/A" ); 180 180 display::setCursor( 4, 112 ); 181 - display::printf( "RTC: %s", ctx_.rtcAvailable ? "OK" : "N/A" ); 181 + display::printf( "RTC: %s", ctx_.boot.rtcAvailable ? "OK" : "N/A" ); 182 182 drawFooter(); 183 183 } 184 184 ··· 246 246 void HwTestStateHandler::drawRtcTest() { 247 247 drawHeader( "RTC Test" ); 248 248 display::setFont( display::Font::BODY ); 249 - if ( !ctx_.rtcAvailable ) { 249 + if ( !ctx_.boot.rtcAvailable ) { 250 250 display::setTextColor( theme::kWarning565, theme::kBg565 ); 251 251 display::setCursor( 4, 50 ); 252 252 display::print( "RTC not detected" ); ··· 351 351 } 352 352 display::setCursor( 4, 88 ); 353 353 display::setTextColor( theme::kPrimary565, theme::kBg565 ); 354 - if ( preSleepMv_ > 0 && ctx_.rtcAvailable ) { 354 + if ( preSleepMv_ > 0 && ctx_.boot.rtcAvailable ) { 355 355 RtcTime tNow = rtc::getTime(); 356 356 int sleepSec = ( tNow.hours - rtcTimePre_.hours ) * 3600 357 357 + ( tNow.minutes - rtcTimePre_.minutes ) * 60 ··· 373 373 display::setCursor( 4, 118 ); 374 374 display::setTextColor( theme::kTextSec565, theme::kBg565 ); 375 375 uint32_t up = platformClock::millis32() / 1000; 376 - display::printf( "Up:%" PRIu32 "s B:%" PRIu32, up, ctx_.bootCount ); 376 + display::printf( "Up:%" PRIu32 "s B:%" PRIu32, up, ctx_.boot.bootCount ); 377 377 drawFooter(); 378 378 }
+48 -44
src/states/keyboard_vault_view.cpp
··· 233 233 // VaultMenuView lifecycle 234 234 // --------------------------------------------------------------------------- 235 235 void KeyboardVaultView::enter( AppContext& ctx ) { 236 - panel_.init( ctx.masterKey.data() ); 236 + panel_.init( ctx.vault.masterKey.data() ); 237 237 panelActive_ = false; 238 238 redraw_ = true; 239 239 } ··· 246 246 // Item model helpers 247 247 // --------------------------------------------------------------------------- 248 248 uint8_t KeyboardVaultView::totalMenuItems( const AppContext& ctx ) const { 249 - return static_cast<uint8_t>( ctx.credCount + ctx.totpCount + kVaultActionCount ); 249 + return static_cast<uint8_t>( ctx.menu.credCount + ctx.menu.totpCount + kVaultActionCount ); 250 250 } 251 251 252 252 bool KeyboardVaultView::isActionItem( uint8_t idx, const AppContext& ctx ) const { 253 - return idx >= ctx.credCount + ctx.totpCount; 253 + return idx >= ctx.menu.credCount + ctx.menu.totpCount; 254 254 } 255 255 256 256 VaultAction KeyboardVaultView::actionForIndex( uint8_t idx, const AppContext& ctx ) const { 257 - return static_cast<VaultAction>( idx - ctx.credCount - ctx.totpCount ); 257 + return static_cast<VaultAction>( idx - ctx.menu.credCount - ctx.menu.totpCount ); 258 258 } 259 259 260 260 // --------------------------------------------------------------------------- ··· 262 262 // --------------------------------------------------------------------------- 263 263 AppState KeyboardVaultView::activateMenuItem( uint8_t totalItems, AppContext& ctx, 264 264 VaultMenuHost& host ) { 265 - uint8_t sel = ctx.menuSelected; 265 + uint8_t sel = ctx.menu.menuSelected; 266 266 if ( sel >= totalItems ) { 267 267 return AppState::VaultMenu; 268 268 } ··· 300 300 } 301 301 302 302 // TOTP entry — the parent-owned TOTP screen loads + shows the code. 303 - bool isTotp = sel >= ctx.credCount && sel < ctx.credCount + ctx.totpCount; 304 - if ( isTotp && ctx.totpCount > 0 ) { 305 - uint8_t tIdx = sel - ctx.credCount; 303 + bool isTotp = sel >= ctx.menu.credCount && sel < ctx.menu.credCount + ctx.menu.totpCount; 304 + if ( isTotp && ctx.menu.totpCount > 0 ) { 305 + uint8_t tIdx = sel - ctx.menu.credCount; 306 306 return host.showTotp( tIdx ); 307 307 } 308 308 309 309 // Password credential 310 - if ( !isTotp && sel < ctx.credCount ) { 311 - const auto& entry = ctx.credEntries[sel]; 310 + if ( !isTotp && sel < ctx.menu.credCount ) { 311 + const auto& entry = ctx.menu.credEntries[sel]; 312 312 #ifdef DEBUG_SERIAL_BUTTONS 313 - if ( ctx.debugMockVaultPreview ) { 313 + if ( ctx.vault.debugMockVaultPreview ) { 314 314 Credential preview = {}; 315 315 fillDebugPreviewCredential( entry, sel, preview ); 316 316 panel_.openCredentialPreview( preview ); ··· 346 346 // Panel closed 347 347 panelActive_ = false; 348 348 if ( panel_.needsReload() ) { 349 - ctx.credListLoaded = false; 349 + ctx.menu.credListLoaded = false; 350 350 panel_.clearReloadFlag(); 351 351 } 352 - if ( panel_.consumeBleSendRequest( ctx.bleCredential ) ) { 352 + if ( panel_.consumeBleSendRequest( ctx.vault.bleCredential ) ) { 353 353 return host.typeStagedCredential(); 354 354 } 355 355 redraw_ = true; ··· 374 374 constexpr int8_t kStepsPerItem = 2; 375 375 while ( accumY >= kStepsPerItem ) { 376 376 accumY -= kStepsPerItem; 377 - ctx.menuSelected = static_cast<uint8_t>( ( ctx.menuSelected + 1 ) % totalItems ); 378 - redraw_ = true; 377 + ctx.menu.menuSelected = 378 + static_cast<uint8_t>( ( ctx.menu.menuSelected + 1 ) % totalItems ); 379 + redraw_ = true; 379 380 ctx.audio.beepClick(); 380 381 } 381 382 while ( accumY <= -kStepsPerItem ) { 382 383 accumY += kStepsPerItem; 383 - ctx.menuSelected = ( ctx.menuSelected == 0 ) ? totalItems - 1 : ctx.menuSelected - 1; 384 - redraw_ = true; 384 + ctx.menu.menuSelected = 385 + ( ctx.menu.menuSelected == 0 ) ? totalItems - 1 : ctx.menu.menuSelected - 1; 386 + redraw_ = true; 385 387 ctx.audio.beepClick(); 386 388 } 387 389 } ··· 405 407 // Hit-test against the same row geometry the renderer draws so a tap 406 408 // lands on the row the user sees (see kbMenuScrollStart / drawVaultMenuKbRows). 407 409 if ( pt.y >= kKbMenuRowTop ) { 408 - const uint8_t startIdx = kbMenuScrollStart( ctx.menuSelected, totalItems ); 410 + const uint8_t startIdx = kbMenuScrollStart( ctx.menu.menuSelected, totalItems ); 409 411 const auto tappedRow = static_cast<uint8_t>( ( pt.y - kKbMenuRowTop ) / kKbMenuRowH ); 410 412 const auto tappedIdx = static_cast<uint8_t>( startIdx + tappedRow ); 411 413 if ( tappedRow < kKbMenuVisibleRows && tappedIdx < totalItems ) { 412 - if ( tappedIdx == ctx.menuSelected ) { 414 + if ( tappedIdx == ctx.menu.menuSelected ) { 413 415 // Tap on already selected item = activate. 414 416 AppState next = activateMenuItem( totalItems, ctx, host ); 415 417 if ( next != AppState::VaultMenu ) 416 418 return next; 417 419 } else { 418 - ctx.menuSelected = tappedIdx; 419 - redraw_ = true; 420 + ctx.menu.menuSelected = tappedIdx; 421 + redraw_ = true; 420 422 ctx.audio.beepClick(); 421 423 } 422 424 } ··· 438 440 // Navigate: arrow keys, Vim-style keys, or n/p aliases. 439 441 if ( key == 'j' || key == 'n' || key == key_code::kDown ) { 440 442 if ( totalItems > 0 ) { 441 - ctx.menuSelected = static_cast<uint8_t>( ( ctx.menuSelected + 1 ) % totalItems ); 442 - redraw_ = true; 443 + ctx.menu.menuSelected = 444 + static_cast<uint8_t>( ( ctx.menu.menuSelected + 1 ) % totalItems ); 445 + redraw_ = true; 443 446 ctx.audio.beepClick(); 444 447 } 445 448 } 446 449 if ( key == 'k' || key == 'p' || key == key_code::kUp ) { 447 450 if ( totalItems > 0 ) { 448 - ctx.menuSelected = ( ctx.menuSelected == 0 ) ? totalItems - 1 : ctx.menuSelected - 1; 449 - redraw_ = true; 451 + ctx.menu.menuSelected = 452 + ( ctx.menu.menuSelected == 0 ) ? totalItems - 1 : ctx.menu.menuSelected - 1; 453 + redraw_ = true; 450 454 ctx.audio.beepClick(); 451 455 } 452 456 } ··· 471 475 panel_.draw(); 472 476 return AppState::VaultMenu; 473 477 } 474 - if ( ( key == 'e' || key == 'E' ) && ctx.credCount > 0 ) { 475 - bool isTotp = ctx.menuSelected >= ctx.credCount; 478 + if ( ( key == 'e' || key == 'E' ) && ctx.menu.credCount > 0 ) { 479 + bool isTotp = ctx.menu.menuSelected >= ctx.menu.credCount; 476 480 if ( !isTotp ) { 477 - auto& entry = ctx.credEntries[ctx.menuSelected]; 481 + auto& entry = ctx.menu.credEntries[ctx.menu.menuSelected]; 478 482 panel_.openCredDetail( entry.id, entry.name.c_str() ); 479 483 panelActive_ = true; 480 484 panel_.draw(); ··· 545 549 kMiddleLeft, /*bold=*/true ); 546 550 547 551 kleidos::platform::str::InplaceString<15> countBuf; 548 - countBuf.format( "%u items", static_cast<unsigned>( ctx.credCount + ctx.totpCount ) ); 552 + countBuf.format( "%u items", static_cast<unsigned>( ctx.menu.credCount + ctx.menu.totpCount ) ); 549 553 canvas.drawText( countBuf.c_str(), kScrCx, kHeaderH / 2 + 1, display::Font::SMALL, 550 554 theme::kTextSec565, kMiddleCenter ); 551 555 ··· 582 586 583 587 void KeyboardVaultView::drawVaultMenuKbRows( ui::Canvas& canvas, uint8_t totalItems, 584 588 const AppContext& ctx ) { 585 - const uint8_t startIdx = kbMenuScrollStart( ctx.menuSelected, totalItems ); 589 + const uint8_t startIdx = kbMenuScrollStart( ctx.menu.menuSelected, totalItems ); 586 590 for ( uint8_t row = 0; row < kKbMenuVisibleRows && ( startIdx + row ) < totalItems; ++row ) { 587 591 const auto idx = static_cast<uint8_t>( startIdx + row ); 588 592 const auto y = static_cast<int16_t>( kKbMenuRowTop + row * kKbMenuRowH ); ··· 594 598 const AppContext& ctx ) { 595 599 constexpr int16_t kRowH = 19; 596 600 constexpr int16_t kListW = 118; 597 - const bool selected = ( idx == ctx.menuSelected ); 601 + const bool selected = ( idx == ctx.menu.menuSelected ); 598 602 const bool action = isActionItem( idx, ctx ); 599 - const bool isTotp = !action && idx >= ctx.credCount; 603 + const bool isTotp = !action && idx >= ctx.menu.credCount; 600 604 if ( selected ) { 601 605 canvas.fillRect( { 0, y, kListW, kRowH - 1 }, theme::kAccent565 ); 602 606 canvas.fillRect( { 0, y, 3, kRowH - 1 }, theme::kPrimary565 ); ··· 606 610 if ( action ) { 607 611 label = vaultMenuActionLabel( actionForIndex( idx, ctx ) ); 608 612 } else if ( isTotp ) { 609 - label = ctx.totpEntries[idx - ctx.credCount].name.c_str(); 613 + label = ctx.menu.totpEntries[idx - ctx.menu.credCount].name.c_str(); 610 614 } else { 611 - label = ctx.credEntries[idx].name.c_str(); 615 + label = ctx.menu.credEntries[idx].name.c_str(); 612 616 } 613 617 614 618 const int16_t listLabelMaxW = kListW - 31 - ( isTotp ? 12 : 4 ); ··· 637 641 canvas.fillRect( { kPanelX, kRowTop, kPanelW, kFooterY - kRowTop - 4 }, theme::kSurface565 ); 638 642 canvas.drawRect( { kPanelX, kRowTop, kPanelW, kFooterY - kRowTop - 4 }, theme::kSubtle565 ); 639 643 640 - const bool selectedAction = isActionItem( ctx.menuSelected, ctx ); 641 - const bool selectedTotp = !selectedAction && ctx.menuSelected >= ctx.credCount; 642 - if ( !selectedAction && !selectedTotp && ctx.menuSelected < ctx.credCount ) { 643 - cache_.prefetch( ctx.menuSelected, ctx ); 644 - const char* name = ctx.credEntries[ctx.menuSelected].name.c_str(); 644 + const bool selectedAction = isActionItem( ctx.menu.menuSelected, ctx ); 645 + const bool selectedTotp = !selectedAction && ctx.menu.menuSelected >= ctx.menu.credCount; 646 + if ( !selectedAction && !selectedTotp && ctx.menu.menuSelected < ctx.menu.credCount ) { 647 + cache_.prefetch( ctx.menu.menuSelected, ctx ); 648 + const char* name = ctx.menu.credEntries[ctx.menu.menuSelected].name.c_str(); 645 649 const char* user = 646 - ( cache_.state[ctx.menuSelected] == 1 && cache_.users[ctx.menuSelected][0] ) 647 - ? cache_.users[ctx.menuSelected].data() 650 + ( cache_.state[ctx.menu.menuSelected] == 1 && cache_.users[ctx.menu.menuSelected][0] ) 651 + ? cache_.users[ctx.menu.menuSelected].data() 648 652 : "user"; 649 653 kleidos::platform::str::InplaceString<23> nameBuf; 650 654 copyFittedVaultText( name, nameBuf.data(), nameBuf.bufferBytes(), kPanelW - 12 ); ··· 662 666 canvas.drawText( "********", kPanelX + 6, kRowTop + 66, MONO, theme::kPrimary565, 663 667 kTopLeft ); 664 668 } else if ( selectedTotp ) { 665 - const uint8_t tIdx = ctx.menuSelected - ctx.credCount; 669 + const uint8_t tIdx = ctx.menu.menuSelected - ctx.menu.credCount; 666 670 kleidos::platform::str::InplaceString<23> nameBuf; 667 - copyFittedVaultText( ctx.totpEntries[tIdx].name.c_str(), nameBuf.data(), 671 + copyFittedVaultText( ctx.menu.totpEntries[tIdx].name.c_str(), nameBuf.data(), 668 672 nameBuf.bufferBytes(), kPanelW - 12 ); 669 673 nameBuf.resyncLength(); 670 674 canvas.drawText( nameBuf.c_str(), kPanelX + 6, kRowTop + 8, BODY, theme::kPrimary565, ··· 674 678 canvas.drawText( "Enter to open", kPanelX + 6, kRowTop + 58, SMALL, theme::kInfo565, 675 679 kTopLeft ); 676 680 } else { 677 - const char* label = vaultMenuActionLabel( actionForIndex( ctx.menuSelected, ctx ) ); 681 + const char* label = vaultMenuActionLabel( actionForIndex( ctx.menu.menuSelected, ctx ) ); 678 682 canvas.drawText( "Action", kPanelX + 6, kRowTop + 8, BODY, theme::kPrimary565, kTopLeft, 679 683 /*bold=*/true ); 680 684 canvas.drawText( label, kPanelX + 6, kRowTop + 32, SMALL, theme::kText565, kTopLeft );
+4 -4
src/states/onboarding_state.cpp
··· 146 146 { 147 147 CpuBoostGuard boost; 148 148 ok = VaultStore::provision( s_logic->createdPin(), s_logic->createdPinLen(), 149 - s_ctx->masterKey.data() ); 149 + s_ctx->vault.masterKey.data() ); 150 150 } 151 151 VaultStore::end(); 152 152 s_logic->wipe(); // burn the stored PIN now that provision is done ··· 1073 1073 if ( finished_ ) { 1074 1074 // Persist the "did onboarding" flag and hand off to VAULT_MENU. 1075 1075 // The vault was already provisioned in the confirm callback; the 1076 - // master key is live in ctx_.masterKey. 1076 + // master key is live in ctx_.vault.masterKey. 1077 1077 markOnboardingDone(); 1078 - ctx_.vaultUnlocked = true; 1079 - ctx_.credListLoaded = false; 1078 + ctx_.vault.vaultUnlocked = true; 1079 + ctx_.menu.credListLoaded = false; 1080 1080 ctx_.resetActivity(); 1081 1081 return AppState::VaultMenu; 1082 1082 }
+15 -15
src/states/pin_state.cpp
··· 319 319 320 320 if ( !VaultStore::isProvisioned() ) { 321 321 KLEIDOS_LOGI( kTag, "First use — provisioning vault (v2)" ); 322 - bool ok = VaultStore::provision( derivPin, derivLen, pinCtx->masterKey.data() ); 322 + bool ok = VaultStore::provision( derivPin, derivLen, pinCtx->vault.masterKey.data() ); 323 323 VaultStore::end(); 324 324 #if HAS_SDCARD 325 325 VaultCrypto::secureWipe( combinedPin, sizeof( combinedPin ) ); 326 326 #endif // HAS_SDCARD 327 327 if ( ok ) { 328 328 BruteForceGuard::resetAttempts(); 329 - pinCtx->vaultUnlocked = true; 329 + pinCtx->vault.vaultUnlocked = true; 330 330 } 331 331 return ok; 332 332 } ··· 334 334 bool ok = false; 335 335 kleidos::vault::execute( [&ok, &derivPin, &derivLen]() { 336 336 // Combined verify + derive: single PBKDF2 for v2, legacy path for v1 337 - ok = VaultStore::unlockVault( derivPin, derivLen, pinCtx->masterKey.data() ); 337 + ok = VaultStore::unlockVault( derivPin, derivLen, pinCtx->vault.masterKey.data() ); 338 338 if ( ok ) { 339 - pinCtx->credCount = VaultStore::loadIndex( 340 - pinCtx->masterKey.data(), pinCtx->credEntries.data(), kMaxCredentials ); 341 - pinCtx->totpCount = VaultStore::loadTotpIndex( pinCtx->masterKey.data(), 342 - pinCtx->totpEntries.data(), kMaxTotp ); 343 - pinCtx->credListLoaded = true; 344 - if ( pinCtx->menuSelected >= pinCtx->credCount + pinCtx->totpCount 345 - && pinCtx->credCount + pinCtx->totpCount > 0 ) { 346 - pinCtx->menuSelected = 0; 339 + pinCtx->menu.credCount = VaultStore::loadIndex( 340 + pinCtx->vault.masterKey.data(), pinCtx->menu.credEntries.data(), kMaxCredentials ); 341 + pinCtx->menu.totpCount = VaultStore::loadTotpIndex( 342 + pinCtx->vault.masterKey.data(), pinCtx->menu.totpEntries.data(), kMaxTotp ); 343 + pinCtx->menu.credListLoaded = true; 344 + if ( pinCtx->menu.menuSelected >= pinCtx->menu.credCount + pinCtx->menu.totpCount 345 + && pinCtx->menu.credCount + pinCtx->menu.totpCount > 0 ) { 346 + pinCtx->menu.menuSelected = 0; 347 347 } 348 348 } 349 349 VaultStore::end(); ··· 356 356 357 357 if ( ok ) { 358 358 BruteForceGuard::resetAttempts(); 359 - pinCtx->vaultUnlocked = true; 359 + pinCtx->vault.vaultUnlocked = true; 360 360 KLEIDOS_LOGI( kTag, "Vault unlocked" ); 361 361 AuditLog::record( AuditEvent::PinOk ); 362 362 } else { ··· 405 405 } 406 406 // Apply the owner-configurable timing-arc sweep duration (no-op for entry 407 407 // methods without an arc). The setter re-clamps to the viable window. 408 - ctx_.authUI.setSweepDurationMs( ctx_.pinArcSweepMs ); 408 + ctx_.authUI.setSweepDurationMs( ctx_.display.pinArcSweepMs ); 409 409 AppContext::configureDFS( 160, 80 ); 410 410 ctx_.resetActivity(); 411 411 KLEIDOS_LOGI( kTag, "Entering auth (%s)", ctx_.authUI.getMethodLabel() ); ··· 444 444 if ( unlocked ) { 445 445 display::wakeup(); 446 446 display::setBrightness( kBrightnessFull ); 447 - ctx_.displayDimmed = false; 447 + ctx_.display.displayDimmed = false; 448 448 ctx_.resetActivity(); 449 449 450 450 // If we rebooted to enter admin (BLE→WiFi NVS workaround), skip ··· 466 466 467 467 // Idle timeout during auth entry 468 468 #if !HAS_KEYBOARD 469 - if ( const uint32_t idle = platformClock::millis32() - ctx_.lastActivity; 469 + if ( const uint32_t idle = platformClock::millis32() - ctx_.display.lastActivity; 470 470 ( ctx_.authUI.getState() == AuthUIState::IDLE 471 471 || ctx_.authUI.getState() == AuthUIState::LockedOut ) 472 472 && idle >= kPinIdleSleepMs ) {
+30 -30
src/states/settings_controller.cpp
··· 130 130 return { i18n::tr( i18n::StringId::SetBrightness ), s_valBuf.c_str() }; 131 131 } 132 132 case AutoOffSec: { 133 - s_valBuf.format( "%us", static_cast<unsigned>( ctx.displayOffMs / 1000U ) ); 133 + s_valBuf.format( "%us", static_cast<unsigned>( ctx.display.displayOffMs / 1000U ) ); 134 134 return { i18n::tr( i18n::StringId::SetAutoOff ), s_valBuf.c_str() }; 135 135 } 136 136 case ChangePin: 137 137 return { i18n::tr( i18n::StringId::SetChangePin ), nullptr }; 138 138 case AutoLockSec: { 139 - s_valBuf.format( "%us", static_cast<unsigned>( ctx.idleSleepMs / 1000U ) ); 139 + s_valBuf.format( "%us", static_cast<unsigned>( ctx.display.idleSleepMs / 1000U ) ); 140 140 return { i18n::tr( i18n::StringId::SetAutoLock ), s_valBuf.c_str() }; 141 141 } 142 142 case PinSweep: { 143 143 // Sweep duration in seconds, one decimal (e.g. "3.0 s"). 144 - const uint32_t ms = pin_arc::clampSweepMs( ctx.pinArcSweepMs ); 144 + const uint32_t ms = pin_arc::clampSweepMs( ctx.display.pinArcSweepMs ); 145 145 const auto whole = static_cast<unsigned>( ms / 1000 ); 146 146 const auto tenth = static_cast<unsigned>( ( ms % 1000 ) / 100 ); 147 147 s_valBuf.format( "%u.%u s", whole, tenth ); ··· 201 201 i18n::StringId::SetBrightness, display::getBrightness(), 10, 100, 5, U::Percent }; 202 202 case AutoOffSec: 203 203 return { i18n::StringId::SetAutoOff, 204 - static_cast<int32_t>( ctx.displayOffMs / 1000U ), 204 + static_cast<int32_t>( ctx.display.displayOffMs / 1000U ), 205 205 5, 206 206 60, 207 207 5, 208 208 U::Seconds }; 209 209 case AutoLockSec: 210 210 return { i18n::StringId::SetAutoLock, 211 - static_cast<int32_t>( ctx.idleSleepMs / 1000U ), 211 + static_cast<int32_t>( ctx.display.idleSleepMs / 1000U ), 212 212 15, 213 213 300, 214 214 5, ··· 216 216 case PinSweep: 217 217 default: 218 218 return { i18n::StringId::SetPinSweep, 219 - static_cast<int32_t>( pin_arc::clampSweepMs( ctx.pinArcSweepMs ) ), 219 + static_cast<int32_t>( pin_arc::clampSweepMs( ctx.display.pinArcSweepMs ) ), 220 220 static_cast<int32_t>( pin_arc::kMinSweepMs ), 221 221 static_cast<int32_t>( pin_arc::kMaxSweepMs ), 222 222 static_cast<int32_t>( pin_arc::kSweepStepMs ), ··· 456 456 display::setBrightness( next ); 457 457 prefs.setU8( "disp_br", next ); 458 458 } else if ( it == SettingsItem::AutoOffSec ) { 459 - uint8_t i = autoOffIndex( ctx.displayOffMs ); 460 - i = static_cast<uint8_t>( ( i + 1 ) % kAutoOffCount ); 461 - ctx.displayOffMs = kAutoOffValues[i]; 462 - if ( ctx.displayDimMs > ctx.displayOffMs ) { 463 - ctx.displayDimMs = ctx.displayOffMs / 2; 459 + uint8_t i = autoOffIndex( ctx.display.displayOffMs ); 460 + i = static_cast<uint8_t>( ( i + 1 ) % kAutoOffCount ); 461 + ctx.display.displayOffMs = kAutoOffValues[i]; 462 + if ( ctx.display.displayDimMs > ctx.display.displayOffMs ) { 463 + ctx.display.displayDimMs = ctx.display.displayOffMs / 2; 464 464 } 465 - prefs.setU32( "disp_off_ms", ctx.displayOffMs ); 466 - prefs.setU32( "disp_dim_ms", ctx.displayDimMs ); 465 + prefs.setU32( "disp_off_ms", ctx.display.displayOffMs ); 466 + prefs.setU32( "disp_dim_ms", ctx.display.displayDimMs ); 467 467 } else if ( it == SettingsItem::AutoLockSec ) { 468 - uint8_t i = autoLockIndex( ctx.idleSleepMs ); 469 - i = static_cast<uint8_t>( ( i + 1 ) % kAutoLockCount ); 470 - ctx.idleSleepMs = kAutoLockValues[i]; 471 - prefs.setU32( "idle_slp_ms", ctx.idleSleepMs ); 468 + uint8_t i = autoLockIndex( ctx.display.idleSleepMs ); 469 + i = static_cast<uint8_t>( ( i + 1 ) % kAutoLockCount ); 470 + ctx.display.idleSleepMs = kAutoLockValues[i]; 471 + prefs.setU32( "idle_slp_ms", ctx.display.idleSleepMs ); 472 472 } else if ( it == SettingsItem::LANGUAGE ) { 473 473 auto cur = std::to_underlying( i18n::getLang() ); 474 474 auto next = static_cast<uint8_t>( ( cur + 1 ) % i18n::langCount() ); ··· 846 846 #endif // !HAS_KEYBOARD 847 847 } 848 848 ctx.handleDisplayDimming(); 849 - if ( platformClock::millis32() - ctx.lastActivity >= ctx.idleSleepMs ) { 849 + if ( platformClock::millis32() - ctx.display.lastActivity >= ctx.display.idleSleepMs ) { 850 850 active_ = false; 851 851 infoScreenActive_ = false; 852 852 ctx.goToSleep(); ··· 993 993 } 994 994 995 995 ctx.handleDisplayDimming(); 996 - if ( platformClock::millis32() - ctx.lastActivity >= ctx.idleSleepMs ) { 996 + if ( platformClock::millis32() - ctx.display.lastActivity >= ctx.display.idleSleepMs ) { 997 997 stepperEditActive_ = false; 998 998 active_ = false; 999 999 ctx.goToSleep(); ··· 1015 1015 break; 1016 1016 } 1017 1017 case AutoOffSec: 1018 - ctx.displayOffMs = static_cast<uint32_t>( value ) * 1000U; 1019 - if ( ctx.displayDimMs > ctx.displayOffMs ) { 1020 - ctx.displayDimMs = ctx.displayOffMs / 2; 1018 + ctx.display.displayOffMs = static_cast<uint32_t>( value ) * 1000U; 1019 + if ( ctx.display.displayDimMs > ctx.display.displayOffMs ) { 1020 + ctx.display.displayDimMs = ctx.display.displayOffMs / 2; 1021 1021 } 1022 1022 if ( haveNvs ) { 1023 - prefs.setU32( "disp_off_ms", ctx.displayOffMs ); 1024 - prefs.setU32( "disp_dim_ms", ctx.displayDimMs ); 1023 + prefs.setU32( "disp_off_ms", ctx.display.displayOffMs ); 1024 + prefs.setU32( "disp_dim_ms", ctx.display.displayDimMs ); 1025 1025 } 1026 1026 break; 1027 1027 case AutoLockSec: 1028 - ctx.idleSleepMs = static_cast<uint32_t>( value ) * 1000U; 1028 + ctx.display.idleSleepMs = static_cast<uint32_t>( value ) * 1000U; 1029 1029 if ( haveNvs ) { 1030 - prefs.setU32( "idle_slp_ms", ctx.idleSleepMs ); 1030 + prefs.setU32( "idle_slp_ms", ctx.display.idleSleepMs ); 1031 1031 } 1032 1032 break; 1033 1033 case PinSweep: 1034 1034 default: { 1035 - const uint32_t ms = pin_arc::clampSweepMs( static_cast<uint32_t>( value ) ); 1036 - ctx.pinArcSweepMs = ms; 1035 + const uint32_t ms = pin_arc::clampSweepMs( static_cast<uint32_t>( value ) ); 1036 + ctx.display.pinArcSweepMs = ms; 1037 1037 ctx.authUI.setSweepDurationMs( ms ); 1038 1038 if ( haveNvs ) { 1039 1039 prefs.setU32( "pin_arc_ms", ms ); ··· 1145 1145 } 1146 1146 1147 1147 ctx.handleDisplayDimming(); 1148 - if ( platformClock::millis32() - ctx.lastActivity >= ctx.idleSleepMs ) { 1148 + if ( platformClock::millis32() - ctx.display.lastActivity >= ctx.display.idleSleepMs ) { 1149 1149 dropdownPickActive_ = false; 1150 1150 active_ = false; 1151 1151 ctx.goToSleep(); ··· 1315 1315 1316 1316 // Display dimming and idle sleep 1317 1317 ctx.handleDisplayDimming(); 1318 - if ( platformClock::millis32() - ctx.lastActivity >= ctx.idleSleepMs ) { 1318 + if ( platformClock::millis32() - ctx.display.lastActivity >= ctx.display.idleSleepMs ) { 1319 1319 active_ = false; 1320 1320 ctx.goToSleep(); 1321 1321 }
+29 -29
src/states/stick_vault_view.cpp
··· 79 79 stickPage_ = kleidos::states::twobtn::Page::Credentials; 80 80 stickPageSelected_.fill( 0 ); 81 81 stickPageScrollTop_.fill( 0 ); 82 - ctx.menuSelected = 0; 83 - stickScrollTop_ = 0; 82 + ctx.menu.menuSelected = 0; 83 + stickScrollTop_ = 0; 84 84 stickMultiTapA_.reset(); 85 85 stickMultiTapB_.reset(); 86 86 ··· 90 90 // Seed the Stick scroll animation at the resting selection so the first nav 91 91 // press tweens from the correct position (no spurious snap on the first move). 92 92 scrollTopAnim_ = ui::Tween::snap( static_cast<float>( stickScrollTop_ ) ); 93 - highlightAnim_ = ui::Tween::snap( static_cast<float>( ctx.menuSelected ) ); 93 + highlightAnim_ = ui::Tween::snap( static_cast<float>( ctx.menu.menuSelected ) ); 94 94 menuAnimActive_ = false; 95 95 // Seed the tab-indicator pill at rest on the active page's segment so the 96 96 // first render shows it in place (no spurious slide from x=0). ··· 122 122 using enum kleidos::states::twobtn::Page; 123 123 switch ( stickPage_ ) { 124 124 case Credentials: 125 - return ctx.credCount; 125 + return ctx.menu.credCount; 126 126 case Totp: 127 - return ctx.totpCount; 127 + return ctx.menu.totpCount; 128 128 case Settings: 129 129 return kStickSettingCount; 130 130 } ··· 138 138 void StickVaultView::switchStickPage( bool toNext, AppContext& ctx, VaultMenuHost& host ) { 139 139 const auto idx = std::to_underlying( stickPage_ ); 140 140 // Save the cursor/window of the page we are leaving. 141 - stickPageSelected_[idx] = ctx.menuSelected; 141 + stickPageSelected_[idx] = ctx.menu.menuSelected; 142 142 stickPageScrollTop_[idx] = stickScrollTop_; 143 143 144 144 stickPage_ = toNext ? kleidos::states::twobtn::nextPage( stickPage_ ) ··· 165 165 } else if ( sel >= count ) { 166 166 sel = static_cast<uint8_t>( count - 1 ); 167 167 } 168 - ctx.menuSelected = sel; 169 - stickScrollTop_ = stickPageScrollTop_[newIdx]; 168 + ctx.menu.menuSelected = sel; 169 + stickScrollTop_ = stickPageScrollTop_[newIdx]; 170 170 171 171 // Any in-flight tap/animation belongs to the page we left. 172 172 stickMultiTapA_.reset(); ··· 174 174 menuAnimActive_ = false; 175 175 // Seed the scroll tweens at rest on the restored selection (no spurious slide). 176 176 scrollTopAnim_ = ui::Tween::snap( static_cast<float>( stickScrollTop_ ) ); 177 - highlightAnim_ = ui::Tween::snap( static_cast<float>( ctx.menuSelected ) ); 177 + highlightAnim_ = ui::Tween::snap( static_cast<float>( ctx.menu.menuSelected ) ); 178 178 179 179 // Slide the tab-indicator pill to the new page's segment (snaps on e-paper). 180 180 startTabIndicatorAnim( ui::LayoutContext::fromActiveDisplay(), stickPage_ ); ··· 191 191 // --------------------------------------------------------------------------- 192 192 void StickVaultView::restoreCredentialsPage( AppContext& ctx ) { 193 193 using enum kleidos::states::twobtn::Page; 194 - const auto credIdx = std::to_underlying( Credentials ); 195 - stickPage_ = Credentials; 196 - ctx.menuSelected = stickPageSelected_[credIdx]; 197 - stickScrollTop_ = stickPageScrollTop_[credIdx]; 198 - scrollTopAnim_ = ui::Tween::snap( static_cast<float>( stickScrollTop_ ) ); 199 - highlightAnim_ = ui::Tween::snap( static_cast<float>( ctx.menuSelected ) ); 194 + const auto credIdx = std::to_underlying( Credentials ); 195 + stickPage_ = Credentials; 196 + ctx.menu.menuSelected = stickPageSelected_[credIdx]; 197 + stickScrollTop_ = stickPageScrollTop_[credIdx]; 198 + scrollTopAnim_ = ui::Tween::snap( static_cast<float>( stickScrollTop_ ) ); 199 + highlightAnim_ = ui::Tween::snap( static_cast<float>( ctx.menu.menuSelected ) ); 200 200 startTabIndicatorAnim( ui::LayoutContext::fromActiveDisplay(), Credentials ); 201 201 redraw_ = true; 202 202 } ··· 224 224 if ( listHoldStartA_ == 0 ) { 225 225 listHoldStartA_ = platformClock::millis32(); 226 226 listHoldLastSeg_ = -1; 227 - listHoldRowIdx_ = ctx.menuSelected; 227 + listHoldRowIdx_ = ctx.menu.menuSelected; 228 228 } 229 229 // If selection moved while pressed (shouldn't normally), reset. 230 - if ( listHoldRowIdx_ != ctx.menuSelected ) { 230 + if ( listHoldRowIdx_ != ctx.menu.menuSelected ) { 231 231 redraw_ = true; 232 232 listHoldLastSeg_ = -1; 233 - listHoldRowIdx_ = ctx.menuSelected; 233 + listHoldRowIdx_ = ctx.menu.menuSelected; 234 234 listHoldStartA_ = platformClock::millis32(); 235 235 } 236 236 uint32_t held = platformClock::millis32() - listHoldStartA_; ··· 268 268 listHoldStartA_ = 0; 269 269 listHoldLastSeg_ = -1; 270 270 271 - const uint8_t sel = ctx.menuSelected; 271 + const uint8_t sel = ctx.menu.menuSelected; 272 272 const uint8_t count = stickPageItemCount( ctx ); 273 273 274 274 // --- Settings page: WiFi Admin / Lock / About --- ··· 380 380 ctx.wakeDisplay(); 381 381 ctx.resetActivity(); 382 382 if ( totalItems > 0 ) { 383 - ctx.menuSelected = twobtn::clampUp( ctx.menuSelected, totalItems ); 383 + ctx.menu.menuSelected = twobtn::clampUp( ctx.menu.menuSelected, totalItems ); 384 384 startStickMenuAnim( totalItems, ctx ); 385 385 ctx.audio.beepClick(); 386 386 } ··· 391 391 ctx.wakeDisplay(); 392 392 ctx.resetActivity(); 393 393 if ( totalItems > 0 ) { 394 - ctx.menuSelected = twobtn::clampDown( ctx.menuSelected, totalItems ); 394 + ctx.menu.menuSelected = twobtn::clampDown( ctx.menu.menuSelected, totalItems ); 395 395 startStickMenuAnim( totalItems, ctx ); 396 396 ctx.audio.beepClick(); 397 397 } ··· 552 552 // Sticky scroll window: keep the previous window top and only move it the 553 553 // minimum needed so the highlight tracks the selection within the window, 554 554 // scrolling only when the selection would leave it. 555 - return ui::scrollWindowTop( ctx.menuSelected, totalItems, visible, stickScrollTop_ ); 555 + return ui::scrollWindowTop( ctx.menu.menuSelected, totalItems, visible, stickScrollTop_ ); 556 556 } 557 557 558 558 ui::Rect StickVaultView::stickSelectedRowRect( const AppContext& ctx ) const { ··· 565 565 return ui::Rect{}; 566 566 } 567 567 const uint8_t scrollTop = stickMenuScrollTop( totalItems, visible, ctx ); 568 - if ( ctx.menuSelected < scrollTop || ctx.menuSelected >= scrollTop + visible ) { 568 + if ( ctx.menu.menuSelected < scrollTop || ctx.menu.menuSelected >= scrollTop + visible ) { 569 569 return ui::Rect{}; 570 570 } 571 571 const int16_t rowH = ui::listRowHeight( layout, /*twoLine=*/true, kStickRowSize ); 572 - const auto localRow = static_cast<int16_t>( ctx.menuSelected - scrollTop ); 572 + const auto localRow = static_cast<int16_t>( ctx.menu.menuSelected - scrollTop ); 573 573 return ui::Rect{ body.x, static_cast<int16_t>( body.y + localRow * rowH ), body.w, rowH }; 574 574 } 575 575 ··· 611 611 612 612 const bool credPage = ( stickPage_ == Page::Credentials ); 613 613 for ( uint8_t idx = 0; idx < totalItems && idx < stickMenuItems_.size(); ++idx ) { 614 - const char* name = 615 - credPage ? ctx.credEntries[idx].name.c_str() : ctx.totpEntries[idx].name.c_str(); 614 + const char* name = credPage ? ctx.menu.credEntries[idx].name.c_str() 615 + : ctx.menu.totpEntries[idx].name.c_str(); 616 616 617 617 ui::MenuItemDesc item{}; 618 618 item.label = name; ··· 670 670 ui::MenuListProps props{}; 671 671 props.items = stickMenuItems_.data(); 672 672 props.count = totalItems; 673 - props.selected = ctx.menuSelected; 673 + props.selected = ctx.menu.menuSelected; 674 674 props.scrollTop = scrollTop; 675 675 props.hasSecondaryLines = true; 676 676 props.rowSize = kStickRowSize; ··· 769 769 ? uint8_t{ 0 } 770 770 : stickMenuScrollTop( totalItems, visible, ctx ); 771 771 const auto targetTop = static_cast<float>( newTop ); 772 - const auto targetHigh = static_cast<float>( ctx.menuSelected ); 772 + const auto targetHigh = static_cast<float>( ctx.menu.menuSelected ); 773 773 774 774 // Snap (no slide) when the panel cannot animate (e-paper: slow refresh + 775 775 // ghosting) or when the selection jumps more than one row — e.g. the wrap ··· 874 874 ui::MenuListProps props{}; 875 875 props.items = stickMenuItems_.data(); 876 876 props.count = totalItems; 877 - props.selected = ctx.menuSelected; 877 + props.selected = ctx.menu.menuSelected; 878 878 props.scrollTop = static_cast<uint8_t>( baseTopI ); 879 879 props.hasSecondaryLines = true; 880 880 props.rowSize = kStickRowSize;
+1 -1
src/states/stick_vault_view.h
··· 132 132 void decorateRowFromCache( ui::MenuItemDesc& item, uint8_t idx ) const; 133 133 134 134 // --- Scroll geometry ----------------------------------------------------- 135 - /// First visible row index that keeps @c ctx.menuSelected on screen, derived 135 + /// First visible row index that keeps @c ctx.menu.menuSelected on screen, derived 136 136 /// from the retained @c stickScrollTop_ (sticky window, not re-centered). 137 137 uint8_t stickMenuScrollTop( uint8_t totalItems, uint8_t visible, const AppContext& ctx ) const; 138 138 /// Screen rect of the selected Stick list row (for the HOLD-A progress
+19 -19
src/states/tabbed_vault_view.cpp
··· 61 61 void TabbedVaultView::enter( AppContext& ctx ) { 62 62 currentTab_ = 0; 63 63 tabSelected_.fill( 0 ); 64 - btnAPressTime_ = 0; 65 - btnCPressTime_ = 0; 66 - btnATabSwitched_ = false; 67 - btnCTabSwitched_ = false; 68 - ctx.menuSelected = 0; 69 - redraw_ = true; 64 + btnAPressTime_ = 0; 65 + btnCPressTime_ = 0; 66 + btnATabSwitched_ = false; 67 + btnCTabSwitched_ = false; 68 + ctx.menu.menuSelected = 0; 69 + redraw_ = true; 70 70 } 71 71 72 72 void TabbedVaultView::reset() { ··· 82 82 uint8_t TabbedVaultView::tabItemCount( uint8_t tab, const AppContext& ctx ) const { 83 83 switch ( tab ) { 84 84 case 0: 85 - return ctx.credCount; // Pass: credentials only 85 + return ctx.menu.credCount; // Pass: credentials only 86 86 case 1: 87 - return ctx.totpCount; // TOTP 87 + return ctx.menu.totpCount; // TOTP 88 88 case 2: 89 89 return kThreeBtnActionCount; // Actions 90 90 default: ··· 152 152 kMiddleCenter ); 153 153 canvas.frameCommit(); 154 154 if ( ok ) 155 - ctx.credListLoaded = false; 155 + ctx.menu.credListLoaded = false; 156 156 kleidos::platform::rtos::delayMs( 1500 ); 157 157 redraw_ = true; 158 158 return std::nullopt; ··· 167 167 canvas.drawText( i18n::tr( i18n::StringId::VaultImportingCsv ), device::kScreenCx, 168 168 device::kScreenH / 2, display::Font::BODY, theme::kInfo565, kMiddleCenter ); 169 169 canvas.frameCommit(); 170 - CsvImportResult r = SdVault::importCsv( ctx.masterKey.data() ); 170 + CsvImportResult r = SdVault::importCsv( ctx.vault.masterKey.data() ); 171 171 canvas.frameBegin(); 172 172 canvas.fillScreen( theme::kBg565 ); 173 173 kleidos::platform::str::InplaceString<47> msg; ··· 175 175 if ( r.imported > 0 ) { 176 176 msgColor = theme::kSuccess565; 177 177 msg.format( "Imported %u, skipped %u", r.imported, r.skipped ); 178 - ctx.credListLoaded = false; 178 + ctx.menu.credListLoaded = false; 179 179 } else { 180 180 msg.format( "Skipped %u, imported 0", r.skipped ); 181 181 } ··· 217 217 VaultMenuHost& host ) { 218 218 switch ( tab ) { 219 219 case 0: { // Passwords (credentials only) — open detail per mockup 220 - if ( idx >= ctx.credCount ) { 220 + if ( idx >= ctx.menu.credCount ) { 221 221 break; 222 222 } 223 - const auto& entry = ctx.credEntries[idx]; 223 + const auto& entry = ctx.menu.credEntries[idx]; 224 224 KLEIDOS_LOGI( kTag, "Tab: open credential detail %u", entry.id ); 225 225 host.openCredential( idx ); 226 226 break; 227 227 } 228 228 case 1: { // TOTP entries (separate tab per mockup) 229 - if ( idx >= ctx.totpCount ) { 229 + if ( idx >= ctx.menu.totpCount ) { 230 230 break; 231 231 } 232 - KLEIDOS_LOGI( kTag, "Tab: TOTP %u", ctx.totpEntries[idx].id ); 232 + KLEIDOS_LOGI( kTag, "Tab: TOTP %u", ctx.menu.totpEntries[idx].id ); 233 233 host.showTotp( idx ); 234 234 break; 235 235 } ··· 336 336 activateTabItem( currentTab_, tabSelected_[currentTab_], ctx, host ) ) { 337 337 return next; 338 338 } 339 - } else if ( currentTab_ == 0 && ctx.credCount == 0 ) { 339 + } else if ( currentTab_ == 0 && ctx.menu.credCount == 0 ) { 340 340 // Empty passwords tab: enter admin 341 341 if ( host.enterAdmin() == AppState::AdminMode ) { 342 342 return AppState::AdminMode; ··· 528 528 // Item count (center) — body font, secondary color 529 529 { 530 530 kleidos::platform::str::InplaceString<23> count; 531 - uint16_t total = ctx.credCount + ctx.totpCount; 531 + uint16_t total = ctx.menu.credCount + ctx.menu.totpCount; 532 532 count.format( "%u item%s", total, total == 1 ? "" : "s" ); 533 533 canvas.drawText( count.c_str(), kScrCx, kHeaderH / 2, display::Font::BODY, 534 534 theme::kTextSec565, kMiddleCenter ); ··· 562 562 void TabbedVaultView::drawTabbedCredRow( ui::Canvas& canvas, uint8_t idx, int16_t y, int16_t rowH, 563 563 uint16_t /*rowBg*/, bool selected, AppContext& ctx ) { 564 564 constexpr int16_t kTileSz = 28; 565 - const auto& e = ctx.credEntries[idx]; 565 + const auto& e = ctx.menu.credEntries[idx]; 566 566 567 567 // Decrypt username on demand for visible rows 568 568 cache_.prefetch( idx, ctx ); ··· 607 607 uint16_t /*rowBg*/, bool selected, 608 608 AppContext& ctx ) const { 609 609 constexpr int16_t kTileSz = 28; 610 - const auto& t = ctx.totpEntries[idx]; 610 + const auto& t = ctx.menu.totpEntries[idx]; 611 611 int16_t tileX = 10; 612 612 auto tileY = static_cast<int16_t>( y + ( rowH - kTileSz ) / 2 ); 613 613 drawFaviconTile( canvas, tileX, tileY, kTileSz, t.name.c_str(), selected );
+3 -3
src/states/totp_screen_controller.cpp
··· 292 292 current_.wipe(); 293 293 active_ = false; 294 294 295 - memset( &ctx.bleCredential, 0, sizeof( ctx.bleCredential ) ); 296 - kleidos::platform::str::copy( ctx.bleCredential.pass, codeStr.c_str() ); 295 + memset( &ctx.vault.bleCredential, 0, sizeof( ctx.vault.bleCredential ) ); 296 + kleidos::platform::str::copy( ctx.vault.bleCredential.pass, codeStr.c_str() ); 297 297 codeStr.secureZeroize(); 298 298 299 299 return TotpScreenResult::TypeBle; 300 300 } 301 301 302 - if ( platformClock::millis32() - ctx.lastActivity > ctx.idleSleepMs ) { 302 + if ( platformClock::millis32() - ctx.display.lastActivity > ctx.display.idleSleepMs ) { 303 303 current_.wipe(); 304 304 ctx.goToSleep(); 305 305 }
+1 -1
src/states/totp_screen_controller.h
··· 37 37 enum class TotpScreenResult : uint8_t { 38 38 Stay = 0, ///< Remain on the TOTP screen (or sleep was requested in place). 39 39 BackToMenu = 1, ///< B-long: leave the TOTP screen for the vault list. 40 - TypeBle = 2, ///< A-long: code staged into ctx.bleCredential; caller types it. 40 + TypeBle = 2, ///< A-long: code staged into ctx.vault.bleCredential; caller types it. 41 41 }; 42 42 43 43 /**
+4 -4
src/states/vault_menu_view.h
··· 39 39 * one of these methods, which perform the exact side effects the parent used to 40 40 * run inline: 41 41 * 42 - * - @ref openCredential → @c credDetail_.open( ctx.credEntries[i].id, ctx ) 42 + * - @ref openCredential → @c credDetail_.open( ctx.menu.credEntries[i].id, ctx ) 43 43 * (stays in @c VaultMenu; the detail overlay takes the next tick). 44 44 * - @ref showTotp → load the TOTP entry + @c totp_.begin( ..., ctx ) 45 45 * (stays in @c VaultMenu; the TOTP screen takes the next tick). ··· 56 56 57 57 /** 58 58 * @brief Open the credential-detail overlay for credential row @p credIndex. 59 - * @param[in] credIndex Index into @c ctx.credEntries of the selected row. 59 + * @param[in] credIndex Index into @c ctx.menu.credEntries of the selected row. 60 60 * @return @c AppState::VaultMenu (stay; the overlay owns the next tick). 61 61 */ 62 62 virtual AppState openCredential( uint8_t credIndex ) = 0; 63 63 64 64 /** 65 65 * @brief Load TOTP row @p totpIndex and open the full-screen TOTP display. 66 - * @param[in] totpIndex Index into @c ctx.totpEntries of the selected row. 66 + * @param[in] totpIndex Index into @c ctx.menu.totpEntries of the selected row. 67 67 * @return @c AppState::VaultMenu (stay; the TOTP screen owns the next tick). 68 68 */ 69 69 virtual AppState showTotp( uint8_t totpIndex ) = 0; ··· 87 87 virtual void openAboutOverlay() = 0; 88 88 89 89 /** 90 - * @brief Type the credential already staged in @c ctx.bleCredential over BLE 90 + * @brief Type the credential already staged in @c ctx.vault.bleCredential over BLE 91 91 * HID (the keyboard @c DevicePanel's "send" action). 92 92 * @return @c AppState::BleHidTyping on a successful dispatch, else 93 93 * @c AppState::VaultMenu (see @c startBleTyping).
+20 -17
src/states/vault_state.cpp
··· 134 134 // Vault-fill warning: surface a single non-blocking toast per session 135 135 // when the vault is approaching its hard cap (leaves ~5 slots to spare). 136 136 if ( !vaultFullWarned_ 137 - && ctx_.credCount >= ( kMaxCredentials > 5 ? kMaxCredentials - 5 : kMaxCredentials ) ) { 137 + && ctx_.menu.credCount 138 + >= ( kMaxCredentials > 5 ? kMaxCredentials - 5 : kMaxCredentials ) ) { 138 139 kleidos::platform::str::InplaceString<31> detail; 139 - detail.format( "%u of %u", static_cast<unsigned>( ctx_.credCount ), 140 + detail.format( "%u of %u", static_cast<unsigned>( ctx_.menu.credCount ), 140 141 static_cast<unsigned>( kMaxCredentials ) ); 141 142 popup::toast( i18n::tr( i18n::StringId::PopVaultAlmostFull ), detail.c_str(), 142 143 popup::Severity::CAUTION, 3000 ); ··· 144 145 } 145 146 146 147 ctx_.resetActivity(); 147 - KLEIDOS_LOGI( kTag, "Entering vault menu (%u creds, %u TOTP)", ctx_.credCount, ctx_.totpCount ); 148 + KLEIDOS_LOGI( kTag, "Entering vault menu (%u creds, %u TOTP)", ctx_.menu.credCount, 149 + ctx_.menu.totpCount ); 148 150 } 149 151 150 152 const char* VaultStateHandler::debugScreenName() const { ··· 180 182 * or @c AppState::BleHidTyping (when the user has selected a credential). 181 183 * 182 184 * @warning SECURITY: this function is the single chokepoint for credential 183 - * staging into @c ctx_.bleCredential. It must zeroize the buffer 185 + * staging into @c ctx_.vault.bleCredential. It must zeroize the buffer 184 186 * immediately after dispatching the BLE state. 185 187 * 186 188 * @return The next @c AppState — see the class header for the transition table. ··· 283 285 case TypeBle: 284 286 return startBleTyping(); 285 287 case Deleted: 286 - ctx_.credListLoaded = false; 287 - menuRedraw_ = true; // ensure full repaint after toast clears 288 + ctx_.menu.credListLoaded = false; 289 + menuRedraw_ = true; // ensure full repaint after toast clears 288 290 return AppState::VaultMenu; 289 291 } 290 292 } ··· 334 336 // Mirrors the exact side effects the Stick / Gray input used to run inline in 335 337 // activateStickPageSelection() / activateTabItem() before the view extraction. 336 338 AppState VaultStateHandler::openCredential( uint8_t credIndex ) { 337 - const auto& entry = ctx_.credEntries[credIndex]; 339 + const auto& entry = ctx_.menu.credEntries[credIndex]; 338 340 KLEIDOS_LOGI( kTag, "Opening credential detail: %s", entry.name.c_str() ); 339 341 const bool opened = credDetail_.open( entry.id, ctx_ ); 340 342 // Gray (3-button) surfaces a toast on open-failure (verbatim from the legacy ··· 349 351 #endif // HAS_KEYBOARD 350 352 351 353 AppState VaultStateHandler::showTotp( uint8_t totpIndex ) { 352 - KLEIDOS_LOGI( kTag, "Showing TOTP %u", ctx_.totpEntries[totpIndex].id ); 354 + KLEIDOS_LOGI( kTag, "Showing TOTP %u", ctx_.menu.totpEntries[totpIndex].id ); 353 355 TotpEntry tmp = {}; 354 356 bool loaded = false; 355 357 #if defined( DEBUG_SERIAL_BUTTONS ) 356 - if ( ctx_.debugMockVaultPreview ) { 357 - fillDebugPreviewTotp( ctx_.totpEntries[totpIndex], tmp ); 358 + if ( ctx_.vault.debugMockVaultPreview ) { 359 + fillDebugPreviewTotp( ctx_.menu.totpEntries[totpIndex], tmp ); 358 360 loaded = true; 359 361 } else 360 362 #endif // defined( DEBUG_SERIAL_BUTTONS ) ··· 363 365 KLEIDOS_LOGE( kTag, "Vault open failed" ); 364 366 return AppState::VaultMenu; 365 367 } 366 - loaded = VaultStore::loadTotp( ctx_.masterKey.data(), ctx_.totpEntries[totpIndex].id, tmp ); 368 + loaded = VaultStore::loadTotp( ctx_.vault.masterKey.data(), 369 + ctx_.menu.totpEntries[totpIndex].id, tmp ); 367 370 VaultStore::end(); 368 371 } 369 372 if ( loaded ) { ··· 460 463 461 464 // Display dimming and idle sleep 462 465 ctx_.handleDisplayDimming(); 463 - if ( platformClock::millis32() - ctx_.lastActivity >= ctx_.idleSleepMs ) { 466 + if ( platformClock::millis32() - ctx_.display.lastActivity >= ctx_.display.idleSleepMs ) { 464 467 KLEIDOS_LOGI( kTag, "Vault idle timeout — sleeping" ); 465 468 ctx_.goToSleep(); 466 469 } ··· 529 532 530 533 // --------------------------------------------------------------------------- 531 534 /** 532 - * @brief Load the active credential into @c ctx_.bleCredential and transition 535 + * @brief Load the active credential into @c ctx_.vault.bleCredential and transition 533 536 * to @c AppState::BleHidTyping. 534 537 * 535 538 * Shared by the regular credential path and the TOTP path (which packs the 536 539 * 6-digit code as a synthetic credential). Performs a sanity check on the 537 540 * loaded payload, sets a "typing" popup transition, and dispatches. 538 541 * 539 - * @warning SECURITY: this is the only writer of @c ctx_.bleCredential 542 + * @warning SECURITY: this is the only writer of @c ctx_.vault.bleCredential 540 543 * outside the debug path. The BLE state handler always wipes 541 544 * the buffer on @c exit() and on every error branch. 542 545 * ··· 565 568 return AppState::BleHidTyping; 566 569 } 567 570 KLEIDOS_LOGE( kTag, "BLE init failed" ); 568 - ctx_.bleCredential.wipe(); 571 + ctx_.vault.bleCredential.wipe(); 569 572 popup::transition( i18n::tr( i18n::StringId::PopBleInitFail ), 570 573 i18n::tr( i18n::StringId::CommonTryAgain ), popup::Severity::WARNING, 1800 ); 571 574 menuRedraw_ = true; ··· 616 619 617 620 display::wakeup(); 618 621 display::setBrightness( kBrightnessFull ); 619 - ctx_.displayDimmed = false; 622 + ctx_.display.displayDimmed = false; 620 623 621 624 ui::RasterCanvas rasterCanvas; 622 625 ui::Canvas& canvas = rasterCanvas; ··· 654 657 ctx_.inputB.clearEvents(); 655 658 656 659 drawStep( kY2, ".", "Starting WiFi", theme::kLockout565 ); 657 - bool wifiOk = AdminPortal::begin( ctx_.masterKey.data(), ctx_.power ); 660 + bool wifiOk = AdminPortal::begin( ctx_.vault.masterKey.data(), ctx_.power ); 658 661 ctx_.inputB.clearEvents(); 659 662 660 663 if ( wifiOk ) {
+3 -3
src/states/vault_state.h
··· 17 17 * @c kleidos::totp worker (also Core 0). Results are returned through 18 18 * the vault store facade and indexed by the per-row username cache 19 19 * populated lazily as rows become visible. The on-screen user-cache and 20 - * the @c ctx_.bleCredential staging buffer are private to the loop task. 20 + * the @c ctx_.vault.bleCredential staging buffer are private to the loop task. 21 21 * 22 - * @warning SECURITY: the master key is live in @c ctx_.masterKey for the 22 + * @warning SECURITY: the master key is live in @c ctx_.vault.masterKey for the 23 23 * entire @c AppState::VaultMenu session. Every code path that 24 24 * stages a credential for BLE HID must zeroize @c 25 - * ctx_.bleCredential immediately after dispatching 25 + * ctx_.vault.bleCredential immediately after dispatching 26 26 * @c AppState::BleHidTyping — see @c startBleTyping() in the 27 27 * .cpp for the canonical pattern. The on-device cache of 28 28 * decrypted usernames (@c credRowCache_) lives only for the
+21 -21
src/ui/debug/debug_ble.cpp
··· 86 86 87 87 /** 88 88 * @brief Load a credential slot from the vault and project a single field 89 - * (All / User / Pass) into @c ctx().bleCredential for debug typing. 89 + * (All / User / Pass) into @c ctx().vault.bleCredential for debug typing. 90 90 * 91 91 * @param[in] id Vault slot id to load. 92 92 * @param[in] field Which field(s) of the credential to project. 93 - * @retval true Credential was loaded and projected to @c ctx().bleCredential. 93 + * @retval true Credential was loaded and projected to @c ctx().vault.bleCredential. 94 94 * @retval false Vault is locked, already open, or the slot is empty. 95 95 */ 96 96 static bool loadDebugBleCredential( uint8_t id, DebugBleField field ) { 97 - if ( !ctx().vaultUnlocked ) { 97 + if ( !ctx().vault.vaultUnlocked ) { 98 98 serial::printf( "BLETYPE_FAIL: vault locked\n" ); 99 99 return false; 100 100 } ··· 104 104 serial::printf( "BLETYPE_FAIL: vault open failed\n" ); 105 105 return false; 106 106 } 107 - const bool loaded = VaultStore::loadCredential( ctx().masterKey.data(), id, cred ); 107 + const bool loaded = VaultStore::loadCredential( ctx().vault.masterKey.data(), id, cred ); 108 108 VaultStore::end(); 109 109 if ( !loaded ) { 110 110 cred.wipe(); ··· 112 112 return false; 113 113 } 114 114 115 - ctx().bleCredential.wipe(); 115 + ctx().vault.bleCredential.wipe(); 116 116 using enum DebugBleField; 117 117 switch ( field ) { 118 118 case All: 119 - ctx().bleCredential = cred; 119 + ctx().vault.bleCredential = cred; 120 120 break; 121 121 case User: 122 - kleidos::platform::str::copy( ctx().bleCredential.name, cred.name.c_str() ); 123 - kleidos::platform::str::copy( ctx().bleCredential.pass, cred.user.c_str() ); 122 + kleidos::platform::str::copy( ctx().vault.bleCredential.name, cred.name.c_str() ); 123 + kleidos::platform::str::copy( ctx().vault.bleCredential.pass, cred.user.c_str() ); 124 124 break; 125 125 case Pass: 126 - kleidos::platform::str::copy( ctx().bleCredential.name, cred.name.c_str() ); 127 - kleidos::platform::str::copy( ctx().bleCredential.pass, cred.pass.c_str() ); 126 + kleidos::platform::str::copy( ctx().vault.bleCredential.name, cred.name.c_str() ); 127 + kleidos::platform::str::copy( ctx().vault.bleCredential.pass, cred.pass.c_str() ); 128 128 break; 129 129 } 130 130 cred.wipe(); ··· 188 188 const auto state = kleidos::ble::Facade::state(); 189 189 if ( state == kleidos::ble::StackState::OFF ) { 190 190 if ( !kleidos::ble::Facade::start( kleidos::ble::StartMode::Bounded30s ) ) { 191 - ctx().bleCredential.wipe(); 191 + ctx().vault.bleCredential.wipe(); 192 192 serial::printf( "BLETYPE_FAIL: start failed\n" ); 193 193 return false; 194 194 } ··· 196 196 && state != kleidos::ble::StackState::STARTING 197 197 && state != kleidos::ble::StackState::AUTHENTICATING 198 198 && state != kleidos::ble::StackState::CONNECTED ) { 199 - ctx().bleCredential.wipe(); 199 + ctx().vault.bleCredential.wipe(); 200 200 serial::printf( "BLETYPE_FAIL: reset required\n" ); 201 201 return false; 202 202 } ··· 209 209 210 210 /** 211 211 * @brief Begin a debug BLE HID typing session using the credential staged in 212 - * @c ctx().bleCredential. 212 + * @c ctx().vault.bleCredential. 213 213 * 214 214 * Issues a `typeCredential` request (user + password) when the credential 215 215 * carries a user, or a `typePassword` request otherwise. Stores the typing ··· 230 230 return false; 231 231 } 232 232 233 - if ( const size_t userLen = kleidos::platform::str::length( ctx().bleCredential.user ); 233 + if ( const size_t userLen = kleidos::platform::str::length( ctx().vault.bleCredential.user ); 234 234 userLen > 0 ) { 235 235 s_debugBleTypingToken = kleidos::ble::Facade::typeCredential( 236 - std::string_view{ ctx().bleCredential.user.c_str(), userLen }, 237 - std::string_view{ ctx().bleCredential.pass.c_str(), 238 - kleidos::platform::str::length( ctx().bleCredential.pass ) } ); 236 + std::string_view{ ctx().vault.bleCredential.user.c_str(), userLen }, 237 + std::string_view{ ctx().vault.bleCredential.pass.c_str(), 238 + kleidos::platform::str::length( ctx().vault.bleCredential.pass ) } ); 239 239 } else { 240 240 s_debugBleTypingToken = kleidos::ble::Facade::typePassword( 241 - std::string_view{ ctx().bleCredential.pass.c_str(), 242 - kleidos::platform::str::length( ctx().bleCredential.pass ) } ); 241 + std::string_view{ ctx().vault.bleCredential.pass.c_str(), 242 + kleidos::platform::str::length( ctx().vault.bleCredential.pass ) } ); 243 243 } 244 244 245 245 if ( s_debugBleTypingToken == 0 ) { 246 246 serial::printf( "BLESEND_FAIL: begin failed\n" ); 247 247 return false; 248 248 } 249 - ctx().bleCredential.wipe(); 249 + ctx().vault.bleCredential.wipe(); 250 250 ctx().resetActivity(); 251 251 serial::printf( "BLESEND_OK\n" ); 252 252 return true; ··· 278 278 s_debugBleTypingToken = 0; 279 279 serial::printf( "BLESEND_FAIL: status=%d\n", static_cast<int>( status ) ); 280 280 kleidos::ble::Facade::stop(); 281 - ctx().bleCredential.wipe(); 281 + ctx().vault.bleCredential.wipe(); 282 282 transitionTo( AppState::VaultMenu ); 283 283 break; 284 284 }
+39 -37
src/ui/debug/debug_console.cpp
··· 553 553 []( const char* ) { 554 554 enterDebugVaultPreview(); 555 555 serial::printf( "MOCKVAULT_OK: creds=%u totp=%u\n", 556 - static_cast<unsigned>( ctx().credCount ), 557 - static_cast<unsigned>( ctx().totpCount ) ); 556 + static_cast<unsigned>( ctx().menu.credCount ), 557 + static_cast<unsigned>( ctx().menu.totpCount ) ); 558 558 return true; 559 559 }, 560 560 "Vault", ··· 739 739 { Match::Exact, 740 740 { "VAULT?", "VAULTDIAG", nullptr, nullptr }, 741 741 []( const char* ) { 742 - if ( ctx().vaultUnlocked ) { 742 + if ( ctx().vault.vaultUnlocked ) { 743 743 ctx().loadCredentialList(); 744 744 } 745 745 serial::printf( "VAULT unlocked=%u creds=%u totp=%u selected=%u\n", 746 - ctx().vaultUnlocked ? 1U : 0U, static_cast<unsigned>( ctx().credCount ), 747 - static_cast<unsigned>( ctx().totpCount ), 748 - static_cast<unsigned>( ctx().menuSelected ) ); 746 + ctx().vault.vaultUnlocked ? 1U : 0U, 747 + static_cast<unsigned>( ctx().menu.credCount ), 748 + static_cast<unsigned>( ctx().menu.totpCount ), 749 + static_cast<unsigned>( ctx().menu.menuSelected ) ); 749 750 return true; 750 751 }, 751 752 "Vault", ··· 944 945 } 945 946 if ( str::equalsIgnoreCase( arg, "ON" ) ) { 946 947 prefs.setU8( "disp_stayon", 1 ); 947 - ctx().displayDimMs = UINT32_MAX; 948 - ctx().displayOffMs = UINT32_MAX; 949 - ctx().idleSleepMs = UINT32_MAX; 950 - ctx().displayDimmed = false; 948 + ctx().display.displayDimMs = UINT32_MAX; 949 + ctx().display.displayOffMs = UINT32_MAX; 950 + ctx().display.idleSleepMs = UINT32_MAX; 951 + ctx().display.displayDimmed = false; 951 952 ctx().wakeDisplay(); 952 953 ctx().resetActivity(); 953 954 serial::printf( "DISPKEEP_OK on=1\n" ); 954 955 } else if ( str::equalsIgnoreCase( arg, "OFF" ) ) { 955 956 prefs.setU8( "disp_stayon", 0 ); 956 - ctx().displayDimMs = prefs.getU32( "disp_dim_ms", kDefaultDisplayDimMs ); 957 - ctx().displayOffMs = prefs.getU32( "disp_off_ms", kDefaultDisplayOffMs ); 958 - ctx().idleSleepMs = prefs.getU32( "idle_slp_ms", kDefaultIdleSleepMs ); 957 + ctx().display.displayDimMs = prefs.getU32( "disp_dim_ms", kDefaultDisplayDimMs ); 958 + ctx().display.displayOffMs = prefs.getU32( "disp_off_ms", kDefaultDisplayOffMs ); 959 + ctx().display.idleSleepMs = prefs.getU32( "idle_slp_ms", kDefaultIdleSleepMs ); 959 960 ctx().resetActivity(); 960 961 serial::printf( "DISPKEEP_OK on=0\n" ); 961 962 } else { 962 963 serial::printf( "DISPKEEP on=%u dim_ms=%u off_ms=%u idle_ms=%u\n", 963 964 static_cast<unsigned>( prefs.getU8( "disp_stayon", 0 ) ), 964 - static_cast<unsigned>( ctx().displayDimMs ), 965 - static_cast<unsigned>( ctx().displayOffMs ), 966 - static_cast<unsigned>( ctx().idleSleepMs ) ); 965 + static_cast<unsigned>( ctx().display.displayDimMs ), 966 + static_cast<unsigned>( ctx().display.displayOffMs ), 967 + static_cast<unsigned>( ctx().display.idleSleepMs ) ); 967 968 } 968 969 return true; 969 970 }, ··· 983 984 if ( str::equalsIgnoreCase( arg, "OFF" ) || str::equalsIgnoreCase( arg, "ON" ) ) { 984 985 const bool on = str::equalsIgnoreCase( arg, "ON" ); 985 986 prefs.setU8( "sleep_stayoff", on ? 1U : 0U ); 986 - ctx().idleSleepMs = 987 + ctx().display.idleSleepMs = 987 988 on ? UINT32_MAX : prefs.getU32( "idle_slp_ms", kDefaultIdleSleepMs ); 988 989 ctx().resetActivity(); 989 990 serial::printf( "SLEEP_OK on=%u idle_ms=%u\n", on ? 1U : 0U, 990 - static_cast<unsigned>( ctx().idleSleepMs ) ); 991 + static_cast<unsigned>( ctx().display.idleSleepMs ) ); 991 992 } else { 992 993 serial::printf( "SLEEP on=%u idle_ms=%u\n", 993 994 static_cast<unsigned>( prefs.getU8( "sleep_stayoff", 0 ) ), 994 - static_cast<unsigned>( ctx().idleSleepMs ) ); 995 + static_cast<unsigned>( ctx().display.idleSleepMs ) ); 995 996 } 996 997 return true; 997 998 }, ··· 1146 1147 []( const char* ) { 1147 1148 s_debugBleTypingToken = 0; 1148 1149 kleidos::ble::Facade::stop(); 1149 - ctx().bleCredential.wipe(); 1150 - if ( ctx().vaultUnlocked ) { 1150 + ctx().vault.bleCredential.wipe(); 1151 + if ( ctx().vault.vaultUnlocked ) { 1151 1152 transitionTo( AppState::VaultMenu ); 1152 1153 } 1153 1154 serial::printf( "BLESTOP_OK\n" ); ··· 1313 1314 bool ok; 1314 1315 if ( !VaultStore::isProvisioned() ) { 1315 1316 KLEIDOS_LOGI( kTag, "DEBUG: Provisioning vault with PIN" ); 1316 - ok = VaultStore::provision( pin, len, ctx().masterKey.data() ); 1317 + ok = VaultStore::provision( pin, len, ctx().vault.masterKey.data() ); 1317 1318 } else { 1318 - ok = VaultStore::unlockVault( pin, len, ctx().masterKey.data() ); 1319 + ok = VaultStore::unlockVault( pin, len, ctx().vault.masterKey.data() ); 1319 1320 } 1320 1321 VaultStore::end(); 1321 1322 if ( !ok ) { ··· 1323 1324 return true; 1324 1325 } 1325 1326 #ifdef DEBUG_SERIAL_BUTTONS 1326 - ctx().debugMockVaultPreview = false; 1327 + ctx().vault.debugMockVaultPreview = false; 1327 1328 #endif // DEBUG_SERIAL_BUTTONS 1328 1329 BruteForceGuard::resetAttempts(); 1329 - ctx().vaultUnlocked = true; 1330 + ctx().vault.vaultUnlocked = true; 1330 1331 ctx().resetActivity(); 1331 1332 display::wakeup(); 1332 1333 display::setBrightness( kBrightnessFull ); 1333 - ctx().displayDimmed = false; 1334 + ctx().display.displayDimmed = false; 1334 1335 transitionTo( AppState::VaultMenu ); 1335 1336 serial::printf( "PIN_OK\n" ); 1336 1337 return true; ··· 1340 1341 { Match::Prefix, 1341 1342 { "ADDCRED ", nullptr, nullptr, nullptr }, 1342 1343 []( const char* cmd ) { 1343 - if ( !ctx().vaultUnlocked ) { 1344 + if ( !ctx().vault.vaultUnlocked ) { 1344 1345 serial::printf( "ADDCRED_FAIL: vault locked\n" ); 1345 1346 return true; 1346 1347 } ··· 1381 1382 kleidos::platform::str::copy( cred.user, user ); 1382 1383 kleidos::platform::str::copy( cred.pass, pass ); 1383 1384 buf.secureZeroize(); 1384 - bool ok = VaultStore::saveCredential( ctx().masterKey.data(), slot, cred ); 1385 + bool ok = VaultStore::saveCredential( ctx().vault.masterKey.data(), slot, cred ); 1385 1386 cred.wipe(); 1386 1387 VaultStore::end(); 1387 1388 if ( ok ) { 1388 - ctx().credListLoaded = false; // Force reload on next draw 1389 - ctx().resetActivity(); // Reset idle timer after slow PBKDF2 1389 + ctx().menu.credListLoaded = false; // Force reload on next draw 1390 + ctx().resetActivity(); // Reset idle timer after slow PBKDF2 1390 1391 serial::printf( "ADDCRED_OK: slot=%u\n", slot ); 1391 1392 } else { 1392 1393 serial::printf( "ADDCRED_FAIL: save failed\n" ); ··· 1406 1407 } 1407 1408 if ( ( st == AppState::VaultMenu || st == AppState::AdminMode 1408 1409 || st == AppState::BleHidTyping ) 1409 - && !ctx().vaultUnlocked ) { 1410 + && !ctx().vault.vaultUnlocked ) { 1410 1411 serial::printf( "GOTO_FAIL: vault locked; use PIN <digits> first\n" ); 1411 1412 return true; 1412 1413 } ··· 1465 1466 } 1466 1467 VaultStore::wipeAll(); 1467 1468 VaultStore::end(); 1468 - ctx().vaultUnlocked = false; 1469 - ctx().credListLoaded = false; 1469 + ctx().vault.vaultUnlocked = false; 1470 + ctx().menu.credListLoaded = false; 1470 1471 #ifdef DEBUG_SERIAL_BUTTONS 1471 - ctx().debugMockVaultPreview = false; 1472 + ctx().vault.debugMockVaultPreview = false; 1472 1473 #endif // DEBUG_SERIAL_BUTTONS 1473 - VaultCrypto::secureWipe( ctx().masterKey.data(), sizeof( ctx().masterKey.data() ) ); 1474 + VaultCrypto::secureWipe( ctx().vault.masterKey.data(), 1475 + sizeof( ctx().vault.masterKey.data() ) ); 1474 1476 // Leave any vault-dependent state before the wiped+locked vault is 1475 1477 // rendered again (avoids rendering against torn-down vault state). 1476 1478 if ( ctx().appState == AppState::VaultMenu || ctx().appState == AppState::AdminMode ··· 1485 1487 { Match::Exact, 1486 1488 { "ADDDUMMY", nullptr, nullptr, nullptr }, 1487 1489 []( const char* ) { 1488 - if ( !ctx().vaultUnlocked ) { 1490 + if ( !ctx().vault.vaultUnlocked ) { 1489 1491 serial::printf( "ADDDUMMY_FAIL: vault locked\n" ); 1490 1492 return true; 1491 1493 } 1492 - uint8_t added = loadDummyCredentials( ctx().masterKey.data(), ctx() ); 1494 + uint8_t added = loadDummyCredentials( ctx().vault.masterKey.data(), ctx() ); 1493 1495 serial::printf( "ADDDUMMY_OK: %u credentials added\n", added ); 1494 1496 return true; 1495 1497 },
+1 -1
src/ui/debug/debug_diagnostics.cpp
··· 106 106 static_cast<unsigned long>( largestBlock ), static_cast<unsigned>( taskCount ), 107 107 static_cast<unsigned>( loopStackFree ), rtos::currentCoreId(), 108 108 appStateName( ctx().appState ), bleStackStateName( bleStackState ), 109 - ctx().vaultUnlocked ? 1U : 0U, static_cast<unsigned long>( bleStackFree ), 109 + ctx().vault.vaultUnlocked ? 1U : 0U, static_cast<unsigned long>( bleStackFree ), 110 110 static_cast<unsigned long>( kleidos::ble::Facade::droppedCommands() ), 111 111 static_cast<unsigned long>( kleidos::ble::Facade::droppedEvents() ), 112 112 static_cast<unsigned long>( kleidos::ble::Facade::typingLatencyMedianUs() ) );
+18 -18
src/ui/debug/debug_popup.cpp
··· 159 159 * flags @c debugMockVaultPreview, and requests a full redraw. 160 160 */ 161 161 static void seedDebugVaultPreview() { 162 - VaultCrypto::secureWipe( ctx().masterKey.data(), sizeof( ctx().masterKey.data() ) ); 163 - memset( ctx().credEntries.data(), 0, sizeof( ctx().credEntries.data() ) ); 164 - memset( ctx().totpEntries.data(), 0, sizeof( ctx().totpEntries.data() ) ); 162 + VaultCrypto::secureWipe( ctx().vault.masterKey.data(), sizeof( ctx().vault.masterKey.data() ) ); 163 + memset( ctx().menu.credEntries.data(), 0, sizeof( ctx().menu.credEntries.data() ) ); 164 + memset( ctx().menu.totpEntries.data(), 0, sizeof( ctx().menu.totpEntries.data() ) ); 165 165 166 - ctx().masterKey[0] = 0xA5; 167 - ctx().masterKey[1] = 0x5A; 168 - ctx().vaultUnlocked = true; 166 + ctx().vault.masterKey[0] = 0xA5; 167 + ctx().vault.masterKey[1] = 0x5A; 168 + ctx().vault.vaultUnlocked = true; 169 169 #ifdef DEBUG_SERIAL_BUTTONS 170 - ctx().debugMockVaultPreview = true; 170 + ctx().vault.debugMockVaultPreview = true; 171 171 #endif // DEBUG_SERIAL_BUTTONS 172 - ctx().credCount = 4; 173 - ctx().totpCount = 2; 174 - ctx().menuSelected = 0; 175 - ctx().credListLoaded = true; 176 - ctx().fullRedraw = true; 172 + ctx().menu.credCount = 4; 173 + ctx().menu.totpCount = 2; 174 + ctx().menu.menuSelected = 0; 175 + ctx().menu.credListLoaded = true; 176 + ctx().display.fullRedraw = true; 177 177 178 - setDebugCredentialEntry( ctx().credEntries[0], 0, "github.com", true, 0 ); 179 - setDebugCredentialEntry( ctx().credEntries[1], 1, "mail.example", false, 1 ); 180 - setDebugCredentialEntry( ctx().credEntries[2], 2, "admin portal", false, 2 ); 181 - setDebugCredentialEntry( ctx().credEntries[3], 3, "bank login", false, 3 ); 178 + setDebugCredentialEntry( ctx().menu.credEntries[0], 0, "github.com", true, 0 ); 179 + setDebugCredentialEntry( ctx().menu.credEntries[1], 1, "mail.example", false, 1 ); 180 + setDebugCredentialEntry( ctx().menu.credEntries[2], 2, "admin portal", false, 2 ); 181 + setDebugCredentialEntry( ctx().menu.credEntries[3], 3, "bank login", false, 3 ); 182 182 183 - setDebugCredentialEntry( ctx().totpEntries[0], 0, "github 2fa", true, 0 ); 184 - setDebugCredentialEntry( ctx().totpEntries[1], 1, "vpn token", false, 1 ); 183 + setDebugCredentialEntry( ctx().menu.totpEntries[0], 0, "github 2fa", true, 0 ); 184 + setDebugCredentialEntry( ctx().menu.totpEntries[1], 1, "vpn token", false, 1 ); 185 185 } 186 186 187 187 /**