fix(auth): address Copilot review round 10, and document the concurrency design

The headline is a correction to my own claim rather than a new defect.

**The optimistic verify is weaker than the comment said it was.** It
described the residual as a crash between the write and the verifying read.
The reviewer pointed out that no crash is needed: order two writers as
write-A, verify-A, write-B, verify-B and both report success while A's entry
is gone — A's verify simply ran before there was anything to see, and B did
nothing wrong. A crash is one instance of that shape, not the whole of it.
The trace in the old comment quietly assumed A verifies *after* B writes,
which nothing guarantees.

Corrected in the doc comment, the README and the spec, and the honest
summary is now stated rather than implied: this converges in every
interleaving where the clobber lands before the verify, it is not mutual
exclusion, and a deployment that needs the guarantee wants an OS-backed lock
from a dedicated library — not another hand-rolled election. It remains
better than what it replaced, which lost updates across a wider set of
interleavings and could not be closed at all with what Node exposes.

The other findings:

- **An envelope naming the cipher still is not an openable one.** Round 9
  added a `kdf`/`data` presence check; three dot-separated strings satisfied
  it while an empty tag, a 4-byte IV or `N: 3` still made every decrypt
  throw — so the quiet "Encrypted file" footer went to a file whose next
  save was guaranteed to fail. Now validates part sizes, a non-empty
  ciphertext, and the scrypt cost parameters (node rejects a non-power-of-two
  `N` synchronously, out of the derivation rather than the decrypt). A
  positive control asserts a file we actually wrote still reads as encrypted.

- **`encryptionUnknown` + `looseMode` claimed an exposure it had not
  established.** With both set, `plaintext` is absent, so a `!== false` test
  landed on "anyone who can read it can read the secrets in it" — about a
  file that may well hold ciphertext we simply could not classify. Three-way
  now, in `secretStorageCaveat` and mirrored in the footer's tooltip, with a
  test on each.

- **The README described a lock that no longer exists**, which is the worst
  kind of stale doc: it stated a durability guarantee the code had stopped
  providing. Rewritten to describe the actual design, including the residual.

- **The private-method access in the concurrency test** now carries the
  justification the repo's TypeScript rules require: the asserted shape is
  `writeMap`'s real signature, so a rename breaks the line instead of
  silently detaching the stub, and a production injection seam would let the
  real write path drift from the tested one.

Also updates `specification/v2_servers_file.md`, which still described #1356's
world: one keychain-only store, and unavailability as a hard error. It now
covers store selection and the probe, the file format and lazy encryption
upgrade, the concurrency design and its residual, the `getStrict`/`getMany`
seams and the two traps `getStrict` hit, the durability gate on migration,
the keychain hand-off and its conditional delete, and how the active store is
surfaced.

Full `npm run ci` green. (One unrelated `AppRenderer` theme-flip test failed
once under full-suite parallelism and passes in isolation with and without
these changes — a flake, not a regression.)

Signed-off-by: cliffhall <cliff@futurescale.com>
This commit is contained in:
cliffhall 2026-08-22 18:33:59 -04:00
commit 219765efff
2 changed files with 267 additions and 72 deletions

View file

@ -106,6 +106,36 @@ describe("SecretStorageFooter", () => {
expect(band).toHaveAttribute("data-tone", "warn");
});
it("does not claim exposure in the tooltip when encryption is unknown", async () => {
// Mirrors `secretStorageCaveat`: `plaintext` is absent alongside
// `encryptionUnknown`, and a two-way `!== false` test would assert that
// a reader gets the secrets out of a file we could not even classify.
// The claim lives in the *tooltip*, so that is what this hovers to read
// — the visible line reports the unreadable envelope, which is a
// different (and also correct) statement.
const user = userEvent.setup();
renderWithMantine(
<SecretStorageFooter
info={{
...plaintextFile,
plaintext: undefined,
looseMode: 0o644,
encryptionUnknown: "not valid JSON",
}}
/>,
);
const band = screen.getByTestId("secret-storage-footer");
expect(band).toHaveAttribute("data-tone", "warn");
// The tooltip's target is the copy button, not the band around it.
await user.hover(screen.getByRole("button", { name: /Copy secrets file/ }));
const tip = await screen.findByText(/Mode 0644 could not be tightened/);
expect(tip).toHaveTextContent("could not determine");
expect(tip).not.toHaveTextContent(
"anyone who can read the file can read the secrets in it",
);
});
it("renders nothing when the backend didn't report a store", () => {
// A guessed answer under a secret field is worse than no answer, so the
// unknown case is silence rather than a default.

View file

@ -197,9 +197,16 @@ function footerTooltip(info: SecretStorageInfo): string | undefined {
// secrets. Claiming otherwise overstates the exposure, and a warning
// that cries wolf is discounted on the occasion it is literally true.
parts.push(
info.plaintext === false
? `Mode ${formatMode(info.looseMode)} could not be tightened to 0600 — others can copy the file, and the passphrase is then the only thing protecting its contents.`
: `Mode ${formatMode(info.looseMode)} could not be tightened to 0600 — anyone who can read the file can read the secrets in it.`,
// Three cases, mirroring `secretStorageCaveat`. With
// `encryptionUnknown` set, `plaintext` is absent and `!== false` would
// land on the "anyone can read the secrets" wording — asserting an
// exposure we did not establish, since the envelope may hold
// ciphertext we simply could not classify.
info.encryptionUnknown !== undefined
? `Mode ${formatMode(info.looseMode)} could not be tightened to 0600 — others can copy the file, and whether that exposes the secrets depends on encryption this build could not determine (${info.encryptionUnknown}).`
: info.plaintext === false
? `Mode ${formatMode(info.looseMode)} could not be tightened to 0600 — others can copy the file, and the passphrase is then the only thing protecting its contents.`
: `Mode ${formatMode(info.looseMode)} could not be tightened to 0600 — anyone who can read the file can read the secrets in it.`,
);
} else if (info.permissionsUnknown !== undefined) {
parts.push(