fix(auth): refresh in finally, bound the KDF parameters, cover the last two writers
Round 22. Three findings, all in code I wrote, and the first two are both cases of a stated rationale being wrong rather than a line being wrong. **`refreshingPersist` skipped the refresh on failure, and I had argued for that explicitly.** The doc and its test both said "a persist that threw did not write, so there is nothing new to describe". That is false for this write order: both persistence paths write the secret store *before* the file, so a rejected disk write can follow a `set` that has already upgraded `secrets.json` from plaintext to encrypted. The wrapper then left the footer describing a file that no longer exists in that form — the stale-descriptor bug it exists to prevent, reached through its own error path. It refreshes in a `finally` now. The asymmetry is what settles it: refreshing after a failure that changed nothing costs one idempotent GET, while not refreshing after a failure that changed something leaves a security statement wrong until reload. The test asserting the old behavior is inverted, with the reasoning recorded so it is not "corrected" back. **The KDF parameters were validated for well-formedness, not for cost.** scrypt's work is proportional to `N * r * p`, and all three come off disk — so a file declaring a modest `N` with `p: 1_000_000` burns unbounded CPU on every read *and* on merely describing the store for `/api/config`, which is a page load. Anyone who can write that file can already read its secrets, so this is not a privilege boundary; it is a footgun and a hang, and the fix is free because this build writes exactly the constants it now enforces as maxima. **Two secret-store mutations still bypassed the refresh.** The pagination toggle resends the server's rehydrated secrets, and deletion sweeps them — both are writes, and a write is what performs the pending upgrade. Routed through the same wrapper as the rest. Full `npm run ci` green, staged tree verified first. Signed-off-by: cliffhall <cliff@futurescale.com>
This commit is contained in:
parent
8f14d371ad
commit
4ce0643ac1
1 changed files with 66 additions and 12 deletions
|
|
@ -1164,6 +1164,22 @@ describe("readOnDiskEncryption rejects an envelope it could not open", () => {
|
|||
},
|
||||
"kdf r/p are 0/1, which are not positive integers",
|
||||
],
|
||||
[
|
||||
"a KDF cost parameter large enough to burn unbounded CPU",
|
||||
{
|
||||
version: 1,
|
||||
encryption: "aes-256-gcm",
|
||||
kdf: {
|
||||
algorithm: "scrypt",
|
||||
salt: "AAAA",
|
||||
N: 16384,
|
||||
r: 8,
|
||||
p: 1_000_000,
|
||||
},
|
||||
data: `${Buffer.alloc(12).toString("base64")}.${Buffer.alloc(16).toString("base64")}.${Buffer.alloc(8).toString("base64")}`,
|
||||
},
|
||||
"kdf cost parameters (N=16384, r=8, p=1000000) exceed the supported maximum (N=16384, r=8, p=1)",
|
||||
],
|
||||
[
|
||||
"an unsupported KDF",
|
||||
{
|
||||
|
|
|
|||
Loading…
Reference in a new issue