`LiveFileTransferTest` covers the one seam nothing else does: a document
leaving one vault, crossing a real relay, and arriving in another's with
its label intact. `FileShareWireTest` proves the envelope's shape and
`LocalVaultTest` proves the vault at both ends; neither puts a byte on a
wire.
⚠️ **Written, compiles, UNRUN** — the same state `LiveSignUpFleetTest` is
in, and the same caveat applies: an unrun test proves nothing. It creates
accounts, so it needs a throwaway relay and an explicit decision to point
it at one.
🔴 **Writing it surfaced four published passwords and a guard that could
not see them.**
`TestSafetyTest` exists to stop a credential being written into a suite
that creates real accounts. It matched `val X = "…"` only. So:
- `LiveConversationTest` passed the literal straight as an ARGUMENT,
twice — invisible to the check.
- `LiveSignUpTest`, `LiveKeyLogTest` and `LiveSealedSenderTest` wrote
`password = "…"` on its own line inside a multi-line argument list, the
most ordinary Kotlin formatting there is — also invisible.
I copied the same literal into the new suite before noticing, which is
how I found it.
**A guard that passes while the thing it guards against is present in the
tree is worse than no guard, because it is read as evidence.** All five
call sites now read `LiveTarget.passphrase()` from the environment with no
fallback, and announce a SKIP when it is unset.
The check itself took two wrong turns worth recording:
- Skipping every named argument, to avoid flagging
`product = "messenger"` — which let the three `password = "…"` suites
through. Trading one blind spot for another is not a fix; a named
argument is now judged by its NAME.
- Widening the sign-up match to a few lines around the call — which
flagged forty test tags, assertion messages and message bodies as
credentials. That is how a check stops being read, so it is same-line
only, and rule 1 covers the multi-line case properly.
"secret" was dropped from the credential names for the same reason: test
prose is full of it, and a marker string called `secret` is a fixture.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Marked ⚪ and implemented the whole time. Every read goes to disk:
`Session.history` calls `Vault.history`, which decrypts the record. There
is no in-memory copy to be fooled by.
Checked against iOS's `E2EHistoryStore` rather than assumed. Same cipher,
same posture, two honest differences recorded rather than papered over:
- 🟢 Android binds the RECORD NAME into the AAD, so a sealed blob cannot
be moved between slots. iOS keeps one snapshot file and does not need
that — but would, the day it splits.
- iOS's `.completeFileProtection` is a per-file assertion. Android's
equivalent is Credential-Encrypted storage, a property of the volume
rather than of this app's request.
Neither favours one platform overall, and both clients agree on the
behaviour that matters: a plain sign-out KEEPS history, and erasing the
device destroys it — on Android by wiping every record and deleting the
Keystore key, so what is left is unopenable as well as gone.
⚠️ Also recorded: `Vault.history` prunes expired messages on the way out,
so disappearing messages are enforced on READ rather than by a timer
nothing runs — and it rewrites the record only when something actually
expired, so a read is not a write.
Services (AIM/ICQ) retired rather than left as a gap: not a goal for this
app (owner, 2026-09-04).
That leaves only the two post-quantum rows, and the note above them
already says why they are not a gap in effect: iOS's PQ files are inert
because CryptoKit exposes ML-KEM on iOS 26 and the app targets 16, so
neither client does post-quantum anything today. Closing it properly is
one decision for both clients, not an Android task.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>