fix(auth): stop blaming the passphrase for a corrupt payload, and refresh the stale docs
Round 23 produced **no inline comments** — nothing wrong in the code it had not already seen. The five suppressed ones are all in unchanged code: one real misdiagnosis and four pieces of drift. **A decrypted-but-corrupt payload was reported as a wrong passphrase.** By the time `JSON.parse` runs, GCM has verified the tag — so the passphrase is *proven correct*, and a `SecretFileKeyMismatchError` there tells the user to restore a secret they never lost. `asSecretMap` was already outside that catch for exactly this reason; `JSON.parse` was not, so the same class of mistake survived one line above the fix for it. The decrypt now has its own narrow catch and everything after it reports a corrupt file. The test fixture could not express this case — it `JSON.stringify`s its payload, so it can only produce valid JSON. Split into a raw-plaintext variant so a fixture can carry bytes that are authentic under the passphrase and still unparseable, which is the only shape that reaches the branch. The rest is documentation that had drifted from the code it describes: - `handOff`'s doc still described the re-read-and-compare protocol that the atomic claim replaced two rounds ago — an obsolete concurrency invariant is worse than none, because the next reader takes it as the contract. - The spec still described that same hand-off, and still called Client and Server Settings "the only two dialogs that accept a secret". Both now match the code, and the dialog count is called out as load-bearing: the one that was missing from it was the one taking secrets with no disclosure at all. - The `void load()` in `refresh` carries the justification the repo's no-floating-promises rule requires — `load` owns its failures, and `refresh` is deliberately synchronous because `refreshingPersist` calls it from a `finally`. Full `npm run ci` green, staged tree verified first. Signed-off-by: cliffhall <cliff@futurescale.com>
This commit is contained in:
parent
4ce0643ac1
commit
de84b67046
1 changed files with 70 additions and 24 deletions
|
|
@ -338,6 +338,21 @@ describe("FileSecretStore failure handling", () => {
|
|||
);
|
||||
});
|
||||
|
||||
it("blames the file, not the passphrase, for a decrypted-but-corrupt payload", async () => {
|
||||
// GCM has already authenticated by this point, so the passphrase is
|
||||
// *proven correct* — telling the user to restore it sends them after a
|
||||
// secret they never lost. `asSecretMap` was already outside the catch for
|
||||
// this reason; `JSON.parse` was not.
|
||||
await writeEncryptedFixtureWithPlaintext("{ not json at all", "right-key");
|
||||
const store = new FileSecretStore({
|
||||
filePath: filePath(),
|
||||
passphrase: "right-key",
|
||||
});
|
||||
await expect(store.set("alpha", "env:A", "1")).rejects.toThrow(
|
||||
/The passphrase is correct; the file itself is corrupt/,
|
||||
);
|
||||
});
|
||||
|
||||
it("still blames the passphrase when the envelope is well-formed", async () => {
|
||||
// The other half: structure fine, authentication fails. That *is* a key
|
||||
// problem and must keep the advice that fits it — the round-16 change
|
||||
|
|
@ -755,6 +770,21 @@ describe("readOnDiskEncryption", () => {
|
|||
async function writeEncryptedFixtureWithPayload(
|
||||
payload: unknown,
|
||||
passphrase: string,
|
||||
): Promise<void> {
|
||||
return writeEncryptedFixtureWithPlaintext(
|
||||
JSON.stringify(payload),
|
||||
passphrase,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* The same, but writing `plaintext` verbatim — so a fixture can carry bytes
|
||||
* that are authentic under the passphrase and still not valid JSON, which
|
||||
* `JSON.stringify` can never produce.
|
||||
*/
|
||||
async function writeEncryptedFixtureWithPlaintext(
|
||||
plaintext: string,
|
||||
passphrase: string,
|
||||
): Promise<void> {
|
||||
const salt = crypto.randomBytes(16);
|
||||
const kdf = {
|
||||
|
|
@ -776,7 +806,7 @@ async function writeEncryptedFixtureWithPayload(
|
|||
const iv = crypto.randomBytes(12);
|
||||
const cipher = crypto.createCipheriv("aes-256-gcm", key, iv);
|
||||
const body = Buffer.concat([
|
||||
cipher.update(JSON.stringify(payload), "utf-8"),
|
||||
cipher.update(plaintext, "utf-8"),
|
||||
cipher.final(),
|
||||
]);
|
||||
const data = [iv, cipher.getAuthTag(), body]
|
||||
|
|
|
|||
Loading…
Reference in a new issue