cursedbelt-server 3.0.0 → 3.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/server/activity/index.d.ts +1 -1
- package/dist/server/activity/index.js +1 -1
- package/dist/server/master-lock/guard.js +18 -0
- package/package.json +1 -1
- package/src/server/activity/index.ts +1 -1
- package/src/server/master-lock/guard.spec.ts +25 -0
- package/src/server/master-lock/guard.ts +21 -0
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
*
|
|
11
11
|
* ── Retention ───────────────────────────────────────────────────────────────
|
|
12
12
|
* This package never deletes an event. An app whose stream is high-volume opts
|
|
13
|
-
* into the fleet standard (`
|
|
13
|
+
* into the fleet standard (`cursedbelt-server/retention`, in this package) by
|
|
14
14
|
* declaring the table, which puts the number under the owner's control in the
|
|
15
15
|
* station rather than in a constant here:
|
|
16
16
|
*
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
*
|
|
11
11
|
* ── Retention ───────────────────────────────────────────────────────────────
|
|
12
12
|
* This package never deletes an event. An app whose stream is high-volume opts
|
|
13
|
-
* into the fleet standard (`
|
|
13
|
+
* into the fleet standard (`cursedbelt-server/retention`, in this package) by
|
|
14
14
|
* declaring the table, which puts the number under the owner's control in the
|
|
15
15
|
* station rather than in a constant here:
|
|
16
16
|
*
|
|
@@ -168,6 +168,24 @@ export function createMasterLockGuard(options) {
|
|
|
168
168
|
}
|
|
169
169
|
if (path === MASTER_LOCK_PATHS.change) {
|
|
170
170
|
const body = await readJson(req);
|
|
171
|
+
/*
|
|
172
|
+
* 🔴 An OLDER client sends a freshly-minted `kdf` with a rotation, because that is
|
|
173
|
+
* what the wire looked like before accounts existed. Ignoring it silently would be
|
|
174
|
+
* a lockout, not a compatibility shim: the verifier in the same body was derived
|
|
175
|
+
* under THAT salt, so storing it would leave a hash nothing can ever match — the
|
|
176
|
+
* owner would set a new password, be logged out by the rotation, and find that
|
|
177
|
+
* neither the old nor the new one opens the app.
|
|
178
|
+
*
|
|
179
|
+
* There is no safe way to honour it either, since every account on this app shares
|
|
180
|
+
* one descriptor. So it is refused, loudly, with the one thing that distinguishes
|
|
181
|
+
* it from a wrong password.
|
|
182
|
+
*/
|
|
183
|
+
if (body?.kdf !== undefined) {
|
|
184
|
+
const supplied = body.kdf;
|
|
185
|
+
if (!lock.kdf || supplied?.salt !== lock.kdf.salt) {
|
|
186
|
+
return Response.json({ error: "stale-client" }, { status: 409, headers: noStore });
|
|
187
|
+
}
|
|
188
|
+
}
|
|
171
189
|
const result = await lock.change(req, {
|
|
172
190
|
currentVerifier: typeof body?.currentVerifier === "string" ? body.currentVerifier : "",
|
|
173
191
|
verifier: typeof body?.verifier === "string" ? body.verifier : "",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cursedbelt-server",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.1",
|
|
4
4
|
"license": "ISC",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "The app-facing Bun/Hono server tier of the cursedbelt split \u2014 storage, sharing, activity, guard, sync. React-free; cursedbelt-core below it.",
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
*
|
|
11
11
|
* ── Retention ───────────────────────────────────────────────────────────────
|
|
12
12
|
* This package never deletes an event. An app whose stream is high-volume opts
|
|
13
|
-
* into the fleet standard (`
|
|
13
|
+
* into the fleet standard (`cursedbelt-server/retention`, in this package) by
|
|
14
14
|
* declaring the table, which puts the number under the owner's control in the
|
|
15
15
|
* station rather than in a constant here:
|
|
16
16
|
*
|
|
@@ -157,6 +157,31 @@ describe("while locked", () => {
|
|
|
157
157
|
expect(JSON.stringify(body)).not.toContain("argon2");
|
|
158
158
|
});
|
|
159
159
|
|
|
160
|
+
test("🔴 a rotation carrying a STALE kdf is refused, not silently honoured", async () => {
|
|
161
|
+
// The pre-accounts wire minted a fresh salt on every rotation. Every account now
|
|
162
|
+
// derives under one shared descriptor, so honouring that would store a verifier hash
|
|
163
|
+
// nothing can ever match — the owner sets a new password, the rotation logs him out,
|
|
164
|
+
// and neither the old nor the new one opens the app. `409 stale-client` is the one
|
|
165
|
+
// refusal here that is deliberately distinguishable from a wrong password.
|
|
166
|
+
const { guard } = build();
|
|
167
|
+
const cookie = await open(guard);
|
|
168
|
+
const stale = await guard.handle(
|
|
169
|
+
post(
|
|
170
|
+
MASTER_LOCK_PATHS.change,
|
|
171
|
+
{
|
|
172
|
+
currentVerifier: VERIFIER,
|
|
173
|
+
kdf: { v: 1, alg: "PBKDF2-SHA256", iter: 600_000, salt: "ZZZZZZZZZZZZZZZZZZZZZZ" },
|
|
174
|
+
verifier: "whatever-that-salt-produced",
|
|
175
|
+
},
|
|
176
|
+
cookie,
|
|
177
|
+
),
|
|
178
|
+
);
|
|
179
|
+
expect(stale?.status).toBe(409);
|
|
180
|
+
expect(await stale?.json()).toEqual({ error: "stale-client" });
|
|
181
|
+
// And the password that already worked still does.
|
|
182
|
+
expect((await guard.handle(post(MASTER_LOCK_PATHS.unlock, { verifier: VERIFIER })))?.status).toBe(200);
|
|
183
|
+
});
|
|
184
|
+
|
|
160
185
|
test("🔴 status never says how many accounts this app has", async () => {
|
|
161
186
|
// The lock screen must read identically whether the owner keeps one tenant or ten:
|
|
162
187
|
// the number of master passwords is itself a fact worth hiding, and `status` is
|
|
@@ -259,6 +259,27 @@ export function createMasterLockGuard(options: MasterLockGuardOptions): MasterLo
|
|
|
259
259
|
}
|
|
260
260
|
if (path === MASTER_LOCK_PATHS.change) {
|
|
261
261
|
const body = await readJson(req);
|
|
262
|
+
/*
|
|
263
|
+
* 🔴 An OLDER client sends a freshly-minted `kdf` with a rotation, because that is
|
|
264
|
+
* what the wire looked like before accounts existed. Ignoring it silently would be
|
|
265
|
+
* a lockout, not a compatibility shim: the verifier in the same body was derived
|
|
266
|
+
* under THAT salt, so storing it would leave a hash nothing can ever match — the
|
|
267
|
+
* owner would set a new password, be logged out by the rotation, and find that
|
|
268
|
+
* neither the old nor the new one opens the app.
|
|
269
|
+
*
|
|
270
|
+
* There is no safe way to honour it either, since every account on this app shares
|
|
271
|
+
* one descriptor. So it is refused, loudly, with the one thing that distinguishes
|
|
272
|
+
* it from a wrong password.
|
|
273
|
+
*/
|
|
274
|
+
if (body?.kdf !== undefined) {
|
|
275
|
+
const supplied = body.kdf as { salt?: unknown };
|
|
276
|
+
if (!lock.kdf || supplied?.salt !== lock.kdf.salt) {
|
|
277
|
+
return Response.json(
|
|
278
|
+
{ error: "stale-client" },
|
|
279
|
+
{ status: 409, headers: noStore },
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
262
283
|
const result = await lock.change(req, {
|
|
263
284
|
currentVerifier: typeof body?.currentVerifier === "string" ? body.currentVerifier : "",
|
|
264
285
|
verifier: typeof body?.verifier === "string" ? body.verifier : "",
|