fix(credentials): mark the encrypted store so a random nonce cannot fake plaintext
The on-disk format was detected by sniffing the first byte for `[` or `{`.
Encrypted blobs begin with a random 12-byte nonce, so roughly 1 in 128
saves produced a valid encrypted file whose first byte was 0x5B or 0x7B;
those were misread as plaintext JSON, failed `String::from_utf8`, and the
store became permanently unreadable. This was surfacing as a flaky
`test_list_credentials_no_filter`, but it is a real data-loss bug: a node
whose ciphertext happened to start with one of those bytes could not load
its credentials.
Writes now carry a fixed `ARCHYCRED1` marker, which cannot collide with a
random nonce, so detection of the current format is exact.
Legacy unmarked files are detected by SUCCESSFUL AEAD DECRYPTION rather
than by another byte sniff. A verifying Poly1305 tag under the node key is
a cryptographic discriminator (~2^-128 false-positive rate), strictly
stronger than any structural guess — which is why the deferred item's
suggested "keep the first-byte sniff as the legacy fallback" was not the
shape adopted. Plaintext JSON remains the last resort, and is still
reachable on a node that has no node key at all.
An undecodable file now errors instead of returning an empty store, so a
transiently unreadable file is never silently replaced by an empty one
that the next save would commit to disk (CLAUDE.md: migrations never
destroy data). Legacy files upgrade on write, never on read.
Tests drive the collision deterministically via an explicit nonce rather
than waiting on the 1-in-128 draw, and cover all three on-disk
populations, the read-path-does-not-rewrite guarantee, and tamper
rejection. 28 passed, 0 failed.
Closes the 10-01 deferred item.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
937d836c53
commit
c5a82cba06
@@ -0,0 +1,31 @@
|
||||
# Deferred items — Phase 10
|
||||
|
||||
Out-of-scope discoveries found while executing this phase. Logged, not fixed.
|
||||
|
||||
## From 10-01 (KEY-01 / F-01)
|
||||
|
||||
**Flaky test: `credentials::operations::tests::test_list_credentials_no_filter`**
|
||||
|
||||
- Discovered: 2026-08-02, during the post-plan full-suite run (`cargo test -p archipelago`).
|
||||
- Symptom: `called Result::unwrap() on an Err value: UTF-8 credentials / invalid utf-8 sequence
|
||||
of 1 bytes from index 3`.
|
||||
- Root cause (read, not fixed): `credentials/store.rs:29` sniffs the FIRST BYTE of the stored
|
||||
blob for `[` or `{` to distinguish a plaintext-JSON legacy store from the encrypted binary
|
||||
one. When the encrypted ciphertext happens to begin with `0x5B` or `0x7B` — about a 1-in-128
|
||||
chance per run — the encrypted store is misread as plaintext and `String::from_utf8` fails.
|
||||
This is a real bug in the migration sniffing, not just a test problem: a real node whose
|
||||
credential ciphertext starts with one of those bytes cannot load its credentials.
|
||||
- Why deferred: unrelated to KEY-01, different subsystem, untouched by this plan
|
||||
(`git status` shows `credentials/` unmodified). Fixing it means adding a format marker or
|
||||
version header to the store, which is an envelope change.
|
||||
- Suggested fix: prepend an explicit magic/version byte on write and branch on that, keeping the
|
||||
first-byte sniff only as the legacy fallback.
|
||||
- **RESOLVED 2026-08-02** — fixed along the suggested lines, with one improvement. Writes are now
|
||||
prefixed with a fixed `ARCHYCRED1` marker, which cannot collide with a random nonce. Legacy
|
||||
unmarked files are detected by *successful AEAD decryption* rather than by a byte sniff: a
|
||||
Poly1305 tag that verifies under the node key is a cryptographic discriminator (~2^-128 false
|
||||
positive), strictly stronger than the structural sniff the fallback would have kept. Plaintext
|
||||
JSON stays the last resort, and an undecodable file now errors instead of silently becoming an
|
||||
empty store that the next save would overwrite. Legacy files upgrade on write, never on read.
|
||||
Regression tests drive the collision deterministically via an explicit nonce (`0x5B`/`0x7B`)
|
||||
instead of waiting on the 1-in-128 draw.
|
||||
Reference in New Issue
Block a user