···7171 simultaneously. Stop BLE before starting OTA.
7272- **Display**: dim after 5 s, off after 10 s of inactivity.
7373 Never leave backlight on during deep sleep.
7474-- **LittleFS**: call `LittleFS.end()` before every deep sleep call.
7474+- **LittleFS**: call `kleidos::platform::littlefs::end()` before every deep sleep call.
7575 LittleFS is NOT erased by OTA updates — vault data persists.
76767777### 5. Platform abstraction is mandatory
···9090- Abstraction-first rule: if a platform API is likely to be reused, tested,
9191 replaced, or ported in the future, create or extend a facade instead of
9292 spreading direct calls. Current high-priority candidates include watchdog,
9393- LittleFS lifecycle, WiFi/httpd/OTA, heap/reset diagnostics, and sleep/wake
9393+ WiFi/httpd/OTA, heap/reset diagnostics, and sleep/wake
9494 APIs. Reusable timing/RNG logic should use `platform/Clock.h` and
9595 `platform/Random.h`; debug serial I/O should use
9696- `platform/SerialTransport.h`.
9696+ `platform/SerialTransport.h`; LittleFS mount/unmount and file I/O should use
9797+ `platform/LittleFs.h`.
9798- Raw platform calls are allowed only as narrow, documented exceptions when a
9899 facade would hide important semantics. Keep exceptions local and update
99100 `docs/platform-abstraction.md`.
···108109109110- `constexpr` for all compile-time constants. No magic numbers.
110111- `enum class` for all state definitions.
111111-- RAII guards for subsystems: `BleSessionGuard`, `WifiSessionGuard`, `LittleFSGuard`
112112+- RAII guards for subsystems: `BleSessionGuard`, `WifiSessionGuard`, `LittleFsGuard`
112113- Error paths must log failure reason through `KLEIDOS_LOG*` — never credential data.
113114- PlatformIO unit tests (`test/`) must cover all crypto functions.
114115
+3-2
.github/memories/platform-abstraction.md
···11# Platform abstraction memory
2233- Application firmware must use `src/platform/` facades for RTOS, logging, and reusable platform services.
44-- Current facade modules: `Logger`, `RtosTypes`, `RtosTime`, `RtosTask`, `RtosSync`, `RtosQueue`, `NvsStore`, `Clock`, `Random`, `SerialTransport`; `Rtos.h` is an umbrella include.
44+- Current facade modules: `Logger`, `RtosTypes`, `RtosTime`, `RtosTask`, `RtosSync`, `RtosQueue`, `NvsStore`, `Clock`, `Random`, `SerialTransport`, `LittleFs`; `Rtos.h` is an umbrella include.
55- `SerialTransport` uses ESP-IDF UART and USB Serial/JTAG drivers; do not use Arduino `Serial` from application code.
66+- `LittleFs` uses ESP-IDF `esp_littlefs` plus VFS/POSIX calls; do not use Arduino `LittleFS` from application code.
67- Do not spread raw ESP-IDF/FreeRTOS APIs outside `src/platform/`; create/extend facades for future abstraction candidates.
78- `NimbleHost.cpp` uses `rtos::CriticalMux`/`rtos::CriticalLock` for SMP callback mailbox critical sections, not raw `portMUX_TYPE`.
88-- Future candidates: watchdog, LittleFS lifecycle, WiFi/httpd/OTA, heap/reset diagnostics, sleep/wake.
99+- Future candidates: watchdog, WiFi/httpd/OTA, heap/reset diagnostics, sleep/wake.
+3-3
.github/skills/platform-abstraction/SKILL.md
···3636| `platform/Clock.h/.cpp` | Monotonic milliseconds/microseconds for reusable logic and native builds. |
3737| `platform/Random.h/.cpp` | Hardware RNG-backed byte/integer helpers plus bounded/range helpers. |
3838| `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. |
3939+| `platform/LittleFs.h/.cpp` | LittleFS lifecycle, format, file, directory, and VFS-backed path helpers over ESP-IDF `esp_littlefs`. |
39404041Templates and compile-time constants may stay in headers. Non-template
4142functions should live in `.cpp` files when possible.
···6364from application-level modules:
64656566- Watchdog APIs (`esp_task_wdt_*`)
6666-- LittleFS lifecycle and filesystem mount/unmount policy
6767- WiFi AP/client, `esp_netif`, `esp_event`, `esp_http_server`, OTA ops
6868- BLE host lifecycle primitives that are not already isolated under `src/ble/`
6969- Heap, stack, reset reason, chip diagnostics, and runtime stats
···83838484Current normal exception boundary: `src/platform/Logger.*`,
8585`src/platform/Rtos*.*`, `src/platform/Clock.*`, `src/platform/Random.*`,
8686-`src/platform/NvsStore.*`, and `src/platform/SerialTransport.*`. Existing
8787-HAL/UI adapters may retain Arduino timing
8686+`src/platform/NvsStore.*`, `src/platform/SerialTransport.*`, and
8787+`src/platform/LittleFs.*`. Existing HAL/UI adapters may retain Arduino timing
8888calls while they are still Arduino/M5Unified-owned. `NimbleHost.cpp` may need SMP
8989critical-section semantics, but must use `rtos::CriticalMux` /
9090`rtos::CriticalLock` rather than raw FreeRTOS.
+1-1
docs/architecture.md
···3803802. **WiFi**: active only in ADMIN_MODE. BLE must be off before WiFi starts.
3813813. **Display**: dims after configurable timeout, turns off after extended idle.
382382 Never left on during deep sleep.
383383-4. **LittleFS**: `LittleFS.end()` called before every deep sleep entry.
383383+4. **LittleFS**: `kleidos::platform::littlefs::end()` called before every deep sleep entry.
3843845. **Audio**: `audio.end()` called before deep sleep.
385385386386### Deep Sleep Entry
+4
docs/developer-guide.md
···260260WifiAdminPortal::begin(); // init
261261// ... use WiFi ...
262262WifiAdminPortal::end(); // deinit — MANDATORY
263263+264264+kleidos::platform::littlefs::begin(); // mount vault filesystem
265265+// ... use vault files through the facade or VaultStore ...
266266+kleidos::platform::littlefs::end(); // unmount before deep sleep — MANDATORY
263267```
264268265269Missing shutdown paths are **bugs**, not warnings.
+1-1
docs/hardware.md
···2992993. BLE fully deinitialized after typing (`BLEDevice::deinit(true)`)
3003004. WiFi only in admin mode — never concurrent with BLE
3013015. Display: dim 5 s → off 10 s → sleep 60 s (all configurable)
302302-6. LittleFS closed before deep sleep (`LittleFS.end()`)
302302+6. LittleFS unmounted before deep sleep (`kleidos::platform::littlefs::end()`)
3033037. DFS: CPU frequency adjusted per state (80–240 MHz)
304304305305## USB and Debugging
+3-3
docs/platform-abstraction.md
···2626| `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. |
2727| `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. |
2828| `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. |
2929+| `src/platform/LittleFs.h/.cpp` | LittleFS mount/unmount/format and file I/O facade over ESP-IDF `esp_littlefs` plus VFS/POSIX calls. |
29303031## Logging Rules
3132···8384module:
84858586- Watchdog APIs (`esp_task_wdt_*`)
8686-- LittleFS lifecycle and filesystem mount/unmount policy
8787- WiFi AP/client, `esp_netif`, `esp_event`, `esp_http_server`, and OTA ops
8888- BLE host lifecycle primitives not already isolated under `src/ble/`
8989- Heap, reset reason, chip diagnostics, runtime stats, and stack telemetry
···97979898- `src/platform/Logger.*`, `src/platform/Rtos*.*`, `src/platform/Clock.*`,
9999 `src/platform/Random.*`, `src/platform/NvsStore.*`, and
100100- `src/platform/SerialTransport.*` own their matching ESP-IDF platform APIs for
101101- normal firmware code.
100100+ `src/platform/SerialTransport.*`, and `src/platform/LittleFs.*` own their
101101+ matching ESP-IDF platform APIs for normal firmware code.
102102- `src/ble/stack/NimbleHost.cpp` still needs SMP critical-section semantics for
103103 NimBLE callback mailbox state, but it must use `rtos::CriticalMux` and
104104 `rtos::CriticalLock` instead of raw `portMUX_TYPE` or `portENTER_CRITICAL`.
···44#include <Arduino.h>
55#include <esp32-hal-cpu.h>
66#include "platform/Clock.h"
77+#include "platform/LittleFs.h"
78#include "platform/Logger.h"
89#include "platform/Rtos.h"
910#include "platform/SerialTransport.h"
···224225 kleidos::render::shutdown();
225226#endif
226227227227- // Stop the vault worker BEFORE LittleFS.end() so any in-flight
228228+ // Stop the vault worker BEFORE LittleFS unmount so any in-flight
228229 // request finishes cleanly. Drains the queue with a bounded
229230 // timeout, then deletes the task. Idempotent.
230231 kleidos::vault::shutdown();
232232+ kleidos::platform::littlefs::end();
231233232234#ifdef DEBUG_RESTART_INSTEAD_OF_DEEP_SLEEP
233235 // Debug builds: restart instead of deep sleep so serial connection
···24242525#include <cstdint>
2626#include <cstddef>
2727-#include <FS.h>
28272928// ---------------------------------------------------------------------------
3029// SD Vault Manifest — metadata for cross-device portability
···191190 static bool deleteKeyfile();
192191193192private:
194194- /// Copy a single file between filesystems (LittleFS ↔ SD).
195195- static bool copyFile(fs::FS& srcFs, const char* srcPath,
196196- fs::FS& dstFs, const char* dstPath);
193193+ /// Copy a single vault file from LittleFS to SD.
194194+ static bool copyLittleFsToSd(const char* srcPath, const char* dstPath);
195195+196196+ /// Copy a single vault file from SD to LittleFS.
197197+ static bool copySdToLittleFs(const char* srcPath, const char* dstPath);
197198198199 /// Write manifest.json to SD with current device info.
199200 static bool writeManifest(uint8_t credCount, uint8_t totpCount);
+153-151
src/vault/VaultStore.cpp
···7788#include "platform/Logger.h"
99#include "platform/Clock.h"
1010+#include "platform/LittleFs.h"
1011#include "platform/NvsStore.h"
1112#include <esp_task_wdt.h>
1212-#include <LittleFS.h>
1313#include <ArduinoJson.h>
1414#include <mbedtls/base64.h>
1515···2727static constexpr const char* TOTP_INDEX_FILE = "/vault/totp_idx.bin";
28282929using kleidos::platform::NvsStore;
3030+namespace lfs = kleidos::platform::littlefs;
3131+using File = kleidos::platform::littlefs::File;
30323133// Internal size constants
3234static constexpr size_t MAX_PLAINTEXT_SIZE = MAX_NAME_LEN + MAX_USER_LEN
···110112// compatible with vaults provisioned before keyVersion existed).
111113// ---------------------------------------------------------------------------
112114static uint32_t readKeyVersion(const char* path) {
113113- if (!LittleFS.exists(path)) return 0;
114114- File f = LittleFS.open(path, "r");
115115+ if (!lfs::exists(path)) return 0;
116116+ File f = lfs::open(path, "r");
115117 if (!f) return 0;
116118 uint8_t buf[4] = {};
117119 size_t n = f.read(buf, sizeof(buf));
···124126}
125127126128static bool writeKeyVersion(const char* path, uint32_t v) {
127127- File f = LittleFS.open(path, "w");
129129+ File f = lfs::open(path, "w");
128130 if (!f) return false;
129131 uint8_t buf[4] = {
130132 static_cast<uint8_t>( v & 0xFF),
···151153 constexpr const char* OLD_DIR = "/vault/old";
152154 constexpr const char* BAK_META = "/vault/meta.bak";
153155154154- const bool hasShadow = LittleFS.exists(SHADOW_DIR);
155155- const bool hasOld = LittleFS.exists(OLD_DIR);
156156- const bool hasBak = LittleFS.exists(BAK_META);
157157- const bool hasMeta = LittleFS.exists(META_FILE);
156156+ const bool hasShadow = lfs::exists(SHADOW_DIR);
157157+ const bool hasOld = lfs::exists(OLD_DIR);
158158+ const bool hasBak = lfs::exists(BAK_META);
159159+ const bool hasMeta = lfs::exists(META_FILE);
158160159161 // Scan canonical credential sidecars to detect partial-rename inconsistency.
160162 vaultrec::InspectionInput in{};
···170172 for (uint8_t id = 0; id < MAX_CREDENTIALS; ++id) {
171173 char credPath[32];
172174 snprintf(credPath, sizeof(credPath), "%s/cred_%02u.bin", VAULT_DIR, id);
173173- if (!LittleFS.exists(credPath)) continue;
175175+ if (!lfs::exists(credPath)) continue;
174176 char kvPath[32];
175177 snprintf(kvPath, sizeof(kvPath), "%s/cred_%02u.kv", VAULT_DIR, id);
176178 const uint32_t kv = readKeyVersion(kvPath); // missing = 0 (legacy)
···189191 // cleanup didn't run must still be dropped (otherwise it leaks
190192 // disk space and confuses future inspections).
191193 if (hasOld) {
192192- File dir = LittleFS.open(OLD_DIR);
194194+ File dir = lfs::open(OLD_DIR);
193195 if (dir && dir.isDirectory()) {
194196 File f = dir.openNextFile();
195197 while (f) {
196198 const char* n = f.name();
197199 if (n) {
198198- char path[64];
200200+ char path[128];
199201 snprintf(path, sizeof(path), "%s/%s", OLD_DIR, n);
200200- LittleFS.remove(path);
202202+ lfs::remove(path);
201203 }
202204 f = dir.openNextFile();
203205 }
204206 }
205205- LittleFS.rmdir(OLD_DIR);
207207+ lfs::rmdir(OLD_DIR);
206208 // If meta.bak is also leftover staging from the same successful
207209 // rekey, drop it too.
208208- if (hasBak) LittleFS.remove(BAK_META);
209209- if (LittleFS.exists(META_KV_BAK)) LittleFS.remove(META_KV_BAK);
210210+ if (hasBak) lfs::remove(BAK_META);
211211+ if (lfs::exists(META_KV_BAK)) lfs::remove(META_KV_BAK);
210212 KLEIDOS_LOGI(TAG, "Rekey recovery: cleaned leftover /vault/old/ staging");
211213 }
212214 return;
213215 }
214216215217 auto purgeShadowDir = []() {
216216- File dir = LittleFS.open("/vault/new");
218218+ File dir = lfs::open("/vault/new");
217219 if (!dir || !dir.isDirectory()) return;
218220 File f = dir.openNextFile();
219221 while (f) {
220222 const char* n = f.name();
221223 if (n) {
222222- char path[64];
224224+ char path[128];
223225 snprintf(path, sizeof(path), "/vault/new/%s", n);
224224- LittleFS.remove(path);
226226+ lfs::remove(path);
225227 }
226228 f = dir.openNextFile();
227229 }
228228- LittleFS.rmdir("/vault/new");
230230+ lfs::rmdir("/vault/new");
229231 };
230232231233 auto purgeOldDirIO = []() {
232232- File dir = LittleFS.open("/vault/old");
234234+ File dir = lfs::open("/vault/old");
233235 if (!dir || !dir.isDirectory()) return;
234236 File f = dir.openNextFile();
235237 while (f) {
236238 const char* n = f.name();
237239 if (n) {
238238- char path[64];
240240+ char path[128];
239241 snprintf(path, sizeof(path), "/vault/old/%s", n);
240240- LittleFS.remove(path);
242242+ lfs::remove(path);
241243 }
242244 f = dir.openNextFile();
243245 }
244244- LittleFS.rmdir("/vault/old");
246246+ lfs::rmdir("/vault/old");
245247 };
246248247249 auto restoreMetaFromBak = []() {
248248- if (LittleFS.exists(META_FILE)) LittleFS.remove(META_FILE);
249249- if (!LittleFS.rename("/vault/meta.bak", META_FILE)) {
250250+ if (lfs::exists(META_FILE)) lfs::remove(META_FILE);
251251+ if (!lfs::rename("/vault/meta.bak", META_FILE)) {
250252 KLEIDOS_LOGE(TAG, "Rekey recovery: meta.bak → meta.bin rename FAILED");
251253 }
252254 // Mirror the meta.kv side: if a backup exists, swap it back too.
253253- if (LittleFS.exists(META_KV_BAK)) {
254254- if (LittleFS.exists(META_KV_FILE)) LittleFS.remove(META_KV_FILE);
255255- LittleFS.rename(META_KV_BAK, META_KV_FILE);
255255+ if (lfs::exists(META_KV_BAK)) {
256256+ if (lfs::exists(META_KV_FILE)) lfs::remove(META_KV_FILE);
257257+ lfs::rename(META_KV_BAK, META_KV_FILE);
256258 }
257259 };
258260259261 // H2 — Restore canonical creds + sidecars from /vault/old/. Used by
260262 // ROLLBACK_FROM_OLD_DIR to undo a partial swap losslessly.
261263 auto restoreCanonicalsFromOldDir = []() {
262262- File dir = LittleFS.open("/vault/old");
264264+ File dir = lfs::open("/vault/old");
263265 if (!dir || !dir.isDirectory()) return;
264266 File f = dir.openNextFile();
265267 while (f) {
···268270 char src[80], dst[80];
269271 snprintf(src, sizeof(src), "/vault/old/%s", n);
270272 snprintf(dst, sizeof(dst), "/vault/%s", n);
271271- if (LittleFS.exists(dst)) LittleFS.remove(dst);
272272- if (!LittleFS.rename(src, dst)) {
273273+ if (lfs::exists(dst)) lfs::remove(dst);
274274+ if (!lfs::rename(src, dst)) {
273275 KLEIDOS_LOGE(TAG,
274276 "Rekey recovery: rename %s → %s FAILED", src, dst);
275277 }
276278 }
277279 f = dir.openNextFile();
278280 }
279279- LittleFS.rmdir("/vault/old");
281281+ lfs::rmdir("/vault/old");
280282 };
281283282284 switch (action) {
···338340 if (!kleidos::vault::onWorker()) {
339341 return kleidos::vault::execute([&](){ return begin(); });
340342 }
341341- if (!LittleFS.begin(true, "/littlefs", 10, "littlefs")) {
343343+ if (!lfs::begin(true)) {
342344 KLEIDOS_LOGE(TAG, "LittleFS mount failed");
343345 return false;
344346 }
345347346346- if (!LittleFS.exists(VAULT_DIR)) {
347347- LittleFS.mkdir(VAULT_DIR);
348348+ if (!lfs::exists(VAULT_DIR)) {
349349+ lfs::mkdir(VAULT_DIR);
348350 KLEIDOS_LOGI(TAG, "Created vault directory");
349351 }
350352351353 // Bug #5 fix: rekey recovery walks the entire vault dir doing many
352352- // LittleFS.exists() calls (each = fopen + newlib lock alloc). The
354354+ // lfs::exists() calls (each = fopen + newlib lock alloc). The
353355 // admin portal calls VaultStore::begin() many times concurrently per
354356 // request, exhausting the newlib FILE / lock pool and aborting in
355357 // lock_init_generic. Recovery is a boot-time concern — once it has
···364366 }
365367366368 KLEIDOS_LOGI(TAG, "Vault initialized — %u bytes used, %u total",
367367- LittleFS.usedBytes(), LittleFS.totalBytes());
369369+ lfs::usedBytes(), lfs::totalBytes());
368370 return true;
369371}
370372···378380 // provision() and rekey(). begin()/end() pairs around individual
379381 // operations were wiping the cache and forcing a 33s PBKDF2 on
380382 // every credential open.
381381- LittleFS.end();
383383+ lfs::end();
382384}
383385384386bool VaultStore::isProvisioned() {
385387 if (!kleidos::vault::onWorker()) {
386388 return kleidos::vault::execute([&](){ return isProvisioned(); });
387389 }
388388- return LittleFS.exists(META_FILE);
390390+ return lfs::exists(META_FILE);
389391}
390392391393bool VaultStore::needsManualRecovery() {
···461463 return _r;
462464 }
463465 uint8_t n = 0;
464464- File dir = LittleFS.open(VAULT_DIR);
466466+ File dir = lfs::open(VAULT_DIR);
465467 if (!dir || !dir.isDirectory()) return 0;
466468 File f = dir.openNextFile();
467469 while (f) {
···477479478480uint8_t VaultStore::countTotp() {
479481 uint8_t n = 0;
480480- File dir = LittleFS.open(VAULT_DIR);
482482+ File dir = lfs::open(VAULT_DIR);
481483 if (!dir || !dir.isDirectory()) return 0;
482484 File f = dir.openNextFile();
483485 while (f) {
···500502 for (uint8_t i = 0; i < MAX_CREDENTIALS; i++) {
501503 char path[32];
502504 snprintf(path, sizeof(path), "%s/cred_%02u.bin", VAULT_DIR, i);
503503- if (!LittleFS.exists(path)) return i;
505505+ if (!lfs::exists(path)) return i;
504506 }
505507 return 0xFF;
506508}
···516518 uint8_t existingIds[MAX_CREDENTIALS];
517519 uint8_t existCount = 0;
518520 {
519519- File dir = LittleFS.open(VAULT_DIR);
521521+ File dir = lfs::open(VAULT_DIR);
520522 if (dir && dir.isDirectory()) {
521523 File f = dir.openNextFile();
522524 while (f && existCount < MAX_CREDENTIALS) {
···541543 // Load favorites bitfield
542544 uint8_t favBits[(MAX_CREDENTIALS + 7) / 8] = {};
543545 {
544544- File f = LittleFS.open("/vault/favorites.bin", "r");
546546+ File f = lfs::open("/vault/favorites.bin", "r");
545547 if (f) {
546548 f.read(favBits, sizeof(favBits));
547549 f.close();
···552554 uint8_t order[MAX_CREDENTIALS];
553555 memset(order, 0xFF, sizeof(order));
554556 {
555555- File f = LittleFS.open("/vault/order.bin", "r");
557557+ File f = lfs::open("/vault/order.bin", "r");
556558 if (f) {
557559 f.read(order, f.size() < sizeof(order) ? f.size() : sizeof(order));
558560 f.close();
···652654 char path[32];
653655 snprintf(path, sizeof(path), "%s/cred_%02u.bin", VAULT_DIR, id);
654656655655- File f = LittleFS.open(path, "w");
657657+ File f = lfs::open(path, "w");
656658 if (!f) {
657659 KLEIDOS_LOGE(TAG, "Failed to open %s for writing", path);
658660 VaultCrypto::secureWipe(credSalt, sizeof(credSalt));
···721723 }
722724723725 // G1: Invalidate index cache — caller must rebuild via loadCredentialList()
724724- LittleFS.remove(INDEX_FILE);
726726+ lfs::remove(INDEX_FILE);
725727726728 KLEIDOS_LOGI(TAG, "Credential %u saved (%u bytes cipher)", id, cipherLen);
727729 return true;
···744746 char path[32];
745747 snprintf(path, sizeof(path), "%s/cred_%02u.bin", VAULT_DIR, id);
746748747747- File f = LittleFS.open(path, "r");
749749+ File f = lfs::open(path, "r");
748750 if (!f) {
749751 KLEIDOS_LOGE(TAG, "Failed to open %s", path);
750752 return false;
···812814 char path[32];
813815 snprintf(path, sizeof(path), "%s/cred_%02u.bin", VAULT_DIR, id);
814816815815- if (LittleFS.exists(path)) {
816816- LittleFS.remove(path);
817817+ if (lfs::exists(path)) {
818818+ lfs::remove(path);
817819 // Also remove HMAC file
818820 char hmacPath[32];
819821 snprintf(hmacPath, sizeof(hmacPath), "%s/hmac_%02u.bin", VAULT_DIR, id);
820820- if (LittleFS.exists(hmacPath)) LittleFS.remove(hmacPath);
822822+ if (lfs::exists(hmacPath)) lfs::remove(hmacPath);
821823 // G4: remove keyVersion sidecar.
822824 char kvPath[32];
823825 snprintf(kvPath, sizeof(kvPath), "%s/cred_%02u.kv", VAULT_DIR, id);
824824- if (LittleFS.exists(kvPath)) LittleFS.remove(kvPath);
826826+ if (lfs::exists(kvPath)) lfs::remove(kvPath);
825827 // G1: Invalidate index cache
826826- LittleFS.remove(INDEX_FILE);
828828+ lfs::remove(INDEX_FILE);
827829 KLEIDOS_LOGI(TAG, "Credential %u deleted", id);
828830 return true;
829831 }
···842844 for (uint8_t i = 0; i < MAX_CREDENTIALS; i++) {
843845 char path[32];
844846 snprintf(path, sizeof(path), "%s/cred_%02u.bin", VAULT_DIR, i);
845845- if (LittleFS.exists(path)) {
846846- LittleFS.remove(path);
847847+ if (lfs::exists(path)) {
848848+ lfs::remove(path);
847849 }
848850 char hmacPath[32];
849851 snprintf(hmacPath, sizeof(hmacPath), "%s/hmac_%02u.bin", VAULT_DIR, i);
850850- if (LittleFS.exists(hmacPath)) {
851851- LittleFS.remove(hmacPath);
852852+ if (lfs::exists(hmacPath)) {
853853+ lfs::remove(hmacPath);
852854 }
853855 // G4: also remove keyVersion sidecar.
854856 char kvPath[32];
855857 snprintf(kvPath, sizeof(kvPath), "%s/cred_%02u.kv", VAULT_DIR, i);
856856- if (LittleFS.exists(kvPath)) LittleFS.remove(kvPath);
858858+ if (lfs::exists(kvPath)) lfs::remove(kvPath);
857859 }
858860859861 for (uint8_t i = 0; i < MAX_TOTP; i++) {
860862 char kvPath[32];
861863 snprintf(kvPath, sizeof(kvPath), "%s/totp_%02u.kv", VAULT_DIR, i);
862862- if (LittleFS.exists(kvPath)) LittleFS.remove(kvPath);
864864+ if (lfs::exists(kvPath)) lfs::remove(kvPath);
863865 }
864866865865- if (LittleFS.exists(META_FILE)) {
866866- LittleFS.remove(META_FILE);
867867+ if (lfs::exists(META_FILE)) {
868868+ lfs::remove(META_FILE);
867869 }
868868- if (LittleFS.exists(META_KV_FILE)) LittleFS.remove(META_KV_FILE);
869869- if (LittleFS.exists(META_KV_BAK)) LittleFS.remove(META_KV_BAK);
870870- if (LittleFS.exists("/vault/meta.bak")) LittleFS.remove("/vault/meta.bak");
870870+ if (lfs::exists(META_KV_FILE)) lfs::remove(META_KV_FILE);
871871+ if (lfs::exists(META_KV_BAK)) lfs::remove(META_KV_BAK);
872872+ if (lfs::exists("/vault/meta.bak")) lfs::remove("/vault/meta.bak");
871873872874 // H2 — drop any leftover staging trees from an in-flight rekey so a
873875 // post-wipe boot starts from a fully empty /vault/.
874876 auto wipeDir = [](const char* path) {
875875- File dir = LittleFS.open(path);
877877+ File dir = lfs::open(path);
876878 if (!dir || !dir.isDirectory()) return;
877879 File f = dir.openNextFile();
878880 while (f) {
879881 const char* n = f.name();
880882 if (n) {
881881- char p[64];
883883+ char p[128];
882884 snprintf(p, sizeof(p), "%s/%s", path, n);
883883- LittleFS.remove(p);
885885+ lfs::remove(p);
884886 }
885887 f = dir.openNextFile();
886888 }
887887- LittleFS.rmdir(path);
889889+ lfs::rmdir(path);
888890 };
889891 wipeDir("/vault/new");
890892 wipeDir("/vault/old");
···905907 doc["version"] = 1;
906908907909 // Export meta.bin
908908- File metaFile = LittleFS.open(META_FILE, "r");
910910+ File metaFile = lfs::open(META_FILE, "r");
909911 if (metaFile) {
910912 size_t sz = metaFile.size();
911913 uint8_t buf[sizeof(VaultMeta)];
···926928 for (uint8_t i = 0; i < MAX_CREDENTIALS; i++) {
927929 char path[32];
928930 snprintf(path, sizeof(path), "%s/cred_%02u.bin", VAULT_DIR, i);
929929- if (!LittleFS.exists(path)) continue;
931931+ if (!lfs::exists(path)) continue;
930932931931- File f = LittleFS.open(path, "r");
933933+ File f = lfs::open(path, "r");
932934 if (!f) continue;
933935934936 size_t sz = f.size();
···996998 mbedtls_base64_decode(buf, sizeof(buf), &binLen,
997999 reinterpret_cast<const uint8_t*>(metaB64), b64StrLen);
9981000999999- File f = LittleFS.open(META_FILE, "w");
10011001+ File f = lfs::open(META_FILE, "w");
10001002 if (f) { f.write(buf, binLen); f.close(); }
10011003 }
10021004 }
···10231025 char path[32];
10241026 snprintf(path, sizeof(path), "%s/cred_%02u.bin", VAULT_DIR, id);
1025102710261026- File f = LittleFS.open(path, "w");
10281028+ File f = lfs::open(path, "w");
10271029 if (f) { f.write(buf, binLen); f.close(); }
10281030 }
10291031 }
···1130113211311133// ---------------------------------------------------------------------------
11321134bool VaultStore::isMetaV2() {
11331133- File f = LittleFS.open(META_FILE, "r");
11351135+ File f = lfs::open(META_FILE, "r");
11341136 if (!f) return false;
1135113711361138 bool v2 = false;
···11461148}
1147114911481150bool VaultStore::readMetaV2(VaultMetaV2& meta) {
11491149- File f = LittleFS.open(META_FILE, "r");
11511151+ File f = lfs::open(META_FILE, "r");
11501152 if (!f) {
11511153 KLEIDOS_LOGE(TAG, "No vault meta file");
11521154 return false;
···11691171}
1170117211711173bool VaultStore::writeMetaV2(const VaultMetaV2& meta) {
11721172- File f = LittleFS.open(META_FILE, "w");
11741174+ File f = lfs::open(META_FILE, "w");
11731175 if (!f) {
11741176 KLEIDOS_LOGE(TAG, "Failed to write v2 meta file");
11751177 return false;
···12101212// Private
12111213// ---------------------------------------------------------------------------
12121214bool VaultStore::readMeta(VaultMeta& meta) {
12131213- File f = LittleFS.open(META_FILE, "r");
12151215+ File f = lfs::open(META_FILE, "r");
12141216 if (!f) {
12151217 KLEIDOS_LOGE(TAG, "No vault meta — not provisioned");
12161218 return false;
···12381240}
1239124112401242bool VaultStore::writeMeta(const VaultMeta& meta) {
12411241- File f = LittleFS.open(META_FILE, "w");
12431243+ File f = lfs::open(META_FILE, "w");
12421244 if (!f) {
12431245 KLEIDOS_LOGE(TAG, "Failed to write meta file");
12441246 return false;
···12821284 char path[32];
12831285 snprintf(path, sizeof(path), "%s/cred_%02u.bin", VAULT_DIR, id);
1284128612851285- File f = LittleFS.open(path, "r");
12871287+ File f = lfs::open(path, "r");
12861288 if (!f) {
12871289 VaultCrypto::secureWipe(hmacKey, sizeof(hmacKey));
12881290 return false;
···13111313 char path[32];
13121314 snprintf(path, sizeof(path), "%s/hmac_%02u.bin", VAULT_DIR, id);
1313131513141314- File f = LittleFS.open(path, "w");
13161316+ File f = lfs::open(path, "w");
13151317 if (!f) {
13161318 KLEIDOS_LOGE(TAG, "Failed to write HMAC file for credential %u", id);
13171319 return false;
···13251327 char hmacPath[32];
13261328 snprintf(hmacPath, sizeof(hmacPath), "%s/hmac_%02u.bin", VAULT_DIR, id);
1327132913281328- if (!LittleFS.exists(hmacPath)) {
13301330+ if (!lfs::exists(hmacPath)) {
13291331 KLEIDOS_LOGW(TAG, "Missing HMAC file for credential %u", id);
13301332 return false;
13311333 }
1332133413331333- File f = LittleFS.open(hmacPath, "r");
13351335+ File f = lfs::open(hmacPath, "r");
13341336 if (!f || f.size() != HMAC_SIZE) {
13351337 if (f) f.close();
13361338 return false;
···13691371 for (uint8_t i = 0; i < MAX_TOTP; i++) {
13701372 char path[32];
13711373 snprintf(path, sizeof(path), "%s/totp_%02u.bin", VAULT_DIR, i);
13721372- if (!LittleFS.exists(path)) return i;
13741374+ if (!lfs::exists(path)) return i;
13731375 }
13741376 return 0xFF;
13751377}
···13841386 uint8_t existingIds[MAX_TOTP];
13851387 uint8_t existCount = 0;
13861388 {
13871387- File dir = LittleFS.open(VAULT_DIR);
13891389+ File dir = lfs::open(VAULT_DIR);
13881390 if (dir && dir.isDirectory()) {
13891391 File f = dir.openNextFile();
13901392 while (f && existCount < MAX_TOTP) {
···14771479 char path[32];
14781480 snprintf(path, sizeof(path), "%s/totp_%02u.bin", VAULT_DIR, id);
1479148114801480- File f = LittleFS.open(path, "w");
14821482+ File f = lfs::open(path, "w");
14811483 if (!f) {
14821484 KLEIDOS_LOGE(TAG, "Failed to open %s for writing", path);
14831485 VaultCrypto::secureWipe(credSalt, sizeof(credSalt));
···15011503 }
1502150415031505 // G1: Invalidate TOTP index cache
15041504- LittleFS.remove(TOTP_INDEX_FILE);
15061506+ lfs::remove(TOTP_INDEX_FILE);
1505150715061508 KLEIDOS_LOGI(TAG, "TOTP %u saved", id);
15071509 return true;
···15161518 char path[32];
15171519 snprintf(path, sizeof(path), "%s/totp_%02u.bin", VAULT_DIR, id);
1518152015191519- File f = LittleFS.open(path, "r");
15211521+ File f = lfs::open(path, "r");
15201522 if (!f) return false;
1521152315221524 size_t fileSize = f.size();
···15741576 if (id >= MAX_TOTP) return false;
15751577 char path[32];
15761578 snprintf(path, sizeof(path), "%s/totp_%02u.bin", VAULT_DIR, id);
15771577- if (!LittleFS.exists(path)) return false;
15791579+ if (!lfs::exists(path)) return false;
15781580 // G4: remove matching keyVersion sidecar.
15791581 char kvPath[32];
15801582 snprintf(kvPath, sizeof(kvPath), "%s/totp_%02u.kv", VAULT_DIR, id);
15811581- if (LittleFS.exists(kvPath)) LittleFS.remove(kvPath);
15831583+ if (lfs::exists(kvPath)) lfs::remove(kvPath);
15821584 // G1: Invalidate TOTP index cache
15831583- LittleFS.remove(TOTP_INDEX_FILE);
15841584- return LittleFS.remove(path);
15851585+ lfs::remove(TOTP_INDEX_FILE);
15861586+ return lfs::remove(path);
15851587}
1586158815871589// ---------------------------------------------------------------------------
···15941596 if (id >= MAX_CREDENTIALS) return false;
15951597 uint8_t favBits[(MAX_CREDENTIALS + 7) / 8] = {};
15961598 {
15971597- File f = LittleFS.open("/vault/favorites.bin", "r");
15991599+ File f = lfs::open("/vault/favorites.bin", "r");
15981600 if (f) { f.read(favBits, sizeof(favBits)); f.close(); }
15991601 }
16001602 if (favorite) {
···16021604 } else {
16031605 favBits[id / 8] &= ~(1 << (id % 8));
16041606 }
16051605- File f = LittleFS.open("/vault/favorites.bin", "w");
16071607+ File f = lfs::open("/vault/favorites.bin", "w");
16061608 if (!f) return false;
16071609 f.write(favBits, sizeof(favBits));
16081610 f.close();
16091611 // G1: Invalidate index cache (favorites affect sort order)
16101610- LittleFS.remove(INDEX_FILE);
16121612+ lfs::remove(INDEX_FILE);
16111613 return true;
16121614}
1613161516141616bool VaultStore::isFavorite(uint8_t id) {
16151617 if (id >= MAX_CREDENTIALS) return false;
16161618 uint8_t favBits[(MAX_CREDENTIALS + 7) / 8] = {};
16171617- File f = LittleFS.open("/vault/favorites.bin", "r");
16191619+ File f = lfs::open("/vault/favorites.bin", "r");
16181620 if (!f) return false;
16191621 f.read(favBits, sizeof(favBits));
16201622 f.close();
···16321634 order[ids[i]] = i;
16331635 }
16341636 }
16351635- File f = LittleFS.open("/vault/order.bin", "w");
16371637+ File f = lfs::open("/vault/order.bin", "w");
16361638 if (!f) return false;
16371639 f.write(order, sizeof(order));
16381640 f.close();
16391641 // G1: Invalidate index cache (order affects sort)
16401640- LittleFS.remove(INDEX_FILE);
16421642+ lfs::remove(INDEX_FILE);
16411643 return true;
16421644}
16431645···16881690constexpr const char* BAK_META_FILE = "/vault/meta.bak";
1689169116901692void purgeShadow() {
16911691- File dir = LittleFS.open(NEW_DIR);
16931693+ File dir = lfs::open(NEW_DIR);
16921694 if (!dir || !dir.isDirectory()) return;
16931695 File f = dir.openNextFile();
16941696 while (f) {
16951697 const char* n = f.name();
16961696- char path[48];
16981698+ char path[128];
16971699 if (n) {
16981700 // f.name() returns basename on some versions — prepend dir.
16991701 snprintf(path, sizeof(path), "%s/%s", NEW_DIR, n);
17001700- LittleFS.remove(path);
17021702+ lfs::remove(path);
17011703 }
17021704 f = dir.openNextFile();
17031705 }
17041704- LittleFS.rmdir(NEW_DIR);
17061706+ lfs::rmdir(NEW_DIR);
17051707}
1706170817071709// H2 — Drop the pre-swap snapshot tree. Mirror of purgeShadow() against
17081710// the OLD_DIR. Safe to call when the directory does not exist.
17091711void purgeOldDir() {
17101710- File dir = LittleFS.open(OLD_DIR);
17121712+ File dir = lfs::open(OLD_DIR);
17111713 if (!dir || !dir.isDirectory()) return;
17121714 File f = dir.openNextFile();
17131715 while (f) {
17141716 const char* n = f.name();
17151715- char path[48];
17171717+ char path[128];
17161718 if (n) {
17171719 snprintf(path, sizeof(path), "%s/%s", OLD_DIR, n);
17181718- LittleFS.remove(path);
17201720+ lfs::remove(path);
17191721 }
17201722 f = dir.openNextFile();
17211723 }
17221722- LittleFS.rmdir(OLD_DIR);
17241724+ lfs::rmdir(OLD_DIR);
17231725}
1724172617251727// Byte-for-byte copy used by the H2 snapshot step. Returns false on any
17261728// I/O error — caller treats failure as fatal and aborts the rekey.
17271729bool copyFileLfs(const char* src, const char* dst) {
17281728- File rf = LittleFS.open(src, "r");
17301730+ File rf = lfs::open(src, "r");
17291731 if (!rf) return false;
17301730- File wf = LittleFS.open(dst, "w");
17321732+ File wf = lfs::open(dst, "w");
17311733 if (!wf) { rf.close(); return false; }
17321734 uint8_t buf[256];
17331735 bool ok = true;
···17381740 }
17391741 rf.close();
17401742 wf.close();
17411741- if (!ok) LittleFS.remove(dst);
17431743+ if (!ok) lfs::remove(dst);
17421744 return ok;
17431745}
17441746} // namespace
···17551757 // Clean any prior aborted attempt.
17561758 purgeShadow();
17571759 purgeOldDir();
17581758- if (LittleFS.exists(BAK_META_FILE)) LittleFS.remove(BAK_META_FILE);
17601760+ if (lfs::exists(BAK_META_FILE)) lfs::remove(BAK_META_FILE);
1759176117601760- if (!LittleFS.mkdir(NEW_DIR)) {
17621762+ if (!lfs::mkdir(NEW_DIR)) {
17611763 KLEIDOS_LOGE(TAG_RK, "mkdir %s failed", NEW_DIR);
17621764 return false;
17631765 }
···18091811 }
18101812 char path[48];
18111813 snprintf(path, sizeof(path), "%s/%s_%02u.bin", NEW_DIR, prefix, id);
18121812- File f = LittleFS.open(path, "w");
18141814+ File f = lfs::open(path, "w");
18131815 if (!f) {
18141816 VaultCrypto::secureWipe(cipherBuf, sizeof(cipherBuf));
18151817 return false;
···18271829 return false;
18281830 }
18291831 // Re-read whole file bytes to feed HMAC.
18301830- File rf = LittleFS.open(path, "r");
18321832+ File rf = lfs::open(path, "r");
18311833 if (!rf) {
18321834 VaultCrypto::secureWipe(hmacKey, sizeof(hmacKey));
18331835 VaultCrypto::secureWipe(cipherBuf, sizeof(cipherBuf));
···18571859 esp_task_wdt_reset(); // G4.2: prevent WDT timeout during large vaults
18581860 char path[32];
18591861 snprintf(path, sizeof(path), "%s/cred_%02u.bin", VAULT_DIR, id);
18601860- if (!LittleFS.exists(path)) continue;
18621862+ if (!lfs::exists(path)) continue;
1861186318621864 Credential cred = {};
18631865 if (!loadCredential(oldKey, id, cred, false)) {
···18871889 // Write shadow HMAC alongside.
18881890 char hpath[48];
18891891 snprintf(hpath, sizeof(hpath), "%s/hmac_%02u.bin", NEW_DIR, id);
18901890- File hf = LittleFS.open(hpath, "w");
18921892+ File hf = lfs::open(hpath, "w");
18911893 if (!hf) { okAll = false; break; }
18921894 hf.write(hmac, HMAC_SIZE);
18931895 hf.close();
···19041906 esp_task_wdt_reset(); // G4.2: prevent WDT timeout during large vaults
19051907 char path[32];
19061908 snprintf(path, sizeof(path), "%s/totp_%02u.bin", VAULT_DIR, id);
19071907- if (!LittleFS.exists(path)) continue;
19091909+ if (!lfs::exists(path)) continue;
1908191019091911 TotpEntry te = {};
19101912 if (!loadTotp(oldKey, id, te)) {
···1943194519441946 // --- Write shadow meta ---
19451947 if (okAll) {
19461946- File mf = LittleFS.open(NEW_META_FILE, "w");
19481948+ File mf = lfs::open(NEW_META_FILE, "w");
19471949 if (!mf) { okAll = false; }
19481950 else {
19491951 mf.write(reinterpret_cast<const uint8_t*>(&newMeta), sizeof(newMeta));
···19701972 // /vault/ so the active vault is not disturbed; the copies act as a
19711973 // lossless rollback target if any later step fails.
19721974 // ---------------------------------------------------------------------
19731973- if (LittleFS.exists(OLD_DIR)) purgeOldDir();
19741974- if (!LittleFS.mkdir(OLD_DIR)) {
19751975+ if (lfs::exists(OLD_DIR)) purgeOldDir();
19761976+ if (!lfs::mkdir(OLD_DIR)) {
19751977 KLEIDOS_LOGE(TAG_RK, "mkdir %s failed", OLD_DIR);
19761978 purgeShadow();
19771979 VaultCrypto::secureWipe(newKey, sizeof(newKey));
···19831985 for (uint8_t id = 0; id < maxId; ++id) {
19841986 char src[40], dst[40];
19851987 snprintf(src, sizeof(src), "%s/%s_%02u.bin", VAULT_DIR, prefix, id);
19861986- if (!LittleFS.exists(src)) continue;
19881988+ if (!lfs::exists(src)) continue;
19871989 snprintf(dst, sizeof(dst), "%s/%s_%02u.bin", OLD_DIR, prefix, id);
19881990 if (!copyFileLfs(src, dst)) return false;
1989199119901992 // Per-cred .kv sidecar (legacy vaults have none → skip silently).
19911993 snprintf(src, sizeof(src), "%s/%s_%02u.kv", VAULT_DIR, prefix, id);
19921992- if (LittleFS.exists(src)) {
19941994+ if (lfs::exists(src)) {
19931995 snprintf(dst, sizeof(dst), "%s/%s_%02u.kv", OLD_DIR, prefix, id);
19941996 if (!copyFileLfs(src, dst)) return false;
19951997 }
···20052007 for (uint8_t id = 0; id < MAX_CREDENTIALS && snapOk; ++id) {
20062008 char src[40], dst[40];
20072009 snprintf(src, sizeof(src), "%s/hmac_%02u.bin", VAULT_DIR, id);
20082008- if (!LittleFS.exists(src)) continue;
20102010+ if (!lfs::exists(src)) continue;
20092011 snprintf(dst, sizeof(dst), "%s/hmac_%02u.bin", OLD_DIR, id);
20102012 if (!copyFileLfs(src, dst)) snapOk = false;
20112013 }
···2022202420232025 // --- COMMIT: swap meta first ---
20242026 // rename() overwrites? Some LittleFS builds require explicit remove.
20252025- if (LittleFS.exists(BAK_META_FILE)) LittleFS.remove(BAK_META_FILE);
20262026- if (!LittleFS.rename(META_FILE, BAK_META_FILE)) {
20272027+ if (lfs::exists(BAK_META_FILE)) lfs::remove(BAK_META_FILE);
20282028+ if (!lfs::rename(META_FILE, BAK_META_FILE)) {
20272029 KLEIDOS_LOGE(TAG_RK, "commit: rename old meta→bak failed");
20282030 purgeShadow();
20292031 VaultCrypto::secureWipe(newKey, sizeof(newKey));
20302032 VaultCrypto::secureWipe(&newMeta, sizeof(newMeta));
20312033 return false;
20322034 }
20332033- if (!LittleFS.rename(NEW_META_FILE, META_FILE)) {
20352035+ if (!lfs::rename(NEW_META_FILE, META_FILE)) {
20342036 KLEIDOS_LOGE(TAG_RK, "commit: rename new meta→canonical FAILED");
20352037 // Roll meta back.
20362036- LittleFS.rename(BAK_META_FILE, META_FILE);
20382038+ lfs::rename(BAK_META_FILE, META_FILE);
20372039 purgeShadow();
20382040 VaultCrypto::secureWipe(newKey, sizeof(newKey));
20392041 VaultCrypto::secureWipe(&newMeta, sizeof(newMeta));
···20432045 // G4: swap meta.kv in lockstep with meta.bin.
20442046 // Old meta.kv may be missing (legacy vault, oldKv == 0); that's fine
20452047 // — we only need the bak as a marker for the recovery path.
20462046- if (LittleFS.exists(META_KV_BAK)) LittleFS.remove(META_KV_BAK);
20472047- if (LittleFS.exists(META_KV_FILE)) {
20482048- LittleFS.rename(META_KV_FILE, META_KV_BAK);
20482048+ if (lfs::exists(META_KV_BAK)) lfs::remove(META_KV_BAK);
20492049+ if (lfs::exists(META_KV_FILE)) {
20502050+ lfs::rename(META_KV_FILE, META_KV_BAK);
20492051 } else {
20502052 // Synthesize a bak holding oldKv so a partial-rename rollback
20512053 // can still restore a sensible canonical meta.kv.
20522054 writeKeyVersion(META_KV_BAK, oldKv);
20532055 }
20542054- if (!LittleFS.rename("/vault/new/meta.kv", META_KV_FILE)) {
20562056+ if (!lfs::rename("/vault/new/meta.kv", META_KV_FILE)) {
20552057 // Best-effort: write the new value directly. We'd rather have
20562058 // the canonical meta.kv slightly inconsistent than abort the
20572059 // commit at this point.
···20622064 for (uint8_t id = 0; id < MAX_CREDENTIALS; ++id) {
20632065 char newPath[48];
20642066 snprintf(newPath, sizeof(newPath), "%s/cred_%02u.bin", NEW_DIR, id);
20652065- if (!LittleFS.exists(newPath)) continue;
20672067+ if (!lfs::exists(newPath)) continue;
20662068 char canPath[32];
20672069 snprintf(canPath, sizeof(canPath), "%s/cred_%02u.bin", VAULT_DIR, id);
20682068- LittleFS.remove(canPath);
20692069- LittleFS.rename(newPath, canPath);
20702070+ lfs::remove(canPath);
20712071+ lfs::rename(newPath, canPath);
2070207220712073 char newH[48], canH[32];
20722074 snprintf(newH, sizeof(newH), "%s/hmac_%02u.bin", NEW_DIR, id);
20732075 snprintf(canH, sizeof(canH), "%s/hmac_%02u.bin", VAULT_DIR, id);
20742074- if (LittleFS.exists(newH)) {
20752075- LittleFS.remove(canH);
20762076- LittleFS.rename(newH, canH);
20762076+ if (lfs::exists(newH)) {
20772077+ lfs::remove(canH);
20782078+ lfs::rename(newH, canH);
20772079 }
2078208020792081 // G4: rename keyVersion sidecar in lockstep.
20802082 char newKvP[48], canKvP[32];
20812083 snprintf(newKvP, sizeof(newKvP), "%s/cred_%02u.kv", NEW_DIR, id);
20822084 snprintf(canKvP, sizeof(canKvP), "%s/cred_%02u.kv", VAULT_DIR, id);
20832083- if (LittleFS.exists(newKvP)) {
20842084- LittleFS.remove(canKvP);
20852085- LittleFS.rename(newKvP, canKvP);
20852085+ if (lfs::exists(newKvP)) {
20862086+ lfs::remove(canKvP);
20872087+ lfs::rename(newKvP, canKvP);
20862088 } else {
20872089 // Defensive: ensure canonical kv reports the new version.
20882090 writeKeyVersion(canKvP, newKv);
···20932095 for (uint8_t id = 0; id < MAX_TOTP; ++id) {
20942096 char newPath[48];
20952097 snprintf(newPath, sizeof(newPath), "%s/totp_%02u.bin", NEW_DIR, id);
20962096- if (!LittleFS.exists(newPath)) continue;
20982098+ if (!lfs::exists(newPath)) continue;
20972099 char canPath[32];
20982100 snprintf(canPath, sizeof(canPath), "%s/totp_%02u.bin", VAULT_DIR, id);
20992099- LittleFS.remove(canPath);
21002100- LittleFS.rename(newPath, canPath);
21012101+ lfs::remove(canPath);
21022102+ lfs::rename(newPath, canPath);
2101210321022104 // G4: rename TOTP keyVersion sidecar.
21032105 char newKvP[48], canKvP[32];
21042106 snprintf(newKvP, sizeof(newKvP), "%s/totp_%02u.kv", NEW_DIR, id);
21052107 snprintf(canKvP, sizeof(canKvP), "%s/totp_%02u.kv", VAULT_DIR, id);
21062106- if (LittleFS.exists(newKvP)) {
21072107- LittleFS.remove(canKvP);
21082108- LittleFS.rename(newKvP, canKvP);
21082108+ if (lfs::exists(newKvP)) {
21092109+ lfs::remove(canKvP);
21102110+ lfs::rename(newKvP, canKvP);
21092111 } else {
21102112 writeKeyVersion(canKvP, newKv);
21112113 }
···21162118 // per-credential rename has succeeded — this preserves the marker
21172119 // that boot recovery (G4 / H2) uses to distinguish in-flight rekey
21182120 // crashes from a clean filesystem.
21192119- LittleFS.remove(BAK_META_FILE);
21202120- if (LittleFS.exists(META_KV_BAK)) LittleFS.remove(META_KV_BAK);
21212121- LittleFS.rmdir(NEW_DIR); // may fail if anything left behind; ignore.
21212121+ lfs::remove(BAK_META_FILE);
21222122+ if (lfs::exists(META_KV_BAK)) lfs::remove(META_KV_BAK);
21232123+ lfs::rmdir(NEW_DIR); // may fail if anything left behind; ignore.
21222124 purgeOldDir();
2123212521242126 VaultCrypto::secureWipe(newKey, sizeof(newKey));
···21642166 }
21652167 VaultCrypto::secureWipe(jsonBuf, sizeof(jsonBuf));
2166216821672167- File f = LittleFS.open(INDEX_FILE, "w");
21692169+ File f = lfs::open(INDEX_FILE, "w");
21682170 if (!f) return false;
21692171 f.write(iv, IV_SIZE);
21702172 f.write(cipherBuf, cipherLen);
···21802182 kleidos::vault::execute([&](){ _r = loadIndex(key, entries, maxEntries); return true; });
21812183 return _r;
21822184 }
21832183- File f = LittleFS.open(INDEX_FILE, "r");
21852185+ File f = lfs::open(INDEX_FILE, "r");
21842186 if (!f) {
21852187 KLEIDOS_LOGI(TAG, "No index cache — falling back to credential scan");
21862188 return list(key, entries, maxEntries);
···22602262 }
22612263 VaultCrypto::secureWipe(jsonBuf, sizeof(jsonBuf));
2262226422632263- File f = LittleFS.open(TOTP_INDEX_FILE, "w");
22652265+ File f = lfs::open(TOTP_INDEX_FILE, "w");
22642266 if (!f) return false;
22652267 f.write(iv, IV_SIZE);
22662268 f.write(cipherBuf, cipherLen);
···22762278 kleidos::vault::execute([&](){ _r = loadTotpIndex(key, entries, max); return true; });
22772279 return _r;
22782280 }
22792279- File f = LittleFS.open(TOTP_INDEX_FILE, "r");
22812281+ File f = lfs::open(TOTP_INDEX_FILE, "r");
22802282 if (!f) {
22812283 KLEIDOS_LOGI(TAG, "No TOTP index cache — falling back to TOTP scan");
22822284 return listTotp(key, entries, max);
+7-8
src/vault/VaultTask.h
···55// through a single FreeRTOS task pinned to PRO_CPU (Core 0). This guarantees
66// that:
77//
88-// 1. LittleFS is never accessed by two tasks concurrently. The Arduino
99-// LittleFS wrapper (esp_littlefs) is *not* fully reentrant once you
1010-// keep file descriptors open across calls, and even at API level a
1111-// concurrent format/erase from another task is a recipe for
1212-// corruption. With a single owner task this race is impossible.
88+// 1. LittleFS is never accessed by two tasks concurrently. The platform
99+// LittleFs facade is backed by esp_littlefs/VFS, and concurrent file
1010+// access plus format/erase from another task is a recipe for corruption.
1111+// With a single owner task this race is impossible.
1312//
1413// 2. Heavy mbedTLS work that runs alongside vault I/O (HMAC verification
1514// of every credential, AES decrypt during list/load) runs on Core 0,
1615// keeping Core 1 free for UI render and button input.
1716//
1817// 3. Pre-deep-sleep teardown is deterministic: stop accepting new work,
1919-// drain in-flight requests, then `LittleFS.end()`, then delete the
2020-// task. Without a single owner there is no safe ordering.
1818+// drain in-flight requests, then unmount LittleFS, then delete the task.
1919+// Without a single owner there is no safe ordering.
2120//
2221// API model
2322// ---------
···85848685/// Stop the worker. Drains the queue first (with a bounded timeout), then
8786/// signals the task to exit and joins it. Must be called before
8888-/// `LittleFS.end()` in the deep-sleep teardown sequence.
8787+/// LittleFS unmount in the deep-sleep teardown sequence.
8988void shutdown();
90899190/// True between `begin()` returning true and `shutdown()` returning. Used