[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.

Migrate LittleFS to platform facade

José Manuel Requena Plens (May 22, 2026, 12:09 AM +0200) 79a612f5 751a667f

+802 -207
+5 -4
.github/copilot-instructions.md
··· 71 71 simultaneously. Stop BLE before starting OTA. 72 72 - **Display**: dim after 5 s, off after 10 s of inactivity. 73 73 Never leave backlight on during deep sleep. 74 - - **LittleFS**: call `LittleFS.end()` before every deep sleep call. 74 + - **LittleFS**: call `kleidos::platform::littlefs::end()` before every deep sleep call. 75 75 LittleFS is NOT erased by OTA updates — vault data persists. 76 76 77 77 ### 5. Platform abstraction is mandatory ··· 90 90 - Abstraction-first rule: if a platform API is likely to be reused, tested, 91 91 replaced, or ported in the future, create or extend a facade instead of 92 92 spreading direct calls. Current high-priority candidates include watchdog, 93 - LittleFS lifecycle, WiFi/httpd/OTA, heap/reset diagnostics, and sleep/wake 93 + WiFi/httpd/OTA, heap/reset diagnostics, and sleep/wake 94 94 APIs. Reusable timing/RNG logic should use `platform/Clock.h` and 95 95 `platform/Random.h`; debug serial I/O should use 96 - `platform/SerialTransport.h`. 96 + `platform/SerialTransport.h`; LittleFS mount/unmount and file I/O should use 97 + `platform/LittleFs.h`. 97 98 - Raw platform calls are allowed only as narrow, documented exceptions when a 98 99 facade would hide important semantics. Keep exceptions local and update 99 100 `docs/platform-abstraction.md`. ··· 108 109 109 110 - `constexpr` for all compile-time constants. No magic numbers. 110 111 - `enum class` for all state definitions. 111 - - RAII guards for subsystems: `BleSessionGuard`, `WifiSessionGuard`, `LittleFSGuard` 112 + - RAII guards for subsystems: `BleSessionGuard`, `WifiSessionGuard`, `LittleFsGuard` 112 113 - Error paths must log failure reason through `KLEIDOS_LOG*` — never credential data. 113 114 - PlatformIO unit tests (`test/`) must cover all crypto functions. 114 115
+3 -2
.github/memories/platform-abstraction.md
··· 1 1 # Platform abstraction memory 2 2 3 3 - Application firmware must use `src/platform/` facades for RTOS, logging, and reusable platform services. 4 - - Current facade modules: `Logger`, `RtosTypes`, `RtosTime`, `RtosTask`, `RtosSync`, `RtosQueue`, `NvsStore`, `Clock`, `Random`, `SerialTransport`; `Rtos.h` is an umbrella include. 4 + - Current facade modules: `Logger`, `RtosTypes`, `RtosTime`, `RtosTask`, `RtosSync`, `RtosQueue`, `NvsStore`, `Clock`, `Random`, `SerialTransport`, `LittleFs`; `Rtos.h` is an umbrella include. 5 5 - `SerialTransport` uses ESP-IDF UART and USB Serial/JTAG drivers; do not use Arduino `Serial` from application code. 6 + - `LittleFs` uses ESP-IDF `esp_littlefs` plus VFS/POSIX calls; do not use Arduino `LittleFS` from application code. 6 7 - Do not spread raw ESP-IDF/FreeRTOS APIs outside `src/platform/`; create/extend facades for future abstraction candidates. 7 8 - `NimbleHost.cpp` uses `rtos::CriticalMux`/`rtos::CriticalLock` for SMP callback mailbox critical sections, not raw `portMUX_TYPE`. 8 - - Future candidates: watchdog, LittleFS lifecycle, WiFi/httpd/OTA, heap/reset diagnostics, sleep/wake. 9 + - Future candidates: watchdog, WiFi/httpd/OTA, heap/reset diagnostics, sleep/wake.
+3 -3
.github/skills/platform-abstraction/SKILL.md
··· 36 36 | `platform/Clock.h/.cpp` | Monotonic milliseconds/microseconds for reusable logic and native builds. | 37 37 | `platform/Random.h/.cpp` | Hardware RNG-backed byte/integer helpers plus bounded/range helpers. | 38 38 | `platform/SerialTransport.h/.cpp` | Serial/debug transport over ESP-IDF UART and USB Serial/JTAG drivers for command responses, screenshot streaming, harness protocol output, and flush semantics. | 39 + | `platform/LittleFs.h/.cpp` | LittleFS lifecycle, format, file, directory, and VFS-backed path helpers over ESP-IDF `esp_littlefs`. | 39 40 40 41 Templates and compile-time constants may stay in headers. Non-template 41 42 functions should live in `.cpp` files when possible. ··· 63 64 from application-level modules: 64 65 65 66 - Watchdog APIs (`esp_task_wdt_*`) 66 - - LittleFS lifecycle and filesystem mount/unmount policy 67 67 - WiFi AP/client, `esp_netif`, `esp_event`, `esp_http_server`, OTA ops 68 68 - BLE host lifecycle primitives that are not already isolated under `src/ble/` 69 69 - Heap, stack, reset reason, chip diagnostics, and runtime stats ··· 83 83 84 84 Current normal exception boundary: `src/platform/Logger.*`, 85 85 `src/platform/Rtos*.*`, `src/platform/Clock.*`, `src/platform/Random.*`, 86 - `src/platform/NvsStore.*`, and `src/platform/SerialTransport.*`. Existing 87 - HAL/UI adapters may retain Arduino timing 86 + `src/platform/NvsStore.*`, `src/platform/SerialTransport.*`, and 87 + `src/platform/LittleFs.*`. Existing HAL/UI adapters may retain Arduino timing 88 88 calls while they are still Arduino/M5Unified-owned. `NimbleHost.cpp` may need SMP 89 89 critical-section semantics, but must use `rtos::CriticalMux` / 90 90 `rtos::CriticalLock` rather than raw FreeRTOS.
+1 -1
docs/architecture.md
··· 380 380 2. **WiFi**: active only in ADMIN_MODE. BLE must be off before WiFi starts. 381 381 3. **Display**: dims after configurable timeout, turns off after extended idle. 382 382 Never left on during deep sleep. 383 - 4. **LittleFS**: `LittleFS.end()` called before every deep sleep entry. 383 + 4. **LittleFS**: `kleidos::platform::littlefs::end()` called before every deep sleep entry. 384 384 5. **Audio**: `audio.end()` called before deep sleep. 385 385 386 386 ### Deep Sleep Entry
+4
docs/developer-guide.md
··· 260 260 WifiAdminPortal::begin(); // init 261 261 // ... use WiFi ... 262 262 WifiAdminPortal::end(); // deinit — MANDATORY 263 + 264 + kleidos::platform::littlefs::begin(); // mount vault filesystem 265 + // ... use vault files through the facade or VaultStore ... 266 + kleidos::platform::littlefs::end(); // unmount before deep sleep — MANDATORY 263 267 ``` 264 268 265 269 Missing shutdown paths are **bugs**, not warnings.
+1 -1
docs/hardware.md
··· 299 299 3. BLE fully deinitialized after typing (`BLEDevice::deinit(true)`) 300 300 4. WiFi only in admin mode — never concurrent with BLE 301 301 5. Display: dim 5 s → off 10 s → sleep 60 s (all configurable) 302 - 6. LittleFS closed before deep sleep (`LittleFS.end()`) 302 + 6. LittleFS unmounted before deep sleep (`kleidos::platform::littlefs::end()`) 303 303 7. DFS: CPU frequency adjusted per state (80–240 MHz) 304 304 305 305 ## USB and Debugging
+3 -3
docs/platform-abstraction.md
··· 26 26 | `src/platform/Clock.h/.cpp` | Monotonic millisecond/microsecond clock facade backed by `esp_timer_get_time()` on firmware and `std::chrono::steady_clock` on native builds. | 27 27 | `src/platform/Random.h/.cpp` | Random byte and integer facade backed by ESP-IDF hardware RNG on firmware and native host entropy stubs for tests. | 28 28 | `src/platform/SerialTransport.h/.cpp` | Serial/debug transport facade over ESP-IDF UART and USB Serial/JTAG drivers for command responses, screenshot streaming, harness protocol lines, and debug flush semantics. | 29 + | `src/platform/LittleFs.h/.cpp` | LittleFS mount/unmount/format and file I/O facade over ESP-IDF `esp_littlefs` plus VFS/POSIX calls. | 29 30 30 31 ## Logging Rules 31 32 ··· 83 84 module: 84 85 85 86 - Watchdog APIs (`esp_task_wdt_*`) 86 - - LittleFS lifecycle and filesystem mount/unmount policy 87 87 - WiFi AP/client, `esp_netif`, `esp_event`, `esp_http_server`, and OTA ops 88 88 - BLE host lifecycle primitives not already isolated under `src/ble/` 89 89 - Heap, reset reason, chip diagnostics, runtime stats, and stack telemetry ··· 97 97 98 98 - `src/platform/Logger.*`, `src/platform/Rtos*.*`, `src/platform/Clock.*`, 99 99 `src/platform/Random.*`, `src/platform/NvsStore.*`, and 100 - `src/platform/SerialTransport.*` own their matching ESP-IDF platform APIs for 101 - normal firmware code. 100 + `src/platform/SerialTransport.*`, and `src/platform/LittleFs.*` own their 101 + matching ESP-IDF platform APIs for normal firmware code. 102 102 - `src/ble/stack/NimbleHost.cpp` still needs SMP critical-section semantics for 103 103 NimBLE callback mailbox state, but it must use `rtos::CriticalMux` and 104 104 `rtos::CriticalLock` instead of raw `portMUX_TYPE` or `portENTER_CRITICAL`.
+3 -3
src/CMakeLists.txt
··· 26 26 idf_component_register( 27 27 SRCS ${app_sources} 28 28 INCLUDE_DIRS "." 29 - REQUIRES framework-arduinoespressif32 bt h2zero__esp-nimble-cpp nvs_flash esp_timer esp_hw_support esp_driver_uart esp_driver_usb_serial_jtag 29 + REQUIRES framework-arduinoespressif32 bt h2zero__esp-nimble-cpp nvs_flash esp_timer esp_hw_support esp_driver_uart esp_driver_usb_serial_jtag joltwallet__littlefs 30 30 ) 31 31 elseif(KLEIDOS_WIFI_HOST_HARNESS) 32 32 idf_component_register( 33 33 SRCS ${app_sources} 34 34 INCLUDE_DIRS "." 35 - REQUIRES framework-arduinoespressif32 esp_wifi esp_netif esp_event esp_http_client nvs_flash mbedtls esp_timer esp_hw_support esp_driver_uart esp_driver_usb_serial_jtag 35 + REQUIRES framework-arduinoespressif32 esp_wifi esp_netif esp_event esp_http_client nvs_flash mbedtls esp_timer esp_hw_support esp_driver_uart esp_driver_usb_serial_jtag joltwallet__littlefs 36 36 ) 37 37 else() 38 38 idf_component_register( 39 39 SRCS ${app_sources} 40 40 INCLUDE_DIRS "." 41 - REQUIRES framework-arduinoespressif32 kleidos_ble nvs_flash esp_timer esp_hw_support esp_driver_uart esp_driver_usb_serial_jtag 41 + REQUIRES framework-arduinoespressif32 kleidos_ble nvs_flash esp_timer esp_hw_support esp_driver_uart esp_driver_usb_serial_jtag joltwallet__littlefs 42 42 ) 43 43 endif()
+2 -2
src/hal/m5/PowerManagerAXP2101.cpp
··· 9 9 #include <M5Unified.h> 10 10 #include <esp_sleep.h> 11 11 #include "platform/Logger.h" 12 + #include "platform/LittleFs.h" 12 13 #include <esp_wifi.h> 13 - #include <LittleFS.h> 14 14 15 15 static constexpr const char* TAG = "PowerMgr"; 16 16 static constexpr gpio_num_t BTN_A_GPIO = static_cast<gpio_num_t>(PIN_BTN_A); ··· 39 39 esp_wifi_deinit(); 40 40 } 41 41 42 - LittleFS.end(); 42 + kleidos::platform::littlefs::end(); 43 43 44 44 esp_sleep_enable_ext0_wakeup(BTN_A_GPIO, WAKE_LEVEL_LOW); 45 45
+3 -3
src/hal/m5/PowerManagerM5PM1.cpp
··· 11 11 #include <Wire.h> 12 12 #include <esp_sleep.h> 13 13 #include "platform/Logger.h" 14 + #include "platform/LittleFs.h" 14 15 #include <esp_wifi.h> 15 - #include <LittleFS.h> 16 16 17 17 static constexpr const char* TAG = "PowerMgr-S3"; 18 18 static constexpr gpio_num_t BTN_A_GPIO = static_cast<gpio_num_t>(PIN_BTN_A); ··· 103 103 esp_wifi_deinit(); 104 104 } 105 105 106 - LittleFS.end(); 106 + kleidos::platform::littlefs::end(); 107 107 108 108 if (pm1Ready_) { 109 109 // Ensure high-draw peripherals are released before PMIC shutdown. ··· 241 241 if (err == ESP_OK) { 242 242 esp_wifi_deinit(); 243 243 } 244 - LittleFS.end(); 244 + kleidos::platform::littlefs::end(); 245 245 246 246 M5.Speaker.end(); 247 247 M5.Mic.end();
+2 -2
src/hal/tdeck/PowerManagerTDeck.cpp
··· 12 12 #include <Arduino.h> 13 13 #include <esp_sleep.h> 14 14 #include "platform/Logger.h" 15 + #include "platform/LittleFs.h" 15 16 #include <esp_wifi.h> 16 - #include <LittleFS.h> 17 17 18 18 static constexpr const char* TAG = "PowerMgr-TDeck"; 19 19 ··· 71 71 esp_wifi_deinit(); 72 72 } 73 73 74 - LittleFS.end(); 74 + kleidos::platform::littlefs::end(); 75 75 76 76 // Turn off peripherals (display, keyboard, etc.) 77 77 digitalWrite(PIN_BOARD_POWERON, LOW);
+5 -3
src/main.cpp
··· 9 9 #include "hal/common/RtcHAL.h" 10 10 #include "ui/Theme.h" 11 11 #include "platform/Logger.h" 12 + #include "platform/LittleFs.h" 12 13 #include "platform/NvsStore.h" 13 14 #include "platform/Rtos.h" 14 15 #include "platform/SerialTransport.h" ··· 17 18 #include <esp_task_wdt.h> 18 19 #include <esp_heap_caps.h> 19 20 #include <esp_system.h> 20 - #include <LittleFS.h> 21 21 #include <cstdlib> 22 22 #include <cstring> 23 23 #include <string_view> ··· 147 147 148 148 // Convenience alias for the rest of this file 149 149 #define ctx (*ctxPtr) 150 + 151 + namespace lfs = kleidos::platform::littlefs; 150 152 151 153 static void configureApplicationWatchdog() { 152 154 static constexpr uint32_t kTaskWatchdogTimeoutMs = 5000U; ··· 841 843 } 842 844 // Hard wipe: format the LittleFS partition so no stale meta/shadow 843 845 // can survive into the next boot. 844 - LittleFS.end(); 845 - bool fmt = LittleFS.format(); 846 + lfs::end(); 847 + bool fmt = lfs::format(); 846 848 serial::printf("RESETOB: format=%d, rebooting\n", fmt ? 1 : 0); 847 849 serial::flush(); 848 850 rtos::delayMs(50);
+445
src/platform/LittleFs.cpp
··· 1 + // Kleidos - LittleFS lifecycle and file I/O facade implementation 2 + 3 + #include "LittleFs.h" 4 + 5 + #include "Logger.h" 6 + 7 + #include <stddef.h> 8 + #include <stdint.h> 9 + #include <stdio.h> 10 + #include <string.h> 11 + 12 + #ifdef ESP_PLATFORM 13 + #include <dirent.h> 14 + #include <errno.h> 15 + #include <fcntl.h> 16 + #include <sys/stat.h> 17 + #include <sys/types.h> 18 + #include <unistd.h> 19 + 20 + #include <esp_err.h> 21 + #include <esp_littlefs.h> 22 + #endif 23 + 24 + namespace kleidos::platform::littlefs { 25 + namespace { 26 + 27 + constexpr const char* TAG = "LittleFs"; 28 + constexpr const char* kBasePath = "/littlefs"; 29 + constexpr const char* kPartitionLabel = "littlefs"; 30 + constexpr size_t kMaxPhysicalPathBytes = 160; 31 + 32 + #ifdef ESP_PLATFORM 33 + bool s_mounted = false; 34 + 35 + bool buildPhysicalPath(const char* logicalPath, char* out, size_t outSize) { 36 + if (logicalPath == nullptr || logicalPath[0] == '\0' || out == nullptr || outSize == 0) { 37 + return false; 38 + } 39 + 40 + const char* path = logicalPath; 41 + const size_t baseLen = strlen(kBasePath); 42 + if (strncmp(path, kBasePath, baseLen) == 0 && (path[baseLen] == '/' || path[baseLen] == '\0')) { 43 + path += baseLen; 44 + if (path[0] == '\0') path = "/"; 45 + } 46 + if (path[0] != '/') return false; 47 + 48 + const int written = snprintf(out, outSize, "%s%s", kBasePath, path); 49 + return written > 0 && static_cast<size_t>(written) < outSize; 50 + } 51 + 52 + bool joinLogicalPath(const char* parent, const char* child, char* out, size_t outSize) { 53 + if (parent == nullptr || child == nullptr || out == nullptr || outSize == 0) return false; 54 + const bool root = strcmp(parent, "/") == 0; 55 + const int written = snprintf(out, outSize, root ? "/%s" : "%s/%s", parent, child); 56 + return written > 0 && static_cast<size_t>(written) < outSize; 57 + } 58 + 59 + const char* basenameOf(const char* path) { 60 + if (path == nullptr) return ""; 61 + const char* slash = strrchr(path, '/'); 62 + return slash == nullptr ? path : slash + 1; 63 + } 64 + 65 + int flagsForMode(const char* mode) { 66 + if (mode == nullptr || mode[0] == '\0' || strchr(mode, 'r') != nullptr) { 67 + if (mode != nullptr && strchr(mode, '+') != nullptr) return O_RDWR; 68 + return O_RDONLY; 69 + } 70 + if (strchr(mode, 'w') != nullptr) { 71 + return (strchr(mode, '+') != nullptr ? O_RDWR : O_WRONLY) | O_CREAT | O_TRUNC; 72 + } 73 + if (strchr(mode, 'a') != nullptr) { 74 + return (strchr(mode, '+') != nullptr ? O_RDWR : O_WRONLY) | O_CREAT | O_APPEND; 75 + } 76 + return O_RDONLY; 77 + } 78 + #endif 79 + 80 + } // namespace 81 + 82 + File::~File() { 83 + close(); 84 + } 85 + 86 + File::File(File&& other) noexcept { 87 + moveFrom(static_cast<File&&>(other)); 88 + } 89 + 90 + File& File::operator=(File&& other) noexcept { 91 + if (this != &other) { 92 + close(); 93 + moveFrom(static_cast<File&&>(other)); 94 + } 95 + return *this; 96 + } 97 + 98 + bool File::valid() const { 99 + #ifdef ESP_PLATFORM 100 + return fd_ >= 0 || dir_ != nullptr; 101 + #else 102 + return false; 103 + #endif 104 + } 105 + 106 + size_t File::size() const { 107 + #ifdef ESP_PLATFORM 108 + if (fd_ < 0) return 0; 109 + struct stat st = {}; 110 + if (fstat(fd_, &st) != 0 || st.st_size < 0) return 0; 111 + return static_cast<size_t>(st.st_size); 112 + #else 113 + return 0; 114 + #endif 115 + } 116 + 117 + size_t File::read(uint8_t* out, size_t outSize) { 118 + #ifdef ESP_PLATFORM 119 + if (fd_ < 0 || out == nullptr || outSize == 0) return 0; 120 + const ssize_t readBytes = ::read(fd_, out, outSize); 121 + return readBytes > 0 ? static_cast<size_t>(readBytes) : 0; 122 + #else 123 + (void)out; 124 + (void)outSize; 125 + return 0; 126 + #endif 127 + } 128 + 129 + int File::read() { 130 + uint8_t byte = 0; 131 + return read(&byte, sizeof(byte)) == 1 ? static_cast<int>(byte) : -1; 132 + } 133 + 134 + size_t File::write(const uint8_t* data, size_t dataSize) { 135 + #ifdef ESP_PLATFORM 136 + if (fd_ < 0 || data == nullptr || dataSize == 0) return 0; 137 + 138 + size_t written = 0; 139 + while (written < dataSize) { 140 + const ssize_t chunk = ::write(fd_, data + written, dataSize - written); 141 + if (chunk <= 0) break; 142 + written += static_cast<size_t>(chunk); 143 + } 144 + return written; 145 + #else 146 + (void)data; 147 + (void)dataSize; 148 + return 0; 149 + #endif 150 + } 151 + 152 + bool File::available() { 153 + #ifdef ESP_PLATFORM 154 + if (fd_ < 0) return false; 155 + const off_t current = lseek(fd_, 0, SEEK_CUR); 156 + if (current < 0) return false; 157 + const off_t endPos = lseek(fd_, 0, SEEK_END); 158 + if (endPos < 0) return false; 159 + (void)lseek(fd_, current, SEEK_SET); 160 + return endPos > current; 161 + #else 162 + return false; 163 + #endif 164 + } 165 + 166 + File File::openNextFile() { 167 + #ifdef ESP_PLATFORM 168 + if (dir_ == nullptr) return File{}; 169 + 170 + DIR* dir = static_cast<DIR*>(dir_); 171 + while (dirent* entry = readdir(dir)) { 172 + if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) continue; 173 + 174 + char childLogical[sizeof(logicalPath_)] = {}; 175 + if (!joinLogicalPath(logicalPath_, entry->d_name, childLogical, sizeof(childLogical))) { 176 + continue; 177 + } 178 + return openPath(childLogical, "r", entry->d_name); 179 + } 180 + #endif 181 + return File{}; 182 + } 183 + 184 + void File::close() { 185 + #ifdef ESP_PLATFORM 186 + if (fd_ >= 0) { 187 + (void)::close(fd_); 188 + fd_ = -1; 189 + } 190 + if (dir_ != nullptr) { 191 + (void)closedir(static_cast<DIR*>(dir_)); 192 + dir_ = nullptr; 193 + } 194 + #endif 195 + isDirectory_ = false; 196 + } 197 + 198 + File File::openPath(const char* logicalPath, const char* mode, const char* displayName) { 199 + File file; 200 + #ifdef ESP_PLATFORM 201 + if (!mounted()) return file; 202 + 203 + char physicalPath[kMaxPhysicalPathBytes] = {}; 204 + if (!buildPhysicalPath(logicalPath, physicalPath, sizeof(physicalPath))) return file; 205 + 206 + struct stat st = {}; 207 + const bool statOk = stat(physicalPath, &st) == 0; 208 + const bool writeMode = mode != nullptr && (strchr(mode, 'w') != nullptr || strchr(mode, 'a') != nullptr); 209 + 210 + if (statOk && S_ISDIR(st.st_mode) && !writeMode) { 211 + DIR* dir = opendir(physicalPath); 212 + if (dir == nullptr) return file; 213 + file.dir_ = dir; 214 + file.isDirectory_ = true; 215 + file.setLogicalPath(logicalPath); 216 + file.setName(displayName != nullptr ? displayName : basenameOf(logicalPath)); 217 + return file; 218 + } 219 + 220 + const int fd = ::open(physicalPath, flagsForMode(mode), S_IRUSR | S_IWUSR); 221 + if (fd < 0) return file; 222 + 223 + file.fd_ = fd; 224 + file.isDirectory_ = false; 225 + file.setLogicalPath(logicalPath); 226 + file.setName(displayName != nullptr ? displayName : basenameOf(logicalPath)); 227 + #else 228 + (void)logicalPath; 229 + (void)mode; 230 + (void)displayName; 231 + #endif 232 + return file; 233 + } 234 + 235 + void File::moveFrom(File&& other) noexcept { 236 + fd_ = other.fd_; 237 + dir_ = other.dir_; 238 + isDirectory_ = other.isDirectory_; 239 + memcpy(logicalPath_, other.logicalPath_, sizeof(logicalPath_)); 240 + memcpy(name_, other.name_, sizeof(name_)); 241 + 242 + other.fd_ = -1; 243 + other.dir_ = nullptr; 244 + other.isDirectory_ = false; 245 + other.logicalPath_[0] = '\0'; 246 + other.name_[0] = '\0'; 247 + } 248 + 249 + void File::setLogicalPath(const char* path) { 250 + if (path == nullptr) { 251 + logicalPath_[0] = '\0'; 252 + return; 253 + } 254 + snprintf(logicalPath_, sizeof(logicalPath_), "%s", path); 255 + } 256 + 257 + void File::setName(const char* name) { 258 + if (name == nullptr) { 259 + name_[0] = '\0'; 260 + return; 261 + } 262 + snprintf(name_, sizeof(name_), "%s", name); 263 + } 264 + 265 + LittleFsGuard::~LittleFsGuard() { 266 + if (owned_) end(); 267 + } 268 + 269 + bool LittleFsGuard::mount(bool formatOnFail) { 270 + if (mounted_) return true; 271 + const bool wasMounted = ::kleidos::platform::littlefs::mounted(); 272 + mounted_ = begin(formatOnFail); 273 + owned_ = mounted_ && !wasMounted; 274 + return mounted_; 275 + } 276 + 277 + bool begin(bool formatOnFail) { 278 + #ifdef ESP_PLATFORM 279 + if (esp_littlefs_mounted(kPartitionLabel)) { 280 + s_mounted = true; 281 + return true; 282 + } 283 + 284 + esp_vfs_littlefs_conf_t config = {}; 285 + config.base_path = kBasePath; 286 + config.partition_label = kPartitionLabel; 287 + config.format_if_mount_failed = formatOnFail ? 1 : 0; 288 + config.read_only = 0; 289 + config.dont_mount = 0; 290 + config.grow_on_mount = 0; 291 + 292 + const esp_err_t err = esp_vfs_littlefs_register(&config); 293 + if (err != ESP_OK) { 294 + KLEIDOS_LOGE(TAG, "mount failed: %s", esp_err_to_name(err)); 295 + s_mounted = false; 296 + return false; 297 + } 298 + 299 + s_mounted = true; 300 + return true; 301 + #else 302 + (void)formatOnFail; 303 + return false; 304 + #endif 305 + } 306 + 307 + void end() { 308 + #ifdef ESP_PLATFORM 309 + if (!esp_littlefs_mounted(kPartitionLabel)) { 310 + s_mounted = false; 311 + return; 312 + } 313 + 314 + const esp_err_t err = esp_vfs_littlefs_unregister(kPartitionLabel); 315 + if (err != ESP_OK) { 316 + KLEIDOS_LOGW(TAG, "unmount failed: %s", esp_err_to_name(err)); 317 + return; 318 + } 319 + s_mounted = false; 320 + #endif 321 + } 322 + 323 + bool mounted() { 324 + #ifdef ESP_PLATFORM 325 + s_mounted = esp_littlefs_mounted(kPartitionLabel); 326 + return s_mounted; 327 + #else 328 + return false; 329 + #endif 330 + } 331 + 332 + bool format() { 333 + #ifdef ESP_PLATFORM 334 + const bool wasMounted = mounted(); 335 + if (wasMounted) end(); 336 + 337 + const esp_err_t err = esp_littlefs_format(kPartitionLabel); 338 + if (err != ESP_OK) { 339 + KLEIDOS_LOGE(TAG, "format failed: %s", esp_err_to_name(err)); 340 + if (wasMounted) (void)begin(false); 341 + return false; 342 + } 343 + 344 + if (wasMounted) (void)begin(false); 345 + return true; 346 + #else 347 + return false; 348 + #endif 349 + } 350 + 351 + bool exists(const char* path) { 352 + #ifdef ESP_PLATFORM 353 + if (!mounted()) return false; 354 + char physicalPath[kMaxPhysicalPathBytes] = {}; 355 + if (!buildPhysicalPath(path, physicalPath, sizeof(physicalPath))) return false; 356 + struct stat st = {}; 357 + return stat(physicalPath, &st) == 0; 358 + #else 359 + (void)path; 360 + return false; 361 + #endif 362 + } 363 + 364 + bool mkdir(const char* path) { 365 + #ifdef ESP_PLATFORM 366 + if (!mounted()) return false; 367 + char physicalPath[kMaxPhysicalPathBytes] = {}; 368 + if (!buildPhysicalPath(path, physicalPath, sizeof(physicalPath))) return false; 369 + struct stat st = {}; 370 + if (stat(physicalPath, &st) == 0) return S_ISDIR(st.st_mode); 371 + return ::mkdir(physicalPath, S_IRWXU) == 0; 372 + #else 373 + (void)path; 374 + return false; 375 + #endif 376 + } 377 + 378 + bool rmdir(const char* path) { 379 + #ifdef ESP_PLATFORM 380 + if (!mounted()) return false; 381 + char physicalPath[kMaxPhysicalPathBytes] = {}; 382 + if (!buildPhysicalPath(path, physicalPath, sizeof(physicalPath))) return false; 383 + return ::rmdir(physicalPath) == 0; 384 + #else 385 + (void)path; 386 + return false; 387 + #endif 388 + } 389 + 390 + bool remove(const char* path) { 391 + #ifdef ESP_PLATFORM 392 + if (!mounted()) return false; 393 + char physicalPath[kMaxPhysicalPathBytes] = {}; 394 + if (!buildPhysicalPath(path, physicalPath, sizeof(physicalPath))) return false; 395 + return ::unlink(physicalPath) == 0; 396 + #else 397 + (void)path; 398 + return false; 399 + #endif 400 + } 401 + 402 + bool rename(const char* from, const char* to) { 403 + #ifdef ESP_PLATFORM 404 + if (!mounted()) return false; 405 + char physicalFrom[kMaxPhysicalPathBytes] = {}; 406 + char physicalTo[kMaxPhysicalPathBytes] = {}; 407 + if (!buildPhysicalPath(from, physicalFrom, sizeof(physicalFrom)) || 408 + !buildPhysicalPath(to, physicalTo, sizeof(physicalTo))) { 409 + return false; 410 + } 411 + return ::rename(physicalFrom, physicalTo) == 0; 412 + #else 413 + (void)from; 414 + (void)to; 415 + return false; 416 + #endif 417 + } 418 + 419 + size_t usedBytes() { 420 + #ifdef ESP_PLATFORM 421 + size_t total = 0; 422 + size_t used = 0; 423 + if (!mounted()) return 0; 424 + return esp_littlefs_info(kPartitionLabel, &total, &used) == ESP_OK ? used : 0; 425 + #else 426 + return 0; 427 + #endif 428 + } 429 + 430 + size_t totalBytes() { 431 + #ifdef ESP_PLATFORM 432 + size_t total = 0; 433 + size_t used = 0; 434 + if (!mounted()) return 0; 435 + return esp_littlefs_info(kPartitionLabel, &total, &used) == ESP_OK ? total : 0; 436 + #else 437 + return 0; 438 + #endif 439 + } 440 + 441 + File open(const char* path, const char* mode) { 442 + return File::openPath(path, mode, nullptr); 443 + } 444 + 445 + } // namespace kleidos::platform::littlefs
+84
src/platform/LittleFs.h
··· 1 + // Kleidos - LittleFS lifecycle and file I/O facade 2 + 3 + #pragma once 4 + 5 + #include <stddef.h> 6 + #include <stdint.h> 7 + 8 + namespace kleidos::platform::littlefs { 9 + 10 + class File { 11 + public: 12 + File() = default; 13 + ~File(); 14 + 15 + File(const File&) = delete; 16 + File& operator=(const File&) = delete; 17 + 18 + File(File&& other) noexcept; 19 + File& operator=(File&& other) noexcept; 20 + 21 + explicit operator bool() const { return valid(); } 22 + 23 + bool valid() const; 24 + bool isDirectory() const { return isDirectory_; } 25 + const char* name() const { return name_; } 26 + size_t size() const; 27 + 28 + size_t read(uint8_t* out, size_t outSize); 29 + int read(); 30 + size_t write(const uint8_t* data, size_t dataSize); 31 + bool available(); 32 + 33 + File openNextFile(); 34 + void close(); 35 + 36 + private: 37 + friend File open(const char* path, const char* mode); 38 + 39 + static File openPath(const char* logicalPath, const char* mode, const char* displayName); 40 + void moveFrom(File&& other) noexcept; 41 + void setLogicalPath(const char* path); 42 + void setName(const char* name); 43 + 44 + int fd_ = -1; 45 + void* dir_ = nullptr; 46 + bool isDirectory_ = false; 47 + char logicalPath_[128] = {}; 48 + char name_[64] = {}; 49 + }; 50 + 51 + class LittleFsGuard { 52 + public: 53 + LittleFsGuard() = default; 54 + explicit LittleFsGuard(bool formatOnFail) { mounted_ = mount(formatOnFail); } 55 + ~LittleFsGuard(); 56 + 57 + LittleFsGuard(const LittleFsGuard&) = delete; 58 + LittleFsGuard& operator=(const LittleFsGuard&) = delete; 59 + 60 + bool mount(bool formatOnFail = true); 61 + bool mounted() const { return mounted_; } 62 + 63 + private: 64 + bool owned_ = false; 65 + bool mounted_ = false; 66 + }; 67 + 68 + bool begin(bool formatOnFail = true); 69 + void end(); 70 + bool mounted(); 71 + bool format(); 72 + 73 + bool exists(const char* path); 74 + bool mkdir(const char* path); 75 + bool rmdir(const char* path); 76 + bool remove(const char* path); 77 + bool rename(const char* from, const char* to); 78 + 79 + size_t usedBytes(); 80 + size_t totalBytes(); 81 + 82 + File open(const char* path, const char* mode = "r"); 83 + 84 + } // namespace kleidos::platform::littlefs
+3 -1
src/states/AppContext.h
··· 4 4 #include <Arduino.h> 5 5 #include <esp32-hal-cpu.h> 6 6 #include "platform/Clock.h" 7 + #include "platform/LittleFs.h" 7 8 #include "platform/Logger.h" 8 9 #include "platform/Rtos.h" 9 10 #include "platform/SerialTransport.h" ··· 224 225 kleidos::render::shutdown(); 225 226 #endif 226 227 227 - // Stop the vault worker BEFORE LittleFS.end() so any in-flight 228 + // Stop the vault worker BEFORE LittleFS unmount so any in-flight 228 229 // request finishes cleanly. Drains the queue with a bounded 229 230 // timeout, then deletes the task. Idempotent. 230 231 kleidos::vault::shutdown(); 232 + kleidos::platform::littlefs::end(); 231 233 232 234 #ifdef DEBUG_RESTART_INSTEAD_OF_DEEP_SLEEP 233 235 // Debug builds: restart instead of deep sleep so serial connection
+70 -16
src/vault/SdVault.cpp
··· 4 4 #include "SdVault.h" 5 5 6 6 #include "platform/Clock.h" 7 + #include "platform/LittleFs.h" 7 8 8 9 #if HAS_SDCARD 9 10 10 11 #include "hal/common/SdCard.h" 11 12 #include "hal/common/BoardRegistry.h" 12 13 #include "vault/VaultStore.h" 14 + #include "vault/VaultTask.h" 13 15 #include "vault/AuditLog.h" 14 16 #include "crypto/VaultCrypto.h" 15 17 16 18 #include <SD.h> 17 - #include <LittleFS.h> 18 19 #include <ArduinoJson.h> 19 20 #include <cinttypes> 20 21 #include "platform/Logger.h" ··· 23 24 #include <cstdio> 24 25 25 26 static constexpr const char* TAG = "SdVault"; 27 + namespace lfs = kleidos::platform::littlefs; 26 28 27 29 // --------------------------------------------------------------------------- 28 30 // Directory setup ··· 120 122 // --------------------------------------------------------------------------- 121 123 // File copy utility 122 124 // --------------------------------------------------------------------------- 123 - bool SdVault::copyFile(fs::FS& srcFs, const char* srcPath, 124 - fs::FS& dstFs, const char* dstPath) { 125 - File src = srcFs.open(srcPath, "r"); 125 + bool SdVault::copyLittleFsToSd(const char* srcPath, const char* dstPath) { 126 + lfs::File src = lfs::open(srcPath, "r"); 126 127 if (!src) { 127 128 KLEIDOS_LOGE(TAG, "Cannot open source: %s", srcPath); 128 129 return false; 129 130 } 130 131 131 - File dst = dstFs.open(dstPath, "w"); 132 + File dst = SD.open(dstPath, "w"); 133 + if (!dst) { 134 + KLEIDOS_LOGE(TAG, "Cannot open dest: %s", dstPath); 135 + src.close(); 136 + return false; 137 + } 138 + 139 + uint8_t buf[COPY_BUF_SIZE]; 140 + size_t totalCopied = 0; 141 + 142 + while (src.available()) { 143 + size_t n = src.read(buf, sizeof(buf)); 144 + if (n == 0) break; 145 + size_t written = dst.write(buf, n); 146 + if (written != n) { 147 + KLEIDOS_LOGE(TAG, "Write error at offset %u", totalCopied); 148 + src.close(); 149 + dst.close(); 150 + return false; 151 + } 152 + totalCopied += n; 153 + } 154 + 155 + src.close(); 156 + dst.close(); 157 + return true; 158 + } 159 + 160 + bool SdVault::copySdToLittleFs(const char* srcPath, const char* dstPath) { 161 + File src = SD.open(srcPath, "r"); 162 + if (!src) { 163 + KLEIDOS_LOGE(TAG, "Cannot open source: %s", srcPath); 164 + return false; 165 + } 166 + 167 + lfs::File dst = lfs::open(dstPath, "w"); 132 168 if (!dst) { 133 169 KLEIDOS_LOGE(TAG, "Cannot open dest: %s", dstPath); 134 170 src.close(); ··· 160 196 // Vault Export: LittleFS /vault/ → SD /kleidos/vault/ 161 197 // --------------------------------------------------------------------------- 162 198 bool SdVault::exportVault() { 199 + if (!kleidos::vault::onWorker()) { 200 + return kleidos::vault::execute([&](){ return exportVault(); }); 201 + } 163 202 if (!SdCard::isMounted()) { 164 203 KLEIDOS_LOGW(TAG, "SD not mounted, skipping export"); 165 204 return false; 166 205 } 167 206 if (!ensureDirectories()) return false; 168 207 208 + lfs::LittleFsGuard lfsGuard; 209 + if (!lfsGuard.mount()) { 210 + KLEIDOS_LOGE(TAG, "Cannot mount LittleFS vault"); 211 + return false; 212 + } 213 + 169 214 // Clean SD vault directory first 170 215 File dir = SD.open(SD_VAULT); 171 216 if (dir && dir.isDirectory()) { 172 217 File entry = dir.openNextFile(); 173 218 while (entry) { 174 - char path[64]; 219 + char path[128]; 175 220 snprintf(path, sizeof(path), "%s/%s", SD_VAULT, entry.name()); 176 221 entry.close(); 177 222 SD.remove(path); ··· 181 226 } 182 227 183 228 // Copy all files from LittleFS /vault/ to SD /kleidos/vault/ 184 - File lfsDir = LittleFS.open(LFS_VAULT); 229 + lfs::File lfsDir = lfs::open(LFS_VAULT); 185 230 if (!lfsDir || !lfsDir.isDirectory()) { 186 231 KLEIDOS_LOGE(TAG, "Cannot open LittleFS vault"); 187 232 return false; ··· 190 235 uint8_t credCount = 0; 191 236 uint8_t totpCount = 0; 192 237 193 - File f = lfsDir.openNextFile(); 238 + lfs::File f = lfsDir.openNextFile(); 194 239 while (f) { 195 240 const char* name = f.name(); 196 241 if (name) { 197 - char srcPath[48]; 198 - char dstPath[64]; 242 + char srcPath[128]; 243 + char dstPath[128]; 199 244 snprintf(srcPath, sizeof(srcPath), "%s/%s", LFS_VAULT, name); 200 245 snprintf(dstPath, sizeof(dstPath), "%s/%s", SD_VAULT, name); 201 246 202 247 f.close(); 203 248 204 - if (copyFile(LittleFS, srcPath, SD, dstPath)) { 249 + if (copyLittleFsToSd(srcPath, dstPath)) { 205 250 KLEIDOS_LOGI(TAG, "Exported: %s", name); 206 251 const char* dot = strrchr(name, '.'); 207 252 bool isBin = dot && strcmp(dot, ".bin") == 0; ··· 231 276 // Vault Import: SD /kleidos/vault/ → LittleFS /vault/ 232 277 // --------------------------------------------------------------------------- 233 278 bool SdVault::importVault() { 279 + if (!kleidos::vault::onWorker()) { 280 + return kleidos::vault::execute([&](){ return importVault(); }); 281 + } 234 282 if (!SdCard::isMounted()) return false; 235 283 if (!hasVault()) { 236 284 KLEIDOS_LOGW(TAG, "No vault on SD"); 285 + return false; 286 + } 287 + 288 + lfs::LittleFsGuard lfsGuard; 289 + if (!lfsGuard.mount()) { 290 + KLEIDOS_LOGE(TAG, "Cannot mount LittleFS vault"); 237 291 return false; 238 292 } 239 293 240 294 // Ensure LittleFS vault directory exists 241 - if (!LittleFS.exists(LFS_VAULT)) { 242 - LittleFS.mkdir(LFS_VAULT); 295 + if (!lfs::exists(LFS_VAULT)) { 296 + lfs::mkdir(LFS_VAULT); 243 297 } 244 298 245 299 File sdDir = SD.open(SD_VAULT); ··· 250 304 while (f) { 251 305 const char* name = f.name(); 252 306 if (name) { 253 - char srcPath[64]; 254 - char dstPath[48]; 307 + char srcPath[128]; 308 + char dstPath[128]; 255 309 snprintf(srcPath, sizeof(srcPath), "%s/%s", SD_VAULT, name); 256 310 snprintf(dstPath, sizeof(dstPath), "%s/%s", LFS_VAULT, name); 257 311 258 312 f.close(); 259 313 260 - if (copyFile(SD, srcPath, LittleFS, dstPath)) { 314 + if (copySdToLittleFs(srcPath, dstPath)) { 261 315 KLEIDOS_LOGI(TAG, "Imported: %s", name); 262 316 fileCount++; 263 317 } else {
+5 -4
src/vault/SdVault.h
··· 24 24 25 25 #include <cstdint> 26 26 #include <cstddef> 27 - #include <FS.h> 28 27 29 28 // --------------------------------------------------------------------------- 30 29 // SD Vault Manifest — metadata for cross-device portability ··· 191 190 static bool deleteKeyfile(); 192 191 193 192 private: 194 - /// Copy a single file between filesystems (LittleFS ↔ SD). 195 - static bool copyFile(fs::FS& srcFs, const char* srcPath, 196 - fs::FS& dstFs, const char* dstPath); 193 + /// Copy a single vault file from LittleFS to SD. 194 + static bool copyLittleFsToSd(const char* srcPath, const char* dstPath); 195 + 196 + /// Copy a single vault file from SD to LittleFS. 197 + static bool copySdToLittleFs(const char* srcPath, const char* dstPath); 197 198 198 199 /// Write manifest.json to SD with current device info. 199 200 static bool writeManifest(uint8_t credCount, uint8_t totpCount);
+153 -151
src/vault/VaultStore.cpp
··· 7 7 8 8 #include "platform/Logger.h" 9 9 #include "platform/Clock.h" 10 + #include "platform/LittleFs.h" 10 11 #include "platform/NvsStore.h" 11 12 #include <esp_task_wdt.h> 12 - #include <LittleFS.h> 13 13 #include <ArduinoJson.h> 14 14 #include <mbedtls/base64.h> 15 15 ··· 27 27 static constexpr const char* TOTP_INDEX_FILE = "/vault/totp_idx.bin"; 28 28 29 29 using kleidos::platform::NvsStore; 30 + namespace lfs = kleidos::platform::littlefs; 31 + using File = kleidos::platform::littlefs::File; 30 32 31 33 // Internal size constants 32 34 static constexpr size_t MAX_PLAINTEXT_SIZE = MAX_NAME_LEN + MAX_USER_LEN ··· 110 112 // compatible with vaults provisioned before keyVersion existed). 111 113 // --------------------------------------------------------------------------- 112 114 static uint32_t readKeyVersion(const char* path) { 113 - if (!LittleFS.exists(path)) return 0; 114 - File f = LittleFS.open(path, "r"); 115 + if (!lfs::exists(path)) return 0; 116 + File f = lfs::open(path, "r"); 115 117 if (!f) return 0; 116 118 uint8_t buf[4] = {}; 117 119 size_t n = f.read(buf, sizeof(buf)); ··· 124 126 } 125 127 126 128 static bool writeKeyVersion(const char* path, uint32_t v) { 127 - File f = LittleFS.open(path, "w"); 129 + File f = lfs::open(path, "w"); 128 130 if (!f) return false; 129 131 uint8_t buf[4] = { 130 132 static_cast<uint8_t>( v & 0xFF), ··· 151 153 constexpr const char* OLD_DIR = "/vault/old"; 152 154 constexpr const char* BAK_META = "/vault/meta.bak"; 153 155 154 - const bool hasShadow = LittleFS.exists(SHADOW_DIR); 155 - const bool hasOld = LittleFS.exists(OLD_DIR); 156 - const bool hasBak = LittleFS.exists(BAK_META); 157 - const bool hasMeta = LittleFS.exists(META_FILE); 156 + const bool hasShadow = lfs::exists(SHADOW_DIR); 157 + const bool hasOld = lfs::exists(OLD_DIR); 158 + const bool hasBak = lfs::exists(BAK_META); 159 + const bool hasMeta = lfs::exists(META_FILE); 158 160 159 161 // Scan canonical credential sidecars to detect partial-rename inconsistency. 160 162 vaultrec::InspectionInput in{}; ··· 170 172 for (uint8_t id = 0; id < MAX_CREDENTIALS; ++id) { 171 173 char credPath[32]; 172 174 snprintf(credPath, sizeof(credPath), "%s/cred_%02u.bin", VAULT_DIR, id); 173 - if (!LittleFS.exists(credPath)) continue; 175 + if (!lfs::exists(credPath)) continue; 174 176 char kvPath[32]; 175 177 snprintf(kvPath, sizeof(kvPath), "%s/cred_%02u.kv", VAULT_DIR, id); 176 178 const uint32_t kv = readKeyVersion(kvPath); // missing = 0 (legacy) ··· 189 191 // cleanup didn't run must still be dropped (otherwise it leaks 190 192 // disk space and confuses future inspections). 191 193 if (hasOld) { 192 - File dir = LittleFS.open(OLD_DIR); 194 + File dir = lfs::open(OLD_DIR); 193 195 if (dir && dir.isDirectory()) { 194 196 File f = dir.openNextFile(); 195 197 while (f) { 196 198 const char* n = f.name(); 197 199 if (n) { 198 - char path[64]; 200 + char path[128]; 199 201 snprintf(path, sizeof(path), "%s/%s", OLD_DIR, n); 200 - LittleFS.remove(path); 202 + lfs::remove(path); 201 203 } 202 204 f = dir.openNextFile(); 203 205 } 204 206 } 205 - LittleFS.rmdir(OLD_DIR); 207 + lfs::rmdir(OLD_DIR); 206 208 // If meta.bak is also leftover staging from the same successful 207 209 // rekey, drop it too. 208 - if (hasBak) LittleFS.remove(BAK_META); 209 - if (LittleFS.exists(META_KV_BAK)) LittleFS.remove(META_KV_BAK); 210 + if (hasBak) lfs::remove(BAK_META); 211 + if (lfs::exists(META_KV_BAK)) lfs::remove(META_KV_BAK); 210 212 KLEIDOS_LOGI(TAG, "Rekey recovery: cleaned leftover /vault/old/ staging"); 211 213 } 212 214 return; 213 215 } 214 216 215 217 auto purgeShadowDir = []() { 216 - File dir = LittleFS.open("/vault/new"); 218 + File dir = lfs::open("/vault/new"); 217 219 if (!dir || !dir.isDirectory()) return; 218 220 File f = dir.openNextFile(); 219 221 while (f) { 220 222 const char* n = f.name(); 221 223 if (n) { 222 - char path[64]; 224 + char path[128]; 223 225 snprintf(path, sizeof(path), "/vault/new/%s", n); 224 - LittleFS.remove(path); 226 + lfs::remove(path); 225 227 } 226 228 f = dir.openNextFile(); 227 229 } 228 - LittleFS.rmdir("/vault/new"); 230 + lfs::rmdir("/vault/new"); 229 231 }; 230 232 231 233 auto purgeOldDirIO = []() { 232 - File dir = LittleFS.open("/vault/old"); 234 + File dir = lfs::open("/vault/old"); 233 235 if (!dir || !dir.isDirectory()) return; 234 236 File f = dir.openNextFile(); 235 237 while (f) { 236 238 const char* n = f.name(); 237 239 if (n) { 238 - char path[64]; 240 + char path[128]; 239 241 snprintf(path, sizeof(path), "/vault/old/%s", n); 240 - LittleFS.remove(path); 242 + lfs::remove(path); 241 243 } 242 244 f = dir.openNextFile(); 243 245 } 244 - LittleFS.rmdir("/vault/old"); 246 + lfs::rmdir("/vault/old"); 245 247 }; 246 248 247 249 auto restoreMetaFromBak = []() { 248 - if (LittleFS.exists(META_FILE)) LittleFS.remove(META_FILE); 249 - if (!LittleFS.rename("/vault/meta.bak", META_FILE)) { 250 + if (lfs::exists(META_FILE)) lfs::remove(META_FILE); 251 + if (!lfs::rename("/vault/meta.bak", META_FILE)) { 250 252 KLEIDOS_LOGE(TAG, "Rekey recovery: meta.bak → meta.bin rename FAILED"); 251 253 } 252 254 // Mirror the meta.kv side: if a backup exists, swap it back too. 253 - if (LittleFS.exists(META_KV_BAK)) { 254 - if (LittleFS.exists(META_KV_FILE)) LittleFS.remove(META_KV_FILE); 255 - LittleFS.rename(META_KV_BAK, META_KV_FILE); 255 + if (lfs::exists(META_KV_BAK)) { 256 + if (lfs::exists(META_KV_FILE)) lfs::remove(META_KV_FILE); 257 + lfs::rename(META_KV_BAK, META_KV_FILE); 256 258 } 257 259 }; 258 260 259 261 // H2 — Restore canonical creds + sidecars from /vault/old/. Used by 260 262 // ROLLBACK_FROM_OLD_DIR to undo a partial swap losslessly. 261 263 auto restoreCanonicalsFromOldDir = []() { 262 - File dir = LittleFS.open("/vault/old"); 264 + File dir = lfs::open("/vault/old"); 263 265 if (!dir || !dir.isDirectory()) return; 264 266 File f = dir.openNextFile(); 265 267 while (f) { ··· 268 270 char src[80], dst[80]; 269 271 snprintf(src, sizeof(src), "/vault/old/%s", n); 270 272 snprintf(dst, sizeof(dst), "/vault/%s", n); 271 - if (LittleFS.exists(dst)) LittleFS.remove(dst); 272 - if (!LittleFS.rename(src, dst)) { 273 + if (lfs::exists(dst)) lfs::remove(dst); 274 + if (!lfs::rename(src, dst)) { 273 275 KLEIDOS_LOGE(TAG, 274 276 "Rekey recovery: rename %s → %s FAILED", src, dst); 275 277 } 276 278 } 277 279 f = dir.openNextFile(); 278 280 } 279 - LittleFS.rmdir("/vault/old"); 281 + lfs::rmdir("/vault/old"); 280 282 }; 281 283 282 284 switch (action) { ··· 338 340 if (!kleidos::vault::onWorker()) { 339 341 return kleidos::vault::execute([&](){ return begin(); }); 340 342 } 341 - if (!LittleFS.begin(true, "/littlefs", 10, "littlefs")) { 343 + if (!lfs::begin(true)) { 342 344 KLEIDOS_LOGE(TAG, "LittleFS mount failed"); 343 345 return false; 344 346 } 345 347 346 - if (!LittleFS.exists(VAULT_DIR)) { 347 - LittleFS.mkdir(VAULT_DIR); 348 + if (!lfs::exists(VAULT_DIR)) { 349 + lfs::mkdir(VAULT_DIR); 348 350 KLEIDOS_LOGI(TAG, "Created vault directory"); 349 351 } 350 352 351 353 // Bug #5 fix: rekey recovery walks the entire vault dir doing many 352 - // LittleFS.exists() calls (each = fopen + newlib lock alloc). The 354 + // lfs::exists() calls (each = fopen + newlib lock alloc). The 353 355 // admin portal calls VaultStore::begin() many times concurrently per 354 356 // request, exhausting the newlib FILE / lock pool and aborting in 355 357 // lock_init_generic. Recovery is a boot-time concern — once it has ··· 364 366 } 365 367 366 368 KLEIDOS_LOGI(TAG, "Vault initialized — %u bytes used, %u total", 367 - LittleFS.usedBytes(), LittleFS.totalBytes()); 369 + lfs::usedBytes(), lfs::totalBytes()); 368 370 return true; 369 371 } 370 372 ··· 378 380 // provision() and rekey(). begin()/end() pairs around individual 379 381 // operations were wiping the cache and forcing a 33s PBKDF2 on 380 382 // every credential open. 381 - LittleFS.end(); 383 + lfs::end(); 382 384 } 383 385 384 386 bool VaultStore::isProvisioned() { 385 387 if (!kleidos::vault::onWorker()) { 386 388 return kleidos::vault::execute([&](){ return isProvisioned(); }); 387 389 } 388 - return LittleFS.exists(META_FILE); 390 + return lfs::exists(META_FILE); 389 391 } 390 392 391 393 bool VaultStore::needsManualRecovery() { ··· 461 463 return _r; 462 464 } 463 465 uint8_t n = 0; 464 - File dir = LittleFS.open(VAULT_DIR); 466 + File dir = lfs::open(VAULT_DIR); 465 467 if (!dir || !dir.isDirectory()) return 0; 466 468 File f = dir.openNextFile(); 467 469 while (f) { ··· 477 479 478 480 uint8_t VaultStore::countTotp() { 479 481 uint8_t n = 0; 480 - File dir = LittleFS.open(VAULT_DIR); 482 + File dir = lfs::open(VAULT_DIR); 481 483 if (!dir || !dir.isDirectory()) return 0; 482 484 File f = dir.openNextFile(); 483 485 while (f) { ··· 500 502 for (uint8_t i = 0; i < MAX_CREDENTIALS; i++) { 501 503 char path[32]; 502 504 snprintf(path, sizeof(path), "%s/cred_%02u.bin", VAULT_DIR, i); 503 - if (!LittleFS.exists(path)) return i; 505 + if (!lfs::exists(path)) return i; 504 506 } 505 507 return 0xFF; 506 508 } ··· 516 518 uint8_t existingIds[MAX_CREDENTIALS]; 517 519 uint8_t existCount = 0; 518 520 { 519 - File dir = LittleFS.open(VAULT_DIR); 521 + File dir = lfs::open(VAULT_DIR); 520 522 if (dir && dir.isDirectory()) { 521 523 File f = dir.openNextFile(); 522 524 while (f && existCount < MAX_CREDENTIALS) { ··· 541 543 // Load favorites bitfield 542 544 uint8_t favBits[(MAX_CREDENTIALS + 7) / 8] = {}; 543 545 { 544 - File f = LittleFS.open("/vault/favorites.bin", "r"); 546 + File f = lfs::open("/vault/favorites.bin", "r"); 545 547 if (f) { 546 548 f.read(favBits, sizeof(favBits)); 547 549 f.close(); ··· 552 554 uint8_t order[MAX_CREDENTIALS]; 553 555 memset(order, 0xFF, sizeof(order)); 554 556 { 555 - File f = LittleFS.open("/vault/order.bin", "r"); 557 + File f = lfs::open("/vault/order.bin", "r"); 556 558 if (f) { 557 559 f.read(order, f.size() < sizeof(order) ? f.size() : sizeof(order)); 558 560 f.close(); ··· 652 654 char path[32]; 653 655 snprintf(path, sizeof(path), "%s/cred_%02u.bin", VAULT_DIR, id); 654 656 655 - File f = LittleFS.open(path, "w"); 657 + File f = lfs::open(path, "w"); 656 658 if (!f) { 657 659 KLEIDOS_LOGE(TAG, "Failed to open %s for writing", path); 658 660 VaultCrypto::secureWipe(credSalt, sizeof(credSalt)); ··· 721 723 } 722 724 723 725 // G1: Invalidate index cache — caller must rebuild via loadCredentialList() 724 - LittleFS.remove(INDEX_FILE); 726 + lfs::remove(INDEX_FILE); 725 727 726 728 KLEIDOS_LOGI(TAG, "Credential %u saved (%u bytes cipher)", id, cipherLen); 727 729 return true; ··· 744 746 char path[32]; 745 747 snprintf(path, sizeof(path), "%s/cred_%02u.bin", VAULT_DIR, id); 746 748 747 - File f = LittleFS.open(path, "r"); 749 + File f = lfs::open(path, "r"); 748 750 if (!f) { 749 751 KLEIDOS_LOGE(TAG, "Failed to open %s", path); 750 752 return false; ··· 812 814 char path[32]; 813 815 snprintf(path, sizeof(path), "%s/cred_%02u.bin", VAULT_DIR, id); 814 816 815 - if (LittleFS.exists(path)) { 816 - LittleFS.remove(path); 817 + if (lfs::exists(path)) { 818 + lfs::remove(path); 817 819 // Also remove HMAC file 818 820 char hmacPath[32]; 819 821 snprintf(hmacPath, sizeof(hmacPath), "%s/hmac_%02u.bin", VAULT_DIR, id); 820 - if (LittleFS.exists(hmacPath)) LittleFS.remove(hmacPath); 822 + if (lfs::exists(hmacPath)) lfs::remove(hmacPath); 821 823 // G4: remove keyVersion sidecar. 822 824 char kvPath[32]; 823 825 snprintf(kvPath, sizeof(kvPath), "%s/cred_%02u.kv", VAULT_DIR, id); 824 - if (LittleFS.exists(kvPath)) LittleFS.remove(kvPath); 826 + if (lfs::exists(kvPath)) lfs::remove(kvPath); 825 827 // G1: Invalidate index cache 826 - LittleFS.remove(INDEX_FILE); 828 + lfs::remove(INDEX_FILE); 827 829 KLEIDOS_LOGI(TAG, "Credential %u deleted", id); 828 830 return true; 829 831 } ··· 842 844 for (uint8_t i = 0; i < MAX_CREDENTIALS; i++) { 843 845 char path[32]; 844 846 snprintf(path, sizeof(path), "%s/cred_%02u.bin", VAULT_DIR, i); 845 - if (LittleFS.exists(path)) { 846 - LittleFS.remove(path); 847 + if (lfs::exists(path)) { 848 + lfs::remove(path); 847 849 } 848 850 char hmacPath[32]; 849 851 snprintf(hmacPath, sizeof(hmacPath), "%s/hmac_%02u.bin", VAULT_DIR, i); 850 - if (LittleFS.exists(hmacPath)) { 851 - LittleFS.remove(hmacPath); 852 + if (lfs::exists(hmacPath)) { 853 + lfs::remove(hmacPath); 852 854 } 853 855 // G4: also remove keyVersion sidecar. 854 856 char kvPath[32]; 855 857 snprintf(kvPath, sizeof(kvPath), "%s/cred_%02u.kv", VAULT_DIR, i); 856 - if (LittleFS.exists(kvPath)) LittleFS.remove(kvPath); 858 + if (lfs::exists(kvPath)) lfs::remove(kvPath); 857 859 } 858 860 859 861 for (uint8_t i = 0; i < MAX_TOTP; i++) { 860 862 char kvPath[32]; 861 863 snprintf(kvPath, sizeof(kvPath), "%s/totp_%02u.kv", VAULT_DIR, i); 862 - if (LittleFS.exists(kvPath)) LittleFS.remove(kvPath); 864 + if (lfs::exists(kvPath)) lfs::remove(kvPath); 863 865 } 864 866 865 - if (LittleFS.exists(META_FILE)) { 866 - LittleFS.remove(META_FILE); 867 + if (lfs::exists(META_FILE)) { 868 + lfs::remove(META_FILE); 867 869 } 868 - if (LittleFS.exists(META_KV_FILE)) LittleFS.remove(META_KV_FILE); 869 - if (LittleFS.exists(META_KV_BAK)) LittleFS.remove(META_KV_BAK); 870 - if (LittleFS.exists("/vault/meta.bak")) LittleFS.remove("/vault/meta.bak"); 870 + if (lfs::exists(META_KV_FILE)) lfs::remove(META_KV_FILE); 871 + if (lfs::exists(META_KV_BAK)) lfs::remove(META_KV_BAK); 872 + if (lfs::exists("/vault/meta.bak")) lfs::remove("/vault/meta.bak"); 871 873 872 874 // H2 — drop any leftover staging trees from an in-flight rekey so a 873 875 // post-wipe boot starts from a fully empty /vault/. 874 876 auto wipeDir = [](const char* path) { 875 - File dir = LittleFS.open(path); 877 + File dir = lfs::open(path); 876 878 if (!dir || !dir.isDirectory()) return; 877 879 File f = dir.openNextFile(); 878 880 while (f) { 879 881 const char* n = f.name(); 880 882 if (n) { 881 - char p[64]; 883 + char p[128]; 882 884 snprintf(p, sizeof(p), "%s/%s", path, n); 883 - LittleFS.remove(p); 885 + lfs::remove(p); 884 886 } 885 887 f = dir.openNextFile(); 886 888 } 887 - LittleFS.rmdir(path); 889 + lfs::rmdir(path); 888 890 }; 889 891 wipeDir("/vault/new"); 890 892 wipeDir("/vault/old"); ··· 905 907 doc["version"] = 1; 906 908 907 909 // Export meta.bin 908 - File metaFile = LittleFS.open(META_FILE, "r"); 910 + File metaFile = lfs::open(META_FILE, "r"); 909 911 if (metaFile) { 910 912 size_t sz = metaFile.size(); 911 913 uint8_t buf[sizeof(VaultMeta)]; ··· 926 928 for (uint8_t i = 0; i < MAX_CREDENTIALS; i++) { 927 929 char path[32]; 928 930 snprintf(path, sizeof(path), "%s/cred_%02u.bin", VAULT_DIR, i); 929 - if (!LittleFS.exists(path)) continue; 931 + if (!lfs::exists(path)) continue; 930 932 931 - File f = LittleFS.open(path, "r"); 933 + File f = lfs::open(path, "r"); 932 934 if (!f) continue; 933 935 934 936 size_t sz = f.size(); ··· 996 998 mbedtls_base64_decode(buf, sizeof(buf), &binLen, 997 999 reinterpret_cast<const uint8_t*>(metaB64), b64StrLen); 998 1000 999 - File f = LittleFS.open(META_FILE, "w"); 1001 + File f = lfs::open(META_FILE, "w"); 1000 1002 if (f) { f.write(buf, binLen); f.close(); } 1001 1003 } 1002 1004 } ··· 1023 1025 char path[32]; 1024 1026 snprintf(path, sizeof(path), "%s/cred_%02u.bin", VAULT_DIR, id); 1025 1027 1026 - File f = LittleFS.open(path, "w"); 1028 + File f = lfs::open(path, "w"); 1027 1029 if (f) { f.write(buf, binLen); f.close(); } 1028 1030 } 1029 1031 } ··· 1130 1132 1131 1133 // --------------------------------------------------------------------------- 1132 1134 bool VaultStore::isMetaV2() { 1133 - File f = LittleFS.open(META_FILE, "r"); 1135 + File f = lfs::open(META_FILE, "r"); 1134 1136 if (!f) return false; 1135 1137 1136 1138 bool v2 = false; ··· 1146 1148 } 1147 1149 1148 1150 bool VaultStore::readMetaV2(VaultMetaV2& meta) { 1149 - File f = LittleFS.open(META_FILE, "r"); 1151 + File f = lfs::open(META_FILE, "r"); 1150 1152 if (!f) { 1151 1153 KLEIDOS_LOGE(TAG, "No vault meta file"); 1152 1154 return false; ··· 1169 1171 } 1170 1172 1171 1173 bool VaultStore::writeMetaV2(const VaultMetaV2& meta) { 1172 - File f = LittleFS.open(META_FILE, "w"); 1174 + File f = lfs::open(META_FILE, "w"); 1173 1175 if (!f) { 1174 1176 KLEIDOS_LOGE(TAG, "Failed to write v2 meta file"); 1175 1177 return false; ··· 1210 1212 // Private 1211 1213 // --------------------------------------------------------------------------- 1212 1214 bool VaultStore::readMeta(VaultMeta& meta) { 1213 - File f = LittleFS.open(META_FILE, "r"); 1215 + File f = lfs::open(META_FILE, "r"); 1214 1216 if (!f) { 1215 1217 KLEIDOS_LOGE(TAG, "No vault meta — not provisioned"); 1216 1218 return false; ··· 1238 1240 } 1239 1241 1240 1242 bool VaultStore::writeMeta(const VaultMeta& meta) { 1241 - File f = LittleFS.open(META_FILE, "w"); 1243 + File f = lfs::open(META_FILE, "w"); 1242 1244 if (!f) { 1243 1245 KLEIDOS_LOGE(TAG, "Failed to write meta file"); 1244 1246 return false; ··· 1282 1284 char path[32]; 1283 1285 snprintf(path, sizeof(path), "%s/cred_%02u.bin", VAULT_DIR, id); 1284 1286 1285 - File f = LittleFS.open(path, "r"); 1287 + File f = lfs::open(path, "r"); 1286 1288 if (!f) { 1287 1289 VaultCrypto::secureWipe(hmacKey, sizeof(hmacKey)); 1288 1290 return false; ··· 1311 1313 char path[32]; 1312 1314 snprintf(path, sizeof(path), "%s/hmac_%02u.bin", VAULT_DIR, id); 1313 1315 1314 - File f = LittleFS.open(path, "w"); 1316 + File f = lfs::open(path, "w"); 1315 1317 if (!f) { 1316 1318 KLEIDOS_LOGE(TAG, "Failed to write HMAC file for credential %u", id); 1317 1319 return false; ··· 1325 1327 char hmacPath[32]; 1326 1328 snprintf(hmacPath, sizeof(hmacPath), "%s/hmac_%02u.bin", VAULT_DIR, id); 1327 1329 1328 - if (!LittleFS.exists(hmacPath)) { 1330 + if (!lfs::exists(hmacPath)) { 1329 1331 KLEIDOS_LOGW(TAG, "Missing HMAC file for credential %u", id); 1330 1332 return false; 1331 1333 } 1332 1334 1333 - File f = LittleFS.open(hmacPath, "r"); 1335 + File f = lfs::open(hmacPath, "r"); 1334 1336 if (!f || f.size() != HMAC_SIZE) { 1335 1337 if (f) f.close(); 1336 1338 return false; ··· 1369 1371 for (uint8_t i = 0; i < MAX_TOTP; i++) { 1370 1372 char path[32]; 1371 1373 snprintf(path, sizeof(path), "%s/totp_%02u.bin", VAULT_DIR, i); 1372 - if (!LittleFS.exists(path)) return i; 1374 + if (!lfs::exists(path)) return i; 1373 1375 } 1374 1376 return 0xFF; 1375 1377 } ··· 1384 1386 uint8_t existingIds[MAX_TOTP]; 1385 1387 uint8_t existCount = 0; 1386 1388 { 1387 - File dir = LittleFS.open(VAULT_DIR); 1389 + File dir = lfs::open(VAULT_DIR); 1388 1390 if (dir && dir.isDirectory()) { 1389 1391 File f = dir.openNextFile(); 1390 1392 while (f && existCount < MAX_TOTP) { ··· 1477 1479 char path[32]; 1478 1480 snprintf(path, sizeof(path), "%s/totp_%02u.bin", VAULT_DIR, id); 1479 1481 1480 - File f = LittleFS.open(path, "w"); 1482 + File f = lfs::open(path, "w"); 1481 1483 if (!f) { 1482 1484 KLEIDOS_LOGE(TAG, "Failed to open %s for writing", path); 1483 1485 VaultCrypto::secureWipe(credSalt, sizeof(credSalt)); ··· 1501 1503 } 1502 1504 1503 1505 // G1: Invalidate TOTP index cache 1504 - LittleFS.remove(TOTP_INDEX_FILE); 1506 + lfs::remove(TOTP_INDEX_FILE); 1505 1507 1506 1508 KLEIDOS_LOGI(TAG, "TOTP %u saved", id); 1507 1509 return true; ··· 1516 1518 char path[32]; 1517 1519 snprintf(path, sizeof(path), "%s/totp_%02u.bin", VAULT_DIR, id); 1518 1520 1519 - File f = LittleFS.open(path, "r"); 1521 + File f = lfs::open(path, "r"); 1520 1522 if (!f) return false; 1521 1523 1522 1524 size_t fileSize = f.size(); ··· 1574 1576 if (id >= MAX_TOTP) return false; 1575 1577 char path[32]; 1576 1578 snprintf(path, sizeof(path), "%s/totp_%02u.bin", VAULT_DIR, id); 1577 - if (!LittleFS.exists(path)) return false; 1579 + if (!lfs::exists(path)) return false; 1578 1580 // G4: remove matching keyVersion sidecar. 1579 1581 char kvPath[32]; 1580 1582 snprintf(kvPath, sizeof(kvPath), "%s/totp_%02u.kv", VAULT_DIR, id); 1581 - if (LittleFS.exists(kvPath)) LittleFS.remove(kvPath); 1583 + if (lfs::exists(kvPath)) lfs::remove(kvPath); 1582 1584 // G1: Invalidate TOTP index cache 1583 - LittleFS.remove(TOTP_INDEX_FILE); 1584 - return LittleFS.remove(path); 1585 + lfs::remove(TOTP_INDEX_FILE); 1586 + return lfs::remove(path); 1585 1587 } 1586 1588 1587 1589 // --------------------------------------------------------------------------- ··· 1594 1596 if (id >= MAX_CREDENTIALS) return false; 1595 1597 uint8_t favBits[(MAX_CREDENTIALS + 7) / 8] = {}; 1596 1598 { 1597 - File f = LittleFS.open("/vault/favorites.bin", "r"); 1599 + File f = lfs::open("/vault/favorites.bin", "r"); 1598 1600 if (f) { f.read(favBits, sizeof(favBits)); f.close(); } 1599 1601 } 1600 1602 if (favorite) { ··· 1602 1604 } else { 1603 1605 favBits[id / 8] &= ~(1 << (id % 8)); 1604 1606 } 1605 - File f = LittleFS.open("/vault/favorites.bin", "w"); 1607 + File f = lfs::open("/vault/favorites.bin", "w"); 1606 1608 if (!f) return false; 1607 1609 f.write(favBits, sizeof(favBits)); 1608 1610 f.close(); 1609 1611 // G1: Invalidate index cache (favorites affect sort order) 1610 - LittleFS.remove(INDEX_FILE); 1612 + lfs::remove(INDEX_FILE); 1611 1613 return true; 1612 1614 } 1613 1615 1614 1616 bool VaultStore::isFavorite(uint8_t id) { 1615 1617 if (id >= MAX_CREDENTIALS) return false; 1616 1618 uint8_t favBits[(MAX_CREDENTIALS + 7) / 8] = {}; 1617 - File f = LittleFS.open("/vault/favorites.bin", "r"); 1619 + File f = lfs::open("/vault/favorites.bin", "r"); 1618 1620 if (!f) return false; 1619 1621 f.read(favBits, sizeof(favBits)); 1620 1622 f.close(); ··· 1632 1634 order[ids[i]] = i; 1633 1635 } 1634 1636 } 1635 - File f = LittleFS.open("/vault/order.bin", "w"); 1637 + File f = lfs::open("/vault/order.bin", "w"); 1636 1638 if (!f) return false; 1637 1639 f.write(order, sizeof(order)); 1638 1640 f.close(); 1639 1641 // G1: Invalidate index cache (order affects sort) 1640 - LittleFS.remove(INDEX_FILE); 1642 + lfs::remove(INDEX_FILE); 1641 1643 return true; 1642 1644 } 1643 1645 ··· 1688 1690 constexpr const char* BAK_META_FILE = "/vault/meta.bak"; 1689 1691 1690 1692 void purgeShadow() { 1691 - File dir = LittleFS.open(NEW_DIR); 1693 + File dir = lfs::open(NEW_DIR); 1692 1694 if (!dir || !dir.isDirectory()) return; 1693 1695 File f = dir.openNextFile(); 1694 1696 while (f) { 1695 1697 const char* n = f.name(); 1696 - char path[48]; 1698 + char path[128]; 1697 1699 if (n) { 1698 1700 // f.name() returns basename on some versions — prepend dir. 1699 1701 snprintf(path, sizeof(path), "%s/%s", NEW_DIR, n); 1700 - LittleFS.remove(path); 1702 + lfs::remove(path); 1701 1703 } 1702 1704 f = dir.openNextFile(); 1703 1705 } 1704 - LittleFS.rmdir(NEW_DIR); 1706 + lfs::rmdir(NEW_DIR); 1705 1707 } 1706 1708 1707 1709 // H2 — Drop the pre-swap snapshot tree. Mirror of purgeShadow() against 1708 1710 // the OLD_DIR. Safe to call when the directory does not exist. 1709 1711 void purgeOldDir() { 1710 - File dir = LittleFS.open(OLD_DIR); 1712 + File dir = lfs::open(OLD_DIR); 1711 1713 if (!dir || !dir.isDirectory()) return; 1712 1714 File f = dir.openNextFile(); 1713 1715 while (f) { 1714 1716 const char* n = f.name(); 1715 - char path[48]; 1717 + char path[128]; 1716 1718 if (n) { 1717 1719 snprintf(path, sizeof(path), "%s/%s", OLD_DIR, n); 1718 - LittleFS.remove(path); 1720 + lfs::remove(path); 1719 1721 } 1720 1722 f = dir.openNextFile(); 1721 1723 } 1722 - LittleFS.rmdir(OLD_DIR); 1724 + lfs::rmdir(OLD_DIR); 1723 1725 } 1724 1726 1725 1727 // Byte-for-byte copy used by the H2 snapshot step. Returns false on any 1726 1728 // I/O error — caller treats failure as fatal and aborts the rekey. 1727 1729 bool copyFileLfs(const char* src, const char* dst) { 1728 - File rf = LittleFS.open(src, "r"); 1730 + File rf = lfs::open(src, "r"); 1729 1731 if (!rf) return false; 1730 - File wf = LittleFS.open(dst, "w"); 1732 + File wf = lfs::open(dst, "w"); 1731 1733 if (!wf) { rf.close(); return false; } 1732 1734 uint8_t buf[256]; 1733 1735 bool ok = true; ··· 1738 1740 } 1739 1741 rf.close(); 1740 1742 wf.close(); 1741 - if (!ok) LittleFS.remove(dst); 1743 + if (!ok) lfs::remove(dst); 1742 1744 return ok; 1743 1745 } 1744 1746 } // namespace ··· 1755 1757 // Clean any prior aborted attempt. 1756 1758 purgeShadow(); 1757 1759 purgeOldDir(); 1758 - if (LittleFS.exists(BAK_META_FILE)) LittleFS.remove(BAK_META_FILE); 1760 + if (lfs::exists(BAK_META_FILE)) lfs::remove(BAK_META_FILE); 1759 1761 1760 - if (!LittleFS.mkdir(NEW_DIR)) { 1762 + if (!lfs::mkdir(NEW_DIR)) { 1761 1763 KLEIDOS_LOGE(TAG_RK, "mkdir %s failed", NEW_DIR); 1762 1764 return false; 1763 1765 } ··· 1809 1811 } 1810 1812 char path[48]; 1811 1813 snprintf(path, sizeof(path), "%s/%s_%02u.bin", NEW_DIR, prefix, id); 1812 - File f = LittleFS.open(path, "w"); 1814 + File f = lfs::open(path, "w"); 1813 1815 if (!f) { 1814 1816 VaultCrypto::secureWipe(cipherBuf, sizeof(cipherBuf)); 1815 1817 return false; ··· 1827 1829 return false; 1828 1830 } 1829 1831 // Re-read whole file bytes to feed HMAC. 1830 - File rf = LittleFS.open(path, "r"); 1832 + File rf = lfs::open(path, "r"); 1831 1833 if (!rf) { 1832 1834 VaultCrypto::secureWipe(hmacKey, sizeof(hmacKey)); 1833 1835 VaultCrypto::secureWipe(cipherBuf, sizeof(cipherBuf)); ··· 1857 1859 esp_task_wdt_reset(); // G4.2: prevent WDT timeout during large vaults 1858 1860 char path[32]; 1859 1861 snprintf(path, sizeof(path), "%s/cred_%02u.bin", VAULT_DIR, id); 1860 - if (!LittleFS.exists(path)) continue; 1862 + if (!lfs::exists(path)) continue; 1861 1863 1862 1864 Credential cred = {}; 1863 1865 if (!loadCredential(oldKey, id, cred, false)) { ··· 1887 1889 // Write shadow HMAC alongside. 1888 1890 char hpath[48]; 1889 1891 snprintf(hpath, sizeof(hpath), "%s/hmac_%02u.bin", NEW_DIR, id); 1890 - File hf = LittleFS.open(hpath, "w"); 1892 + File hf = lfs::open(hpath, "w"); 1891 1893 if (!hf) { okAll = false; break; } 1892 1894 hf.write(hmac, HMAC_SIZE); 1893 1895 hf.close(); ··· 1904 1906 esp_task_wdt_reset(); // G4.2: prevent WDT timeout during large vaults 1905 1907 char path[32]; 1906 1908 snprintf(path, sizeof(path), "%s/totp_%02u.bin", VAULT_DIR, id); 1907 - if (!LittleFS.exists(path)) continue; 1909 + if (!lfs::exists(path)) continue; 1908 1910 1909 1911 TotpEntry te = {}; 1910 1912 if (!loadTotp(oldKey, id, te)) { ··· 1943 1945 1944 1946 // --- Write shadow meta --- 1945 1947 if (okAll) { 1946 - File mf = LittleFS.open(NEW_META_FILE, "w"); 1948 + File mf = lfs::open(NEW_META_FILE, "w"); 1947 1949 if (!mf) { okAll = false; } 1948 1950 else { 1949 1951 mf.write(reinterpret_cast<const uint8_t*>(&newMeta), sizeof(newMeta)); ··· 1970 1972 // /vault/ so the active vault is not disturbed; the copies act as a 1971 1973 // lossless rollback target if any later step fails. 1972 1974 // --------------------------------------------------------------------- 1973 - if (LittleFS.exists(OLD_DIR)) purgeOldDir(); 1974 - if (!LittleFS.mkdir(OLD_DIR)) { 1975 + if (lfs::exists(OLD_DIR)) purgeOldDir(); 1976 + if (!lfs::mkdir(OLD_DIR)) { 1975 1977 KLEIDOS_LOGE(TAG_RK, "mkdir %s failed", OLD_DIR); 1976 1978 purgeShadow(); 1977 1979 VaultCrypto::secureWipe(newKey, sizeof(newKey)); ··· 1983 1985 for (uint8_t id = 0; id < maxId; ++id) { 1984 1986 char src[40], dst[40]; 1985 1987 snprintf(src, sizeof(src), "%s/%s_%02u.bin", VAULT_DIR, prefix, id); 1986 - if (!LittleFS.exists(src)) continue; 1988 + if (!lfs::exists(src)) continue; 1987 1989 snprintf(dst, sizeof(dst), "%s/%s_%02u.bin", OLD_DIR, prefix, id); 1988 1990 if (!copyFileLfs(src, dst)) return false; 1989 1991 1990 1992 // Per-cred .kv sidecar (legacy vaults have none → skip silently). 1991 1993 snprintf(src, sizeof(src), "%s/%s_%02u.kv", VAULT_DIR, prefix, id); 1992 - if (LittleFS.exists(src)) { 1994 + if (lfs::exists(src)) { 1993 1995 snprintf(dst, sizeof(dst), "%s/%s_%02u.kv", OLD_DIR, prefix, id); 1994 1996 if (!copyFileLfs(src, dst)) return false; 1995 1997 } ··· 2005 2007 for (uint8_t id = 0; id < MAX_CREDENTIALS && snapOk; ++id) { 2006 2008 char src[40], dst[40]; 2007 2009 snprintf(src, sizeof(src), "%s/hmac_%02u.bin", VAULT_DIR, id); 2008 - if (!LittleFS.exists(src)) continue; 2010 + if (!lfs::exists(src)) continue; 2009 2011 snprintf(dst, sizeof(dst), "%s/hmac_%02u.bin", OLD_DIR, id); 2010 2012 if (!copyFileLfs(src, dst)) snapOk = false; 2011 2013 } ··· 2022 2024 2023 2025 // --- COMMIT: swap meta first --- 2024 2026 // rename() overwrites? Some LittleFS builds require explicit remove. 2025 - if (LittleFS.exists(BAK_META_FILE)) LittleFS.remove(BAK_META_FILE); 2026 - if (!LittleFS.rename(META_FILE, BAK_META_FILE)) { 2027 + if (lfs::exists(BAK_META_FILE)) lfs::remove(BAK_META_FILE); 2028 + if (!lfs::rename(META_FILE, BAK_META_FILE)) { 2027 2029 KLEIDOS_LOGE(TAG_RK, "commit: rename old meta→bak failed"); 2028 2030 purgeShadow(); 2029 2031 VaultCrypto::secureWipe(newKey, sizeof(newKey)); 2030 2032 VaultCrypto::secureWipe(&newMeta, sizeof(newMeta)); 2031 2033 return false; 2032 2034 } 2033 - if (!LittleFS.rename(NEW_META_FILE, META_FILE)) { 2035 + if (!lfs::rename(NEW_META_FILE, META_FILE)) { 2034 2036 KLEIDOS_LOGE(TAG_RK, "commit: rename new meta→canonical FAILED"); 2035 2037 // Roll meta back. 2036 - LittleFS.rename(BAK_META_FILE, META_FILE); 2038 + lfs::rename(BAK_META_FILE, META_FILE); 2037 2039 purgeShadow(); 2038 2040 VaultCrypto::secureWipe(newKey, sizeof(newKey)); 2039 2041 VaultCrypto::secureWipe(&newMeta, sizeof(newMeta)); ··· 2043 2045 // G4: swap meta.kv in lockstep with meta.bin. 2044 2046 // Old meta.kv may be missing (legacy vault, oldKv == 0); that's fine 2045 2047 // — we only need the bak as a marker for the recovery path. 2046 - if (LittleFS.exists(META_KV_BAK)) LittleFS.remove(META_KV_BAK); 2047 - if (LittleFS.exists(META_KV_FILE)) { 2048 - LittleFS.rename(META_KV_FILE, META_KV_BAK); 2048 + if (lfs::exists(META_KV_BAK)) lfs::remove(META_KV_BAK); 2049 + if (lfs::exists(META_KV_FILE)) { 2050 + lfs::rename(META_KV_FILE, META_KV_BAK); 2049 2051 } else { 2050 2052 // Synthesize a bak holding oldKv so a partial-rename rollback 2051 2053 // can still restore a sensible canonical meta.kv. 2052 2054 writeKeyVersion(META_KV_BAK, oldKv); 2053 2055 } 2054 - if (!LittleFS.rename("/vault/new/meta.kv", META_KV_FILE)) { 2056 + if (!lfs::rename("/vault/new/meta.kv", META_KV_FILE)) { 2055 2057 // Best-effort: write the new value directly. We'd rather have 2056 2058 // the canonical meta.kv slightly inconsistent than abort the 2057 2059 // commit at this point. ··· 2062 2064 for (uint8_t id = 0; id < MAX_CREDENTIALS; ++id) { 2063 2065 char newPath[48]; 2064 2066 snprintf(newPath, sizeof(newPath), "%s/cred_%02u.bin", NEW_DIR, id); 2065 - if (!LittleFS.exists(newPath)) continue; 2067 + if (!lfs::exists(newPath)) continue; 2066 2068 char canPath[32]; 2067 2069 snprintf(canPath, sizeof(canPath), "%s/cred_%02u.bin", VAULT_DIR, id); 2068 - LittleFS.remove(canPath); 2069 - LittleFS.rename(newPath, canPath); 2070 + lfs::remove(canPath); 2071 + lfs::rename(newPath, canPath); 2070 2072 2071 2073 char newH[48], canH[32]; 2072 2074 snprintf(newH, sizeof(newH), "%s/hmac_%02u.bin", NEW_DIR, id); 2073 2075 snprintf(canH, sizeof(canH), "%s/hmac_%02u.bin", VAULT_DIR, id); 2074 - if (LittleFS.exists(newH)) { 2075 - LittleFS.remove(canH); 2076 - LittleFS.rename(newH, canH); 2076 + if (lfs::exists(newH)) { 2077 + lfs::remove(canH); 2078 + lfs::rename(newH, canH); 2077 2079 } 2078 2080 2079 2081 // G4: rename keyVersion sidecar in lockstep. 2080 2082 char newKvP[48], canKvP[32]; 2081 2083 snprintf(newKvP, sizeof(newKvP), "%s/cred_%02u.kv", NEW_DIR, id); 2082 2084 snprintf(canKvP, sizeof(canKvP), "%s/cred_%02u.kv", VAULT_DIR, id); 2083 - if (LittleFS.exists(newKvP)) { 2084 - LittleFS.remove(canKvP); 2085 - LittleFS.rename(newKvP, canKvP); 2085 + if (lfs::exists(newKvP)) { 2086 + lfs::remove(canKvP); 2087 + lfs::rename(newKvP, canKvP); 2086 2088 } else { 2087 2089 // Defensive: ensure canonical kv reports the new version. 2088 2090 writeKeyVersion(canKvP, newKv); ··· 2093 2095 for (uint8_t id = 0; id < MAX_TOTP; ++id) { 2094 2096 char newPath[48]; 2095 2097 snprintf(newPath, sizeof(newPath), "%s/totp_%02u.bin", NEW_DIR, id); 2096 - if (!LittleFS.exists(newPath)) continue; 2098 + if (!lfs::exists(newPath)) continue; 2097 2099 char canPath[32]; 2098 2100 snprintf(canPath, sizeof(canPath), "%s/totp_%02u.bin", VAULT_DIR, id); 2099 - LittleFS.remove(canPath); 2100 - LittleFS.rename(newPath, canPath); 2101 + lfs::remove(canPath); 2102 + lfs::rename(newPath, canPath); 2101 2103 2102 2104 // G4: rename TOTP keyVersion sidecar. 2103 2105 char newKvP[48], canKvP[32]; 2104 2106 snprintf(newKvP, sizeof(newKvP), "%s/totp_%02u.kv", NEW_DIR, id); 2105 2107 snprintf(canKvP, sizeof(canKvP), "%s/totp_%02u.kv", VAULT_DIR, id); 2106 - if (LittleFS.exists(newKvP)) { 2107 - LittleFS.remove(canKvP); 2108 - LittleFS.rename(newKvP, canKvP); 2108 + if (lfs::exists(newKvP)) { 2109 + lfs::remove(canKvP); 2110 + lfs::rename(newKvP, canKvP); 2109 2111 } else { 2110 2112 writeKeyVersion(canKvP, newKv); 2111 2113 } ··· 2116 2118 // per-credential rename has succeeded — this preserves the marker 2117 2119 // that boot recovery (G4 / H2) uses to distinguish in-flight rekey 2118 2120 // crashes from a clean filesystem. 2119 - LittleFS.remove(BAK_META_FILE); 2120 - if (LittleFS.exists(META_KV_BAK)) LittleFS.remove(META_KV_BAK); 2121 - LittleFS.rmdir(NEW_DIR); // may fail if anything left behind; ignore. 2121 + lfs::remove(BAK_META_FILE); 2122 + if (lfs::exists(META_KV_BAK)) lfs::remove(META_KV_BAK); 2123 + lfs::rmdir(NEW_DIR); // may fail if anything left behind; ignore. 2122 2124 purgeOldDir(); 2123 2125 2124 2126 VaultCrypto::secureWipe(newKey, sizeof(newKey)); ··· 2164 2166 } 2165 2167 VaultCrypto::secureWipe(jsonBuf, sizeof(jsonBuf)); 2166 2168 2167 - File f = LittleFS.open(INDEX_FILE, "w"); 2169 + File f = lfs::open(INDEX_FILE, "w"); 2168 2170 if (!f) return false; 2169 2171 f.write(iv, IV_SIZE); 2170 2172 f.write(cipherBuf, cipherLen); ··· 2180 2182 kleidos::vault::execute([&](){ _r = loadIndex(key, entries, maxEntries); return true; }); 2181 2183 return _r; 2182 2184 } 2183 - File f = LittleFS.open(INDEX_FILE, "r"); 2185 + File f = lfs::open(INDEX_FILE, "r"); 2184 2186 if (!f) { 2185 2187 KLEIDOS_LOGI(TAG, "No index cache — falling back to credential scan"); 2186 2188 return list(key, entries, maxEntries); ··· 2260 2262 } 2261 2263 VaultCrypto::secureWipe(jsonBuf, sizeof(jsonBuf)); 2262 2264 2263 - File f = LittleFS.open(TOTP_INDEX_FILE, "w"); 2265 + File f = lfs::open(TOTP_INDEX_FILE, "w"); 2264 2266 if (!f) return false; 2265 2267 f.write(iv, IV_SIZE); 2266 2268 f.write(cipherBuf, cipherLen); ··· 2276 2278 kleidos::vault::execute([&](){ _r = loadTotpIndex(key, entries, max); return true; }); 2277 2279 return _r; 2278 2280 } 2279 - File f = LittleFS.open(TOTP_INDEX_FILE, "r"); 2281 + File f = lfs::open(TOTP_INDEX_FILE, "r"); 2280 2282 if (!f) { 2281 2283 KLEIDOS_LOGI(TAG, "No TOTP index cache — falling back to TOTP scan"); 2282 2284 return listTotp(key, entries, max);
+7 -8
src/vault/VaultTask.h
··· 5 5 // through a single FreeRTOS task pinned to PRO_CPU (Core 0). This guarantees 6 6 // that: 7 7 // 8 - // 1. LittleFS is never accessed by two tasks concurrently. The Arduino 9 - // LittleFS wrapper (esp_littlefs) is *not* fully reentrant once you 10 - // keep file descriptors open across calls, and even at API level a 11 - // concurrent format/erase from another task is a recipe for 12 - // corruption. With a single owner task this race is impossible. 8 + // 1. LittleFS is never accessed by two tasks concurrently. The platform 9 + // LittleFs facade is backed by esp_littlefs/VFS, and concurrent file 10 + // access plus format/erase from another task is a recipe for corruption. 11 + // With a single owner task this race is impossible. 13 12 // 14 13 // 2. Heavy mbedTLS work that runs alongside vault I/O (HMAC verification 15 14 // of every credential, AES decrypt during list/load) runs on Core 0, 16 15 // keeping Core 1 free for UI render and button input. 17 16 // 18 17 // 3. Pre-deep-sleep teardown is deterministic: stop accepting new work, 19 - // drain in-flight requests, then `LittleFS.end()`, then delete the 20 - // task. Without a single owner there is no safe ordering. 18 + // drain in-flight requests, then unmount LittleFS, then delete the task. 19 + // Without a single owner there is no safe ordering. 21 20 // 22 21 // API model 23 22 // --------- ··· 85 84 86 85 /// Stop the worker. Drains the queue first (with a bounded timeout), then 87 86 /// signals the task to exit and joins it. Must be called before 88 - /// `LittleFS.end()` in the deep-sleep teardown sequence. 87 + /// LittleFS unmount in the deep-sleep teardown sequence. 89 88 void shutdown(); 90 89 91 90 /// True between `begin()` returning true and `shutdown()` returning. Used