cursedbelt-server 4.13.0 → 4.13.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/auth/password.js +4 -6
- package/dist/server/guard/passwordVerifier.d.ts +1 -1
- package/dist/server/guard/passwordVerifier.js +3 -3
- package/dist/server/sync/tokens.d.ts +1 -1
- package/dist/server/sync/tokens.js +3 -2
- package/package.json +1 -1
- package/src/server/auth/password.ts +4 -5
- package/src/server/guard/passwordVerifier.ts +3 -3
- package/src/server/sync/tokens.ts +4 -3
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { pbkdf2 as pbkdf2Cb
|
|
1
|
+
import { pbkdf2 as pbkdf2Cb } from 'node:crypto';
|
|
2
|
+
import { constantTimeEqual } from 'cwip/constant-time';
|
|
2
3
|
import { promisify } from 'node:util';
|
|
3
4
|
/**
|
|
4
5
|
* Password hashing on `Bun.password` (argon2id — memory-hard, thread-pooled, zero
|
|
@@ -49,11 +50,8 @@ export async function verifyPassword(plain, stored) {
|
|
|
49
50
|
if (!salt || !expectedHex)
|
|
50
51
|
return false;
|
|
51
52
|
const derived = (await pbkdf2(plain, salt, LEGACY_ITERATIONS, LEGACY_KEYLEN, LEGACY_DIGEST)).toString('hex');
|
|
52
|
-
//
|
|
53
|
-
|
|
54
|
-
if (derived.length !== expectedHex.length)
|
|
55
|
-
return false;
|
|
56
|
-
return timingSafeEqual(Buffer.from(derived), Buffer.from(expectedHex));
|
|
53
|
+
// cwip's fold: no length guard to own, no early exit (task 322).
|
|
54
|
+
return constantTimeEqual(derived, expectedHex);
|
|
57
55
|
}
|
|
58
56
|
/**
|
|
59
57
|
* True when a stored hash is a legacy (non-argon2id) digest and should be upgraded.
|
|
@@ -14,7 +14,7 @@ export declare function sealPasswordVerifier(userId: string, password: Uint8Arra
|
|
|
14
14
|
* stored envelope is corruption, not a wrong password, and re-throws.
|
|
15
15
|
*
|
|
16
16
|
* The comparison is constant-time in the sensitive part: argon2id derivation
|
|
17
|
-
* dominates the timing, and GCM tag verification plus the final `
|
|
17
|
+
* dominates the timing, and GCM tag verification plus the final `constantTimeEqualBytes`
|
|
18
18
|
* are constant-time, so a right and a wrong password of equal length are
|
|
19
19
|
* indistinguishable by timing.
|
|
20
20
|
*/
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
* every path including a thrown KDF. Callers hand over ownership and must not
|
|
30
30
|
* touch the buffer afterwards.
|
|
31
31
|
*/
|
|
32
|
-
import {
|
|
32
|
+
import { constantTimeEqualBytes } from 'cwip/constant-time';
|
|
33
33
|
import { createSecretEnvelopeManager, mintEnvelopeSalt, parseSecretEnvelope, serializeSecretEnvelope, zeroize, } from 'cwip/guard';
|
|
34
34
|
import { isAppError } from '../errors.js';
|
|
35
35
|
/**
|
|
@@ -68,7 +68,7 @@ export function sealPasswordVerifier(userId, password, deriver) {
|
|
|
68
68
|
* stored envelope is corruption, not a wrong password, and re-throws.
|
|
69
69
|
*
|
|
70
70
|
* The comparison is constant-time in the sensitive part: argon2id derivation
|
|
71
|
-
* dominates the timing, and GCM tag verification plus the final `
|
|
71
|
+
* dominates the timing, and GCM tag verification plus the final `constantTimeEqualBytes`
|
|
72
72
|
* are constant-time, so a right and a wrong password of equal length are
|
|
73
73
|
* indistinguishable by timing.
|
|
74
74
|
*/
|
|
@@ -90,7 +90,7 @@ export function verifyPasswordEnvelope(userId, envelopeJson, password, deriver)
|
|
|
90
90
|
// openWithPassphrase re-derives a one-shot key from the envelope's own salt +
|
|
91
91
|
// the presented password, opens, and zeroizes `password` on every path.
|
|
92
92
|
opened = manager.openWithPassphrase(envelope, refFor(userId), password);
|
|
93
|
-
return
|
|
93
|
+
return constantTimeEqualBytes(opened, VERIFIER_PLAINTEXT);
|
|
94
94
|
}
|
|
95
95
|
catch (error) {
|
|
96
96
|
// GCM's rejection is the wrong-password verdict — the ONLY error we swallow.
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* per capability, sha256 at rest so a database read never yields a usable credential).
|
|
5
5
|
*
|
|
6
6
|
* The plaintext token is returned exactly once, at mint. Verification compares sha256
|
|
7
|
-
* digests via `
|
|
7
|
+
* digests via cwip's `constantTimeEqualBytes` — no length guard, no early exit.
|
|
8
8
|
*/
|
|
9
9
|
import type { Database } from "bun:sqlite";
|
|
10
10
|
export interface DeviceToken {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { createHash, randomBytes
|
|
1
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
2
|
+
import { constantTimeEqualBytes } from "cwip/constant-time";
|
|
2
3
|
const sha256 = (value) => createHash("sha256").update(value).digest();
|
|
3
4
|
export function createTokenStore(db) {
|
|
4
5
|
db.exec(`
|
|
@@ -35,7 +36,7 @@ export function createTokenStore(db) {
|
|
|
35
36
|
// the lookup timing depend on the presented value.
|
|
36
37
|
for (const raw of allLive.all()) {
|
|
37
38
|
const want = Buffer.from(raw.token_sha256);
|
|
38
|
-
if (
|
|
39
|
+
if (constantTimeEqualBytes(want, got)) {
|
|
39
40
|
if (raw.scope !== scope)
|
|
40
41
|
return null;
|
|
41
42
|
return toToken(raw);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cursedbelt-server",
|
|
3
|
-
"version": "4.13.
|
|
3
|
+
"version": "4.13.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.",
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { pbkdf2 as pbkdf2Cb
|
|
1
|
+
import { pbkdf2 as pbkdf2Cb } from 'node:crypto';
|
|
2
|
+
import { constantTimeEqual } from 'cwip/constant-time';
|
|
2
3
|
import { promisify } from 'node:util';
|
|
3
4
|
|
|
4
5
|
/**
|
|
@@ -55,10 +56,8 @@ export async function verifyPassword(plain: string, stored: string): Promise<boo
|
|
|
55
56
|
const derived = (
|
|
56
57
|
await pbkdf2(plain, salt, LEGACY_ITERATIONS, LEGACY_KEYLEN, LEGACY_DIGEST)
|
|
57
58
|
).toString('hex');
|
|
58
|
-
//
|
|
59
|
-
|
|
60
|
-
if (derived.length !== expectedHex.length) return false;
|
|
61
|
-
return timingSafeEqual(Buffer.from(derived), Buffer.from(expectedHex));
|
|
59
|
+
// cwip's fold: no length guard to own, no early exit (task 322).
|
|
60
|
+
return constantTimeEqual(derived, expectedHex);
|
|
62
61
|
}
|
|
63
62
|
|
|
64
63
|
/**
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
* every path including a thrown KDF. Callers hand over ownership and must not
|
|
30
30
|
* touch the buffer afterwards.
|
|
31
31
|
*/
|
|
32
|
-
import {
|
|
32
|
+
import { constantTimeEqualBytes } from 'cwip/constant-time';
|
|
33
33
|
import {
|
|
34
34
|
type Argon2idDeriver,
|
|
35
35
|
createSecretEnvelopeManager,
|
|
@@ -79,7 +79,7 @@ export function sealPasswordVerifier(userId: string, password: Uint8Array, deriv
|
|
|
79
79
|
* stored envelope is corruption, not a wrong password, and re-throws.
|
|
80
80
|
*
|
|
81
81
|
* The comparison is constant-time in the sensitive part: argon2id derivation
|
|
82
|
-
* dominates the timing, and GCM tag verification plus the final `
|
|
82
|
+
* dominates the timing, and GCM tag verification plus the final `constantTimeEqualBytes`
|
|
83
83
|
* are constant-time, so a right and a wrong password of equal length are
|
|
84
84
|
* indistinguishable by timing.
|
|
85
85
|
*/
|
|
@@ -106,7 +106,7 @@ export function verifyPasswordEnvelope(
|
|
|
106
106
|
// openWithPassphrase re-derives a one-shot key from the envelope's own salt +
|
|
107
107
|
// the presented password, opens, and zeroizes `password` on every path.
|
|
108
108
|
opened = manager.openWithPassphrase(envelope, refFor(userId), password);
|
|
109
|
-
return
|
|
109
|
+
return constantTimeEqualBytes(opened, VERIFIER_PLAINTEXT);
|
|
110
110
|
} catch (error) {
|
|
111
111
|
// GCM's rejection is the wrong-password verdict — the ONLY error we swallow.
|
|
112
112
|
if (isAppError(error) && error.code === 'SECRET_ENVELOPE_AUTH_FAILED') return false;
|
|
@@ -4,10 +4,11 @@
|
|
|
4
4
|
* per capability, sha256 at rest so a database read never yields a usable credential).
|
|
5
5
|
*
|
|
6
6
|
* The plaintext token is returned exactly once, at mint. Verification compares sha256
|
|
7
|
-
* digests via `
|
|
7
|
+
* digests via cwip's `constantTimeEqualBytes` — no length guard, no early exit.
|
|
8
8
|
*/
|
|
9
9
|
import type { Database } from "bun:sqlite";
|
|
10
|
-
import { createHash, randomBytes
|
|
10
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
11
|
+
import { constantTimeEqualBytes } from "cwip/constant-time";
|
|
11
12
|
|
|
12
13
|
export interface DeviceToken {
|
|
13
14
|
name: string;
|
|
@@ -79,7 +80,7 @@ export function createTokenStore(db: Database): TokenStore {
|
|
|
79
80
|
// the lookup timing depend on the presented value.
|
|
80
81
|
for (const raw of allLive.all() as Row[]) {
|
|
81
82
|
const want = Buffer.from(raw.token_sha256);
|
|
82
|
-
if (
|
|
83
|
+
if (constantTimeEqualBytes(want, got)) {
|
|
83
84
|
if (raw.scope !== scope) return null;
|
|
84
85
|
return toToken(raw);
|
|
85
86
|
}
|