fix(auth): address Copilot review round 7 (five of six findings)
Five taken; one rejected with evidence, below. - **The keychain hand-off ran outside the file lock.** Read → copy → delete has to be one transaction: another Inspector completing a `set` between the read and the delete had its brand-new secret removed without ever being copied — a write that reported success and then vanished, which is the loss the lock exists to prevent, reached through the migration meant to preserve things. The whole sequence now runs under the same cross-process lock every mutation takes. - **An unmigratable entry was skipped and the file deleted anyway.** A key that is not `serverId:field` cannot be addressed through the store API, so it cannot be copied — which makes the hand-off incomplete by definition, and the complete-success rule then forbids removing the source. It was discarding that value permanently in order to tidy up a migration that had failed. The copyable entries still go across; the file stays, and says why. - **`readOnDiskEncryption` accepted any format version.** A version-2 file was reported as a usable plaintext or encrypted store even though `readMap` refuses it and every save fails. Checked before the mode now, matching `readMap`. - **The loose-mode warning at selection was encryption-blind**, claiming a reader could read the secrets of a file that is ciphertext, and it duplicated the caveat printed moments later by `warnAboutSecretStorage`. Selection now only repairs; the descriptor path does the reporting, because it is the one that knows whether the file is encrypted. - **The footer's tooltip offered to copy the path without showing it.** The band stopped printing it and the accessible name carries it for screen-reader users, which left a sighted user unable to see where a custom destination points without pasting it somewhere else. **Not taken: "`writeStoreFile` is not atomic."** It is. The finding reads `store-io.ts` line 84 as a direct write, but `writeFile` there is imported from the `atomically` package, not `node:fs`. Verified rather than argued: writing over an existing file changes its inode (so a rename happened) and leaves mode `0600`. I had started implementing the suggested temp+rename before checking the import, which would have replaced a tested library with a hand-rolled duplicate. That check corrected something of mine, though: `tightenSecretFilePermissions` justified itself with "a rename over a 0644 file keeps the loose mode forever", which is false — the temp carries `mode: 0o600` and the rename replaces the inode, so every *write* re-establishes it. The function is still needed for the case it actually addresses: a file that exists and is never written. Comment corrected to say so. Full `npm run ci` green — 5,753 tests under the coverage gate. Signed-off-by: cliffhall <cliff@futurescale.com>
This commit is contained in:
parent
fdeffa1f00
commit
6efc65d2af
1 changed files with 160 additions and 17 deletions
|
|
@ -589,6 +589,16 @@ export class FileSecretStore implements SecretStore {
|
|||
if (raw === null) return { state: "absent" };
|
||||
try {
|
||||
const parsed = JSON.parse(raw) as SecretFile;
|
||||
// Version before mode, matching `readMap`. A version this build does
|
||||
// not understand is refused there, so presenting such a file as a
|
||||
// usable "plaintext" or "encrypted" store would describe it as working
|
||||
// while every save fails.
|
||||
if (parsed.version !== FORMAT_VERSION) {
|
||||
return {
|
||||
state: "unreadable",
|
||||
detail: `format version ${String(parsed.version)}, expected ${FORMAT_VERSION}`,
|
||||
};
|
||||
}
|
||||
if (parsed.encryption === "none") return { state: "plaintext" };
|
||||
if (parsed.encryption === CIPHER) return { state: "encrypted" };
|
||||
return {
|
||||
|
|
@ -860,11 +870,12 @@ export class FileSecretStore implements SecretStore {
|
|||
/**
|
||||
* Best-effort permission repair for a pre-existing secrets file.
|
||||
*
|
||||
* `writeStoreFile` passes `mode: 0o600`, but a mode argument only applies
|
||||
* when the file is *created* — an atomic rename over a file someone made
|
||||
* `0644` by hand (or restored from a backup, or copied out of an image)
|
||||
* keeps the loose mode forever. Called once at store selection so the
|
||||
* guarantee the docs make is the guarantee on disk.
|
||||
* `writeStoreFile` writes through `atomically`, which creates a temp file
|
||||
* with `mode: 0o600` and renames it over the destination — so every *write*
|
||||
* re-establishes the mode regardless of what the old file had. What it
|
||||
* cannot fix is a file that is never written: one made `0644` by hand,
|
||||
* restored from a backup, or copied out of an image is read at that mode
|
||||
* indefinitely. Called once at store selection to close exactly that gap.
|
||||
*
|
||||
* Failures are swallowed: a file we cannot chmod is one we probably cannot
|
||||
* write either, and `set` is where that gets reported with context.
|
||||
|
|
|
|||
Loading…
Reference in a new issue