dbsc-toolkit 1.4.0 → 2.0.0
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/README.md +104 -47
- package/dist/client/index.d.ts +10 -3
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +126 -3
- package/dist/client/index.js.map +1 -1
- package/dist/client/keystore.d.ts +8 -0
- package/dist/client/keystore.d.ts.map +1 -0
- package/dist/client/keystore.js +44 -0
- package/dist/client/keystore.js.map +1 -0
- package/dist/core/bound/index.d.ts +6 -0
- package/dist/core/bound/index.d.ts.map +1 -0
- package/dist/core/bound/index.js +4 -0
- package/dist/core/bound/index.js.map +1 -0
- package/dist/core/bound/refresh.d.ts +9 -0
- package/dist/core/bound/refresh.d.ts.map +1 -0
- package/dist/core/bound/refresh.js +52 -0
- package/dist/core/bound/refresh.js.map +1 -0
- package/dist/core/bound/registration.d.ts +12 -0
- package/dist/core/bound/registration.d.ts.map +1 -0
- package/dist/core/bound/registration.js +52 -0
- package/dist/core/bound/registration.js.map +1 -0
- package/dist/core/bound/verify.d.ts +2 -0
- package/dist/core/bound/verify.d.ts.map +1 -0
- package/dist/core/bound/verify.js +23 -0
- package/dist/core/bound/verify.js.map +1 -0
- package/dist/core/index.d.ts +3 -4
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +2 -3
- package/dist/core/index.js.map +1 -1
- package/dist/core/types.d.ts +5 -5
- package/dist/core/types.d.ts.map +1 -1
- package/dist/express/index.d.ts +4 -0
- package/dist/express/index.d.ts.map +1 -1
- package/dist/express/index.js +191 -3
- package/dist/express/index.js.map +1 -1
- package/dist/fastify/index.d.ts +5 -1
- package/dist/fastify/index.d.ts.map +1 -1
- package/dist/fastify/index.js +125 -3
- package/dist/fastify/index.js.map +1 -1
- package/dist/hono/index.d.ts +4 -6
- package/dist/hono/index.d.ts.map +1 -1
- package/dist/hono/index.js +118 -6
- package/dist/hono/index.js.map +1 -1
- package/dist/nextjs/index.d.ts +4 -0
- package/dist/nextjs/index.d.ts.map +1 -1
- package/dist/nextjs/index.js +137 -7
- package/dist/nextjs/index.js.map +1 -1
- package/package.json +2 -4
- package/dist/client/detect.d.ts +0 -3
- package/dist/client/detect.d.ts.map +0 -1
- package/dist/client/detect.js +0 -20
- package/dist/client/detect.js.map +0 -1
- package/dist/client/signals.d.ts +0 -9
- package/dist/client/signals.d.ts.map +0 -1
- package/dist/client/signals.js +0 -13
- package/dist/client/signals.js.map +0 -1
- package/dist/client/webauthn.d.ts +0 -3
- package/dist/client/webauthn.d.ts.map +0 -1
- package/dist/client/webauthn.js +0 -8
- package/dist/client/webauthn.js.map +0 -1
- package/dist/core/fallback/hmac.d.ts +0 -9
- package/dist/core/fallback/hmac.d.ts.map +0 -1
- package/dist/core/fallback/hmac.js +0 -37
- package/dist/core/fallback/hmac.js.map +0 -1
- package/dist/core/fallback/negotiate.d.ts +0 -9
- package/dist/core/fallback/negotiate.d.ts.map +0 -1
- package/dist/core/fallback/negotiate.js +0 -22
- package/dist/core/fallback/negotiate.js.map +0 -1
- package/dist/core/fallback/webauthn.d.ts +0 -10
- package/dist/core/fallback/webauthn.d.ts.map +0 -1
- package/dist/core/fallback/webauthn.js +0 -41
- package/dist/core/fallback/webauthn.js.map +0 -1
package/README.md
CHANGED
|
@@ -5,19 +5,36 @@
|
|
|
5
5
|
[](./LICENSE)
|
|
6
6
|
[](https://nodejs.org)
|
|
7
7
|
|
|
8
|
-
Server-side implementation of [Device Bound Session Credentials](https://w3c.github.io/webappsec-dbsc/) (DBSC) for Node.js.
|
|
8
|
+
Server-side implementation of [Device Bound Session Credentials](https://w3c.github.io/webappsec-dbsc/) (DBSC) for Node.js, with a silent Web Crypto polyfill for browsers that don't ship DBSC natively.
|
|
9
9
|
|
|
10
|
-
DBSC
|
|
10
|
+
DBSC binds session cookies to a hardware-resident private key inside the browser. A stolen cookie is useless without that key — which never leaves the user's device. Chromium 145+ does this natively. Firefox, Safari, and older Chromium browsers get the same cryptographic refresh-signing protection via a Web Crypto polyfill that activates automatically after login.
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
| Browser | Tier | Key location |
|
|
13
|
+
|---------|------|--------------|
|
|
14
|
+
| Chromium 145+ (Chrome, Edge, Brave, Opera, Arc, Vivaldi) | `dbsc` | TPM / Secure Enclave / Android Keystore |
|
|
15
|
+
| Firefox, Safari, older Chromium | `bound` | Browser keystore (non-extractable Web Crypto key) |
|
|
16
|
+
|
|
17
|
+
Both tiers defeat the entire "stolen cookie replayed from another device" class of attack. `dbsc` additionally defeats infostealer malware reading the browser profile directory — the `bound` polyfill key is software-bound.
|
|
18
|
+
|
|
19
|
+
Verified end-to-end against Chrome 147 on Windows with a real TPM 2.0.
|
|
20
|
+
|
|
21
|
+
**New here?** Read [HOW-IT-WORKS.md](./HOW-IT-WORKS.md) first.
|
|
13
22
|
|
|
14
23
|
## Live demo
|
|
15
24
|
|
|
16
25
|
Try it: <https://dbsc-toolkit.onrender.com/>
|
|
17
26
|
|
|
18
|
-
|
|
27
|
+
Sign up, log in, then click **Check session**:
|
|
28
|
+
- On Chromium 145+ you land on `tier: "dbsc"` within a second of login.
|
|
29
|
+
- On Firefox/Safari/older Chromium you land on `tier: "bound"` within ~3 seconds (the polyfill activates silently).
|
|
30
|
+
|
|
31
|
+
The demo uses a 60-second bound-cookie TTL so refresh fires fast — watch DevTools Network for `POST /dbsc/refresh` (native) or `POST /dbsc-bound/refresh` (polyfill) after the cookie expires. Source in [examples/express/](./examples/express/).
|
|
32
|
+
|
|
33
|
+
The demo runs on `RedisStorage` (Upstash) by default, so sessions survive deploys, cold starts, and laptop reboots. Locally without `REDIS_URL`, it falls back to `MemoryStorage` — fine for one terminal session, wiped on every restart.
|
|
19
34
|
|
|
20
|
-
|
|
35
|
+
Two protected routes show the gating pattern: `/profile` requires `tier === "dbsc"` specifically (TPM-only flows); `/profile-soft` accepts either `"dbsc"` or `"bound"`.
|
|
36
|
+
|
|
37
|
+
> **Hitting `not authenticated` after a few login/logout cycles?** That's Chrome's DBSC quota — the browser's anti-abuse throttle. The demo surfaces it explicitly now (red banner + reason text in the response). To recover: `chrome://settings/clearBrowserData` → Last hour → Cookies and site data → clear, or open an Incognito window. The quota is per `(browser install, origin)`, so production users (who log in once and stay logged in) essentially never trip it.
|
|
21
38
|
|
|
22
39
|
## Install
|
|
23
40
|
|
|
@@ -68,6 +85,17 @@ app.listen(3000);
|
|
|
68
85
|
|
|
69
86
|
`app.use(dbsc(...))` mounts `POST /dbsc/registration` and `POST /dbsc/refresh` automatically — Chrome drives both, your code never sees them. `bindSession()` is the one-liner you add to your login route: it writes the session row, issues a challenge, builds the registration header (both legacy + new names), and sets the two short-lived cookies Chrome needs to complete binding.
|
|
70
87
|
|
|
88
|
+
Call `bindSession()` after you have verified the user's credentials — in the login route, or in a signup route that auto-logs the user in. Calling it in a bare signup that does not establish an authenticated session is not useful: there is no session to bind yet.
|
|
89
|
+
|
|
90
|
+
### The registration race after login
|
|
91
|
+
|
|
92
|
+
Chrome posts to `/dbsc/registration` *after* the login response returns. The handshake includes TPM key generation, JWS signing, and a network round-trip, so it commonly takes 300 ms to 1.5 s — sometimes longer on a cold device. If the page immediately requests `/me` or `/payment` and gates on `tier === "dbsc"`, the check can land before registration completes and report `tier: "none"` even on a fully supported browser. Two ways to absorb this on the client:
|
|
93
|
+
|
|
94
|
+
- **Status indicator with short polling.** After a successful login, poll a low-cost endpoint (`/me` works) every 500 ms for up to ~8 s — long enough to cover both native DBSC and the bound-polyfill activation window. Stop when `tier !== "none"`; show a small "Session bound" badge. The live demo uses this pattern — see `pollDbscReady` in [examples/express/src/server.js](./examples/express/src/server.js).
|
|
95
|
+
- **One-shot auto-retry on the first call after login.** If a tier-gated request returns `tier: "none"` within the first few seconds of a fresh login, wait ~1 s and retry once. Cheap, invisible to the user, and avoids the false demotion entirely.
|
|
96
|
+
|
|
97
|
+
For server-driven flows (a payment route called from a server redirect immediately after login), either pattern works. The race only matters when the very first authenticated request is also a tier check; routine browsing past the first second is unaffected.
|
|
98
|
+
|
|
71
99
|
A full demo with `/me`, `/logout`, and `/clear-cookies` is in [examples/express/src/server.js](./examples/express/src/server.js).
|
|
72
100
|
|
|
73
101
|
## Adding DBSC to an existing app
|
|
@@ -83,12 +111,12 @@ Full integration story, per-route policy table, and rollout timeline in [docs/in
|
|
|
83
111
|
|
|
84
112
|
| Import | What it is |
|
|
85
113
|
|--------|------------|
|
|
86
|
-
| `dbsc-toolkit` | Core types, crypto, protocol functions |
|
|
114
|
+
| `dbsc-toolkit` | Core types, crypto, protocol functions (native DBSC + bound polyfill) |
|
|
87
115
|
| `dbsc-toolkit/express` | Express middleware |
|
|
88
116
|
| `dbsc-toolkit/fastify` | Fastify plugin |
|
|
89
117
|
| `dbsc-toolkit/hono` | Hono middleware |
|
|
90
118
|
| `dbsc-toolkit/nextjs` | Next.js App Router middleware + handlers |
|
|
91
|
-
| `dbsc-toolkit/client` | Browser SDK for
|
|
119
|
+
| `dbsc-toolkit/client` | Browser SDK — `initBoundDbsc()` for the polyfill |
|
|
92
120
|
| `dbsc-toolkit/storage/memory` | In-memory storage (dev/test) |
|
|
93
121
|
| `dbsc-toolkit/storage/redis` | Redis storage |
|
|
94
122
|
| `dbsc-toolkit/storage/postgres` | Postgres storage |
|
|
@@ -97,49 +125,55 @@ Tree-shaking eliminates anything you don't import.
|
|
|
97
125
|
|
|
98
126
|
## How a verified flow looks
|
|
99
127
|
|
|
128
|
+
On Chromium 145+:
|
|
129
|
+
|
|
100
130
|
1. User hits `POST /login`. Server creates a session, issues a challenge, sets `Secure-Session-Registration` response header and two short-lived cookies (`__Host-dbsc-reg`, `__Host-dbsc-challenge`).
|
|
101
|
-
2.
|
|
102
|
-
3. Middleware verifies the JWS, stores the public key bound to the session, sets `__Host-dbsc-session` cookie
|
|
103
|
-
4.
|
|
131
|
+
2. The browser immediately POSTs to `/dbsc/registration` with `Secure-Session-Response: <jws>`. The JWS carries the device public key signed by the matching private key (held in the platform's hardware key store).
|
|
132
|
+
3. Middleware verifies the JWS, stores the public key bound to the session, sets `__Host-dbsc-session` cookie. `tier` is now `"dbsc"`.
|
|
133
|
+
4. Every refresh cycle (default 10 min) the browser signs a fresh challenge with the hardware-resident key. A copied cookie cannot pass refresh — the attacker has no key.
|
|
134
|
+
|
|
135
|
+
On Firefox / Safari / older Chromium (with the `initBoundDbsc()` client SDK loaded on the page):
|
|
104
136
|
|
|
105
|
-
|
|
137
|
+
1. Same `/login` response — the registration headers are sent, but the browser ignores them.
|
|
138
|
+
2. After a 3-second probe (waiting in case native DBSC is just slow), the client SDK generates a non-extractable ECDSA P-256 keypair via Web Crypto, exports the public key, and POSTs to `/dbsc-bound/registration` with the signed challenge.
|
|
139
|
+
3. Middleware verifies the signature against the JWK, stores the public key, sets `__Host-dbsc-session`. `tier` is now `"bound"`.
|
|
140
|
+
4. Every refresh cycle the SDK calls `/dbsc-bound/refresh` with a fresh signature. A copied cookie alone has no key in IndexedDB on the attacker's machine, so refresh fails.
|
|
141
|
+
|
|
142
|
+
For the complete protocol walk-through with every header value and timing detail, see [HOW-IT-WORKS.md](./HOW-IT-WORKS.md). The bound polyfill protocol is documented in [docs/bound-polyfill.md](./docs/bound-polyfill.md).
|
|
106
143
|
|
|
107
144
|
## Using the tier to actually defend
|
|
108
145
|
|
|
109
|
-
Setting up the middleware does not protect anything on its own. The library
|
|
146
|
+
Setting up the middleware does not protect anything on its own. The library exposes a tier; **enforcing it is your responsibility**.
|
|
147
|
+
|
|
148
|
+
Most routes should gate on `tier !== "none"`:
|
|
110
149
|
|
|
111
150
|
```ts
|
|
112
|
-
app.get("/
|
|
113
|
-
if (res.locals.dbsc.tier
|
|
114
|
-
return res.status(403).json({ error: "
|
|
151
|
+
app.get("/dashboard", (req, res) => {
|
|
152
|
+
if (res.locals.dbsc.tier === "none") {
|
|
153
|
+
return res.status(403).json({ error: "session not bound" });
|
|
115
154
|
}
|
|
116
|
-
// safe to
|
|
155
|
+
// safe — request is bound to a hardware-resident or browser-resident key
|
|
117
156
|
});
|
|
118
157
|
```
|
|
119
158
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
Suggested handling per tier in a real application:
|
|
123
|
-
|
|
124
|
-
- `tier === "dbsc"`: full access. Payments, account changes, anything sensitive.
|
|
125
|
-
- `tier === "webauthn"`: most access. Hardware-bound via platform authenticator.
|
|
126
|
-
- `tier === "hmac"`: read-only or low-risk actions. The binding is best-effort.
|
|
127
|
-
- `tier === "none"`: treat as unauthenticated. Force re-login, revoke the session, log a `session_stolen` candidate, depending on context.
|
|
128
|
-
|
|
129
|
-
Putting this in a single middleware keeps it consistent:
|
|
159
|
+
For genuinely sensitive routes where you want the TPM-backed guarantee specifically (defeats infostealer malware, not just remote cookie theft), gate on `"dbsc"`:
|
|
130
160
|
|
|
131
161
|
```ts
|
|
132
|
-
|
|
162
|
+
app.post("/payment", (req, res) => {
|
|
133
163
|
if (res.locals.dbsc.tier !== "dbsc") {
|
|
134
|
-
return res.status(
|
|
164
|
+
return res.status(403).json({ error: "hardware-bound session required" });
|
|
135
165
|
}
|
|
136
|
-
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
app.post("/payment", requireDbsc, handler);
|
|
140
|
-
app.post("/account/email", requireDbsc, handler);
|
|
166
|
+
});
|
|
141
167
|
```
|
|
142
168
|
|
|
169
|
+
If you skip the tier check entirely, a stolen cookie works just like before — the library bought you nothing. The whole point is the demotion: when a cookie is replayed without a valid refresh signature, tier drops to `"none"` and your gate refuses the request.
|
|
170
|
+
|
|
171
|
+
Tier handling at a glance:
|
|
172
|
+
|
|
173
|
+
- `tier === "dbsc"`: hardware-bound via TPM / Secure Enclave / Android Keystore. Full access.
|
|
174
|
+
- `tier === "bound"`: software-bound via Web Crypto + IndexedDB. Defeats XSS, network capture, and cookie paste-to-other-machine. Does not defeat infostealer malware on the user's device.
|
|
175
|
+
- `tier === "none"`: treat as unauthenticated for any route you care about. Force re-login, log a `session_stolen` candidate, depending on context.
|
|
176
|
+
|
|
143
177
|
See [docs/security/best-practices.md](./docs/security/best-practices.md) for the full tier-policy guidance.
|
|
144
178
|
|
|
145
179
|
## Local testing
|
|
@@ -182,16 +216,13 @@ const result = await handleRefresh({
|
|
|
182
216
|
|
|
183
217
|
## Protection tiers
|
|
184
218
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
|
188
|
-
|
|
189
|
-
| `
|
|
190
|
-
| `webauthn` | Platform authenticator | Hardware binding via platform authenticator |
|
|
191
|
-
| `hmac` | HMAC + browser signals | Best-effort context binding, not hardware |
|
|
192
|
-
| `none` | Standard cookie | No additional binding |
|
|
219
|
+
| Tier | Mechanism | Protects against |
|
|
220
|
+
|------|-----------|------------------|
|
|
221
|
+
| `dbsc` | Native W3C DBSC, key in TPM / Secure Enclave / Android Keystore | Cookie theft (XSS, network, logs, paste-to-other-browser) **and** infostealer malware reading the browser profile |
|
|
222
|
+
| `bound` | Web Crypto polyfill, non-extractable ECDSA P-256 key in IndexedDB | Cookie theft. Does not defeat infostealer malware on the user's machine. |
|
|
223
|
+
| `none` | Plain cookie | Nothing the cookie itself doesn't already do |
|
|
193
224
|
|
|
194
|
-
The tier is available at `res.locals.dbsc.tier` (Express), `
|
|
225
|
+
The tier is available at `res.locals.dbsc.tier` (Express), `req.dbsc.tier` (Fastify), `c.get("dbsc").tier` (Hono), and via `getDbscSession()` (Next.js). Use it to apply per-route policy — for example, block payment flows when `tier !== "dbsc"` and accept any binding for everything else with `tier !== "none"`.
|
|
195
226
|
|
|
196
227
|
## Storage
|
|
197
228
|
|
|
@@ -235,7 +266,7 @@ app.use(dbsc({
|
|
|
235
266
|
}));
|
|
236
267
|
```
|
|
237
268
|
|
|
238
|
-
Event types: `registration`, `refresh`, `verification_failure`, `session_stolen`, `
|
|
269
|
+
Event types: `registration`, `refresh`, `verification_failure`, `session_stolen`, `tier_change`.
|
|
239
270
|
|
|
240
271
|
## Skipped sessions
|
|
241
272
|
|
|
@@ -259,7 +290,7 @@ The quota is scoped per `(browser install, origin)`, not per origin globally. A
|
|
|
259
290
|
|
|
260
291
|
## Header naming
|
|
261
292
|
|
|
262
|
-
The W3C draft renamed the headers from `Sec-Session-*` to `Secure-Session-*`.
|
|
293
|
+
The W3C draft renamed the headers from `Sec-Session-*` to `Secure-Session-*`. Chromium 145+ acts on the new names. The middleware reads both and writes both for compatibility. If you build response headers manually, send both:
|
|
263
294
|
|
|
264
295
|
```ts
|
|
265
296
|
res.setHeader("Secure-Session-Registration", header);
|
|
@@ -270,16 +301,42 @@ res.setHeader("Sec-Session-Registration", header);
|
|
|
270
301
|
|
|
271
302
|
Defense-in-depth layer. Does not replace TLS, secure cookie flags, MFA, or server hardening.
|
|
272
303
|
|
|
273
|
-
|
|
304
|
+
The `bound` polyfill defeats remote cookie theft but is not hardware-bound — the key lives in IndexedDB on the user's disk and can be read by infostealer malware with filesystem access on the user's machine. For routes that must defeat that threat, gate on `tier === "dbsc"` specifically. For everything else, `tier !== "none"` is the right gate.
|
|
274
305
|
|
|
275
|
-
See [SECURITY.md](./SECURITY.md) for reporting vulnerabilities.
|
|
306
|
+
See [SECURITY.md](./SECURITY.md) for reporting vulnerabilities and [docs/security/threat-model.md](./docs/security/threat-model.md) for the per-tier STRIDE breakdown.
|
|
276
307
|
|
|
277
308
|
## Project status
|
|
278
309
|
|
|
279
310
|
- Single package on npm: `dbsc-toolkit`
|
|
280
|
-
-
|
|
311
|
+
- Native DBSC: Chromium 145+ on Windows (TPM 2.0) / macOS Apple Silicon (Secure Enclave) / Android (Keystore)
|
|
312
|
+
- Bound polyfill: every browser with Web Crypto + IndexedDB — Firefox, Safari, older Chromium
|
|
313
|
+
- Verified end-to-end on Chrome 147 / Windows / TPM 2.0 (other Chromium browsers and platforms should work but not independently verified)
|
|
281
314
|
- No third-party security audit yet
|
|
282
315
|
|
|
316
|
+
## Production readiness
|
|
317
|
+
|
|
318
|
+
Honest table — what you're getting and where the rough edges are.
|
|
319
|
+
|
|
320
|
+
| Area | Status | Confidence |
|
|
321
|
+
|------|--------|-----------|
|
|
322
|
+
| Core protocol (registration + refresh + verification) | Stable | High — verified against real Chrome 147 + TPM 2.0 |
|
|
323
|
+
| Bound polyfill (`/dbsc-bound/*` + client SDK) | New in v2.0.0 | Medium — unit-tested; cross-browser verification on the live demo |
|
|
324
|
+
| Express adapter | Stable | High — used in the live demo, exercised on Render |
|
|
325
|
+
| Fastify / Hono / Next.js adapters | Stable | Medium — unit tests pass, share core code with Express, not battle-tested in production |
|
|
326
|
+
| `MemoryStorage` | Dev / test only | N/A — explicitly non-production |
|
|
327
|
+
| `RedisStorage` | Stable | Medium — atomic challenge consume via Lua, tested locally |
|
|
328
|
+
| `PostgresStorage` | Stable | Medium — migrations included, tested locally |
|
|
329
|
+
| Security audit | None | — |
|
|
330
|
+
| W3C spec stability | Draft, library tracks Chromium's implementation | Spec may evolve; expect occasional wire-format adjustments |
|
|
331
|
+
|
|
332
|
+
**Should you use this in production?** Yes, with three conditions:
|
|
333
|
+
|
|
334
|
+
1. **Use Redis or Postgres storage**, not memory. Memory storage on a server that ever restarts produces a broken loop where browsers hold cookies that no longer match any stored key.
|
|
335
|
+
2. **Treat it as defense-in-depth**, never the only auth layer. Your existing session cookie, password, MFA, rate limiting — all still required. This library raises the floor on session-replay attacks; it doesn't replace anything else.
|
|
336
|
+
3. **Pin a version.** Pin `dbsc-toolkit@~2.0.0` (patch updates only) and read the changelog before bumping. v2 dropped the HMAC and WebAuthn tiers — see CHANGELOG for the migration path.
|
|
337
|
+
|
|
338
|
+
The realistic adoption pattern: ship it as the second layer behind your existing auth. The bound polyfill means you don't have to lock non-Chromium users out. Gate genuinely high-value actions (payments, password change, admin) on `tier === "dbsc"`; gate everything else on `tier !== "none"`. See [docs/integrating-existing-auth.md](./docs/integrating-existing-auth.md).
|
|
339
|
+
|
|
283
340
|
## License
|
|
284
341
|
|
|
285
342
|
Apache 2.0
|
package/dist/client/index.d.ts
CHANGED
|
@@ -1,4 +1,11 @@
|
|
|
1
|
-
export
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
export interface InitBoundDbscOptions {
|
|
2
|
+
statePath?: string;
|
|
3
|
+
challengePath?: string;
|
|
4
|
+
registrationPath?: string;
|
|
5
|
+
refreshPath?: string;
|
|
6
|
+
nativeProbeWindowMs?: number;
|
|
7
|
+
refreshMarginMs?: number;
|
|
8
|
+
}
|
|
9
|
+
export declare function initBoundDbsc(options?: InitBoundDbscOptions): Promise<void>;
|
|
10
|
+
export declare function stopBoundDbsc(): void;
|
|
4
11
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/client/index.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/client/index.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,oBAAoB;IACnC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AA0CD,wBAAsB,aAAa,CAAC,OAAO,GAAE,oBAAyB,GAAG,OAAO,CAAC,IAAI,CAAC,CA4CrF;AAED,wBAAgB,aAAa,IAAI,IAAI,CAKpC"}
|
package/dist/client/index.js
CHANGED
|
@@ -1,4 +1,127 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
import { clearKeyRecord, getKeyRecord, setKeyRecord } from "./keystore.js";
|
|
2
|
+
const DEFAULTS = {
|
|
3
|
+
statePath: "/dbsc-bound/state",
|
|
4
|
+
challengePath: "/dbsc-bound/challenge",
|
|
5
|
+
registrationPath: "/dbsc-bound/registration",
|
|
6
|
+
refreshPath: "/dbsc-bound/refresh",
|
|
7
|
+
nativeProbeWindowMs: 3000,
|
|
8
|
+
refreshMarginMs: 5000,
|
|
9
|
+
};
|
|
10
|
+
let refreshTimer = null;
|
|
11
|
+
export async function initBoundDbsc(options = {}) {
|
|
12
|
+
if (typeof window === "undefined" || typeof indexedDB === "undefined")
|
|
13
|
+
return;
|
|
14
|
+
const cfg = {
|
|
15
|
+
statePath: options.statePath ?? DEFAULTS.statePath,
|
|
16
|
+
challengePath: options.challengePath ?? DEFAULTS.challengePath,
|
|
17
|
+
registrationPath: options.registrationPath ?? DEFAULTS.registrationPath,
|
|
18
|
+
refreshPath: options.refreshPath ?? DEFAULTS.refreshPath,
|
|
19
|
+
nativeProbeWindowMs: options.nativeProbeWindowMs ?? DEFAULTS.nativeProbeWindowMs,
|
|
20
|
+
refreshMarginMs: options.refreshMarginMs ?? DEFAULTS.refreshMarginMs,
|
|
21
|
+
};
|
|
22
|
+
const state = await fetchState(cfg.statePath);
|
|
23
|
+
if (state.phase === "unbound") {
|
|
24
|
+
await clearKeyRecord().catch(() => { });
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
if (state.phase === "bound") {
|
|
28
|
+
if (state.tier === "dbsc")
|
|
29
|
+
return;
|
|
30
|
+
const rec = await getKeyRecord().catch(() => null);
|
|
31
|
+
if (!rec || rec.sessionId !== state.sessionId) {
|
|
32
|
+
await clearKeyRecord().catch(() => { });
|
|
33
|
+
const fresh = await fetchState(cfg.statePath);
|
|
34
|
+
if (fresh.phase === "needs-registration") {
|
|
35
|
+
await runRegistration(fresh.sessionId, fresh.challenge, cfg);
|
|
36
|
+
scheduleRefresh(cfg, state.refreshIntervalMs);
|
|
37
|
+
}
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
scheduleRefresh(cfg, state.refreshIntervalMs);
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
await sleep(cfg.nativeProbeWindowMs);
|
|
44
|
+
const recheck = await fetchState(cfg.statePath);
|
|
45
|
+
if (recheck.phase === "bound" && recheck.tier === "dbsc")
|
|
46
|
+
return;
|
|
47
|
+
if (recheck.phase !== "needs-registration")
|
|
48
|
+
return;
|
|
49
|
+
await runRegistration(recheck.sessionId, recheck.challenge, cfg);
|
|
50
|
+
const final = await fetchState(cfg.statePath);
|
|
51
|
+
if (final.phase === "bound")
|
|
52
|
+
scheduleRefresh(cfg, final.refreshIntervalMs);
|
|
53
|
+
}
|
|
54
|
+
export function stopBoundDbsc() {
|
|
55
|
+
if (refreshTimer !== null) {
|
|
56
|
+
clearTimeout(refreshTimer);
|
|
57
|
+
refreshTimer = null;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
async function fetchState(path) {
|
|
61
|
+
const r = await fetch(path, { credentials: "include" });
|
|
62
|
+
return (await r.json());
|
|
63
|
+
}
|
|
64
|
+
async function runRegistration(sessionId, challenge, cfg) {
|
|
65
|
+
await clearKeyRecord().catch(() => { });
|
|
66
|
+
const keyPair = await crypto.subtle.generateKey({ name: "ECDSA", namedCurve: "P-256" }, false, ["sign", "verify"]);
|
|
67
|
+
const publicKey = await crypto.subtle.exportKey("jwk", keyPair.publicKey);
|
|
68
|
+
const signature = await signMessage(keyPair.privateKey, challenge);
|
|
69
|
+
const res = await fetch(cfg.registrationPath, {
|
|
70
|
+
method: "POST",
|
|
71
|
+
credentials: "include",
|
|
72
|
+
headers: { "Content-Type": "application/json" },
|
|
73
|
+
body: JSON.stringify({ publicKey, signature, challenge }),
|
|
74
|
+
});
|
|
75
|
+
if (!res.ok) {
|
|
76
|
+
throw new Error(`bound registration failed: ${res.status}`);
|
|
77
|
+
}
|
|
78
|
+
await setKeyRecord({ sessionId, keyPair });
|
|
79
|
+
}
|
|
80
|
+
async function runRefresh(cfg) {
|
|
81
|
+
const rec = await getKeyRecord().catch(() => null);
|
|
82
|
+
if (!rec)
|
|
83
|
+
return false;
|
|
84
|
+
const cRes = await fetch(cfg.challengePath, { credentials: "include" });
|
|
85
|
+
if (!cRes.ok)
|
|
86
|
+
return false;
|
|
87
|
+
const { challenge } = (await cRes.json());
|
|
88
|
+
const timestamp = Date.now();
|
|
89
|
+
const message = `${challenge}.${timestamp}`;
|
|
90
|
+
const signature = await signMessage(rec.keyPair.privateKey, message);
|
|
91
|
+
const res = await fetch(cfg.refreshPath, {
|
|
92
|
+
method: "POST",
|
|
93
|
+
credentials: "include",
|
|
94
|
+
headers: { "Content-Type": "application/json" },
|
|
95
|
+
body: JSON.stringify({ challenge, signature, timestamp }),
|
|
96
|
+
});
|
|
97
|
+
return res.ok;
|
|
98
|
+
}
|
|
99
|
+
function scheduleRefresh(cfg, intervalMs) {
|
|
100
|
+
if (refreshTimer !== null)
|
|
101
|
+
clearTimeout(refreshTimer);
|
|
102
|
+
const wait = Math.max(1000, intervalMs - cfg.refreshMarginMs);
|
|
103
|
+
refreshTimer = setTimeout(async () => {
|
|
104
|
+
const ok = await runRefresh(cfg).catch(() => false);
|
|
105
|
+
if (ok) {
|
|
106
|
+
scheduleRefresh(cfg, intervalMs);
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
refreshTimer = null;
|
|
110
|
+
}
|
|
111
|
+
}, wait);
|
|
112
|
+
}
|
|
113
|
+
async function signMessage(privateKey, message) {
|
|
114
|
+
const data = new TextEncoder().encode(message);
|
|
115
|
+
const sig = await crypto.subtle.sign({ name: "ECDSA", hash: "SHA-256" }, privateKey, data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength));
|
|
116
|
+
return base64urlEncode(new Uint8Array(sig));
|
|
117
|
+
}
|
|
118
|
+
function base64urlEncode(bytes) {
|
|
119
|
+
let s = "";
|
|
120
|
+
for (let i = 0; i < bytes.length; i++)
|
|
121
|
+
s += String.fromCharCode(bytes[i]);
|
|
122
|
+
return btoa(s).replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_");
|
|
123
|
+
}
|
|
124
|
+
function sleep(ms) {
|
|
125
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
126
|
+
}
|
|
4
127
|
//# sourceMappingURL=index.js.map
|
package/dist/client/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/client/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/client/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export interface KeyRecord {
|
|
2
|
+
sessionId: string;
|
|
3
|
+
keyPair: CryptoKeyPair;
|
|
4
|
+
}
|
|
5
|
+
export declare function getKeyRecord(): Promise<KeyRecord | null>;
|
|
6
|
+
export declare function setKeyRecord(rec: KeyRecord): Promise<void>;
|
|
7
|
+
export declare function clearKeyRecord(): Promise<void>;
|
|
8
|
+
//# sourceMappingURL=keystore.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"keystore.d.ts","sourceRoot":"","sources":["../../src/client/keystore.ts"],"names":[],"mappings":"AAIA,MAAM,WAAW,SAAS;IACxB,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,aAAa,CAAC;CACxB;AAgBD,wBAAsB,YAAY,IAAI,OAAO,CAAC,SAAS,GAAG,IAAI,CAAC,CAQ9D;AAED,wBAAsB,YAAY,CAAC,GAAG,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC,CAQhE;AAED,wBAAsB,cAAc,IAAI,OAAO,CAAC,IAAI,CAAC,CAQpD"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
const DB_NAME = "dbsc-toolkit";
|
|
2
|
+
const STORE_NAME = "bound";
|
|
3
|
+
const KEY_RECORD_KEY = "key-record";
|
|
4
|
+
function openDb() {
|
|
5
|
+
return new Promise((resolve, reject) => {
|
|
6
|
+
const req = indexedDB.open(DB_NAME, 1);
|
|
7
|
+
req.onupgradeneeded = () => {
|
|
8
|
+
const db = req.result;
|
|
9
|
+
if (!db.objectStoreNames.contains(STORE_NAME)) {
|
|
10
|
+
db.createObjectStore(STORE_NAME);
|
|
11
|
+
}
|
|
12
|
+
};
|
|
13
|
+
req.onsuccess = () => resolve(req.result);
|
|
14
|
+
req.onerror = () => reject(req.error);
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
export async function getKeyRecord() {
|
|
18
|
+
const db = await openDb();
|
|
19
|
+
return new Promise((resolve, reject) => {
|
|
20
|
+
const tx = db.transaction(STORE_NAME, "readonly");
|
|
21
|
+
const req = tx.objectStore(STORE_NAME).get(KEY_RECORD_KEY);
|
|
22
|
+
req.onsuccess = () => resolve(req.result ?? null);
|
|
23
|
+
req.onerror = () => reject(req.error);
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
export async function setKeyRecord(rec) {
|
|
27
|
+
const db = await openDb();
|
|
28
|
+
return new Promise((resolve, reject) => {
|
|
29
|
+
const tx = db.transaction(STORE_NAME, "readwrite");
|
|
30
|
+
tx.objectStore(STORE_NAME).put(rec, KEY_RECORD_KEY);
|
|
31
|
+
tx.oncomplete = () => resolve();
|
|
32
|
+
tx.onerror = () => reject(tx.error);
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
export async function clearKeyRecord() {
|
|
36
|
+
const db = await openDb();
|
|
37
|
+
return new Promise((resolve, reject) => {
|
|
38
|
+
const tx = db.transaction(STORE_NAME, "readwrite");
|
|
39
|
+
tx.objectStore(STORE_NAME).delete(KEY_RECORD_KEY);
|
|
40
|
+
tx.oncomplete = () => resolve();
|
|
41
|
+
tx.onerror = () => reject(tx.error);
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
//# sourceMappingURL=keystore.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"keystore.js","sourceRoot":"","sources":["../../src/client/keystore.ts"],"names":[],"mappings":"AAAA,MAAM,OAAO,GAAG,cAAc,CAAC;AAC/B,MAAM,UAAU,GAAG,OAAO,CAAC;AAC3B,MAAM,cAAc,GAAG,YAAY,CAAC;AAOpC,SAAS,MAAM;IACb,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;QACrC,MAAM,GAAG,GAAG,SAAS,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC;QACvC,GAAG,CAAC,eAAe,GAAG,GAAG,EAAE;YACzB,MAAM,EAAE,GAAG,GAAG,CAAC,MAAM,CAAC;YACtB,IAAI,CAAC,EAAE,CAAC,gBAAgB,CAAC,QAAQ,CAAC,UAAU,CAAC,EAAE,CAAC;gBAC9C,EAAE,CAAC,iBAAiB,CAAC,UAAU,CAAC,CAAC;YACnC,CAAC;QACH,CAAC,CAAC;QACF,GAAG,CAAC,SAAS,GAAG,GAAG,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;QAC1C,GAAG,CAAC,OAAO,GAAG,GAAG,EAAE,CAAC,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;IACxC,CAAC,CAAC,CAAC;AACL,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,YAAY;IAChC,MAAM,EAAE,GAAG,MAAM,MAAM,EAAE,CAAC;IAC1B,OAAO,IAAI,OAAO,CAAmB,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;QACvD,MAAM,EAAE,GAAG,EAAE,CAAC,WAAW,CAAC,UAAU,EAAE,UAAU,CAAC,CAAC;QAClD,MAAM,GAAG,GAAG,EAAE,CAAC,WAAW,CAAC,UAAU,CAAC,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC;QAC3D,GAAG,CAAC,SAAS,GAAG,GAAG,EAAE,CAAC,OAAO,CAAE,GAAG,CAAC,MAAgC,IAAI,IAAI,CAAC,CAAC;QAC7E,GAAG,CAAC,OAAO,GAAG,GAAG,EAAE,CAAC,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;IACxC,CAAC,CAAC,CAAC;AACL,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,YAAY,CAAC,GAAc;IAC/C,MAAM,EAAE,GAAG,MAAM,MAAM,EAAE,CAAC;IAC1B,OAAO,IAAI,OAAO,CAAO,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;QAC3C,MAAM,EAAE,GAAG,EAAE,CAAC,WAAW,CAAC,UAAU,EAAE,WAAW,CAAC,CAAC;QACnD,EAAE,CAAC,WAAW,CAAC,UAAU,CAAC,CAAC,GAAG,CAAC,GAAG,EAAE,cAAc,CAAC,CAAC;QACpD,EAAE,CAAC,UAAU,GAAG,GAAG,EAAE,CAAC,OAAO,EAAE,CAAC;QAChC,EAAE,CAAC,OAAO,GAAG,GAAG,EAAE,CAAC,MAAM,CAAC,EAAE,CAAC,KAAK,CAAC,CAAC;IACtC,CAAC,CAAC,CAAC;AACL,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,cAAc;IAClC,MAAM,EAAE,GAAG,MAAM,MAAM,EAAE,CAAC;IAC1B,OAAO,IAAI,OAAO,CAAO,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;QAC3C,MAAM,EAAE,GAAG,EAAE,CAAC,WAAW,CAAC,UAAU,EAAE,WAAW,CAAC,CAAC;QACnD,EAAE,CAAC,WAAW,CAAC,UAAU,CAAC,CAAC,MAAM,CAAC,cAAc,CAAC,CAAC;QAClD,EAAE,CAAC,UAAU,GAAG,GAAG,EAAE,CAAC,OAAO,EAAE,CAAC;QAChC,EAAE,CAAC,OAAO,GAAG,GAAG,EAAE,CAAC,MAAM,CAAC,EAAE,CAAC,KAAK,CAAC,CAAC;IACtC,CAAC,CAAC,CAAC;AACL,CAAC"}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { handleBoundRegistration } from "./registration.js";
|
|
2
|
+
export type { BoundRegistrationRequest, BoundRegistrationResult } from "./registration.js";
|
|
3
|
+
export { handleBoundRefresh } from "./refresh.js";
|
|
4
|
+
export type { BoundRefreshRequest } from "./refresh.js";
|
|
5
|
+
export { verifyP256Signature } from "./verify.js";
|
|
6
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/core/bound/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,uBAAuB,EAAE,MAAM,mBAAmB,CAAC;AAC5D,YAAY,EAAE,wBAAwB,EAAE,uBAAuB,EAAE,MAAM,mBAAmB,CAAC;AAC3F,OAAO,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAClD,YAAY,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAC;AACxD,OAAO,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/core/bound/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,uBAAuB,EAAE,MAAM,mBAAmB,CAAC;AAE5D,OAAO,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAElD,OAAO,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { RefreshProof, StorageAdapter } from "../types.js";
|
|
2
|
+
export interface BoundRefreshRequest {
|
|
3
|
+
sessionId: string;
|
|
4
|
+
signature: string;
|
|
5
|
+
expectedJti: string;
|
|
6
|
+
timestamp: number;
|
|
7
|
+
}
|
|
8
|
+
export declare function handleBoundRefresh(req: BoundRefreshRequest, storage: StorageAdapter): Promise<RefreshProof>;
|
|
9
|
+
//# sourceMappingURL=refresh.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"refresh.d.ts","sourceRoot":"","sources":["../../../src/core/bound/refresh.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAKhE,MAAM,WAAW,mBAAmB;IAClC,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,wBAAsB,kBAAkB,CACtC,GAAG,EAAE,mBAAmB,EACxB,OAAO,EAAE,cAAc,GACtB,OAAO,CAAC,YAAY,CAAC,CAyDvB"}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { DbscProtocolError, DbscVerificationError, ErrorCodes } from "../errors.js";
|
|
2
|
+
import { verifyP256Signature } from "./verify.js";
|
|
3
|
+
const TIMESTAMP_WINDOW_MS = 60_000;
|
|
4
|
+
export async function handleBoundRefresh(req, storage) {
|
|
5
|
+
if (!req.signature) {
|
|
6
|
+
throw new DbscProtocolError(ErrorCodes.MISSING_RESPONSE_HEADER, "signature is required for bound refresh");
|
|
7
|
+
}
|
|
8
|
+
const skew = Math.abs(Date.now() - req.timestamp);
|
|
9
|
+
if (skew > TIMESTAMP_WINDOW_MS) {
|
|
10
|
+
throw new DbscVerificationError(ErrorCodes.SIGNATURE_INVALID, "timestamp outside acceptable window");
|
|
11
|
+
}
|
|
12
|
+
const key = await storage.getBoundKey(req.sessionId);
|
|
13
|
+
if (!key) {
|
|
14
|
+
throw new DbscVerificationError(ErrorCodes.KEY_NOT_FOUND, "no bound key for session");
|
|
15
|
+
}
|
|
16
|
+
const challenge = await storage.getChallenge(req.expectedJti);
|
|
17
|
+
if (!challenge) {
|
|
18
|
+
throw new DbscVerificationError(ErrorCodes.CHALLENGE_NOT_FOUND, "challenge not found");
|
|
19
|
+
}
|
|
20
|
+
if (challenge.consumed) {
|
|
21
|
+
throw new DbscVerificationError(ErrorCodes.CHALLENGE_CONSUMED, "challenge already consumed");
|
|
22
|
+
}
|
|
23
|
+
if (Date.now() > challenge.expiresAt) {
|
|
24
|
+
throw new DbscVerificationError(ErrorCodes.CHALLENGE_EXPIRED, "challenge expired");
|
|
25
|
+
}
|
|
26
|
+
if (challenge.sessionId !== req.sessionId) {
|
|
27
|
+
throw new DbscVerificationError(ErrorCodes.JTI_MISMATCH, "challenge does not belong to this session");
|
|
28
|
+
}
|
|
29
|
+
const message = `${req.expectedJti}.${req.timestamp}`;
|
|
30
|
+
const ok = await verifyP256Signature(key.jwk, req.signature, message);
|
|
31
|
+
if (!ok) {
|
|
32
|
+
const session = await storage.getSession(req.sessionId);
|
|
33
|
+
if (session) {
|
|
34
|
+
await storage.setSession({ ...session, tier: "none" });
|
|
35
|
+
}
|
|
36
|
+
throw new DbscVerificationError(ErrorCodes.SIGNATURE_INVALID, "signature does not verify");
|
|
37
|
+
}
|
|
38
|
+
const consumed = await storage.consumeChallenge(req.expectedJti);
|
|
39
|
+
if (!consumed) {
|
|
40
|
+
throw new DbscVerificationError(ErrorCodes.CHALLENGE_CONSUMED, "challenge already consumed");
|
|
41
|
+
}
|
|
42
|
+
const session = await storage.getSession(req.sessionId);
|
|
43
|
+
if (session) {
|
|
44
|
+
await storage.setSession({ ...session, tier: "bound", lastRefreshAt: Date.now() });
|
|
45
|
+
}
|
|
46
|
+
return {
|
|
47
|
+
sessionId: req.sessionId,
|
|
48
|
+
jti: req.expectedJti,
|
|
49
|
+
verified: true,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
//# sourceMappingURL=refresh.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"refresh.js","sourceRoot":"","sources":["../../../src/core/bound/refresh.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { BoundKey, StorageAdapter } from "../types.js";
|
|
2
|
+
export interface BoundRegistrationRequest {
|
|
3
|
+
sessionId: string;
|
|
4
|
+
publicKey: JsonWebKey;
|
|
5
|
+
signature: string;
|
|
6
|
+
expectedJti: string;
|
|
7
|
+
}
|
|
8
|
+
export interface BoundRegistrationResult {
|
|
9
|
+
boundKey: BoundKey;
|
|
10
|
+
}
|
|
11
|
+
export declare function handleBoundRegistration(req: BoundRegistrationRequest, storage: StorageAdapter): Promise<BoundRegistrationResult>;
|
|
12
|
+
//# sourceMappingURL=registration.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"registration.d.ts","sourceRoot":"","sources":["../../../src/core/bound/registration.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,QAAQ,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAG5D,MAAM,WAAW,wBAAwB;IACvC,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,UAAU,CAAC;IACtB,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,uBAAuB;IACtC,QAAQ,EAAE,QAAQ,CAAC;CACpB;AAED,wBAAsB,uBAAuB,CAC3C,GAAG,EAAE,wBAAwB,EAC7B,OAAO,EAAE,cAAc,GACtB,OAAO,CAAC,uBAAuB,CAAC,CAiElC"}
|