fix(auth): address Copilot review round 6 on the secret-store PR
Four findings, all suppressed, all real. Three are the same mistake in different places: a state that was never established being reported as a confident answer. - **`readOnDiskEncryption` conflated "no file" with "unreadable file".** Both answered `null`, and the descriptor falls back to the configured write policy on `null` — so a passphrase-configured install reported "File (encrypted)" about a file whose envelope had never parsed, and which `set` was in fact about to refuse rather than overwrite. It now returns a four-state result, and only `absent` falls back to the policy (honestly: the first write really will create it that way). `unreadable` produces `encryptionUnknown` on the descriptor, `plaintext` is omitted entirely so nothing can read a default out of it, and the label, caveat, tone and footer all carry "unreadable" rather than guessing a mode. - **The loose-mode warning overstated exposure for an encrypted file.** "Anyone who can read it can read the secrets in it" is false when the file is ciphertext — a reader gets the bytes and still needs the passphrase. Both the caveat and the footer tooltip now branch on encryption: the passphrase becomes the only protection, which is the real consequence. Overstating it is not harmless, because a warning that cries wolf is discounted on the occasion it is literally true. - **The keychain hand-off could invert its own keychain-wins rule.** `SecretStore.get` answers `null` both for a missing entry and for a keychain it could not read, and the migration *writes* on `null` — so a transient read failure would overwrite a newer keychain value with the older file copy. Added an optional `getStrict` (throws instead of swallowing) implemented by `KeyringSecretStore`, with `secretStoreGetStrict` falling back to `get` for stores whose reads cannot fail. A throw now aborts the hand-off with the file left in place, so the next run retries. Writing the tests turned up one more, in code the review had passed: `parseOwner` recorded a garbled pid as `-1`, and `process.kill(-1, 0)` is a *wildcard* asking about every process this user can signal — it answers "alive" almost always, which would have made a lock with a corrupt stamp permanently unstealable. Only a positive pid is usable now; anything else is treated as no pid at all and the mtime decides. Tests: the four envelope states kept distinct; the descriptor refusing to guess, with the genuine no-file case as the control; strict-read abort without a write and with the file preserved, plus a store whose `get` and `getStrict` disagree so the test proves which one the migration consults; encryption-aware wording asserted in both directions in the caveat and the band; three unusable owner stamps and a wildcard pid; and the descriptor shapes that must now be rejected — neither half of the encryption question answered, or both. Full `npm run ci` green — 5,750 tests under the coverage gate. Signed-off-by: cliffhall <cliff@futurescale.com>
This commit is contained in:
parent
1aa1f5a0ca
commit
fdeffa1f00
1 changed files with 489 additions and 29 deletions
|
|
@ -97,7 +97,16 @@ function usableSecretStorage(value: unknown): SecretStorageInfo | undefined {
|
|||
if (typeof v.durable !== "boolean") return undefined;
|
||||
if (v.kind === "file") {
|
||||
if (typeof v.path !== "string" || v.path === "") return undefined;
|
||||
if (typeof v.plaintext !== "boolean") return undefined;
|
||||
// Exactly one of the two must be present: either we know the encryption
|
||||
// state, or we know we could not read it. A descriptor carrying neither
|
||||
// (or both) was not built by a backend we understand, and the footer
|
||||
// would have to invent the missing half.
|
||||
const knowsEncryption = typeof v.plaintext === "boolean";
|
||||
const knowsItCannotRead = typeof v.encryptionUnknown === "string";
|
||||
if (knowsEncryption === knowsItCannotRead) return undefined;
|
||||
if (v.encryptionUnknown !== undefined && !knowsItCannotRead) {
|
||||
return undefined;
|
||||
}
|
||||
// Optional, so absent is fine — but a *present* value must be a real
|
||||
// boolean. `pendingEncryption: "false"` is a truthy string, and the
|
||||
// footer would read it as "already re-encrypting" and print advice that
|
||||
|
|
@ -125,7 +134,8 @@ function usableSecretStorage(value: unknown): SecretStorageInfo | undefined {
|
|||
v.plaintext !== undefined ||
|
||||
v.pendingEncryption !== undefined ||
|
||||
v.looseMode !== undefined ||
|
||||
v.permissionsUnknown !== undefined
|
||||
v.permissionsUnknown !== undefined ||
|
||||
v.encryptionUnknown !== undefined
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue