Extend the record to its final shape, revive the dead panels, honor the index
Three firmware waves of the gap-report program, built in parallel
worktrees against the same tip and integrated with both-sides conflict
resolution (two stitch errors — an unopened Doxygen comment and a lost
closing brace — were caught by the compiler, as intended).
Wave 1 — the record's final shape. CredentialRecord gains flags (bit0 =
auto-submit: username, TAB, password, ENTER as one BLE action — and the
trailing ENTER is now conditional where it used to be unconditional),
three typed custom fields (label/value/hidden — hidden values ride
behind the same plaintext-consent header as passwords), a group id, and
TOTP entries gain an account label plus the algorithm byte everywhere.
Groups are a 100-slot encrypted name table (one littlefs block) managed
over GET/POST/DELETE /api/groups — deleting a group only clears labels,
never credentials. The index row carries group, a derived domain column
and real mtime, with names widened to 32 chars; brand is populated at
save time from the url, ending the per-row decrypt the device paid for
logos. URL capacity rises to 192 (the owner's real data maxed at 190),
and every save/import reports per-field truncation instead of silently
clamping. Backup, restore, the normalized importer and the CSV importer
round-trip all of it; the importer maps 1Password vaults and CSV
folder/tags/vault columns into groups server-side.
Wave 2 — six dead routes live: /api/ota/status (the whole Firmware
panel keyed on it), /api/vault/reindex, /api/time/diag (RTC coin-cell
health), the GPS trio (gated to boards that have one), NTP-sync-on-exit
through the AdminMode teardown choke point, and the staged-SD firmware
apply behind the confirm gate. Both full-replace restores now demand
the unlock secret (X-Unlock-Secret) — a backup passphrase proves
nothing about owning the live vault — and the restore body cap is
computed from the device's own backup bound instead of a 64 KB literal.
Wave 3 — the UI stops lying: the audit export honors Accept: text/csv
with real CSV, the web CSV importer honors the type column (Bitwarden
non-logins skipped and counted), LastPass and Proton Pass get real
header signatures, /api/runtime-stats is gone, AutoLockMode appears on
the six boards that hid a working handler, and the device vault lists
honor favorite-first plus manual order with an allocation-free
comparator.
Integration extras: the index envelope buffers and the rekey crypto
scratch now live on the heap (the rekey frame was already past the
32 KB worker stack at the current 200-slot caps — the latent overflow
the dimensioning study flagged); core2_v13's capacity-scaled view-cache
block moves to PSRAM .ext_ram.bss (keys and passwords stay in internal
DRAM), un-breaking its 48-byte link margin; the groups list
distinguishes a corrupt table from an empty one; custom:null is refused
rather than destructive; and the three amalgam suites learned the new
seams, including rebasing an unreachable handler-413 assertion onto the
gate-logic suite that actually pins the bound.
Builds: sticks3_debug, tdeck, cardputer, m5stickc_plus1_debug,
core2_v13_debug (now with SPIRAM bss). Native: full suite green with
nine new suites registered.
Claude-Session: https://claude.ai/code/session_01Q2J5gQSFMTDLVzPUYog51r
fix(test): configure clangd and clang-tidy for native test tree (#3)
* fix(test): configure clangd and clang-tidy for native test tree
- Generate compile_commands.json from pio run -e native -t compiledb
so clangd resolves src/ headers in test files without errors.
- Fix .clangd PathMatch regex (relative path, not absolute) and add
-I.pio/build/native/unity_config so unity_config.h resolves.
- Remove overly broad Remove: -I* that was stripping all added paths.
- Add -Wno-format-security and UnusedIncludes: None to test block.
- Add test/.clang-tidy that inherits the root config and:
- Allows test_* functions to use snake_case (FunctionIgnoredRegexp)
- Disables cert-err33-c, cert-dcl50-cpp,
cppcoreguidelines-pro-type-cstyle-cast,
bugprone-argument-comment, bugprone-misplaced-widening-cast,
bugprone-implicit-widening-of-multiplication-result,
misc-unused-using-decls — all false positives in test context.
- Add /* intentionally empty */ comment to all empty setUp()/tearDown()
bodies (28 files) to suppress SonarLint c:S1186.
- Fix test_vault_task: change relative include ../../src/vault/vault_task.h
to vault/vault_task.h (consistent with all other test files; -Isrc
already in compile flags).
- Reorganise test/ into a module-mirroring subtree layout:
test/ble/, test/crypto/, test/hal/, test/ota/, test/platform/,
test/states/, test/totp/, test/ui/, test/vault/, test/web/
(old flat directories removed).
- Add sonar-project.properties exclusion e4: suppress c:S1186 on
test/**/*.cpp at SonarCloud level.
All 468 native unit tests pass (pio test -e native).
* fix(clangd): add -DESP_PLATFORM to global CompileFlags
All Kleidos targets are ESP32/ESP32-S3 (framework = espidf), so
ESP_PLATFORM is always defined at compile time. Without this flag in
the clangd config, the language server was activating #else branches
inside #ifdef ESP_PLATFORM blocks, hiding device code and showing
false diagnostics for src/ files.
The test/** fragment already has Remove: -DESP_PLATFORM, so native
unit-test files continue to compile without the flag.
* revert(clangd): remove incorrect -DESP_PLATFORM from global CompileFlags
Adding -DESP_PLATFORM globally caused 'too many errors' in src/ files
because compile_commands.json was generated from the native env and
lacked all ESP-IDF/Xtensa include paths.
The correct fix is to generate compile_commands.json from a device env:
./scripts/generate_compiledb.sh sticks3
This populates the DB with proper -DESP_PLATFORM and all IDF includes
for every src/ translation unit. The symlink at the project root then
points clangd to the right compilation context.
Read the clock once, and make the capacity constants mean one thing
Three findings from investigating whether I2C needs a mutex. The answer to that
question turned out to be no — and the investigation turned up two defects that
had nothing to do with I2C.
BruteForceGuard computed "now" as rtcToEpochSec( getDate(), getTime() ) — two
separate samples of a moving clock. C++ leaves argument evaluation order
unspecified, and one of the two orders places the result a full day in the
FUTURE when the pair straddles midnight, which makes nowEpoch >= unlockEpoch
true and clears the lockout early. A security control failing open, in a
roughly one-second window per day. rtc::getDateTime() already exists and is
documented as filling both halves from a single sample; the guard simply was
not using it. Its 39 tests still pass.
VaultMetaRecord had two sizes in one binary. vault_meta.h branches on
VAULT_MAX_CREDENTIALS without pulling in the variant header that defines it, so
14 translation units — including vault_meta.cpp, the record's own serializer —
compiled with 50 where the rest saw 150, and that constant sizes credGen. I
measured it rather than reasoned about it: a temporary #warning on the absent
branch plus a clean rebuild reported 14, and zero after the fix. Both capacity
headers now include what they depend on.
That changes the on-disk meta layout (the serializer moves from 50 generation
counters to 150), so existing vaults need re-provisioning. The standing
pre-release policy already covers this: full reflash, no migration, no version
bump.
I2C itself needs no lock. ESP-IDF 6.0.2 holds its per-bus mutex across a whole
transaction, and every Kleidos register read is a single combined
write-then-repeated-START-read, so the hazard that motivated the question — a
second task moving the device's address pointer mid-read — cannot occur. What
that leaves is documentation: two headers described a bus mutex that does not
exist and contradicted each other about whose job serialization was. All three
threading notes now say the same verified thing, and name the real caller
obligation: a sequence of transactions, not a single one.
Also: the route table held 61 of 80 with about 20 more planned, and addRoute()
dropped the overflow with one log line — a dropped route 404s forever and
nothing else complains. Capacity 112, the count logged at every bring-up, and a
debug build restarts rather than starting up quietly wrong.
2564/2564 native, cppcheck clean, four variants build.
Claude-Session: https://claude.ai/code/session_01Q2J5gQSFMTDLVzPUYog51r
Widen the slot id to 16 bits so capacity can pass 255
Credential capacity is pinned per variant at up to 700 by the vault
dimensioning study, but the slot id was a u8 across the app, web and
persistence layers: every id above 255 would have aliased onto a live
slot. Widen the id and the counts derived from it to uint16_t through
vault, web, states and ui; TOTP and group ids deliberately stay u8
(their caps stay under 255) and are now pinned by co-located
static_asserts instead of by assumption.
The wire follows: index rows and the row count, and the slot id in the
envelope's authenticated context, become u16 little-endian, growing the
context prefix from 7 to 8 bytes. No version byte moves — the format is
pre-release and every board is reflashed — but the security docs that
quote the prefix byte-for-byte are updated so the audit dossier stays
verifiable against the source.
Adversarial review of the diff found four defects worth naming, all
fixed here:
- promoteAll() held two capacity-sized filename vectors live on the
32 KB vault-worker stack; at the 700-slot pin that is ~78 KB and a
guaranteed overflow on the first SD import or re-key. Both move to
the heap, matching the pattern the rest of the vault already uses.
- The widened staged-filename grammar accepted cred_007.bin as well as
cred_07.bin for the same slot, so an import deleted the canonical
record and wrote a path the repository never opens — silent slot
loss where the previous grammar had failed closed. The grammar is now
canonical-only and lives in one header both readers share.
- DevicePanel kept the last-viewed slot in editCredId_ after BACK, so
the standalone password generator saved into it, blanking that
credential's name, user and url. The id now resets to the invalid-slot
sentinel, and the guard that also made slot 0 unwritable is gone.
- The envelope test asserted the id's new high byte against its own
zero-initialized fixture, so it would have passed against a writer
that never wrote that byte at all.
Verified: native 2843/2843 (including a new wide-capacity suite that
exercises slot 260 end to end), sticks3_debug, cardputer_debug, tdeck
and core2_v13_debug, plus the merge gate — a clean -Werror build at
VAULT_MAX_CREDENTIALS=300, which is what proves no narrowing survives.
Claude-Session: https://claude.ai/code/session_01Q2J5gQSFMTDLVzPUYog51r
Widen the slot id to 16 bits so capacity can pass 255
Credential capacity is pinned per variant at up to 700 by the vault
dimensioning study, but the slot id was a u8 across the app, web and
persistence layers: every id above 255 would have aliased onto a live
slot. Widen the id and the counts derived from it to uint16_t through
vault, web, states and ui; TOTP and group ids deliberately stay u8
(their caps stay under 255) and are now pinned by co-located
static_asserts instead of by assumption.
The wire follows: index rows and the row count, and the slot id in the
envelope's authenticated context, become u16 little-endian, growing the
context prefix from 7 to 8 bytes. No version byte moves — the format is
pre-release and every board is reflashed — but the security docs that
quote the prefix byte-for-byte are updated so the audit dossier stays
verifiable against the source.
Adversarial review of the diff found four defects worth naming, all
fixed here:
- promoteAll() held two capacity-sized filename vectors live on the
32 KB vault-worker stack; at the 700-slot pin that is ~78 KB and a
guaranteed overflow on the first SD import or re-key. Both move to
the heap, matching the pattern the rest of the vault already uses.
- The widened staged-filename grammar accepted cred_007.bin as well as
cred_07.bin for the same slot, so an import deleted the canonical
record and wrote a path the repository never opens — silent slot
loss where the previous grammar had failed closed. The grammar is now
canonical-only and lives in one header both readers share.
- DevicePanel kept the last-viewed slot in editCredId_ after BACK, so
the standalone password generator saved into it, blanking that
credential's name, user and url. The id now resets to the invalid-slot
sentinel, and the guard that also made slot 0 unwritable is gone.
- The envelope test asserted the id's new high byte against its own
zero-initialized fixture, so it would have passed against a writer
that never wrote that byte at all.
Verified: native 2843/2843 (including a new wide-capacity suite that
exercises slot 260 end to end), sticks3_debug, cardputer_debug, tdeck
and core2_v13_debug, plus the merge gate — a clean -Werror build at
VAULT_MAX_CREDENTIALS=300, which is what proves no narrowing survives.
Claude-Session: https://claude.ai/code/session_01Q2J5gQSFMTDLVzPUYog51r
Extend the record to its final shape, revive the dead panels, honor the index
Three firmware waves of the gap-report program, built in parallel
worktrees against the same tip and integrated with both-sides conflict
resolution (two stitch errors — an unopened Doxygen comment and a lost
closing brace — were caught by the compiler, as intended).
Wave 1 — the record's final shape. CredentialRecord gains flags (bit0 =
auto-submit: username, TAB, password, ENTER as one BLE action — and the
trailing ENTER is now conditional where it used to be unconditional),
three typed custom fields (label/value/hidden — hidden values ride
behind the same plaintext-consent header as passwords), a group id, and
TOTP entries gain an account label plus the algorithm byte everywhere.
Groups are a 100-slot encrypted name table (one littlefs block) managed
over GET/POST/DELETE /api/groups — deleting a group only clears labels,
never credentials. The index row carries group, a derived domain column
and real mtime, with names widened to 32 chars; brand is populated at
save time from the url, ending the per-row decrypt the device paid for
logos. URL capacity rises to 192 (the owner's real data maxed at 190),
and every save/import reports per-field truncation instead of silently
clamping. Backup, restore, the normalized importer and the CSV importer
round-trip all of it; the importer maps 1Password vaults and CSV
folder/tags/vault columns into groups server-side.
Wave 2 — six dead routes live: /api/ota/status (the whole Firmware
panel keyed on it), /api/vault/reindex, /api/time/diag (RTC coin-cell
health), the GPS trio (gated to boards that have one), NTP-sync-on-exit
through the AdminMode teardown choke point, and the staged-SD firmware
apply behind the confirm gate. Both full-replace restores now demand
the unlock secret (X-Unlock-Secret) — a backup passphrase proves
nothing about owning the live vault — and the restore body cap is
computed from the device's own backup bound instead of a 64 KB literal.
Wave 3 — the UI stops lying: the audit export honors Accept: text/csv
with real CSV, the web CSV importer honors the type column (Bitwarden
non-logins skipped and counted), LastPass and Proton Pass get real
header signatures, /api/runtime-stats is gone, AutoLockMode appears on
the six boards that hid a working handler, and the device vault lists
honor favorite-first plus manual order with an allocation-free
comparator.
Integration extras: the index envelope buffers and the rekey crypto
scratch now live on the heap (the rekey frame was already past the
32 KB worker stack at the current 200-slot caps — the latent overflow
the dimensioning study flagged); core2_v13's capacity-scaled view-cache
block moves to PSRAM .ext_ram.bss (keys and passwords stay in internal
DRAM), un-breaking its 48-byte link margin; the groups list
distinguishes a corrupt table from an empty one; custom:null is refused
rather than destructive; and the three amalgam suites learned the new
seams, including rebasing an unreachable handler-413 assertion onto the
gate-logic suite that actually pins the bound.
Builds: sticks3_debug, tdeck, cardputer, m5stickc_plus1_debug,
core2_v13_debug (now with SPIRAM bss). Native: full suite green with
nine new suites registered.
Claude-Session: https://claude.ai/code/session_01Q2J5gQSFMTDLVzPUYog51r
Raise the per-variant capacities, and stop shipping unusable vault UI
Every board now pins the capacity its flash and RAM actually support:
tdeck and cores3_se 700, core2_v13 500, sticks3 and cardputer 250,
m5stickc_plus2 180. The two 4 MB boards stay at 100 — their rekey-safe
ceiling is 139, and staging a second vault for a change-PIN on a fuller
one would run the filesystem out of space.
m5stickc_plus2 could not honestly hold 180: it had 3,132 B of internal
DRAM left against the 4,096 B bar. The slack came from a real absurdity.
Every button board compiled BOTH vault-menu layouts and picked one at
runtime from whether a third button existed, though a board's buttons are
fixed at manufacture. Each unused layout carries a row array sized by
capacity, so plus2 was paying 5 KB for a screen it can never draw. Boards
now declare which layout they use and only that one is compiled: plus2
goes to 9,196 B, sticks3 to 143,141, cores3_se to 78,985. core2_v13
recovers only the view object because its row arrays already live in
PSRAM, which is the placement working as intended.
The declaration is deliberate rather than inferred. Button count looked
like the discriminator only because today's fleet correlates: cardputer
and tdeck are wide-screen boards with no third button, and the old rule
would have handed them the narrow layout if their keyboards ever went
away. What actually decides the layout is width — a sliding pill
indicator is what fits 135 px, while a top tab bar plus an action bar
need room, and a past CoreInk header garble came from that view
hardcoding 320x240. Input decides only how you move between tabs. So the
variant states its choice, with no default, and a build-time tie rejects
a board declaring a layout its hardware cannot drive.
The names were lying too. Both layouts draw tabs, so "Tabbed"
distinguished nothing: they are now NarrowVaultView and WideVaultView,
after the constraint that actually separates them. KeyboardVaultView
keeps its name on purpose — cardputer is 240x135 and tdeck is 320x240,
one narrow and one wide, and both use it, so there the discriminator
really is the input device.
Review of the change caught a T-Deck pin that had never been raised while
five other artifacts already advertised 700, a NAV TOTP macro that
regressed on the wide boards so the screenshot suite filed a vault-list
capture as the TOTP screen, a selector whose "no silent default" promise
had a hole (an unknown token preprocesses to zero, which was the one
value exempted, so a typo compiled no view at all), and a variant guard
whose regex rejected correct declarations that carried a trailing
comment.
Verified: native 2861/2861, all three repo guards, and all eight boards
build with their declared layout confirmed present and its siblings
absent in each map.
Claude-Session: https://claude.ai/code/session_01Q2J5gQSFMTDLVzPUYog51r
Raise the per-variant capacities, and stop shipping unusable vault UI
Every board now pins the capacity its flash and RAM actually support:
tdeck and cores3_se 700, core2_v13 500, sticks3 and cardputer 250,
m5stickc_plus2 180. The two 4 MB boards stay at 100 — their rekey-safe
ceiling is 139, and staging a second vault for a change-PIN on a fuller
one would run the filesystem out of space.
m5stickc_plus2 could not honestly hold 180: it had 3,132 B of internal
DRAM left against the 4,096 B bar. The slack came from a real absurdity.
Every button board compiled BOTH vault-menu layouts and picked one at
runtime from whether a third button existed, though a board's buttons are
fixed at manufacture. Each unused layout carries a row array sized by
capacity, so plus2 was paying 5 KB for a screen it can never draw. Boards
now declare which layout they use and only that one is compiled: plus2
goes to 9,196 B, sticks3 to 143,141, cores3_se to 78,985. core2_v13
recovers only the view object because its row arrays already live in
PSRAM, which is the placement working as intended.
The declaration is deliberate rather than inferred. Button count looked
like the discriminator only because today's fleet correlates: cardputer
and tdeck are wide-screen boards with no third button, and the old rule
would have handed them the narrow layout if their keyboards ever went
away. What actually decides the layout is width — a sliding pill
indicator is what fits 135 px, while a top tab bar plus an action bar
need room, and a past CoreInk header garble came from that view
hardcoding 320x240. Input decides only how you move between tabs. So the
variant states its choice, with no default, and a build-time tie rejects
a board declaring a layout its hardware cannot drive.
The names were lying too. Both layouts draw tabs, so "Tabbed"
distinguished nothing: they are now NarrowVaultView and WideVaultView,
after the constraint that actually separates them. KeyboardVaultView
keeps its name on purpose — cardputer is 240x135 and tdeck is 320x240,
one narrow and one wide, and both use it, so there the discriminator
really is the input device.
Review of the change caught a T-Deck pin that had never been raised while
five other artifacts already advertised 700, a NAV TOTP macro that
regressed on the wide boards so the screenshot suite filed a vault-list
capture as the TOTP screen, a selector whose "no silent default" promise
had a hole (an unknown token preprocesses to zero, which was the one
value exempted, so a typo compiled no view at all), and a variant guard
whose regex rejected correct declarations that carried a trailing
comment.
Verified: native 2861/2861, all three repo guards, and all eight boards
build with their declared layout confirmed present and its siblings
absent in each map.
Claude-Session: https://claude.ai/code/session_01Q2J5gQSFMTDLVzPUYog51r
Extend the record to its final shape, revive the dead panels, honor the index
Three firmware waves of the gap-report program, built in parallel
worktrees against the same tip and integrated with both-sides conflict
resolution (two stitch errors — an unopened Doxygen comment and a lost
closing brace — were caught by the compiler, as intended).
Wave 1 — the record's final shape. CredentialRecord gains flags (bit0 =
auto-submit: username, TAB, password, ENTER as one BLE action — and the
trailing ENTER is now conditional where it used to be unconditional),
three typed custom fields (label/value/hidden — hidden values ride
behind the same plaintext-consent header as passwords), a group id, and
TOTP entries gain an account label plus the algorithm byte everywhere.
Groups are a 100-slot encrypted name table (one littlefs block) managed
over GET/POST/DELETE /api/groups — deleting a group only clears labels,
never credentials. The index row carries group, a derived domain column
and real mtime, with names widened to 32 chars; brand is populated at
save time from the url, ending the per-row decrypt the device paid for
logos. URL capacity rises to 192 (the owner's real data maxed at 190),
and every save/import reports per-field truncation instead of silently
clamping. Backup, restore, the normalized importer and the CSV importer
round-trip all of it; the importer maps 1Password vaults and CSV
folder/tags/vault columns into groups server-side.
Wave 2 — six dead routes live: /api/ota/status (the whole Firmware
panel keyed on it), /api/vault/reindex, /api/time/diag (RTC coin-cell
health), the GPS trio (gated to boards that have one), NTP-sync-on-exit
through the AdminMode teardown choke point, and the staged-SD firmware
apply behind the confirm gate. Both full-replace restores now demand
the unlock secret (X-Unlock-Secret) — a backup passphrase proves
nothing about owning the live vault — and the restore body cap is
computed from the device's own backup bound instead of a 64 KB literal.
Wave 3 — the UI stops lying: the audit export honors Accept: text/csv
with real CSV, the web CSV importer honors the type column (Bitwarden
non-logins skipped and counted), LastPass and Proton Pass get real
header signatures, /api/runtime-stats is gone, AutoLockMode appears on
the six boards that hid a working handler, and the device vault lists
honor favorite-first plus manual order with an allocation-free
comparator.
Integration extras: the index envelope buffers and the rekey crypto
scratch now live on the heap (the rekey frame was already past the
32 KB worker stack at the current 200-slot caps — the latent overflow
the dimensioning study flagged); core2_v13's capacity-scaled view-cache
block moves to PSRAM .ext_ram.bss (keys and passwords stay in internal
DRAM), un-breaking its 48-byte link margin; the groups list
distinguishes a corrupt table from an empty one; custom:null is refused
rather than destructive; and the three amalgam suites learned the new
seams, including rebasing an unreachable handler-413 assertion onto the
gate-logic suite that actually pins the bound.
Builds: sticks3_debug, tdeck, cardputer, m5stickc_plus1_debug,
core2_v13_debug (now with SPIRAM bss). Native: full suite green with
nine new suites registered.
Claude-Session: https://claude.ai/code/session_01Q2J5gQSFMTDLVzPUYog51r
Widen the slot id to 16 bits so capacity can pass 255
Credential capacity is pinned per variant at up to 700 by the vault
dimensioning study, but the slot id was a u8 across the app, web and
persistence layers: every id above 255 would have aliased onto a live
slot. Widen the id and the counts derived from it to uint16_t through
vault, web, states and ui; TOTP and group ids deliberately stay u8
(their caps stay under 255) and are now pinned by co-located
static_asserts instead of by assumption.
The wire follows: index rows and the row count, and the slot id in the
envelope's authenticated context, become u16 little-endian, growing the
context prefix from 7 to 8 bytes. No version byte moves — the format is
pre-release and every board is reflashed — but the security docs that
quote the prefix byte-for-byte are updated so the audit dossier stays
verifiable against the source.
Adversarial review of the diff found four defects worth naming, all
fixed here:
- promoteAll() held two capacity-sized filename vectors live on the
32 KB vault-worker stack; at the 700-slot pin that is ~78 KB and a
guaranteed overflow on the first SD import or re-key. Both move to
the heap, matching the pattern the rest of the vault already uses.
- The widened staged-filename grammar accepted cred_007.bin as well as
cred_07.bin for the same slot, so an import deleted the canonical
record and wrote a path the repository never opens — silent slot
loss where the previous grammar had failed closed. The grammar is now
canonical-only and lives in one header both readers share.
- DevicePanel kept the last-viewed slot in editCredId_ after BACK, so
the standalone password generator saved into it, blanking that
credential's name, user and url. The id now resets to the invalid-slot
sentinel, and the guard that also made slot 0 unwritable is gone.
- The envelope test asserted the id's new high byte against its own
zero-initialized fixture, so it would have passed against a writer
that never wrote that byte at all.
Verified: native 2843/2843 (including a new wide-capacity suite that
exercises slot 260 end to end), sticks3_debug, cardputer_debug, tdeck
and core2_v13_debug, plus the merge gate — a clean -Werror build at
VAULT_MAX_CREDENTIALS=300, which is what proves no narrowing survives.
Claude-Session: https://claude.ai/code/session_01Q2J5gQSFMTDLVzPUYog51r
Extend the record to its final shape, revive the dead panels, honor the index
Three firmware waves of the gap-report program, built in parallel
worktrees against the same tip and integrated with both-sides conflict
resolution (two stitch errors — an unopened Doxygen comment and a lost
closing brace — were caught by the compiler, as intended).
Wave 1 — the record's final shape. CredentialRecord gains flags (bit0 =
auto-submit: username, TAB, password, ENTER as one BLE action — and the
trailing ENTER is now conditional where it used to be unconditional),
three typed custom fields (label/value/hidden — hidden values ride
behind the same plaintext-consent header as passwords), a group id, and
TOTP entries gain an account label plus the algorithm byte everywhere.
Groups are a 100-slot encrypted name table (one littlefs block) managed
over GET/POST/DELETE /api/groups — deleting a group only clears labels,
never credentials. The index row carries group, a derived domain column
and real mtime, with names widened to 32 chars; brand is populated at
save time from the url, ending the per-row decrypt the device paid for
logos. URL capacity rises to 192 (the owner's real data maxed at 190),
and every save/import reports per-field truncation instead of silently
clamping. Backup, restore, the normalized importer and the CSV importer
round-trip all of it; the importer maps 1Password vaults and CSV
folder/tags/vault columns into groups server-side.
Wave 2 — six dead routes live: /api/ota/status (the whole Firmware
panel keyed on it), /api/vault/reindex, /api/time/diag (RTC coin-cell
health), the GPS trio (gated to boards that have one), NTP-sync-on-exit
through the AdminMode teardown choke point, and the staged-SD firmware
apply behind the confirm gate. Both full-replace restores now demand
the unlock secret (X-Unlock-Secret) — a backup passphrase proves
nothing about owning the live vault — and the restore body cap is
computed from the device's own backup bound instead of a 64 KB literal.
Wave 3 — the UI stops lying: the audit export honors Accept: text/csv
with real CSV, the web CSV importer honors the type column (Bitwarden
non-logins skipped and counted), LastPass and Proton Pass get real
header signatures, /api/runtime-stats is gone, AutoLockMode appears on
the six boards that hid a working handler, and the device vault lists
honor favorite-first plus manual order with an allocation-free
comparator.
Integration extras: the index envelope buffers and the rekey crypto
scratch now live on the heap (the rekey frame was already past the
32 KB worker stack at the current 200-slot caps — the latent overflow
the dimensioning study flagged); core2_v13's capacity-scaled view-cache
block moves to PSRAM .ext_ram.bss (keys and passwords stay in internal
DRAM), un-breaking its 48-byte link margin; the groups list
distinguishes a corrupt table from an empty one; custom:null is refused
rather than destructive; and the three amalgam suites learned the new
seams, including rebasing an unreachable handler-413 assertion onto the
gate-logic suite that actually pins the bound.
Builds: sticks3_debug, tdeck, cardputer, m5stickc_plus1_debug,
core2_v13_debug (now with SPIRAM bss). Native: full suite green with
nine new suites registered.
Claude-Session: https://claude.ai/code/session_01Q2J5gQSFMTDLVzPUYog51r
Extend the record to its final shape, revive the dead panels, honor the index
Three firmware waves of the gap-report program, built in parallel
worktrees against the same tip and integrated with both-sides conflict
resolution (two stitch errors — an unopened Doxygen comment and a lost
closing brace — were caught by the compiler, as intended).
Wave 1 — the record's final shape. CredentialRecord gains flags (bit0 =
auto-submit: username, TAB, password, ENTER as one BLE action — and the
trailing ENTER is now conditional where it used to be unconditional),
three typed custom fields (label/value/hidden — hidden values ride
behind the same plaintext-consent header as passwords), a group id, and
TOTP entries gain an account label plus the algorithm byte everywhere.
Groups are a 100-slot encrypted name table (one littlefs block) managed
over GET/POST/DELETE /api/groups — deleting a group only clears labels,
never credentials. The index row carries group, a derived domain column
and real mtime, with names widened to 32 chars; brand is populated at
save time from the url, ending the per-row decrypt the device paid for
logos. URL capacity rises to 192 (the owner's real data maxed at 190),
and every save/import reports per-field truncation instead of silently
clamping. Backup, restore, the normalized importer and the CSV importer
round-trip all of it; the importer maps 1Password vaults and CSV
folder/tags/vault columns into groups server-side.
Wave 2 — six dead routes live: /api/ota/status (the whole Firmware
panel keyed on it), /api/vault/reindex, /api/time/diag (RTC coin-cell
health), the GPS trio (gated to boards that have one), NTP-sync-on-exit
through the AdminMode teardown choke point, and the staged-SD firmware
apply behind the confirm gate. Both full-replace restores now demand
the unlock secret (X-Unlock-Secret) — a backup passphrase proves
nothing about owning the live vault — and the restore body cap is
computed from the device's own backup bound instead of a 64 KB literal.
Wave 3 — the UI stops lying: the audit export honors Accept: text/csv
with real CSV, the web CSV importer honors the type column (Bitwarden
non-logins skipped and counted), LastPass and Proton Pass get real
header signatures, /api/runtime-stats is gone, AutoLockMode appears on
the six boards that hid a working handler, and the device vault lists
honor favorite-first plus manual order with an allocation-free
comparator.
Integration extras: the index envelope buffers and the rekey crypto
scratch now live on the heap (the rekey frame was already past the
32 KB worker stack at the current 200-slot caps — the latent overflow
the dimensioning study flagged); core2_v13's capacity-scaled view-cache
block moves to PSRAM .ext_ram.bss (keys and passwords stay in internal
DRAM), un-breaking its 48-byte link margin; the groups list
distinguishes a corrupt table from an empty one; custom:null is refused
rather than destructive; and the three amalgam suites learned the new
seams, including rebasing an unreachable handler-413 assertion onto the
gate-logic suite that actually pins the bound.
Builds: sticks3_debug, tdeck, cardputer, m5stickc_plus1_debug,
core2_v13_debug (now with SPIRAM bss). Native: full suite green with
nine new suites registered.
Claude-Session: https://claude.ai/code/session_01Q2J5gQSFMTDLVzPUYog51r
Extend the record to its final shape, revive the dead panels, honor the index
Three firmware waves of the gap-report program, built in parallel
worktrees against the same tip and integrated with both-sides conflict
resolution (two stitch errors — an unopened Doxygen comment and a lost
closing brace — were caught by the compiler, as intended).
Wave 1 — the record's final shape. CredentialRecord gains flags (bit0 =
auto-submit: username, TAB, password, ENTER as one BLE action — and the
trailing ENTER is now conditional where it used to be unconditional),
three typed custom fields (label/value/hidden — hidden values ride
behind the same plaintext-consent header as passwords), a group id, and
TOTP entries gain an account label plus the algorithm byte everywhere.
Groups are a 100-slot encrypted name table (one littlefs block) managed
over GET/POST/DELETE /api/groups — deleting a group only clears labels,
never credentials. The index row carries group, a derived domain column
and real mtime, with names widened to 32 chars; brand is populated at
save time from the url, ending the per-row decrypt the device paid for
logos. URL capacity rises to 192 (the owner's real data maxed at 190),
and every save/import reports per-field truncation instead of silently
clamping. Backup, restore, the normalized importer and the CSV importer
round-trip all of it; the importer maps 1Password vaults and CSV
folder/tags/vault columns into groups server-side.
Wave 2 — six dead routes live: /api/ota/status (the whole Firmware
panel keyed on it), /api/vault/reindex, /api/time/diag (RTC coin-cell
health), the GPS trio (gated to boards that have one), NTP-sync-on-exit
through the AdminMode teardown choke point, and the staged-SD firmware
apply behind the confirm gate. Both full-replace restores now demand
the unlock secret (X-Unlock-Secret) — a backup passphrase proves
nothing about owning the live vault — and the restore body cap is
computed from the device's own backup bound instead of a 64 KB literal.
Wave 3 — the UI stops lying: the audit export honors Accept: text/csv
with real CSV, the web CSV importer honors the type column (Bitwarden
non-logins skipped and counted), LastPass and Proton Pass get real
header signatures, /api/runtime-stats is gone, AutoLockMode appears on
the six boards that hid a working handler, and the device vault lists
honor favorite-first plus manual order with an allocation-free
comparator.
Integration extras: the index envelope buffers and the rekey crypto
scratch now live on the heap (the rekey frame was already past the
32 KB worker stack at the current 200-slot caps — the latent overflow
the dimensioning study flagged); core2_v13's capacity-scaled view-cache
block moves to PSRAM .ext_ram.bss (keys and passwords stay in internal
DRAM), un-breaking its 48-byte link margin; the groups list
distinguishes a corrupt table from an empty one; custom:null is refused
rather than destructive; and the three amalgam suites learned the new
seams, including rebasing an unreachable handler-413 assertion onto the
gate-logic suite that actually pins the bound.
Builds: sticks3_debug, tdeck, cardputer, m5stickc_plus1_debug,
core2_v13_debug (now with SPIRAM bss). Native: full suite green with
nine new suites registered.
Claude-Session: https://claude.ai/code/session_01Q2J5gQSFMTDLVzPUYog51r
Widen the slot id to 16 bits so capacity can pass 255
Credential capacity is pinned per variant at up to 700 by the vault
dimensioning study, but the slot id was a u8 across the app, web and
persistence layers: every id above 255 would have aliased onto a live
slot. Widen the id and the counts derived from it to uint16_t through
vault, web, states and ui; TOTP and group ids deliberately stay u8
(their caps stay under 255) and are now pinned by co-located
static_asserts instead of by assumption.
The wire follows: index rows and the row count, and the slot id in the
envelope's authenticated context, become u16 little-endian, growing the
context prefix from 7 to 8 bytes. No version byte moves — the format is
pre-release and every board is reflashed — but the security docs that
quote the prefix byte-for-byte are updated so the audit dossier stays
verifiable against the source.
Adversarial review of the diff found four defects worth naming, all
fixed here:
- promoteAll() held two capacity-sized filename vectors live on the
32 KB vault-worker stack; at the 700-slot pin that is ~78 KB and a
guaranteed overflow on the first SD import or re-key. Both move to
the heap, matching the pattern the rest of the vault already uses.
- The widened staged-filename grammar accepted cred_007.bin as well as
cred_07.bin for the same slot, so an import deleted the canonical
record and wrote a path the repository never opens — silent slot
loss where the previous grammar had failed closed. The grammar is now
canonical-only and lives in one header both readers share.
- DevicePanel kept the last-viewed slot in editCredId_ after BACK, so
the standalone password generator saved into it, blanking that
credential's name, user and url. The id now resets to the invalid-slot
sentinel, and the guard that also made slot 0 unwritable is gone.
- The envelope test asserted the id's new high byte against its own
zero-initialized fixture, so it would have passed against a writer
that never wrote that byte at all.
Verified: native 2843/2843 (including a new wide-capacity suite that
exercises slot 260 end to end), sticks3_debug, cardputer_debug, tdeck
and core2_v13_debug, plus the merge gate — a clean -Werror build at
VAULT_MAX_CREDENTIALS=300, which is what proves no narrowing survives.
Claude-Session: https://claude.ai/code/session_01Q2J5gQSFMTDLVzPUYog51r
Widen the slot id to 16 bits so capacity can pass 255
Credential capacity is pinned per variant at up to 700 by the vault
dimensioning study, but the slot id was a u8 across the app, web and
persistence layers: every id above 255 would have aliased onto a live
slot. Widen the id and the counts derived from it to uint16_t through
vault, web, states and ui; TOTP and group ids deliberately stay u8
(their caps stay under 255) and are now pinned by co-located
static_asserts instead of by assumption.
The wire follows: index rows and the row count, and the slot id in the
envelope's authenticated context, become u16 little-endian, growing the
context prefix from 7 to 8 bytes. No version byte moves — the format is
pre-release and every board is reflashed — but the security docs that
quote the prefix byte-for-byte are updated so the audit dossier stays
verifiable against the source.
Adversarial review of the diff found four defects worth naming, all
fixed here:
- promoteAll() held two capacity-sized filename vectors live on the
32 KB vault-worker stack; at the 700-slot pin that is ~78 KB and a
guaranteed overflow on the first SD import or re-key. Both move to
the heap, matching the pattern the rest of the vault already uses.
- The widened staged-filename grammar accepted cred_007.bin as well as
cred_07.bin for the same slot, so an import deleted the canonical
record and wrote a path the repository never opens — silent slot
loss where the previous grammar had failed closed. The grammar is now
canonical-only and lives in one header both readers share.
- DevicePanel kept the last-viewed slot in editCredId_ after BACK, so
the standalone password generator saved into it, blanking that
credential's name, user and url. The id now resets to the invalid-slot
sentinel, and the guard that also made slot 0 unwritable is gone.
- The envelope test asserted the id's new high byte against its own
zero-initialized fixture, so it would have passed against a writer
that never wrote that byte at all.
Verified: native 2843/2843 (including a new wide-capacity suite that
exercises slot 260 end to end), sticks3_debug, cardputer_debug, tdeck
and core2_v13_debug, plus the merge gate — a clean -Werror build at
VAULT_MAX_CREDENTIALS=300, which is what proves no narrowing survives.
Claude-Session: https://claude.ai/code/session_01Q2J5gQSFMTDLVzPUYog51r
Extend the record to its final shape, revive the dead panels, honor the index
Three firmware waves of the gap-report program, built in parallel
worktrees against the same tip and integrated with both-sides conflict
resolution (two stitch errors — an unopened Doxygen comment and a lost
closing brace — were caught by the compiler, as intended).
Wave 1 — the record's final shape. CredentialRecord gains flags (bit0 =
auto-submit: username, TAB, password, ENTER as one BLE action — and the
trailing ENTER is now conditional where it used to be unconditional),
three typed custom fields (label/value/hidden — hidden values ride
behind the same plaintext-consent header as passwords), a group id, and
TOTP entries gain an account label plus the algorithm byte everywhere.
Groups are a 100-slot encrypted name table (one littlefs block) managed
over GET/POST/DELETE /api/groups — deleting a group only clears labels,
never credentials. The index row carries group, a derived domain column
and real mtime, with names widened to 32 chars; brand is populated at
save time from the url, ending the per-row decrypt the device paid for
logos. URL capacity rises to 192 (the owner's real data maxed at 190),
and every save/import reports per-field truncation instead of silently
clamping. Backup, restore, the normalized importer and the CSV importer
round-trip all of it; the importer maps 1Password vaults and CSV
folder/tags/vault columns into groups server-side.
Wave 2 — six dead routes live: /api/ota/status (the whole Firmware
panel keyed on it), /api/vault/reindex, /api/time/diag (RTC coin-cell
health), the GPS trio (gated to boards that have one), NTP-sync-on-exit
through the AdminMode teardown choke point, and the staged-SD firmware
apply behind the confirm gate. Both full-replace restores now demand
the unlock secret (X-Unlock-Secret) — a backup passphrase proves
nothing about owning the live vault — and the restore body cap is
computed from the device's own backup bound instead of a 64 KB literal.
Wave 3 — the UI stops lying: the audit export honors Accept: text/csv
with real CSV, the web CSV importer honors the type column (Bitwarden
non-logins skipped and counted), LastPass and Proton Pass get real
header signatures, /api/runtime-stats is gone, AutoLockMode appears on
the six boards that hid a working handler, and the device vault lists
honor favorite-first plus manual order with an allocation-free
comparator.
Integration extras: the index envelope buffers and the rekey crypto
scratch now live on the heap (the rekey frame was already past the
32 KB worker stack at the current 200-slot caps — the latent overflow
the dimensioning study flagged); core2_v13's capacity-scaled view-cache
block moves to PSRAM .ext_ram.bss (keys and passwords stay in internal
DRAM), un-breaking its 48-byte link margin; the groups list
distinguishes a corrupt table from an empty one; custom:null is refused
rather than destructive; and the three amalgam suites learned the new
seams, including rebasing an unreachable handler-413 assertion onto the
gate-logic suite that actually pins the bound.
Builds: sticks3_debug, tdeck, cardputer, m5stickc_plus1_debug,
core2_v13_debug (now with SPIRAM bss). Native: full suite green with
nine new suites registered.
Claude-Session: https://claude.ai/code/session_01Q2J5gQSFMTDLVzPUYog51r
Extend the record to its final shape, revive the dead panels, honor the index
Three firmware waves of the gap-report program, built in parallel
worktrees against the same tip and integrated with both-sides conflict
resolution (two stitch errors — an unopened Doxygen comment and a lost
closing brace — were caught by the compiler, as intended).
Wave 1 — the record's final shape. CredentialRecord gains flags (bit0 =
auto-submit: username, TAB, password, ENTER as one BLE action — and the
trailing ENTER is now conditional where it used to be unconditional),
three typed custom fields (label/value/hidden — hidden values ride
behind the same plaintext-consent header as passwords), a group id, and
TOTP entries gain an account label plus the algorithm byte everywhere.
Groups are a 100-slot encrypted name table (one littlefs block) managed
over GET/POST/DELETE /api/groups — deleting a group only clears labels,
never credentials. The index row carries group, a derived domain column
and real mtime, with names widened to 32 chars; brand is populated at
save time from the url, ending the per-row decrypt the device paid for
logos. URL capacity rises to 192 (the owner's real data maxed at 190),
and every save/import reports per-field truncation instead of silently
clamping. Backup, restore, the normalized importer and the CSV importer
round-trip all of it; the importer maps 1Password vaults and CSV
folder/tags/vault columns into groups server-side.
Wave 2 — six dead routes live: /api/ota/status (the whole Firmware
panel keyed on it), /api/vault/reindex, /api/time/diag (RTC coin-cell
health), the GPS trio (gated to boards that have one), NTP-sync-on-exit
through the AdminMode teardown choke point, and the staged-SD firmware
apply behind the confirm gate. Both full-replace restores now demand
the unlock secret (X-Unlock-Secret) — a backup passphrase proves
nothing about owning the live vault — and the restore body cap is
computed from the device's own backup bound instead of a 64 KB literal.
Wave 3 — the UI stops lying: the audit export honors Accept: text/csv
with real CSV, the web CSV importer honors the type column (Bitwarden
non-logins skipped and counted), LastPass and Proton Pass get real
header signatures, /api/runtime-stats is gone, AutoLockMode appears on
the six boards that hid a working handler, and the device vault lists
honor favorite-first plus manual order with an allocation-free
comparator.
Integration extras: the index envelope buffers and the rekey crypto
scratch now live on the heap (the rekey frame was already past the
32 KB worker stack at the current 200-slot caps — the latent overflow
the dimensioning study flagged); core2_v13's capacity-scaled view-cache
block moves to PSRAM .ext_ram.bss (keys and passwords stay in internal
DRAM), un-breaking its 48-byte link margin; the groups list
distinguishes a corrupt table from an empty one; custom:null is refused
rather than destructive; and the three amalgam suites learned the new
seams, including rebasing an unreachable handler-413 assertion onto the
gate-logic suite that actually pins the bound.
Builds: sticks3_debug, tdeck, cardputer, m5stickc_plus1_debug,
core2_v13_debug (now with SPIRAM bss). Native: full suite green with
nine new suites registered.
Claude-Session: https://claude.ai/code/session_01Q2J5gQSFMTDLVzPUYog51r
Widen the slot id to 16 bits so capacity can pass 255
Credential capacity is pinned per variant at up to 700 by the vault
dimensioning study, but the slot id was a u8 across the app, web and
persistence layers: every id above 255 would have aliased onto a live
slot. Widen the id and the counts derived from it to uint16_t through
vault, web, states and ui; TOTP and group ids deliberately stay u8
(their caps stay under 255) and are now pinned by co-located
static_asserts instead of by assumption.
The wire follows: index rows and the row count, and the slot id in the
envelope's authenticated context, become u16 little-endian, growing the
context prefix from 7 to 8 bytes. No version byte moves — the format is
pre-release and every board is reflashed — but the security docs that
quote the prefix byte-for-byte are updated so the audit dossier stays
verifiable against the source.
Adversarial review of the diff found four defects worth naming, all
fixed here:
- promoteAll() held two capacity-sized filename vectors live on the
32 KB vault-worker stack; at the 700-slot pin that is ~78 KB and a
guaranteed overflow on the first SD import or re-key. Both move to
the heap, matching the pattern the rest of the vault already uses.
- The widened staged-filename grammar accepted cred_007.bin as well as
cred_07.bin for the same slot, so an import deleted the canonical
record and wrote a path the repository never opens — silent slot
loss where the previous grammar had failed closed. The grammar is now
canonical-only and lives in one header both readers share.
- DevicePanel kept the last-viewed slot in editCredId_ after BACK, so
the standalone password generator saved into it, blanking that
credential's name, user and url. The id now resets to the invalid-slot
sentinel, and the guard that also made slot 0 unwritable is gone.
- The envelope test asserted the id's new high byte against its own
zero-initialized fixture, so it would have passed against a writer
that never wrote that byte at all.
Verified: native 2843/2843 (including a new wide-capacity suite that
exercises slot 260 end to end), sticks3_debug, cardputer_debug, tdeck
and core2_v13_debug, plus the merge gate — a clean -Werror build at
VAULT_MAX_CREDENTIALS=300, which is what proves no narrowing survives.
Claude-Session: https://claude.ai/code/session_01Q2J5gQSFMTDLVzPUYog51r
Widen the slot id to 16 bits so capacity can pass 255
Credential capacity is pinned per variant at up to 700 by the vault
dimensioning study, but the slot id was a u8 across the app, web and
persistence layers: every id above 255 would have aliased onto a live
slot. Widen the id and the counts derived from it to uint16_t through
vault, web, states and ui; TOTP and group ids deliberately stay u8
(their caps stay under 255) and are now pinned by co-located
static_asserts instead of by assumption.
The wire follows: index rows and the row count, and the slot id in the
envelope's authenticated context, become u16 little-endian, growing the
context prefix from 7 to 8 bytes. No version byte moves — the format is
pre-release and every board is reflashed — but the security docs that
quote the prefix byte-for-byte are updated so the audit dossier stays
verifiable against the source.
Adversarial review of the diff found four defects worth naming, all
fixed here:
- promoteAll() held two capacity-sized filename vectors live on the
32 KB vault-worker stack; at the 700-slot pin that is ~78 KB and a
guaranteed overflow on the first SD import or re-key. Both move to
the heap, matching the pattern the rest of the vault already uses.
- The widened staged-filename grammar accepted cred_007.bin as well as
cred_07.bin for the same slot, so an import deleted the canonical
record and wrote a path the repository never opens — silent slot
loss where the previous grammar had failed closed. The grammar is now
canonical-only and lives in one header both readers share.
- DevicePanel kept the last-viewed slot in editCredId_ after BACK, so
the standalone password generator saved into it, blanking that
credential's name, user and url. The id now resets to the invalid-slot
sentinel, and the guard that also made slot 0 unwritable is gone.
- The envelope test asserted the id's new high byte against its own
zero-initialized fixture, so it would have passed against a writer
that never wrote that byte at all.
Verified: native 2843/2843 (including a new wide-capacity suite that
exercises slot 260 end to end), sticks3_debug, cardputer_debug, tdeck
and core2_v13_debug, plus the merge gate — a clean -Werror build at
VAULT_MAX_CREDENTIALS=300, which is what proves no narrowing survives.
Claude-Session: https://claude.ai/code/session_01Q2J5gQSFMTDLVzPUYog51r
Extend the record to its final shape, revive the dead panels, honor the index
Three firmware waves of the gap-report program, built in parallel
worktrees against the same tip and integrated with both-sides conflict
resolution (two stitch errors — an unopened Doxygen comment and a lost
closing brace — were caught by the compiler, as intended).
Wave 1 — the record's final shape. CredentialRecord gains flags (bit0 =
auto-submit: username, TAB, password, ENTER as one BLE action — and the
trailing ENTER is now conditional where it used to be unconditional),
three typed custom fields (label/value/hidden — hidden values ride
behind the same plaintext-consent header as passwords), a group id, and
TOTP entries gain an account label plus the algorithm byte everywhere.
Groups are a 100-slot encrypted name table (one littlefs block) managed
over GET/POST/DELETE /api/groups — deleting a group only clears labels,
never credentials. The index row carries group, a derived domain column
and real mtime, with names widened to 32 chars; brand is populated at
save time from the url, ending the per-row decrypt the device paid for
logos. URL capacity rises to 192 (the owner's real data maxed at 190),
and every save/import reports per-field truncation instead of silently
clamping. Backup, restore, the normalized importer and the CSV importer
round-trip all of it; the importer maps 1Password vaults and CSV
folder/tags/vault columns into groups server-side.
Wave 2 — six dead routes live: /api/ota/status (the whole Firmware
panel keyed on it), /api/vault/reindex, /api/time/diag (RTC coin-cell
health), the GPS trio (gated to boards that have one), NTP-sync-on-exit
through the AdminMode teardown choke point, and the staged-SD firmware
apply behind the confirm gate. Both full-replace restores now demand
the unlock secret (X-Unlock-Secret) — a backup passphrase proves
nothing about owning the live vault — and the restore body cap is
computed from the device's own backup bound instead of a 64 KB literal.
Wave 3 — the UI stops lying: the audit export honors Accept: text/csv
with real CSV, the web CSV importer honors the type column (Bitwarden
non-logins skipped and counted), LastPass and Proton Pass get real
header signatures, /api/runtime-stats is gone, AutoLockMode appears on
the six boards that hid a working handler, and the device vault lists
honor favorite-first plus manual order with an allocation-free
comparator.
Integration extras: the index envelope buffers and the rekey crypto
scratch now live on the heap (the rekey frame was already past the
32 KB worker stack at the current 200-slot caps — the latent overflow
the dimensioning study flagged); core2_v13's capacity-scaled view-cache
block moves to PSRAM .ext_ram.bss (keys and passwords stay in internal
DRAM), un-breaking its 48-byte link margin; the groups list
distinguishes a corrupt table from an empty one; custom:null is refused
rather than destructive; and the three amalgam suites learned the new
seams, including rebasing an unreachable handler-413 assertion onto the
gate-logic suite that actually pins the bound.
Builds: sticks3_debug, tdeck, cardputer, m5stickc_plus1_debug,
core2_v13_debug (now with SPIRAM bss). Native: full suite green with
nine new suites registered.
Claude-Session: https://claude.ai/code/session_01Q2J5gQSFMTDLVzPUYog51r
Right-size the flash layout and write the index once per import
Two capacity items that were blocking the raised per-variant caps.
The partition tables gave the app far more room than the largest image
needs while starving the vault: the 8 MB boards now run 2 x 0x2C0000 app
slots with a 0x260000 LittleFS, the 4 MB boards a 0x290000 app with
0x160000 of filesystem, and the 16 MB tables keep their geometry. The
loaded 8 MB build sits at 78 % of its app slot and the 4 MB ones near
70 %, so the headroom is real rather than asserted. Two rationales that
had gone stale — a ">1500 credentials" claim and an Arduino/ESP-TEE
headroom note — are gone.
Changing the layout means a device must be fully re-flashed and its vault
repopulated, which is the standing pre-release policy; the flashing and
variant docs now say so where someone upgrading would look.
The bulk import used to rewrite index.bin for every row, and each rewrite
materialized the whole capacity-scaled working set: at 500 credentials
that is a flash rewrite and ~89 KB of transient per credential. A batch
guard now defers the rows and rebuilds the index once at the end.
Deferral is only safe if a half-finished batch can never be mistaken for
a finished one, and review found three ways it could be:
- A transient key-derivation failure returned without closing the batch.
Every later index mutation then reported success while writing nothing,
favorites and ordering survived only in RAM, and the portal's
index-repair button reported success while doing nothing — until a
reboot. The batch now closes before the write, so a failed write cannot
leave mutators deferring into a buffer nobody will flush, and the
key-derivation path aborts the batch explicitly.
- If the pre-batch unlink failed — lfs_remove needs a metadata block, so
it fails on a full filesystem — the old index survived and the batch
deferred every row into oblivion, leaving a MAC-valid, complete-looking
index missing every imported credential with no path back. The batch
now refuses to open and per-row writes stand: slower, not wrong.
- The batch depth was atomic to survive the vault worker's inline-
execution fallback but the snapshot pointer was not, leaving a
use-after-free window in exactly that case.
Also: the capacity guard's ceiling search could two-cycle and return
whichever value the iteration budget's parity landed on, one credential
above the self-consistent ceiling — failing open in the direction it
exists to prevent. It now iterates to a real fixpoint and takes the lower
value on a cycle.
Verified: native 2860/2860, the variant-config guard over all 38
environments, and every one of the eight boards builds — including
m5core_ink, which had never been built directly before.
Claude-Session: https://claude.ai/code/session_01Q2J5gQSFMTDLVzPUYog51r
Close the review-batch coverage gaps with mutation-verified suites
Two new native suites and four extended ones, 59 tests, every one
verified to fail against a deliberately broken source line:
- test_touch_auth_entry_ui (NEW, 29): the touch unlock controller had
zero host coverage while carrying the review's security-critical
behaviors. An amalgam TU (vault-harness precedent) compiles the
device-only controller on the host behind a fake pointer/canvas/board
seam; pins numeric auto-submit, the inert under-length OK key, reveal
auto-hide via the clock hook, secret zeroization on every exit path,
layer transitions, and the zone-button double-fire guard. Adds a
host-side LayoutContext::fromActiveDisplay test seam (device branch
untouched).
- test_pin_policy (NEW, 11): isValidPinLength/clampPinLength (the guard
that keeps PROVISION from creating unlockable-by-nobody vaults) and
clampSecretKind's fail-closed coercion of unknown persisted bytes.
- test_vault_rekey/import/meta/store_facade (+8): rekey carries the
authenticated secretKind forward verbatim, staged-import accessors
fall back to defaults on abort/truncation/no-stage, and the
provisioned kind survives a simulated reboot (invalidate + remount)
for both kinds.
- test_header_component/menu_list/settings_timer_clamp (+11): title-
subtitle leading in headerHeight and baseline placement, value-band
slack boundaries (exact fit / one-glyph overflow / two-line reserve),
and the kTimerNeverMs sentinel vs finite-deadline branches of the
relock policy.
Claude-Session: https://claude.ai/code/session_01XBVa8G5jrkprye4gVAkQSU
Right-size the flash layout and write the index once per import
Two capacity items that were blocking the raised per-variant caps.
The partition tables gave the app far more room than the largest image
needs while starving the vault: the 8 MB boards now run 2 x 0x2C0000 app
slots with a 0x260000 LittleFS, the 4 MB boards a 0x290000 app with
0x160000 of filesystem, and the 16 MB tables keep their geometry. The
loaded 8 MB build sits at 78 % of its app slot and the 4 MB ones near
70 %, so the headroom is real rather than asserted. Two rationales that
had gone stale — a ">1500 credentials" claim and an Arduino/ESP-TEE
headroom note — are gone.
Changing the layout means a device must be fully re-flashed and its vault
repopulated, which is the standing pre-release policy; the flashing and
variant docs now say so where someone upgrading would look.
The bulk import used to rewrite index.bin for every row, and each rewrite
materialized the whole capacity-scaled working set: at 500 credentials
that is a flash rewrite and ~89 KB of transient per credential. A batch
guard now defers the rows and rebuilds the index once at the end.
Deferral is only safe if a half-finished batch can never be mistaken for
a finished one, and review found three ways it could be:
- A transient key-derivation failure returned without closing the batch.
Every later index mutation then reported success while writing nothing,
favorites and ordering survived only in RAM, and the portal's
index-repair button reported success while doing nothing — until a
reboot. The batch now closes before the write, so a failed write cannot
leave mutators deferring into a buffer nobody will flush, and the
key-derivation path aborts the batch explicitly.
- If the pre-batch unlink failed — lfs_remove needs a metadata block, so
it fails on a full filesystem — the old index survived and the batch
deferred every row into oblivion, leaving a MAC-valid, complete-looking
index missing every imported credential with no path back. The batch
now refuses to open and per-row writes stand: slower, not wrong.
- The batch depth was atomic to survive the vault worker's inline-
execution fallback but the snapshot pointer was not, leaving a
use-after-free window in exactly that case.
Also: the capacity guard's ceiling search could two-cycle and return
whichever value the iteration budget's parity landed on, one credential
above the self-consistent ceiling — failing open in the direction it
exists to prevent. It now iterates to a real fixpoint and takes the lower
value on a cycle.
Verified: native 2860/2860, the variant-config guard over all 38
environments, and every one of the eight boards builds — including
m5core_ink, which had never been built directly before.
Claude-Session: https://claude.ai/code/session_01Q2J5gQSFMTDLVzPUYog51r
Widen the slot id to 16 bits so capacity can pass 255
Credential capacity is pinned per variant at up to 700 by the vault
dimensioning study, but the slot id was a u8 across the app, web and
persistence layers: every id above 255 would have aliased onto a live
slot. Widen the id and the counts derived from it to uint16_t through
vault, web, states and ui; TOTP and group ids deliberately stay u8
(their caps stay under 255) and are now pinned by co-located
static_asserts instead of by assumption.
The wire follows: index rows and the row count, and the slot id in the
envelope's authenticated context, become u16 little-endian, growing the
context prefix from 7 to 8 bytes. No version byte moves — the format is
pre-release and every board is reflashed — but the security docs that
quote the prefix byte-for-byte are updated so the audit dossier stays
verifiable against the source.
Adversarial review of the diff found four defects worth naming, all
fixed here:
- promoteAll() held two capacity-sized filename vectors live on the
32 KB vault-worker stack; at the 700-slot pin that is ~78 KB and a
guaranteed overflow on the first SD import or re-key. Both move to
the heap, matching the pattern the rest of the vault already uses.
- The widened staged-filename grammar accepted cred_007.bin as well as
cred_07.bin for the same slot, so an import deleted the canonical
record and wrote a path the repository never opens — silent slot
loss where the previous grammar had failed closed. The grammar is now
canonical-only and lives in one header both readers share.
- DevicePanel kept the last-viewed slot in editCredId_ after BACK, so
the standalone password generator saved into it, blanking that
credential's name, user and url. The id now resets to the invalid-slot
sentinel, and the guard that also made slot 0 unwritable is gone.
- The envelope test asserted the id's new high byte against its own
zero-initialized fixture, so it would have passed against a writer
that never wrote that byte at all.
Verified: native 2843/2843 (including a new wide-capacity suite that
exercises slot 260 end to end), sticks3_debug, cardputer_debug, tdeck
and core2_v13_debug, plus the merge gate — a clean -Werror build at
VAULT_MAX_CREDENTIALS=300, which is what proves no narrowing survives.
Claude-Session: https://claude.ai/code/session_01Q2J5gQSFMTDLVzPUYog51r
Right-size the flash layout and write the index once per import
Two capacity items that were blocking the raised per-variant caps.
The partition tables gave the app far more room than the largest image
needs while starving the vault: the 8 MB boards now run 2 x 0x2C0000 app
slots with a 0x260000 LittleFS, the 4 MB boards a 0x290000 app with
0x160000 of filesystem, and the 16 MB tables keep their geometry. The
loaded 8 MB build sits at 78 % of its app slot and the 4 MB ones near
70 %, so the headroom is real rather than asserted. Two rationales that
had gone stale — a ">1500 credentials" claim and an Arduino/ESP-TEE
headroom note — are gone.
Changing the layout means a device must be fully re-flashed and its vault
repopulated, which is the standing pre-release policy; the flashing and
variant docs now say so where someone upgrading would look.
The bulk import used to rewrite index.bin for every row, and each rewrite
materialized the whole capacity-scaled working set: at 500 credentials
that is a flash rewrite and ~89 KB of transient per credential. A batch
guard now defers the rows and rebuilds the index once at the end.
Deferral is only safe if a half-finished batch can never be mistaken for
a finished one, and review found three ways it could be:
- A transient key-derivation failure returned without closing the batch.
Every later index mutation then reported success while writing nothing,
favorites and ordering survived only in RAM, and the portal's
index-repair button reported success while doing nothing — until a
reboot. The batch now closes before the write, so a failed write cannot
leave mutators deferring into a buffer nobody will flush, and the
key-derivation path aborts the batch explicitly.
- If the pre-batch unlink failed — lfs_remove needs a metadata block, so
it fails on a full filesystem — the old index survived and the batch
deferred every row into oblivion, leaving a MAC-valid, complete-looking
index missing every imported credential with no path back. The batch
now refuses to open and per-row writes stand: slower, not wrong.
- The batch depth was atomic to survive the vault worker's inline-
execution fallback but the snapshot pointer was not, leaving a
use-after-free window in exactly that case.
Also: the capacity guard's ceiling search could two-cycle and return
whichever value the iteration budget's parity landed on, one credential
above the self-consistent ceiling — failing open in the direction it
exists to prevent. It now iterates to a real fixpoint and takes the lower
value on a cycle.
Verified: native 2860/2860, the variant-config guard over all 38
environments, and every one of the eight boards builds — including
m5core_ink, which had never been built directly before.
Claude-Session: https://claude.ai/code/session_01Q2J5gQSFMTDLVzPUYog51r
fix(test): configure clangd and clang-tidy for native test tree (#3)
* fix(test): configure clangd and clang-tidy for native test tree
- Generate compile_commands.json from pio run -e native -t compiledb
so clangd resolves src/ headers in test files without errors.
- Fix .clangd PathMatch regex (relative path, not absolute) and add
-I.pio/build/native/unity_config so unity_config.h resolves.
- Remove overly broad Remove: -I* that was stripping all added paths.
- Add -Wno-format-security and UnusedIncludes: None to test block.
- Add test/.clang-tidy that inherits the root config and:
- Allows test_* functions to use snake_case (FunctionIgnoredRegexp)
- Disables cert-err33-c, cert-dcl50-cpp,
cppcoreguidelines-pro-type-cstyle-cast,
bugprone-argument-comment, bugprone-misplaced-widening-cast,
bugprone-implicit-widening-of-multiplication-result,
misc-unused-using-decls — all false positives in test context.
- Add /* intentionally empty */ comment to all empty setUp()/tearDown()
bodies (28 files) to suppress SonarLint c:S1186.
- Fix test_vault_task: change relative include ../../src/vault/vault_task.h
to vault/vault_task.h (consistent with all other test files; -Isrc
already in compile flags).
- Reorganise test/ into a module-mirroring subtree layout:
test/ble/, test/crypto/, test/hal/, test/ota/, test/platform/,
test/states/, test/totp/, test/ui/, test/vault/, test/web/
(old flat directories removed).
- Add sonar-project.properties exclusion e4: suppress c:S1186 on
test/**/*.cpp at SonarCloud level.
All 468 native unit tests pass (pio test -e native).
* fix(clangd): add -DESP_PLATFORM to global CompileFlags
All Kleidos targets are ESP32/ESP32-S3 (framework = espidf), so
ESP_PLATFORM is always defined at compile time. Without this flag in
the clangd config, the language server was activating #else branches
inside #ifdef ESP_PLATFORM blocks, hiding device code and showing
false diagnostics for src/ files.
The test/** fragment already has Remove: -DESP_PLATFORM, so native
unit-test files continue to compile without the flag.
* revert(clangd): remove incorrect -DESP_PLATFORM from global CompileFlags
Adding -DESP_PLATFORM globally caused 'too many errors' in src/ files
because compile_commands.json was generated from the native env and
lacked all ESP-IDF/Xtensa include paths.
The correct fix is to generate compile_commands.json from a device env:
./scripts/generate_compiledb.sh sticks3
This populates the DB with proper -DESP_PLATFORM and all IDF includes
for every src/ translation unit. The symlink at the project root then
points clangd to the right compilation context.
Right-size the flash layout and write the index once per import
Two capacity items that were blocking the raised per-variant caps.
The partition tables gave the app far more room than the largest image
needs while starving the vault: the 8 MB boards now run 2 x 0x2C0000 app
slots with a 0x260000 LittleFS, the 4 MB boards a 0x290000 app with
0x160000 of filesystem, and the 16 MB tables keep their geometry. The
loaded 8 MB build sits at 78 % of its app slot and the 4 MB ones near
70 %, so the headroom is real rather than asserted. Two rationales that
had gone stale — a ">1500 credentials" claim and an Arduino/ESP-TEE
headroom note — are gone.
Changing the layout means a device must be fully re-flashed and its vault
repopulated, which is the standing pre-release policy; the flashing and
variant docs now say so where someone upgrading would look.
The bulk import used to rewrite index.bin for every row, and each rewrite
materialized the whole capacity-scaled working set: at 500 credentials
that is a flash rewrite and ~89 KB of transient per credential. A batch
guard now defers the rows and rebuilds the index once at the end.
Deferral is only safe if a half-finished batch can never be mistaken for
a finished one, and review found three ways it could be:
- A transient key-derivation failure returned without closing the batch.
Every later index mutation then reported success while writing nothing,
favorites and ordering survived only in RAM, and the portal's
index-repair button reported success while doing nothing — until a
reboot. The batch now closes before the write, so a failed write cannot
leave mutators deferring into a buffer nobody will flush, and the
key-derivation path aborts the batch explicitly.
- If the pre-batch unlink failed — lfs_remove needs a metadata block, so
it fails on a full filesystem — the old index survived and the batch
deferred every row into oblivion, leaving a MAC-valid, complete-looking
index missing every imported credential with no path back. The batch
now refuses to open and per-row writes stand: slower, not wrong.
- The batch depth was atomic to survive the vault worker's inline-
execution fallback but the snapshot pointer was not, leaving a
use-after-free window in exactly that case.
Also: the capacity guard's ceiling search could two-cycle and return
whichever value the iteration budget's parity landed on, one credential
above the self-consistent ceiling — failing open in the direction it
exists to prevent. It now iterates to a real fixpoint and takes the lower
value on a cycle.
Verified: native 2860/2860, the variant-config guard over all 38
environments, and every one of the eight boards builds — including
m5core_ink, which had never been built directly before.
Claude-Session: https://claude.ai/code/session_01Q2J5gQSFMTDLVzPUYog51r
Add the WiFi-station store and NTP clock-sync backend
From Settings (vault unlocked) the device can join a saved WiFi network
as a station, sync the clock via one-shot SNTP against trusted public
NTP servers, and always disconnect. Servers are LITERAL IPv4 only —
never a hostname, never DNS (esp_sntp is fed parsed addresses; a strict
dotted-quad validator gates every entry). Defaults are Cloudflare
(162.159.200.1, 162.159.200.123), Google (216.239.35.0) and NIST
(132.163.97.1), tried in order; the user can persist an override list
of up to four IPs so dead defaults never require a firmware update.
Saved networks (SSID+PSK, max 8) and the NTP server list live in a new
wifi.bin vault module using the standard envelope (AES-256-CBC +
HMAC-SHA256, encrypt-then-MAC, verify-before-decrypt, type-bound), only
readable with the vault unlocked, staged through the crash-safe rekey,
and crypto-erased by wipeAll. PSKs are zeroized after use and never
logged or echoed.
The sync engine runs on a Core-0 worker: BLE off, join the strongest
saved network, query servers in order, sanity-clamp the epoch and never
step backward past an armed lockout, commit via the rtc facade with a
new display-only src=ntp tag — clock TRUST is never elevated (NTP is
unauthenticated; the forward-jump lockout fail-closed model is the
spoofing defense) — then a RAII station guard tears WiFi down and
restores BLE on every exit path. The station facade stays a generic
transport for future consumers (on-device OTA).
Debug console gains WIFISCAN / WIFIADD / WIFILIST / WIFIDEL, NTPSYNC
(GPSSYNC-style blocking flow with machine-checkable teardown acks) and
NTPSERVERS? / NTPSERVERSET / NTPSERVERCLEAR — all DEBUG_SERIAL_BUTTONS
gated. HIL-verified end to end on the StickS3: real sync against the
default servers, user-list precedence across reboot, TEST-NET failure
paths, trust unchanged, BLE restored, store left clean.
Claude-Session: https://claude.ai/code/session_01SV58JXhfxhhc9DC6vdjBo4