@noy-db/on-webauthn 0.7.0-pre.9 → 0.8.0-pre.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/LICENSE +202 -21
- package/NOTICE +1 -0
- package/README.md +6 -6
- package/dist/index.d.ts +41 -7
- package/dist/index.js.map +1 -1
- package/package.json +12 -10
package/LICENSE
CHANGED
|
@@ -1,21 +1,202 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
1
|
+
|
|
2
|
+
Apache License
|
|
3
|
+
Version 2.0, January 2004
|
|
4
|
+
http://www.apache.org/licenses/
|
|
5
|
+
|
|
6
|
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
7
|
+
|
|
8
|
+
1. Definitions.
|
|
9
|
+
|
|
10
|
+
"License" shall mean the terms and conditions for use, reproduction,
|
|
11
|
+
and distribution as defined by Sections 1 through 9 of this document.
|
|
12
|
+
|
|
13
|
+
"Licensor" shall mean the copyright owner or entity authorized by
|
|
14
|
+
the copyright owner that is granting the License.
|
|
15
|
+
|
|
16
|
+
"Legal Entity" shall mean the union of the acting entity and all
|
|
17
|
+
other entities that control, are controlled by, or are under common
|
|
18
|
+
control with that entity. For the purposes of this definition,
|
|
19
|
+
"control" means (i) the power, direct or indirect, to cause the
|
|
20
|
+
direction or management of such entity, whether by contract or
|
|
21
|
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
|
22
|
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
|
23
|
+
|
|
24
|
+
"You" (or "Your") shall mean an individual or Legal Entity
|
|
25
|
+
exercising permissions granted by this License.
|
|
26
|
+
|
|
27
|
+
"Source" form shall mean the preferred form for making modifications,
|
|
28
|
+
including but not limited to software source code, documentation
|
|
29
|
+
source, and configuration files.
|
|
30
|
+
|
|
31
|
+
"Object" form shall mean any form resulting from mechanical
|
|
32
|
+
transformation or translation of a Source form, including but
|
|
33
|
+
not limited to compiled object code, generated documentation,
|
|
34
|
+
and conversions to other media types.
|
|
35
|
+
|
|
36
|
+
"Work" shall mean the work of authorship, whether in Source or
|
|
37
|
+
Object form, made available under the License, as indicated by a
|
|
38
|
+
copyright notice that is included in or attached to the work
|
|
39
|
+
(an example is provided in the Appendix below).
|
|
40
|
+
|
|
41
|
+
"Derivative Works" shall mean any work, whether in Source or Object
|
|
42
|
+
form, that is based on (or derived from) the Work and for which the
|
|
43
|
+
editorial revisions, annotations, elaborations, or other modifications
|
|
44
|
+
represent, as a whole, an original work of authorship. For the purposes
|
|
45
|
+
of this License, Derivative Works shall not include works that remain
|
|
46
|
+
separable from, or merely link (or bind by name) to the interfaces of,
|
|
47
|
+
the Work and Derivative Works thereof.
|
|
48
|
+
|
|
49
|
+
"Contribution" shall mean any work of authorship, including
|
|
50
|
+
the original version of the Work and any modifications or additions
|
|
51
|
+
to that Work or Derivative Works thereof, that is intentionally
|
|
52
|
+
submitted to Licensor for inclusion in the Work by the copyright owner
|
|
53
|
+
or by an individual or Legal Entity authorized to submit on behalf of
|
|
54
|
+
the copyright owner. For the purposes of this definition, "submitted"
|
|
55
|
+
means any form of electronic, verbal, or written communication sent
|
|
56
|
+
to the Licensor or its representatives, including but not limited to
|
|
57
|
+
communication on electronic mailing lists, source code control systems,
|
|
58
|
+
and issue tracking systems that are managed by, or on behalf of, the
|
|
59
|
+
Licensor for the purpose of discussing and improving the Work, but
|
|
60
|
+
excluding communication that is conspicuously marked or otherwise
|
|
61
|
+
designated in writing by the copyright owner as "Not a Contribution."
|
|
62
|
+
|
|
63
|
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
|
64
|
+
on behalf of whom a Contribution has been received by Licensor and
|
|
65
|
+
subsequently incorporated within the Work.
|
|
66
|
+
|
|
67
|
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
|
68
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
69
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
70
|
+
copyright license to reproduce, prepare Derivative Works of,
|
|
71
|
+
publicly display, publicly perform, sublicense, and distribute the
|
|
72
|
+
Work and such Derivative Works in Source or Object form.
|
|
73
|
+
|
|
74
|
+
3. Grant of Patent License. Subject to the terms and conditions of
|
|
75
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
76
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
77
|
+
(except as stated in this section) patent license to make, have made,
|
|
78
|
+
use, offer to sell, sell, import, and otherwise transfer the Work,
|
|
79
|
+
where such license applies only to those patent claims licensable
|
|
80
|
+
by such Contributor that are necessarily infringed by their
|
|
81
|
+
Contribution(s) alone or by combination of their Contribution(s)
|
|
82
|
+
with the Work to which such Contribution(s) was submitted. If You
|
|
83
|
+
institute patent litigation against any entity (including a
|
|
84
|
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
|
85
|
+
or a Contribution incorporated within the Work constitutes direct
|
|
86
|
+
or contributory patent infringement, then any patent licenses
|
|
87
|
+
granted to You under this License for that Work shall terminate
|
|
88
|
+
as of the date such litigation is filed.
|
|
89
|
+
|
|
90
|
+
4. Redistribution. You may reproduce and distribute copies of the
|
|
91
|
+
Work or Derivative Works thereof in any medium, with or without
|
|
92
|
+
modifications, and in Source or Object form, provided that You
|
|
93
|
+
meet the following conditions:
|
|
94
|
+
|
|
95
|
+
(a) You must give any other recipients of the Work or
|
|
96
|
+
Derivative Works a copy of this License; and
|
|
97
|
+
|
|
98
|
+
(b) You must cause any modified files to carry prominent notices
|
|
99
|
+
stating that You changed the files; and
|
|
100
|
+
|
|
101
|
+
(c) You must retain, in the Source form of any Derivative Works
|
|
102
|
+
that You distribute, all copyright, patent, trademark, and
|
|
103
|
+
attribution notices from the Source form of the Work,
|
|
104
|
+
excluding those notices that do not pertain to any part of
|
|
105
|
+
the Derivative Works; and
|
|
106
|
+
|
|
107
|
+
(d) If the Work includes a "NOTICE" text file as part of its
|
|
108
|
+
distribution, then any Derivative Works that You distribute must
|
|
109
|
+
include a readable copy of the attribution notices contained
|
|
110
|
+
within such NOTICE file, excluding those notices that do not
|
|
111
|
+
pertain to any part of the Derivative Works, in at least one
|
|
112
|
+
of the following places: within a NOTICE text file distributed
|
|
113
|
+
as part of the Derivative Works; within the Source form or
|
|
114
|
+
documentation, if provided along with the Derivative Works; or,
|
|
115
|
+
within a display generated by the Derivative Works, if and
|
|
116
|
+
wherever such third-party notices normally appear. The contents
|
|
117
|
+
of the NOTICE file are for informational purposes only and
|
|
118
|
+
do not modify the License. You may add Your own attribution
|
|
119
|
+
notices within Derivative Works that You distribute, alongside
|
|
120
|
+
or as an addendum to the NOTICE text from the Work, provided
|
|
121
|
+
that such additional attribution notices cannot be construed
|
|
122
|
+
as modifying the License.
|
|
123
|
+
|
|
124
|
+
You may add Your own copyright statement to Your modifications and
|
|
125
|
+
may provide additional or different license terms and conditions
|
|
126
|
+
for use, reproduction, or distribution of Your modifications, or
|
|
127
|
+
for any such Derivative Works as a whole, provided Your use,
|
|
128
|
+
reproduction, and distribution of the Work otherwise complies with
|
|
129
|
+
the conditions stated in this License.
|
|
130
|
+
|
|
131
|
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
|
132
|
+
any Contribution intentionally submitted for inclusion in the Work
|
|
133
|
+
by You to the Licensor shall be under the terms and conditions of
|
|
134
|
+
this License, without any additional terms or conditions.
|
|
135
|
+
Notwithstanding the above, nothing herein shall supersede or modify
|
|
136
|
+
the terms of any separate license agreement you may have executed
|
|
137
|
+
with Licensor regarding such Contributions.
|
|
138
|
+
|
|
139
|
+
6. Trademarks. This License does not grant permission to use the trade
|
|
140
|
+
names, trademarks, service marks, or product names of the Licensor,
|
|
141
|
+
except as required for reasonable and customary use in describing the
|
|
142
|
+
origin of the Work and reproducing the content of the NOTICE file.
|
|
143
|
+
|
|
144
|
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
|
145
|
+
agreed to in writing, Licensor provides the Work (and each
|
|
146
|
+
Contributor provides its Contributions) on an "AS IS" BASIS,
|
|
147
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
148
|
+
implied, including, without limitation, any warranties or conditions
|
|
149
|
+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
|
150
|
+
PARTICULAR PURPOSE. You are solely responsible for determining the
|
|
151
|
+
appropriateness of using or redistributing the Work and assume any
|
|
152
|
+
risks associated with Your exercise of permissions under this License.
|
|
153
|
+
|
|
154
|
+
8. Limitation of Liability. In no event and under no legal theory,
|
|
155
|
+
whether in tort (including negligence), contract, or otherwise,
|
|
156
|
+
unless required by applicable law (such as deliberate and grossly
|
|
157
|
+
negligent acts) or agreed to in writing, shall any Contributor be
|
|
158
|
+
liable to You for damages, including any direct, indirect, special,
|
|
159
|
+
incidental, or consequential damages of any character arising as a
|
|
160
|
+
result of this License or out of the use or inability to use the
|
|
161
|
+
Work (including but not limited to damages for loss of goodwill,
|
|
162
|
+
work stoppage, computer failure or malfunction, or any and all
|
|
163
|
+
other commercial damages or losses), even if such Contributor
|
|
164
|
+
has been advised of the possibility of such damages.
|
|
165
|
+
|
|
166
|
+
9. Accepting Warranty or Additional Liability. While redistributing
|
|
167
|
+
the Work or Derivative Works thereof, You may choose to offer,
|
|
168
|
+
and charge a fee for, acceptance of support, warranty, indemnity,
|
|
169
|
+
or other liability obligations and/or rights consistent with this
|
|
170
|
+
License. However, in accepting such obligations, You may act only
|
|
171
|
+
on Your own behalf and on Your sole responsibility, not on behalf
|
|
172
|
+
of any other Contributor, and only if You agree to indemnify,
|
|
173
|
+
defend, and hold each Contributor harmless for any liability
|
|
174
|
+
incurred by, or claims asserted against, such Contributor by reason
|
|
175
|
+
of your accepting any such warranty or additional liability.
|
|
176
|
+
|
|
177
|
+
END OF TERMS AND CONDITIONS
|
|
178
|
+
|
|
179
|
+
APPENDIX: How to apply the Apache License to your work.
|
|
180
|
+
|
|
181
|
+
To apply the Apache License to your work, attach the following
|
|
182
|
+
boilerplate notice, with the fields enclosed by brackets "[]"
|
|
183
|
+
replaced with your own identifying information. (Don't include
|
|
184
|
+
the brackets!) The text should be enclosed in the appropriate
|
|
185
|
+
comment syntax for the file format. We also recommend that a
|
|
186
|
+
file or class name and description of purpose be included on the
|
|
187
|
+
same "printed page" as the copyright notice for easier
|
|
188
|
+
identification within third-party archives.
|
|
189
|
+
|
|
190
|
+
Copyright 2026 vLannaAi
|
|
191
|
+
|
|
192
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
193
|
+
you may not use this file except in compliance with the License.
|
|
194
|
+
You may obtain a copy of the License at
|
|
195
|
+
|
|
196
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
197
|
+
|
|
198
|
+
Unless required by applicable law or agreed to in writing, software
|
|
199
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
200
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
201
|
+
See the License for the specific language governing permissions and
|
|
202
|
+
limitations under the License.
|
package/NOTICE
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
noy-db — Copyright 2026 vLannaAi
|
package/README.md
CHANGED
|
@@ -18,7 +18,7 @@ WebAuthn hardware-key keyrings for noy-db — Touch ID, Face ID, Windows Hello,
|
|
|
18
18
|
|
|
19
19
|
## Plumbing into `createNoydb`
|
|
20
20
|
|
|
21
|
-
`unlockWebAuthn(enrollment)` returns an `UnlockedKeyring`. As of `@noy-db/hub@0.1.0-pre.4` ([issue #5](https://github.com/
|
|
21
|
+
`unlockWebAuthn(enrollment)` returns an `UnlockedKeyring`. As of `@noy-db/hub@0.1.0-pre.4` ([issue #5](https://github.com/noy-db/core/issues/5)), pass it directly to `createNoydb` via the `getKeyring` callback — no secret bridge required:
|
|
22
22
|
|
|
23
23
|
```ts
|
|
24
24
|
import { createNoydb } from '@noy-db/hub'
|
|
@@ -45,12 +45,12 @@ For first-time bootstrap (no enrollment exists yet), open the vault with a secre
|
|
|
45
45
|
|
|
46
46
|
## Documentation
|
|
47
47
|
|
|
48
|
-
See the [main repository](https://github.com/
|
|
48
|
+
See the [main repository](https://github.com/noy-db/core#readme) for setup, examples, and the full subsystem catalog.
|
|
49
49
|
|
|
50
|
-
- Source — [`packages/on-webauthn`](https://github.com/
|
|
51
|
-
- Issues — [github.com/
|
|
52
|
-
- Spec — [`SPEC.md`](https://github.com/
|
|
50
|
+
- Source — [`packages/on-webauthn`](https://github.com/noy-db/core/tree/main/packages/on-webauthn)
|
|
51
|
+
- Issues — [github.com/noy-db/core/issues](https://github.com/noy-db/core/issues)
|
|
52
|
+
- Spec — [`SPEC.md`](https://github.com/noy-db/docs/blob/main/SPEC.md)
|
|
53
53
|
|
|
54
54
|
## License
|
|
55
55
|
|
|
56
|
-
[
|
|
56
|
+
[Apache-2.0](./LICENSE) © vLannaAi
|
package/dist/index.d.ts
CHANGED
|
@@ -225,9 +225,11 @@ declare function enrollWebAuthn(keyring: UnlockedKeyring, vault: string, options
|
|
|
225
225
|
* Triggers the WebAuthn assertion prompt. On success, decrypts the keyring
|
|
226
226
|
* payload from the enrollment record and returns an `UnlockedKeyring`.
|
|
227
227
|
*
|
|
228
|
-
* The returned keyring has the same DEKs as at enrollment time. If
|
|
229
|
-
* have been
|
|
230
|
-
*
|
|
228
|
+
* The returned keyring has the same DEKs as at enrollment time. If DEK
|
|
229
|
+
* VALUES have been re-minted since enrollment — `rotateKeys`, not
|
|
230
|
+
* `rotateSecret` — this returns stale DEKs and the caller should detect
|
|
231
|
+
* decryption failures and prompt for re-enrollment. A `rotateSecret`
|
|
232
|
+
* phrase rotation does NOT stale them: it rewraps the same values.
|
|
231
233
|
*
|
|
232
234
|
* Non-PRF unlock (`enrollment.prfUsed === false`) is a presence gate, not
|
|
233
235
|
* zero-knowledge confidentiality — it is kept working here for back-compat
|
|
@@ -251,10 +253,42 @@ declare function unlockWebAuthn(enrollment: WebAuthnEnrollment, options?: WebAut
|
|
|
251
253
|
* wrapping key derived from PRF (or rawId fallback) is bound to the
|
|
252
254
|
* authenticator, not to the secret. What needs to change is the
|
|
253
255
|
* **wrapped payload**: the encrypted blob the slot's `wrapped_kek`
|
|
254
|
-
* field holds. After rotation
|
|
255
|
-
*
|
|
256
|
-
*
|
|
257
|
-
*
|
|
256
|
+
* field holds. After rotation the old payload is wrapped under the
|
|
257
|
+
* OLD KEK, so hub can no longer thread it through the rotated
|
|
258
|
+
* keyring; the new payload must hold `ctx.newDeks`.
|
|
259
|
+
*
|
|
260
|
+
* ⚠️ **The old payload's DEK VALUES are not stale — only its wrapping
|
|
261
|
+
* is.** `rotateSecret` rewraps the SAME DEKs under a freshly-derived
|
|
262
|
+
* KEK; only `rotateKeys` re-mints DEK values. Records are encrypted
|
|
263
|
+
* with DEKs, so a captured `wrappedPayload` keeps decrypting
|
|
264
|
+
* everything across any number of phrase rotations. An earlier
|
|
265
|
+
* revision of this comment said "now stale because rotateSecret
|
|
266
|
+
* rewrapped them under a fresh KEK", which reads as though rotation
|
|
267
|
+
* invalidated the captured blob. It does not, and nothing in this
|
|
268
|
+
* ceremony is a revocation step.
|
|
269
|
+
*
|
|
270
|
+
* ## Revoking a credential — this ceremony is a REQUIRED INPUT to it
|
|
271
|
+
*
|
|
272
|
+
* Hub's `revokeAuthenticator` is the real revocation, and it is three steps:
|
|
273
|
+
* `removeAuthenticator` (hides the slot), `rotateKeys` (mints fresh DEKs —
|
|
274
|
+
* the step that actually revokes), then `rotateSecret` with a
|
|
275
|
+
* `slotCeremonies` entry for every slot that STAYS.
|
|
276
|
+
*
|
|
277
|
+
* ⛔ **A remaining slot handed no ceremony is DROPPED, not preserved.** After
|
|
278
|
+
* the `rotateKeys` step its blob wraps DEKs that no longer exist, so keeping
|
|
279
|
+
* it would leave a credential that silently fails. Its holder must re-enrol.
|
|
280
|
+
*
|
|
281
|
+
* So revoking ONE credential means supplying `webAuthnSlotRewrapCeremony` for
|
|
282
|
+
* EVERY WebAuthn slot you intend to keep — and each requires that
|
|
283
|
+
* credential to be present (a live assertion on the authenticator, which means the user in front of the device). Re-wrapping a credential
|
|
284
|
+
* is the credential's own operation; there is no way to do it on the
|
|
285
|
+
* holder's behalf.
|
|
286
|
+
*
|
|
287
|
+
* ⚠️ **Not in any published hub at the time of writing.**
|
|
288
|
+
* `revokeAuthenticator` landed on noy-db `main` in `0612b4ac`, AFTER
|
|
289
|
+
* `@noy-db/hub@0.7.1-pre.0` was cut and published — so it is in neither
|
|
290
|
+
* `@latest` (0.7.0) nor `@next` (0.7.1-pre.0). Ask npm before promising it to
|
|
291
|
+
* a consumer; do not infer its version from hub's `main`.
|
|
258
292
|
*
|
|
259
293
|
* Single ceremony, two operations:
|
|
260
294
|
* 1. Trigger one WebAuthn assertion to derive the wrapping key.
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["/**\n * @noy-db/on-webauthn —\n *\n * Hardware-key keyring for noy-db using the WebAuthn API.\n *\n * Covers every form factor:\n * - Platform authenticators: Touch ID, Face ID, Windows Hello, Android biometric\n * - Roaming authenticators: YubiKey (5C NFC, Bio), SoloKey, Titan, any FIDO2 key\n * - Passkey-capable platform authenticators: iCloud Keychain, Google Password Manager\n *\n * Key derivation model\n * ────────────────────\n * This package uses the **PRF (Pseudo-Random Function) extension** when\n * available to derive a deterministic wrapping key from the WebAuthn\n * credential. The PRF output is consistent across assertions on the same\n * device/credential, enabling unlock-without-secret while keeping the\n * derived key bound to the physical authenticator.\n *\n * When PRF is not supported by the authenticator (common on older hardware),\n * the package can fall back to HKDF-SHA256 over the credential's `rawId` —\n * but by default, enrollment refuses this path because the `rawId` is stored\n * in the record itself, making the record self-decrypting (presence gate, not\n * confidentiality). Use `allowNonPrfInsecure: true` to opt in; prefer a\n * PRF-capable device or the secret for real confidentiality.\n *\n * The derived key is NEVER persisted. It exists only in memory during the\n * unlock operation. What IS persisted (in the noy-db adapter, not in browser\n * storage) is the wrapped KEK: `encrypt(KEK, derivedKey)`.\n *\n * BE-flag guards\n * ──────────────\n * The backup-eligibility (BE) flag in a WebAuthn authenticator data signals\n * that the credential is (or can be) synced across devices — e.g. stored in\n * iCloud Keychain. For single-device security policies (air-gapped USB sticks,\n * high-security terminals), this is a threat: the credential is available on\n * any device where the user's iCloud account is signed in.\n *\n * The `requireSingleDevice: true` option rejects credentials with the BE flag\n * set during enrollment. Existing enrollments are checked at assertion time —\n * if the authenticator data shows BE=1 but `requireSingleDevice` was set at\n * enrollment, the assertion throws `WebAuthnMultiDeviceError`.\n *\n * Enrollment flow\n * ───────────────\n * 1. User is already authenticated (secret or existing session).\n * 2. Call `enrollWebAuthn(keyring, options)`.\n * 3. WebAuthn credential is created; PRF or rawId-derived key wraps the KEK.\n * 4. Returns a `WebAuthnEnrollment` — persist this to the noy-db adapter\n * via `saveEnrollment()`, or store it yourself in any encrypted collection.\n *\n * Unlock flow\n * ───────────\n * 1. Load the `WebAuthnEnrollment` via `loadEnrollment()`.\n * 2. Call `unlockWebAuthn(enrollment, keyring)` — triggers the WebAuthn\n * assertion prompt.\n * 3. On success, returns the unwrapped `CryptoKey` (the KEK) — use it to\n * re-hydrate the session via `createSession()`.\n */\n\nimport { bufferToBase64, base64ToBuffer } from '@noy-db/hub'\nimport { ValidationError } from '@noy-db/hub'\nimport type { Role } from '@noy-db/hub'\nimport type { UnlockedKeyring, EnrollAuthenticatorOptions, SlotRewrapContext } from '@noy-db/hub/on'\n\n// Re-export from core for convenience\nexport { ValidationError } from '@noy-db/hub'\n\n// ─── Error types ──────────────────────────────────────────────────────\n\n/**\n * Thrown when the WebAuthn API is not available in the current environment.\n *\n * Check `isWebAuthnAvailable()` before calling `enrollWebAuthn()` or\n * `unlockWebAuthn()` and show a fallback UI (secret entry) if this\n * returns false. Common scenarios: Node.js environments, older browsers,\n * non-HTTPS origins (WebAuthn requires a Secure Context).\n */\nexport class WebAuthnNotAvailableError extends Error {\n readonly code = 'WEBAUTHN_NOT_AVAILABLE'\n constructor() {\n super('WebAuthn is not available in this environment. A browser with navigator.credentials support is required.')\n this.name = 'WebAuthnNotAvailableError'\n }\n}\n\n/**\n * Thrown when the user dismisses the WebAuthn prompt without completing it.\n *\n * The `op` field distinguishes enrollment cancellation (user chose not to\n * enroll a hardware key) from assertion cancellation (user dismissed the\n * unlock prompt). Treat this as a user-initiated action, not an error — show\n * a \"use secret instead\" option rather than an error message.\n */\nexport class WebAuthnCancelledError extends Error {\n readonly code = 'WEBAUTHN_CANCELLED'\n constructor(op: 'enrollment' | 'assertion') {\n super(`WebAuthn ${op} was cancelled by the user.`)\n this.name = 'WebAuthnCancelledError'\n }\n}\n\n/**\n * Thrown when the authenticator has the backup-eligible (BE) flag set but\n * the vault requires a single-device credential (`requireSingleDevice: true`).\n *\n * A BE credential is synced across devices (e.g. iCloud Keychain, Google\n * Password Manager), which violates the single-device security model. The\n * user must enroll a hardware security key (YubiKey, Titan, SoloKey) instead.\n */\nexport class WebAuthnMultiDeviceError extends Error {\n readonly code = 'WEBAUTHN_MULTI_DEVICE'\n constructor() {\n super(\n 'This credential is backup-eligible (BE flag set) and may be synced across devices. ' +\n 'The vault requires a single-device credential (requireSingleDevice: true). ' +\n 'Please use a hardware security key (YubiKey, Titan, SoloKey) or a platform ' +\n 'authenticator that does not sync credentials across devices.',\n )\n this.name = 'WebAuthnMultiDeviceError'\n }\n}\n\n/**\n * Thrown when `enrollWebAuthn()` (or `webAuthnSlotRewrapCeremony()`) is\n * asked to enroll/rewrap without a PRF-capable authenticator and without\n * explicit opt-in via `allowNonPrfInsecure: true`.\n *\n * Non-PRF WebAuthn cannot provide confidentiality: its wrapping key derives\n * entirely from the credential's `rawId`, which is stored in cleartext in\n * the enrollment record itself — the record would self-decrypt with no\n * authenticator involved. By default, enrollment/rewrap refuses this path.\n * Use a PRF-capable credential, fall back to the secret, or set\n * `allowNonPrfInsecure: true` to explicitly accept a non-confidential\n * presence gate instead.\n */\nexport class WebAuthnPRFUnavailableError extends Error {\n readonly code = 'WEBAUTHN_PRF_UNAVAILABLE'\n constructor() {\n super(\n 'The PRF extension is not available on this authenticator, so it cannot provide ' +\n 'WebAuthn confidentiality — the non-PRF wrapping key derives entirely from the ' +\n \"credential's rawId, which is stored in the enrollment record itself (the record \" +\n 'would self-decrypt with no authenticator involved). Enrollment refuses this path ' +\n 'by default. Use a PRF-capable authenticator, use the secret instead, or pass ' +\n '{ allowNonPrfInsecure: true } to explicitly accept a non-confidential presence gate.',\n )\n this.name = 'WebAuthnPRFUnavailableError'\n }\n}\n\n// ─── Types ────────────────────────────────────────────────────────────\n\n/**\n * A persisted WebAuthn enrollment record. Store this in a noy-db\n * collection (encrypted like any other record) or return it from\n * `saveEnrollment()` / `loadEnrollment()` helpers.\n */\nexport interface WebAuthnEnrollment {\n /** Enrollment format version. */\n readonly _noydb_webauthn: 1\n /** The vault this enrollment was created for. */\n readonly vault: string\n /** The user ID this enrollment belongs to. */\n readonly userId: string\n /** WebAuthn credential ID (base64). Use for allowCredentials in assertions. */\n readonly credentialId: string\n /** Whether PRF was used for key derivation (vs rawId HKDF fallback). */\n readonly prfUsed: boolean\n /** Whether the BE (backup-eligibility) flag was present at enrollment time. */\n readonly beFlag: boolean\n /** Whether single-device was required at enrollment time. */\n readonly requireSingleDevice: boolean\n /** The wrapped KEK: encrypt(exportedDekMap, derivedKey). Base64. */\n readonly wrappedPayload: string\n /** IV used for the wrapping. Base64. */\n readonly wrapIv: string\n /** ISO timestamp of enrollment. */\n readonly enrolledAt: string\n}\n\n/** Options for `enrollWebAuthn()`. */\nexport interface WebAuthnEnrollOptions {\n /**\n * Relying party ID and name for the WebAuthn credential.\n * Defaults to `{ id: window.location.hostname, name: 'NOYDB' }`.\n */\n rp?: { id?: string; name: string }\n /**\n * If `true`, refuse to enroll credentials with the BE flag set\n * (multi-device / syncable passkeys). Defaults to `false`.\n *\n * Set to `true` for high-security deployments where the credential\n * must be bound to a single physical device (YubiKey, Titan, etc.).\n */\n requireSingleDevice?: boolean\n /**\n * WebAuthn timeout in milliseconds. Default: 60_000.\n */\n timeout?: number\n /**\n * If `true`, prefer a cross-platform authenticator (roaming security key).\n * If `false`, prefer a platform authenticator (Touch ID, Face ID).\n * If undefined, let the browser choose.\n */\n preferCrossPlatform?: boolean\n /**\n * Non-PRF WebAuthn cannot provide confidentiality — its wrapping key\n * derives from `credentialId`, which is stored in the enrollment record,\n * so the record self-decrypts with no authenticator. By default,\n * enrollment REFUSES the non-PRF path (throws `WebAuthnPRFUnavailableError`).\n * Set `true` ONLY to explicitly opt into a non-confidential presence gate;\n * prefer the secret or a PRF-capable credential for real confidentiality.\n */\n allowNonPrfInsecure?: boolean\n}\n\n/** Options for `unlockWebAuthn()`. */\nexport interface WebAuthnUnlockOptions {\n /** WebAuthn timeout in milliseconds. Default: 60_000. */\n timeout?: number\n /**\n * Only consulted by `webAuthnSlotRewrapCeremony()` — `unlockWebAuthn()`\n * ignores it, since unlocking an existing non-PRF record is always\n * permitted (back-compat; that record was never confidential). Same\n * semantics as `WebAuthnEnrollOptions.allowNonPrfInsecure`: required to\n * rewrap a non-PRF slot, since rewrap produces a fresh self-decrypting\n * wrapped payload.\n */\n allowNonPrfInsecure?: boolean\n}\n\n// ─── Environment check ─────────────────────────────────────────────────\n\n/**\n * Returns `true` if WebAuthn is available and can be used for enrollment or unlock.\n *\n * Checks for `navigator.credentials`, `window.PublicKeyCredential`, and a\n * Secure Context (`window.isSecureContext`). Call this before rendering the\n * \"Register hardware key\" button to avoid showing options that will fail.\n */\nexport function isWebAuthnAvailable(): boolean {\n return (\n typeof window !== 'undefined' &&\n typeof window.PublicKeyCredential !== 'undefined' &&\n typeof navigator !== 'undefined' &&\n typeof navigator.credentials !== 'undefined'\n )\n}\n\n// ─── PRF salt ─────────────────────────────────────────────────────────\n\nconst PRF_SALT = new TextEncoder().encode('noydb-webauthn-kek-derive')\n\n// ─── Key derivation helpers ────────────────────────────────────────────\n\n/**\n * Derive a wrapping key from PRF output.\n * PRF output is 32 bytes of authenticator-bound pseudo-random data.\n */\nasync function deriveKeyFromPRF(prfOutput: ArrayBuffer): Promise<CryptoKey> {\n const keyMaterial = await globalThis.crypto.subtle.importKey(\n 'raw',\n prfOutput,\n 'HKDF',\n false,\n ['deriveKey'],\n )\n return globalThis.crypto.subtle.deriveKey(\n {\n name: 'HKDF',\n hash: 'SHA-256',\n salt: PRF_SALT,\n info: new TextEncoder().encode('noydb-kek-wrap-v1'),\n },\n keyMaterial,\n { name: 'AES-GCM', length: 256 },\n false,\n ['encrypt', 'decrypt'],\n )\n}\n\n/**\n * Derive a wrapping key from the credential's rawId (fallback when PRF unavailable).\n * rawId IS the entire secret and is stored in the record itself — the record\n * self-decrypts with no authenticator involved (presence gate, not confidentiality).\n * Universally supported but only usable via explicit `allowNonPrfInsecure: true`.\n */\nasync function deriveKeyFromRawId(rawId: ArrayBuffer): Promise<CryptoKey> {\n const keyMaterial = await globalThis.crypto.subtle.importKey(\n 'raw',\n rawId,\n 'HKDF',\n false,\n ['deriveKey'],\n )\n return globalThis.crypto.subtle.deriveKey(\n {\n name: 'HKDF',\n hash: 'SHA-256',\n salt: new TextEncoder().encode('noydb-webauthn-rawid-fallback'),\n info: new TextEncoder().encode('noydb-kek-wrap-v1'),\n },\n keyMaterial,\n { name: 'AES-GCM', length: 256 },\n false,\n ['encrypt', 'decrypt'],\n )\n}\n\n// ─── BE flag extraction ────────────────────────────────────────────────\n\n/**\n * Extract the BE (backup-eligibility) flag from WebAuthn authenticator data.\n * Authenticator data byte layout (CTAP2 spec):\n * bytes 0-31: rpIdHash\n * byte 32: flags byte\n * bytes 33-36: signCount\n * ...\n *\n * Flags byte bit layout (bit 0 = LSB):\n * bit 0 (UP): user presence\n * bit 2 (UV): user verification\n * bit 3 (BE): backup eligibility\n * bit 4 (BS): backup state\n * bit 6 (AT): attested credential data present\n * bit 7 (ED): extension data present\n */\nfunction extractBEFlag(authData: ArrayBuffer): boolean {\n const bytes = new Uint8Array(authData)\n if (bytes.length < 33) return false\n const flagsByte = bytes[32]!\n return (flagsByte & 0b00001000) !== 0 // bit 3\n}\n\n// ─── Payload wrap/unwrap ───────────────────────────────────────────────\n\n/**\n * Serialize and encrypt the DEK map from `keyring` using `wrappingKey`.\n * The wrapped payload is what gets stored in the enrollment record.\n */\nasync function wrapKeyringSummary(\n keyring: UnlockedKeyring,\n wrappingKey: CryptoKey,\n): Promise<{ wrappedPayload: string; wrapIv: string }> {\n const dekMap: Record<string, string> = {}\n for (const [collName, dek] of keyring.deks) {\n const raw = await globalThis.crypto.subtle.exportKey('raw', dek)\n dekMap[collName] = bufferToBase64(raw)\n }\n\n const payload = JSON.stringify({\n userId: keyring.userId,\n displayName: keyring.displayName,\n role: keyring.role,\n permissions: keyring.permissions,\n deks: dekMap,\n salt: bufferToBase64(keyring.salt),\n })\n\n const iv = globalThis.crypto.getRandomValues(new Uint8Array(12))\n const encrypted = await globalThis.crypto.subtle.encrypt(\n { name: 'AES-GCM', iv },\n wrappingKey,\n new TextEncoder().encode(payload),\n )\n\n return { wrappedPayload: bufferToBase64(encrypted), wrapIv: bufferToBase64(iv) }\n}\n\n/**\n * Decrypt and deserialize the keyring payload using `wrappingKey`.\n */\nasync function unwrapKeyringSummary(\n enrollment: WebAuthnEnrollment,\n wrappingKey: CryptoKey,\n): Promise<UnlockedKeyring> {\n const iv = base64ToBuffer(enrollment.wrapIv)\n const ciphertext = base64ToBuffer(enrollment.wrappedPayload)\n\n let plaintext: ArrayBuffer\n try {\n plaintext = await globalThis.crypto.subtle.decrypt(\n { name: 'AES-GCM', iv },\n wrappingKey,\n ciphertext,\n )\n } catch {\n throw new ValidationError('WebAuthn decryption failed — the authenticator may have changed or the enrollment may be corrupt.')\n }\n\n const parsed = JSON.parse(new TextDecoder().decode(plaintext)) as {\n userId: string\n displayName: string\n role: Role\n permissions: Record<string, 'rw' | 'ro'>\n deks: Record<string, string>\n salt: string\n }\n\n const deks = new Map<string, CryptoKey>()\n for (const [collName, rawBase64] of Object.entries(parsed.deks)) {\n const dek = await globalThis.crypto.subtle.importKey(\n 'raw',\n base64ToBuffer(rawBase64),\n { name: 'AES-GCM', length: 256 },\n true,\n ['encrypt', 'decrypt'],\n )\n deks.set(collName, dek)\n }\n\n return {\n userId: parsed.userId,\n displayName: parsed.displayName,\n role: parsed.role,\n permissions: parsed.permissions,\n deks,\n kek: null,\n salt: base64ToBuffer(parsed.salt),\n authenticators: [],\n }\n}\n\n// ─── Public API ────────────────────────────────────────────────────────\n\n/**\n * Enroll a WebAuthn credential for the given keyring.\n *\n * The caller must already have an unlocked keyring (from secret auth or\n * an existing session). The WebAuthn credential creation prompt is triggered\n * by this call.\n *\n * Returns a `WebAuthnEnrollment` that should be persisted — typically via\n * `saveEnrollment()` into a noy-db collection.\n *\n * @throws `WebAuthnNotAvailableError` if the environment doesn't support WebAuthn.\n * @throws `WebAuthnCancelledError` if the user cancels the credential creation.\n * @throws `WebAuthnMultiDeviceError` if `requireSingleDevice` is true and the\n * authenticator returned a credential with the BE flag set.\n * @throws `WebAuthnPRFUnavailableError` if the authenticator did not provide\n * PRF output and `options.allowNonPrfInsecure` is not `true` — see\n * that option's doc for why the non-PRF path is refused by default.\n */\nexport async function enrollWebAuthn(\n keyring: UnlockedKeyring,\n vault: string,\n options: WebAuthnEnrollOptions = {},\n): Promise<WebAuthnEnrollment> {\n if (!isWebAuthnAvailable()) {\n throw new WebAuthnNotAvailableError()\n }\n\n const rpId = options.rp?.id ?? (typeof window !== 'undefined' ? window.location.hostname : 'localhost')\n const rpName = options.rp?.name ?? 'NOYDB'\n const timeout = options.timeout ?? 60_000\n\n const challenge = globalThis.crypto.getRandomValues(new Uint8Array(32))\n const userIdBytes = new TextEncoder().encode(keyring.userId)\n\n const authenticatorSelection: AuthenticatorSelectionCriteria = {\n userVerification: 'required',\n residentKey: 'preferred',\n }\n if (options.preferCrossPlatform === true) {\n authenticatorSelection.authenticatorAttachment = 'cross-platform'\n } else if (options.preferCrossPlatform === false) {\n authenticatorSelection.authenticatorAttachment = 'platform'\n }\n\n // Request PRF extension for deterministic key derivation\n const extensionsInput = {\n prf: { eval: { first: PRF_SALT } },\n } as AuthenticationExtensionsClientInputs\n\n const credential = await navigator.credentials.create({\n publicKey: {\n challenge,\n rp: { id: rpId, name: rpName },\n user: {\n id: userIdBytes,\n name: keyring.userId,\n displayName: keyring.displayName,\n },\n pubKeyCredParams: [\n { type: 'public-key', alg: -7 }, // ES256\n { type: 'public-key', alg: -257 }, // RS256\n { type: 'public-key', alg: -8 }, // EdDSA\n ],\n authenticatorSelection,\n extensions: extensionsInput,\n timeout,\n },\n }) as PublicKeyCredential | null\n\n if (!credential) {\n throw new WebAuthnCancelledError('enrollment')\n }\n\n const authData = (credential.response as AuthenticatorAttestationResponse).getAuthenticatorData()\n const beFlag = extractBEFlag(authData)\n\n if (options.requireSingleDevice && beFlag) {\n throw new WebAuthnMultiDeviceError()\n }\n\n // Try to get PRF output from extensions\n const extensions = credential.getClientExtensionResults() as {\n prf?: { results?: { first?: ArrayBuffer } }\n }\n const prfOutput = extensions.prf?.results?.first\n const prfUsed = !!prfOutput\n\n if (!prfUsed && !options.allowNonPrfInsecure) {\n throw new WebAuthnPRFUnavailableError()\n }\n\n const wrappingKey = prfOutput\n ? await deriveKeyFromPRF(prfOutput)\n : await deriveKeyFromRawId(credential.rawId)\n\n const { wrappedPayload, wrapIv } = await wrapKeyringSummary(keyring, wrappingKey)\n\n return {\n _noydb_webauthn: 1,\n vault,\n userId: keyring.userId,\n credentialId: bufferToBase64(credential.rawId),\n prfUsed,\n beFlag,\n requireSingleDevice: options.requireSingleDevice ?? false,\n wrappedPayload,\n wrapIv,\n enrolledAt: new Date().toISOString(),\n }\n}\n\n/**\n * Unlock a vault using a previously enrolled WebAuthn credential.\n *\n * Triggers the WebAuthn assertion prompt. On success, decrypts the keyring\n * payload from the enrollment record and returns an `UnlockedKeyring`.\n *\n * The returned keyring has the same DEKs as at enrollment time. If DEKs\n * have been rotated since enrollment, this will return stale DEKs — the\n * caller should detect decryption failures and prompt for re-enrollment.\n *\n * Non-PRF unlock (`enrollment.prfUsed === false`) is a presence gate, not\n * zero-knowledge confidentiality — it is kept working here for back-compat\n * with existing non-PRF enrollments; new enrollments refuse that path by\n * default (see `WebAuthnEnrollOptions.allowNonPrfInsecure`).\n *\n * @throws `WebAuthnNotAvailableError` if the environment doesn't support WebAuthn.\n * @throws `WebAuthnCancelledError` if the user cancels the assertion.\n * @throws `WebAuthnMultiDeviceError` if `requireSingleDevice` was set at\n * enrollment and the authenticator data now shows BE=1.\n * @throws `ValidationError` if decryption of the keyring payload fails.\n */\nexport async function unlockWebAuthn(\n enrollment: WebAuthnEnrollment,\n options: WebAuthnUnlockOptions = {},\n): Promise<UnlockedKeyring> {\n if (!isWebAuthnAvailable()) {\n throw new WebAuthnNotAvailableError()\n }\n\n const timeout = options.timeout ?? 60_000\n const credentialId = base64ToBuffer(enrollment.credentialId)\n\n const extensionsInput = (enrollment.prfUsed\n ? { prf: { eval: { first: PRF_SALT } } }\n : {}\n ) as AuthenticationExtensionsClientInputs\n\n const assertion = await navigator.credentials.get({\n publicKey: {\n challenge: globalThis.crypto.getRandomValues(new Uint8Array(32)),\n allowCredentials: [{ type: 'public-key', id: credentialId as BufferSource }],\n userVerification: 'required',\n extensions: extensionsInput,\n timeout,\n },\n }) as PublicKeyCredential | null\n\n if (!assertion) {\n throw new WebAuthnCancelledError('assertion')\n }\n\n // BE-flag guard at assertion time\n const authData = (assertion.response as AuthenticatorAssertionResponse).authenticatorData\n const beFlag = extractBEFlag(authData)\n if (enrollment.requireSingleDevice && beFlag) {\n throw new WebAuthnMultiDeviceError()\n }\n\n // Derive the wrapping key using the same method as enrollment\n let wrappingKey: CryptoKey\n if (enrollment.prfUsed) {\n const extensions = assertion.getClientExtensionResults() as {\n prf?: { results?: { first?: ArrayBuffer } }\n }\n const prfOutput = extensions.prf?.results?.first\n if (!prfOutput) {\n throw new ValidationError(\n 'PRF extension output not available at assertion time. ' +\n 'The authenticator may not support PRF. Re-enroll without PRF support.',\n )\n }\n wrappingKey = await deriveKeyFromPRF(prfOutput)\n } else {\n wrappingKey = await deriveKeyFromRawId(assertion.rawId)\n }\n\n return unwrapKeyringSummary(enrollment, wrappingKey)\n}\n\n/**\n * `SlotRewrapCeremony` for WebAuthn slots — used by hub's\n * `rotateSecret({ slotCeremonies: { [slotId]: webAuthnSlotRewrapCeremony } })`\n * to preserve a tier-2 WebAuthn enrollment across a tier-1 phrase\n * rotation without requiring re-enrollment of the credential.\n *\n * The credential itself is unaffected by phrase rotation — the\n * wrapping key derived from PRF (or rawId fallback) is bound to the\n * authenticator, not to the secret. What needs to change is the\n * **wrapped payload**: the encrypted blob the slot's `wrapped_kek`\n * field holds. After rotation, the old payload still has the old\n * DEKs (now stale because rotateSecret rewrapped them under a\n * fresh KEK); the new payload must hold the freshly rewrapped\n * `ctx.newDeks`.\n *\n * Single ceremony, two operations:\n * 1. Trigger one WebAuthn assertion to derive the wrapping key.\n * 2. Decrypt the OLD `wrapped_kek` to extract identity fields\n * (userId, displayName, role, permissions, salt) — these don't\n * change on rotation, so they're carried verbatim into the new\n * payload.\n * 3. Encrypt the NEW payload (same identity + `ctx.newDeks`) under\n * the same wrapping key, with a fresh IV.\n * 4. Return `EnrollAuthenticatorOptions` preserving `oldSlot.id`\n * and `method: 'webauthn'` (hub validates these to prevent\n * slot-type swap mid-rotation).\n *\n * Previously a workaround was needed — detect dropped slots after\n * rotate and offer \"Re-enrol Touch ID\" inline. This ceremony\n * eliminates that step.\n *\n * Out of scope: tier-3 PIN. PIN state lives in `QuickUnlockStore`\n * (RAM-only), not in `KeyringFile.authenticators[]`, so\n * `slotCeremonies` doesn't apply. Clear PIN state on rotate; let\n * the user set a fresh PIN via `db.enrollUnlock` immediately after.\n *\n * @throws {WebAuthnNotAvailableError} when the environment lacks WebAuthn.\n * @throws {WebAuthnCancelledError} when the user dismisses the assertion.\n * @throws {WebAuthnMultiDeviceError} when `meta.requireSingleDevice` was\n * set at enrollment and the authenticator now reports BE=1.\n * @throws {ValidationError} when `oldSlot.method !== 'webauthn'`,\n * when required `meta` fields are missing, or when the old\n * payload fails to decrypt (credential changed / payload\n * tampered).\n * @throws {WebAuthnPRFUnavailableError} when the old slot is non-PRF and\n * `options.allowNonPrfInsecure` is not `true` — rewrapping a\n * non-PRF slot produces a fresh self-decrypting payload, so it\n * requires the same explicit acknowledgement as enrollment.\n *\n * @see passwordSlotRewrapCeremony — the password parallel.\n */\nexport async function webAuthnSlotRewrapCeremony(\n ctx: SlotRewrapContext,\n options: WebAuthnUnlockOptions = {},\n): Promise<EnrollAuthenticatorOptions> {\n if (ctx.oldSlot.method !== 'webauthn') {\n throw new ValidationError(\n `webAuthnSlotRewrapCeremony: oldSlot.method is \"${ctx.oldSlot.method}\"; expected \"webauthn\". ` +\n 'This ceremony only handles WebAuthn slots — pair other methods with their own helpers.',\n )\n }\n if (ctx.oldSlot.wrapKind === 'deks') {\n throw new ValidationError(\n 'webAuthnSlotRewrapCeremony: oldSlot is a wrap-DEKs slot; expected wrap-KEK. ' +\n 'WebAuthn slots use the wrap-KEK variant; mismatch indicates the slot was ' +\n 'enrolled by a different on-* package.',\n )\n }\n if (!isWebAuthnAvailable()) {\n throw new WebAuthnNotAvailableError()\n }\n\n // Pull the per-slot fields needed for assertion + decrypt. These were\n // written into `meta` by `enrollWebAuthn` (via `db.enrollWebAuthn`).\n const meta = ctx.oldSlot.meta as {\n credentialId?: unknown\n wrapIv?: unknown\n prfUsed?: unknown\n beFlag?: unknown\n requireSingleDevice?: unknown\n }\n if (typeof meta.credentialId !== 'string' || meta.credentialId.length === 0) {\n throw new ValidationError(\n 'webAuthnSlotRewrapCeremony: oldSlot.meta.credentialId is missing or invalid. ' +\n 'The slot was not enrolled via @noy-db/on-webauthn.',\n )\n }\n if (typeof meta.wrapIv !== 'string' || meta.wrapIv.length === 0) {\n throw new ValidationError(\n 'webAuthnSlotRewrapCeremony: oldSlot.meta.wrapIv is missing — the slot may be ' +\n 'pre-#16 (synthetic-keyring shape) and must be re-enrolled via db.enrollWebAuthn.',\n )\n }\n const prfUsed = meta.prfUsed === true\n\n if (!prfUsed && !options.allowNonPrfInsecure) {\n throw new WebAuthnPRFUnavailableError()\n }\n\n // 1. Trigger the assertion. Same path `unlockWebAuthn` uses.\n const credentialIdBuf = base64ToBuffer(meta.credentialId)\n const timeout = options.timeout ?? 60_000\n const extensionsInput = (prfUsed\n ? { prf: { eval: { first: PRF_SALT } } }\n : {}\n ) as AuthenticationExtensionsClientInputs\n\n const assertion = await navigator.credentials.get({\n publicKey: {\n challenge: globalThis.crypto.getRandomValues(new Uint8Array(32)),\n allowCredentials: [{ type: 'public-key', id: credentialIdBuf as BufferSource }],\n userVerification: 'required',\n extensions: extensionsInput,\n timeout,\n },\n }) as PublicKeyCredential | null\n\n if (!assertion) {\n throw new WebAuthnCancelledError('assertion')\n }\n\n // BE-flag guard at assertion time — same rule unlockWebAuthn enforces.\n const authData = (assertion.response as AuthenticatorAssertionResponse).authenticatorData\n const beFlag = extractBEFlag(authData)\n if (meta.requireSingleDevice === true && beFlag) {\n throw new WebAuthnMultiDeviceError()\n }\n\n // 2. Derive the wrapping key — deterministic per credential, so this\n // is the SAME key the original enrollment used.\n let wrappingKey: CryptoKey\n if (prfUsed) {\n const extensions = assertion.getClientExtensionResults() as {\n prf?: { results?: { first?: ArrayBuffer } }\n }\n const prfOutput = extensions.prf?.results?.first\n if (!prfOutput) {\n throw new ValidationError(\n 'webAuthnSlotRewrapCeremony: PRF output missing at assertion time. ' +\n 'The authenticator may have lost PRF capability — re-enrol the slot via db.enrollWebAuthn.',\n )\n }\n wrappingKey = await deriveKeyFromPRF(prfOutput)\n } else {\n wrappingKey = await deriveKeyFromRawId(assertion.rawId)\n }\n\n // 3. Decrypt the OLD wrapped_kek to extract carry-through identity\n // fields. The slot's wrapped_kek IS the old wrappedPayload\n // (mapping established by db.enrollWebAuthn at noydb.ts:1178).\n const oldIv = base64ToBuffer(meta.wrapIv)\n const oldCiphertext = base64ToBuffer(ctx.oldSlot.wrapped_kek)\n let oldPlaintext: ArrayBuffer\n try {\n oldPlaintext = await globalThis.crypto.subtle.decrypt(\n { name: 'AES-GCM', iv: oldIv },\n wrappingKey,\n oldCiphertext,\n )\n } catch {\n throw new ValidationError(\n 'webAuthnSlotRewrapCeremony: failed to decrypt the old wrapped payload. ' +\n 'The wrapping key derived from this credential does not match — possible ' +\n 'cross-tenant slot mix-up or a corrupted enrollment.',\n )\n }\n const oldPayload = JSON.parse(new TextDecoder().decode(oldPlaintext)) as {\n userId: string\n displayName: string\n role: Role\n permissions: Record<string, 'rw' | 'ro'>\n deks: Record<string, string>\n salt: string\n }\n\n // 4. Build the NEW payload — identity fields preserved, deks\n // replaced with ctx.newDeks (rewrapped under the new KEK in the\n // keyring file; here we serialize them as raw bytes for the\n // self-contained webauthn-side reconstruction).\n const newDekMap: Record<string, string> = {}\n for (const [collName, dek] of ctx.newDeks) {\n const raw = await globalThis.crypto.subtle.exportKey('raw', dek)\n newDekMap[collName] = bufferToBase64(raw)\n }\n const newPayload = JSON.stringify({\n userId: oldPayload.userId,\n displayName: oldPayload.displayName,\n role: oldPayload.role,\n permissions: oldPayload.permissions,\n deks: newDekMap,\n salt: oldPayload.salt,\n })\n\n // 5. Encrypt with the same wrapping key under a fresh IV.\n const newIv = globalThis.crypto.getRandomValues(new Uint8Array(12))\n const newCiphertext = await globalThis.crypto.subtle.encrypt(\n { name: 'AES-GCM', iv: newIv },\n wrappingKey,\n new TextEncoder().encode(newPayload),\n )\n\n // 6. Return EnrollAuthenticatorOptions preserving id + method.\n // Hub's rotate validates these — a mismatch throws ValidationError\n // (slot-type swap defense; see SlotRewrapContext docstring).\n return {\n id: ctx.oldSlot.id,\n method: 'webauthn',\n wrapped_kek: bufferToBase64(newCiphertext),\n enrolled_via_tier: ctx.oldSlot.enrolled_via_tier,\n meta: {\n credentialId: meta.credentialId,\n wrapIv: bufferToBase64(newIv),\n prfUsed,\n beFlag,\n requireSingleDevice: meta.requireSingleDevice === true,\n },\n }\n}\n\n/**\n * Check whether a `WebAuthnEnrollment` record looks well-formed.\n * Does not perform any cryptographic verification.\n */\nexport function isValidEnrollment(value: unknown): value is WebAuthnEnrollment {\n if (!value || typeof value !== 'object') return false\n const e = value as Record<string, unknown>\n return (\n e._noydb_webauthn === 1 &&\n typeof e.vault === 'string' &&\n typeof e.userId === 'string' &&\n typeof e.credentialId === 'string' &&\n typeof e.wrappedPayload === 'string' &&\n typeof e.wrapIv === 'string'\n )\n}\n"],"mappings":"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","names":["ValidationError"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["/**\n * @noy-db/on-webauthn —\n *\n * Hardware-key keyring for noy-db using the WebAuthn API.\n *\n * Covers every form factor:\n * - Platform authenticators: Touch ID, Face ID, Windows Hello, Android biometric\n * - Roaming authenticators: YubiKey (5C NFC, Bio), SoloKey, Titan, any FIDO2 key\n * - Passkey-capable platform authenticators: iCloud Keychain, Google Password Manager\n *\n * Key derivation model\n * ────────────────────\n * This package uses the **PRF (Pseudo-Random Function) extension** when\n * available to derive a deterministic wrapping key from the WebAuthn\n * credential. The PRF output is consistent across assertions on the same\n * device/credential, enabling unlock-without-secret while keeping the\n * derived key bound to the physical authenticator.\n *\n * When PRF is not supported by the authenticator (common on older hardware),\n * the package can fall back to HKDF-SHA256 over the credential's `rawId` —\n * but by default, enrollment refuses this path because the `rawId` is stored\n * in the record itself, making the record self-decrypting (presence gate, not\n * confidentiality). Use `allowNonPrfInsecure: true` to opt in; prefer a\n * PRF-capable device or the secret for real confidentiality.\n *\n * The derived key is NEVER persisted. It exists only in memory during the\n * unlock operation. What IS persisted (in the noy-db adapter, not in browser\n * storage) is the wrapped KEK: `encrypt(KEK, derivedKey)`.\n *\n * BE-flag guards\n * ──────────────\n * The backup-eligibility (BE) flag in a WebAuthn authenticator data signals\n * that the credential is (or can be) synced across devices — e.g. stored in\n * iCloud Keychain. For single-device security policies (air-gapped USB sticks,\n * high-security terminals), this is a threat: the credential is available on\n * any device where the user's iCloud account is signed in.\n *\n * The `requireSingleDevice: true` option rejects credentials with the BE flag\n * set during enrollment. Existing enrollments are checked at assertion time —\n * if the authenticator data shows BE=1 but `requireSingleDevice` was set at\n * enrollment, the assertion throws `WebAuthnMultiDeviceError`.\n *\n * Enrollment flow\n * ───────────────\n * 1. User is already authenticated (secret or existing session).\n * 2. Call `enrollWebAuthn(keyring, options)`.\n * 3. WebAuthn credential is created; PRF or rawId-derived key wraps the KEK.\n * 4. Returns a `WebAuthnEnrollment` — persist this to the noy-db adapter\n * via `saveEnrollment()`, or store it yourself in any encrypted collection.\n *\n * Unlock flow\n * ───────────\n * 1. Load the `WebAuthnEnrollment` via `loadEnrollment()`.\n * 2. Call `unlockWebAuthn(enrollment, keyring)` — triggers the WebAuthn\n * assertion prompt.\n * 3. On success, returns the unwrapped `CryptoKey` (the KEK) — use it to\n * re-hydrate the session via `createSession()`.\n */\n\nimport { bufferToBase64, base64ToBuffer } from '@noy-db/hub'\nimport { ValidationError } from '@noy-db/hub'\nimport type { Role } from '@noy-db/hub'\nimport type { UnlockedKeyring, EnrollAuthenticatorOptions, SlotRewrapContext } from '@noy-db/hub/on'\n\n// Re-export from core for convenience\nexport { ValidationError } from '@noy-db/hub'\n\n// ─── Error types ──────────────────────────────────────────────────────\n\n/**\n * Thrown when the WebAuthn API is not available in the current environment.\n *\n * Check `isWebAuthnAvailable()` before calling `enrollWebAuthn()` or\n * `unlockWebAuthn()` and show a fallback UI (secret entry) if this\n * returns false. Common scenarios: Node.js environments, older browsers,\n * non-HTTPS origins (WebAuthn requires a Secure Context).\n */\nexport class WebAuthnNotAvailableError extends Error {\n readonly code = 'WEBAUTHN_NOT_AVAILABLE'\n constructor() {\n super('WebAuthn is not available in this environment. A browser with navigator.credentials support is required.')\n this.name = 'WebAuthnNotAvailableError'\n }\n}\n\n/**\n * Thrown when the user dismisses the WebAuthn prompt without completing it.\n *\n * The `op` field distinguishes enrollment cancellation (user chose not to\n * enroll a hardware key) from assertion cancellation (user dismissed the\n * unlock prompt). Treat this as a user-initiated action, not an error — show\n * a \"use secret instead\" option rather than an error message.\n */\nexport class WebAuthnCancelledError extends Error {\n readonly code = 'WEBAUTHN_CANCELLED'\n constructor(op: 'enrollment' | 'assertion') {\n super(`WebAuthn ${op} was cancelled by the user.`)\n this.name = 'WebAuthnCancelledError'\n }\n}\n\n/**\n * Thrown when the authenticator has the backup-eligible (BE) flag set but\n * the vault requires a single-device credential (`requireSingleDevice: true`).\n *\n * A BE credential is synced across devices (e.g. iCloud Keychain, Google\n * Password Manager), which violates the single-device security model. The\n * user must enroll a hardware security key (YubiKey, Titan, SoloKey) instead.\n */\nexport class WebAuthnMultiDeviceError extends Error {\n readonly code = 'WEBAUTHN_MULTI_DEVICE'\n constructor() {\n super(\n 'This credential is backup-eligible (BE flag set) and may be synced across devices. ' +\n 'The vault requires a single-device credential (requireSingleDevice: true). ' +\n 'Please use a hardware security key (YubiKey, Titan, SoloKey) or a platform ' +\n 'authenticator that does not sync credentials across devices.',\n )\n this.name = 'WebAuthnMultiDeviceError'\n }\n}\n\n/**\n * Thrown when `enrollWebAuthn()` (or `webAuthnSlotRewrapCeremony()`) is\n * asked to enroll/rewrap without a PRF-capable authenticator and without\n * explicit opt-in via `allowNonPrfInsecure: true`.\n *\n * Non-PRF WebAuthn cannot provide confidentiality: its wrapping key derives\n * entirely from the credential's `rawId`, which is stored in cleartext in\n * the enrollment record itself — the record would self-decrypt with no\n * authenticator involved. By default, enrollment/rewrap refuses this path.\n * Use a PRF-capable credential, fall back to the secret, or set\n * `allowNonPrfInsecure: true` to explicitly accept a non-confidential\n * presence gate instead.\n */\nexport class WebAuthnPRFUnavailableError extends Error {\n readonly code = 'WEBAUTHN_PRF_UNAVAILABLE'\n constructor() {\n super(\n 'The PRF extension is not available on this authenticator, so it cannot provide ' +\n 'WebAuthn confidentiality — the non-PRF wrapping key derives entirely from the ' +\n \"credential's rawId, which is stored in the enrollment record itself (the record \" +\n 'would self-decrypt with no authenticator involved). Enrollment refuses this path ' +\n 'by default. Use a PRF-capable authenticator, use the secret instead, or pass ' +\n '{ allowNonPrfInsecure: true } to explicitly accept a non-confidential presence gate.',\n )\n this.name = 'WebAuthnPRFUnavailableError'\n }\n}\n\n// ─── Types ────────────────────────────────────────────────────────────\n\n/**\n * A persisted WebAuthn enrollment record. Store this in a noy-db\n * collection (encrypted like any other record) or return it from\n * `saveEnrollment()` / `loadEnrollment()` helpers.\n */\nexport interface WebAuthnEnrollment {\n /** Enrollment format version. */\n readonly _noydb_webauthn: 1\n /** The vault this enrollment was created for. */\n readonly vault: string\n /** The user ID this enrollment belongs to. */\n readonly userId: string\n /** WebAuthn credential ID (base64). Use for allowCredentials in assertions. */\n readonly credentialId: string\n /** Whether PRF was used for key derivation (vs rawId HKDF fallback). */\n readonly prfUsed: boolean\n /** Whether the BE (backup-eligibility) flag was present at enrollment time. */\n readonly beFlag: boolean\n /** Whether single-device was required at enrollment time. */\n readonly requireSingleDevice: boolean\n /** The wrapped KEK: encrypt(exportedDekMap, derivedKey). Base64. */\n readonly wrappedPayload: string\n /** IV used for the wrapping. Base64. */\n readonly wrapIv: string\n /** ISO timestamp of enrollment. */\n readonly enrolledAt: string\n}\n\n/** Options for `enrollWebAuthn()`. */\nexport interface WebAuthnEnrollOptions {\n /**\n * Relying party ID and name for the WebAuthn credential.\n * Defaults to `{ id: window.location.hostname, name: 'NOYDB' }`.\n */\n rp?: { id?: string; name: string }\n /**\n * If `true`, refuse to enroll credentials with the BE flag set\n * (multi-device / syncable passkeys). Defaults to `false`.\n *\n * Set to `true` for high-security deployments where the credential\n * must be bound to a single physical device (YubiKey, Titan, etc.).\n */\n requireSingleDevice?: boolean\n /**\n * WebAuthn timeout in milliseconds. Default: 60_000.\n */\n timeout?: number\n /**\n * If `true`, prefer a cross-platform authenticator (roaming security key).\n * If `false`, prefer a platform authenticator (Touch ID, Face ID).\n * If undefined, let the browser choose.\n */\n preferCrossPlatform?: boolean\n /**\n * Non-PRF WebAuthn cannot provide confidentiality — its wrapping key\n * derives from `credentialId`, which is stored in the enrollment record,\n * so the record self-decrypts with no authenticator. By default,\n * enrollment REFUSES the non-PRF path (throws `WebAuthnPRFUnavailableError`).\n * Set `true` ONLY to explicitly opt into a non-confidential presence gate;\n * prefer the secret or a PRF-capable credential for real confidentiality.\n */\n allowNonPrfInsecure?: boolean\n}\n\n/** Options for `unlockWebAuthn()`. */\nexport interface WebAuthnUnlockOptions {\n /** WebAuthn timeout in milliseconds. Default: 60_000. */\n timeout?: number\n /**\n * Only consulted by `webAuthnSlotRewrapCeremony()` — `unlockWebAuthn()`\n * ignores it, since unlocking an existing non-PRF record is always\n * permitted (back-compat; that record was never confidential). Same\n * semantics as `WebAuthnEnrollOptions.allowNonPrfInsecure`: required to\n * rewrap a non-PRF slot, since rewrap produces a fresh self-decrypting\n * wrapped payload.\n */\n allowNonPrfInsecure?: boolean\n}\n\n// ─── Environment check ─────────────────────────────────────────────────\n\n/**\n * Returns `true` if WebAuthn is available and can be used for enrollment or unlock.\n *\n * Checks for `navigator.credentials`, `window.PublicKeyCredential`, and a\n * Secure Context (`window.isSecureContext`). Call this before rendering the\n * \"Register hardware key\" button to avoid showing options that will fail.\n */\nexport function isWebAuthnAvailable(): boolean {\n return (\n typeof window !== 'undefined' &&\n typeof window.PublicKeyCredential !== 'undefined' &&\n typeof navigator !== 'undefined' &&\n typeof navigator.credentials !== 'undefined'\n )\n}\n\n// ─── PRF salt ─────────────────────────────────────────────────────────\n\nconst PRF_SALT = new TextEncoder().encode('noydb-webauthn-kek-derive')\n\n// ─── Key derivation helpers ────────────────────────────────────────────\n\n/**\n * Derive a wrapping key from PRF output.\n * PRF output is 32 bytes of authenticator-bound pseudo-random data.\n */\nasync function deriveKeyFromPRF(prfOutput: ArrayBuffer): Promise<CryptoKey> {\n const keyMaterial = await globalThis.crypto.subtle.importKey(\n 'raw',\n prfOutput,\n 'HKDF',\n false,\n ['deriveKey'],\n )\n return globalThis.crypto.subtle.deriveKey(\n {\n name: 'HKDF',\n hash: 'SHA-256',\n salt: PRF_SALT,\n info: new TextEncoder().encode('noydb-kek-wrap-v1'),\n },\n keyMaterial,\n { name: 'AES-GCM', length: 256 },\n false,\n ['encrypt', 'decrypt'],\n )\n}\n\n/**\n * Derive a wrapping key from the credential's rawId (fallback when PRF unavailable).\n * rawId IS the entire secret and is stored in the record itself — the record\n * self-decrypts with no authenticator involved (presence gate, not confidentiality).\n * Universally supported but only usable via explicit `allowNonPrfInsecure: true`.\n */\nasync function deriveKeyFromRawId(rawId: ArrayBuffer): Promise<CryptoKey> {\n const keyMaterial = await globalThis.crypto.subtle.importKey(\n 'raw',\n rawId,\n 'HKDF',\n false,\n ['deriveKey'],\n )\n return globalThis.crypto.subtle.deriveKey(\n {\n name: 'HKDF',\n hash: 'SHA-256',\n salt: new TextEncoder().encode('noydb-webauthn-rawid-fallback'),\n info: new TextEncoder().encode('noydb-kek-wrap-v1'),\n },\n keyMaterial,\n { name: 'AES-GCM', length: 256 },\n false,\n ['encrypt', 'decrypt'],\n )\n}\n\n// ─── BE flag extraction ────────────────────────────────────────────────\n\n/**\n * Extract the BE (backup-eligibility) flag from WebAuthn authenticator data.\n * Authenticator data byte layout (CTAP2 spec):\n * bytes 0-31: rpIdHash\n * byte 32: flags byte\n * bytes 33-36: signCount\n * ...\n *\n * Flags byte bit layout (bit 0 = LSB):\n * bit 0 (UP): user presence\n * bit 2 (UV): user verification\n * bit 3 (BE): backup eligibility\n * bit 4 (BS): backup state\n * bit 6 (AT): attested credential data present\n * bit 7 (ED): extension data present\n */\nfunction extractBEFlag(authData: ArrayBuffer): boolean {\n const bytes = new Uint8Array(authData)\n if (bytes.length < 33) return false\n const flagsByte = bytes[32]!\n return (flagsByte & 0b00001000) !== 0 // bit 3\n}\n\n// ─── Payload wrap/unwrap ───────────────────────────────────────────────\n\n/**\n * Serialize and encrypt the DEK map from `keyring` using `wrappingKey`.\n * The wrapped payload is what gets stored in the enrollment record.\n */\nasync function wrapKeyringSummary(\n keyring: UnlockedKeyring,\n wrappingKey: CryptoKey,\n): Promise<{ wrappedPayload: string; wrapIv: string }> {\n const dekMap: Record<string, string> = {}\n for (const [collName, dek] of keyring.deks) {\n const raw = await globalThis.crypto.subtle.exportKey('raw', dek)\n dekMap[collName] = bufferToBase64(raw)\n }\n\n const payload = JSON.stringify({\n userId: keyring.userId,\n displayName: keyring.displayName,\n role: keyring.role,\n permissions: keyring.permissions,\n deks: dekMap,\n salt: bufferToBase64(keyring.salt),\n })\n\n const iv = globalThis.crypto.getRandomValues(new Uint8Array(12))\n const encrypted = await globalThis.crypto.subtle.encrypt(\n { name: 'AES-GCM', iv },\n wrappingKey,\n new TextEncoder().encode(payload),\n )\n\n return { wrappedPayload: bufferToBase64(encrypted), wrapIv: bufferToBase64(iv) }\n}\n\n/**\n * Decrypt and deserialize the keyring payload using `wrappingKey`.\n */\nasync function unwrapKeyringSummary(\n enrollment: WebAuthnEnrollment,\n wrappingKey: CryptoKey,\n): Promise<UnlockedKeyring> {\n const iv = base64ToBuffer(enrollment.wrapIv)\n const ciphertext = base64ToBuffer(enrollment.wrappedPayload)\n\n let plaintext: ArrayBuffer\n try {\n plaintext = await globalThis.crypto.subtle.decrypt(\n { name: 'AES-GCM', iv },\n wrappingKey,\n ciphertext,\n )\n } catch {\n throw new ValidationError('WebAuthn decryption failed — the authenticator may have changed or the enrollment may be corrupt.')\n }\n\n const parsed = JSON.parse(new TextDecoder().decode(plaintext)) as {\n userId: string\n displayName: string\n role: Role\n permissions: Record<string, 'rw' | 'ro'>\n deks: Record<string, string>\n salt: string\n }\n\n const deks = new Map<string, CryptoKey>()\n for (const [collName, rawBase64] of Object.entries(parsed.deks)) {\n const dek = await globalThis.crypto.subtle.importKey(\n 'raw',\n base64ToBuffer(rawBase64),\n { name: 'AES-GCM', length: 256 },\n true,\n ['encrypt', 'decrypt'],\n )\n deks.set(collName, dek)\n }\n\n return {\n userId: parsed.userId,\n displayName: parsed.displayName,\n role: parsed.role,\n permissions: parsed.permissions,\n deks,\n kek: null,\n salt: base64ToBuffer(parsed.salt),\n authenticators: [],\n }\n}\n\n// ─── Public API ────────────────────────────────────────────────────────\n\n/**\n * Enroll a WebAuthn credential for the given keyring.\n *\n * The caller must already have an unlocked keyring (from secret auth or\n * an existing session). The WebAuthn credential creation prompt is triggered\n * by this call.\n *\n * Returns a `WebAuthnEnrollment` that should be persisted — typically via\n * `saveEnrollment()` into a noy-db collection.\n *\n * @throws `WebAuthnNotAvailableError` if the environment doesn't support WebAuthn.\n * @throws `WebAuthnCancelledError` if the user cancels the credential creation.\n * @throws `WebAuthnMultiDeviceError` if `requireSingleDevice` is true and the\n * authenticator returned a credential with the BE flag set.\n * @throws `WebAuthnPRFUnavailableError` if the authenticator did not provide\n * PRF output and `options.allowNonPrfInsecure` is not `true` — see\n * that option's doc for why the non-PRF path is refused by default.\n */\nexport async function enrollWebAuthn(\n keyring: UnlockedKeyring,\n vault: string,\n options: WebAuthnEnrollOptions = {},\n): Promise<WebAuthnEnrollment> {\n if (!isWebAuthnAvailable()) {\n throw new WebAuthnNotAvailableError()\n }\n\n const rpId = options.rp?.id ?? (typeof window !== 'undefined' ? window.location.hostname : 'localhost')\n const rpName = options.rp?.name ?? 'NOYDB'\n const timeout = options.timeout ?? 60_000\n\n const challenge = globalThis.crypto.getRandomValues(new Uint8Array(32))\n const userIdBytes = new TextEncoder().encode(keyring.userId)\n\n const authenticatorSelection: AuthenticatorSelectionCriteria = {\n userVerification: 'required',\n residentKey: 'preferred',\n }\n if (options.preferCrossPlatform === true) {\n authenticatorSelection.authenticatorAttachment = 'cross-platform'\n } else if (options.preferCrossPlatform === false) {\n authenticatorSelection.authenticatorAttachment = 'platform'\n }\n\n // Request PRF extension for deterministic key derivation\n const extensionsInput = {\n prf: { eval: { first: PRF_SALT } },\n } as AuthenticationExtensionsClientInputs\n\n const credential = await navigator.credentials.create({\n publicKey: {\n challenge,\n rp: { id: rpId, name: rpName },\n user: {\n id: userIdBytes,\n name: keyring.userId,\n displayName: keyring.displayName,\n },\n pubKeyCredParams: [\n { type: 'public-key', alg: -7 }, // ES256\n { type: 'public-key', alg: -257 }, // RS256\n { type: 'public-key', alg: -8 }, // EdDSA\n ],\n authenticatorSelection,\n extensions: extensionsInput,\n timeout,\n },\n }) as PublicKeyCredential | null\n\n if (!credential) {\n throw new WebAuthnCancelledError('enrollment')\n }\n\n const authData = (credential.response as AuthenticatorAttestationResponse).getAuthenticatorData()\n const beFlag = extractBEFlag(authData)\n\n if (options.requireSingleDevice && beFlag) {\n throw new WebAuthnMultiDeviceError()\n }\n\n // Try to get PRF output from extensions\n const extensions = credential.getClientExtensionResults() as {\n prf?: { results?: { first?: ArrayBuffer } }\n }\n const prfOutput = extensions.prf?.results?.first\n const prfUsed = !!prfOutput\n\n if (!prfUsed && !options.allowNonPrfInsecure) {\n throw new WebAuthnPRFUnavailableError()\n }\n\n const wrappingKey = prfOutput\n ? await deriveKeyFromPRF(prfOutput)\n : await deriveKeyFromRawId(credential.rawId)\n\n const { wrappedPayload, wrapIv } = await wrapKeyringSummary(keyring, wrappingKey)\n\n return {\n _noydb_webauthn: 1,\n vault,\n userId: keyring.userId,\n credentialId: bufferToBase64(credential.rawId),\n prfUsed,\n beFlag,\n requireSingleDevice: options.requireSingleDevice ?? false,\n wrappedPayload,\n wrapIv,\n enrolledAt: new Date().toISOString(),\n }\n}\n\n/**\n * Unlock a vault using a previously enrolled WebAuthn credential.\n *\n * Triggers the WebAuthn assertion prompt. On success, decrypts the keyring\n * payload from the enrollment record and returns an `UnlockedKeyring`.\n *\n * The returned keyring has the same DEKs as at enrollment time. If DEK\n * VALUES have been re-minted since enrollment — `rotateKeys`, not\n * `rotateSecret` — this returns stale DEKs and the caller should detect\n * decryption failures and prompt for re-enrollment. A `rotateSecret`\n * phrase rotation does NOT stale them: it rewraps the same values.\n *\n * Non-PRF unlock (`enrollment.prfUsed === false`) is a presence gate, not\n * zero-knowledge confidentiality — it is kept working here for back-compat\n * with existing non-PRF enrollments; new enrollments refuse that path by\n * default (see `WebAuthnEnrollOptions.allowNonPrfInsecure`).\n *\n * @throws `WebAuthnNotAvailableError` if the environment doesn't support WebAuthn.\n * @throws `WebAuthnCancelledError` if the user cancels the assertion.\n * @throws `WebAuthnMultiDeviceError` if `requireSingleDevice` was set at\n * enrollment and the authenticator data now shows BE=1.\n * @throws `ValidationError` if decryption of the keyring payload fails.\n */\nexport async function unlockWebAuthn(\n enrollment: WebAuthnEnrollment,\n options: WebAuthnUnlockOptions = {},\n): Promise<UnlockedKeyring> {\n if (!isWebAuthnAvailable()) {\n throw new WebAuthnNotAvailableError()\n }\n\n const timeout = options.timeout ?? 60_000\n const credentialId = base64ToBuffer(enrollment.credentialId)\n\n const extensionsInput = (enrollment.prfUsed\n ? { prf: { eval: { first: PRF_SALT } } }\n : {}\n ) as AuthenticationExtensionsClientInputs\n\n const assertion = await navigator.credentials.get({\n publicKey: {\n challenge: globalThis.crypto.getRandomValues(new Uint8Array(32)),\n allowCredentials: [{ type: 'public-key', id: credentialId }],\n userVerification: 'required',\n extensions: extensionsInput,\n timeout,\n },\n }) as PublicKeyCredential | null\n\n if (!assertion) {\n throw new WebAuthnCancelledError('assertion')\n }\n\n // BE-flag guard at assertion time\n const authData = (assertion.response as AuthenticatorAssertionResponse).authenticatorData\n const beFlag = extractBEFlag(authData)\n if (enrollment.requireSingleDevice && beFlag) {\n throw new WebAuthnMultiDeviceError()\n }\n\n // Derive the wrapping key using the same method as enrollment\n let wrappingKey: CryptoKey\n if (enrollment.prfUsed) {\n const extensions = assertion.getClientExtensionResults() as {\n prf?: { results?: { first?: ArrayBuffer } }\n }\n const prfOutput = extensions.prf?.results?.first\n if (!prfOutput) {\n throw new ValidationError(\n 'PRF extension output not available at assertion time. ' +\n 'The authenticator may not support PRF. Re-enroll without PRF support.',\n )\n }\n wrappingKey = await deriveKeyFromPRF(prfOutput)\n } else {\n wrappingKey = await deriveKeyFromRawId(assertion.rawId)\n }\n\n return unwrapKeyringSummary(enrollment, wrappingKey)\n}\n\n/**\n * `SlotRewrapCeremony` for WebAuthn slots — used by hub's\n * `rotateSecret({ slotCeremonies: { [slotId]: webAuthnSlotRewrapCeremony } })`\n * to preserve a tier-2 WebAuthn enrollment across a tier-1 phrase\n * rotation without requiring re-enrollment of the credential.\n *\n * The credential itself is unaffected by phrase rotation — the\n * wrapping key derived from PRF (or rawId fallback) is bound to the\n * authenticator, not to the secret. What needs to change is the\n * **wrapped payload**: the encrypted blob the slot's `wrapped_kek`\n * field holds. After rotation the old payload is wrapped under the\n * OLD KEK, so hub can no longer thread it through the rotated\n * keyring; the new payload must hold `ctx.newDeks`.\n *\n * ⚠️ **The old payload's DEK VALUES are not stale — only its wrapping\n * is.** `rotateSecret` rewraps the SAME DEKs under a freshly-derived\n * KEK; only `rotateKeys` re-mints DEK values. Records are encrypted\n * with DEKs, so a captured `wrappedPayload` keeps decrypting\n * everything across any number of phrase rotations. An earlier\n * revision of this comment said \"now stale because rotateSecret\n * rewrapped them under a fresh KEK\", which reads as though rotation\n * invalidated the captured blob. It does not, and nothing in this\n * ceremony is a revocation step.\n *\n * ## Revoking a credential — this ceremony is a REQUIRED INPUT to it\n *\n * Hub's `revokeAuthenticator` is the real revocation, and it is three steps:\n * `removeAuthenticator` (hides the slot), `rotateKeys` (mints fresh DEKs —\n * the step that actually revokes), then `rotateSecret` with a\n * `slotCeremonies` entry for every slot that STAYS.\n *\n * ⛔ **A remaining slot handed no ceremony is DROPPED, not preserved.** After\n * the `rotateKeys` step its blob wraps DEKs that no longer exist, so keeping\n * it would leave a credential that silently fails. Its holder must re-enrol.\n *\n * So revoking ONE credential means supplying `webAuthnSlotRewrapCeremony` for\n * EVERY WebAuthn slot you intend to keep — and each requires that\n * credential to be present (a live assertion on the authenticator, which means the user in front of the device). Re-wrapping a credential\n * is the credential's own operation; there is no way to do it on the\n * holder's behalf.\n *\n * ⚠️ **Not in any published hub at the time of writing.**\n * `revokeAuthenticator` landed on noy-db `main` in `0612b4ac`, AFTER\n * `@noy-db/hub@0.7.1-pre.0` was cut and published — so it is in neither\n * `@latest` (0.7.0) nor `@next` (0.7.1-pre.0). Ask npm before promising it to\n * a consumer; do not infer its version from hub's `main`.\n *\n * Single ceremony, two operations:\n * 1. Trigger one WebAuthn assertion to derive the wrapping key.\n * 2. Decrypt the OLD `wrapped_kek` to extract identity fields\n * (userId, displayName, role, permissions, salt) — these don't\n * change on rotation, so they're carried verbatim into the new\n * payload.\n * 3. Encrypt the NEW payload (same identity + `ctx.newDeks`) under\n * the same wrapping key, with a fresh IV.\n * 4. Return `EnrollAuthenticatorOptions` preserving `oldSlot.id`\n * and `method: 'webauthn'` (hub validates these to prevent\n * slot-type swap mid-rotation).\n *\n * Previously a workaround was needed — detect dropped slots after\n * rotate and offer \"Re-enrol Touch ID\" inline. This ceremony\n * eliminates that step.\n *\n * Out of scope: tier-3 PIN. PIN state lives in `QuickUnlockStore`\n * (RAM-only), not in `KeyringFile.authenticators[]`, so\n * `slotCeremonies` doesn't apply. Clear PIN state on rotate; let\n * the user set a fresh PIN via `db.enrollUnlock` immediately after.\n *\n * @throws {WebAuthnNotAvailableError} when the environment lacks WebAuthn.\n * @throws {WebAuthnCancelledError} when the user dismisses the assertion.\n * @throws {WebAuthnMultiDeviceError} when `meta.requireSingleDevice` was\n * set at enrollment and the authenticator now reports BE=1.\n * @throws {ValidationError} when `oldSlot.method !== 'webauthn'`,\n * when required `meta` fields are missing, or when the old\n * payload fails to decrypt (credential changed / payload\n * tampered).\n * @throws {WebAuthnPRFUnavailableError} when the old slot is non-PRF and\n * `options.allowNonPrfInsecure` is not `true` — rewrapping a\n * non-PRF slot produces a fresh self-decrypting payload, so it\n * requires the same explicit acknowledgement as enrollment.\n *\n * @see passwordSlotRewrapCeremony — the password parallel.\n */\nexport async function webAuthnSlotRewrapCeremony(\n ctx: SlotRewrapContext,\n options: WebAuthnUnlockOptions = {},\n): Promise<EnrollAuthenticatorOptions> {\n if (ctx.oldSlot.method !== 'webauthn') {\n throw new ValidationError(\n `webAuthnSlotRewrapCeremony: oldSlot.method is \"${ctx.oldSlot.method}\"; expected \"webauthn\". ` +\n 'This ceremony only handles WebAuthn slots — pair other methods with their own helpers.',\n )\n }\n if (ctx.oldSlot.wrapKind === 'deks') {\n throw new ValidationError(\n 'webAuthnSlotRewrapCeremony: oldSlot is a wrap-DEKs slot; expected wrap-KEK. ' +\n 'WebAuthn slots use the wrap-KEK variant; mismatch indicates the slot was ' +\n 'enrolled by a different on-* package.',\n )\n }\n if (!isWebAuthnAvailable()) {\n throw new WebAuthnNotAvailableError()\n }\n\n // Pull the per-slot fields needed for assertion + decrypt. These were\n // written into `meta` by `enrollWebAuthn` (via `db.enrollWebAuthn`).\n const meta = ctx.oldSlot.meta as {\n credentialId?: unknown\n wrapIv?: unknown\n prfUsed?: unknown\n beFlag?: unknown\n requireSingleDevice?: unknown\n }\n if (typeof meta.credentialId !== 'string' || meta.credentialId.length === 0) {\n throw new ValidationError(\n 'webAuthnSlotRewrapCeremony: oldSlot.meta.credentialId is missing or invalid. ' +\n 'The slot was not enrolled via @noy-db/on-webauthn.',\n )\n }\n if (typeof meta.wrapIv !== 'string' || meta.wrapIv.length === 0) {\n throw new ValidationError(\n 'webAuthnSlotRewrapCeremony: oldSlot.meta.wrapIv is missing — the slot may be ' +\n 'pre-#16 (synthetic-keyring shape) and must be re-enrolled via db.enrollWebAuthn.',\n )\n }\n const prfUsed = meta.prfUsed === true\n\n if (!prfUsed && !options.allowNonPrfInsecure) {\n throw new WebAuthnPRFUnavailableError()\n }\n\n // 1. Trigger the assertion. Same path `unlockWebAuthn` uses.\n const credentialIdBuf = base64ToBuffer(meta.credentialId)\n const timeout = options.timeout ?? 60_000\n const extensionsInput = (prfUsed\n ? { prf: { eval: { first: PRF_SALT } } }\n : {}\n ) as AuthenticationExtensionsClientInputs\n\n const assertion = await navigator.credentials.get({\n publicKey: {\n challenge: globalThis.crypto.getRandomValues(new Uint8Array(32)),\n allowCredentials: [{ type: 'public-key', id: credentialIdBuf }],\n userVerification: 'required',\n extensions: extensionsInput,\n timeout,\n },\n }) as PublicKeyCredential | null\n\n if (!assertion) {\n throw new WebAuthnCancelledError('assertion')\n }\n\n // BE-flag guard at assertion time — same rule unlockWebAuthn enforces.\n const authData = (assertion.response as AuthenticatorAssertionResponse).authenticatorData\n const beFlag = extractBEFlag(authData)\n if (meta.requireSingleDevice === true && beFlag) {\n throw new WebAuthnMultiDeviceError()\n }\n\n // 2. Derive the wrapping key — deterministic per credential, so this\n // is the SAME key the original enrollment used.\n let wrappingKey: CryptoKey\n if (prfUsed) {\n const extensions = assertion.getClientExtensionResults() as {\n prf?: { results?: { first?: ArrayBuffer } }\n }\n const prfOutput = extensions.prf?.results?.first\n if (!prfOutput) {\n throw new ValidationError(\n 'webAuthnSlotRewrapCeremony: PRF output missing at assertion time. ' +\n 'The authenticator may have lost PRF capability — re-enrol the slot via db.enrollWebAuthn.',\n )\n }\n wrappingKey = await deriveKeyFromPRF(prfOutput)\n } else {\n wrappingKey = await deriveKeyFromRawId(assertion.rawId)\n }\n\n // 3. Decrypt the OLD wrapped_kek to extract carry-through identity\n // fields. The slot's wrapped_kek IS the old wrappedPayload\n // (mapping established by db.enrollWebAuthn at noydb.ts:1178).\n const oldIv = base64ToBuffer(meta.wrapIv)\n const oldCiphertext = base64ToBuffer(ctx.oldSlot.wrapped_kek)\n let oldPlaintext: ArrayBuffer\n try {\n oldPlaintext = await globalThis.crypto.subtle.decrypt(\n { name: 'AES-GCM', iv: oldIv },\n wrappingKey,\n oldCiphertext,\n )\n } catch {\n throw new ValidationError(\n 'webAuthnSlotRewrapCeremony: failed to decrypt the old wrapped payload. ' +\n 'The wrapping key derived from this credential does not match — possible ' +\n 'cross-tenant slot mix-up or a corrupted enrollment.',\n )\n }\n const oldPayload = JSON.parse(new TextDecoder().decode(oldPlaintext)) as {\n userId: string\n displayName: string\n role: Role\n permissions: Record<string, 'rw' | 'ro'>\n deks: Record<string, string>\n salt: string\n }\n\n // 4. Build the NEW payload — identity fields preserved, deks\n // replaced with ctx.newDeks (rewrapped under the new KEK in the\n // keyring file; here we serialize them as raw bytes for the\n // self-contained webauthn-side reconstruction).\n const newDekMap: Record<string, string> = {}\n for (const [collName, dek] of ctx.newDeks) {\n const raw = await globalThis.crypto.subtle.exportKey('raw', dek)\n newDekMap[collName] = bufferToBase64(raw)\n }\n const newPayload = JSON.stringify({\n userId: oldPayload.userId,\n displayName: oldPayload.displayName,\n role: oldPayload.role,\n permissions: oldPayload.permissions,\n deks: newDekMap,\n salt: oldPayload.salt,\n })\n\n // 5. Encrypt with the same wrapping key under a fresh IV.\n const newIv = globalThis.crypto.getRandomValues(new Uint8Array(12))\n const newCiphertext = await globalThis.crypto.subtle.encrypt(\n { name: 'AES-GCM', iv: newIv },\n wrappingKey,\n new TextEncoder().encode(newPayload),\n )\n\n // 6. Return EnrollAuthenticatorOptions preserving id + method.\n // Hub's rotate validates these — a mismatch throws ValidationError\n // (slot-type swap defense; see SlotRewrapContext docstring).\n return {\n id: ctx.oldSlot.id,\n method: 'webauthn',\n wrapped_kek: bufferToBase64(newCiphertext),\n enrolled_via_tier: ctx.oldSlot.enrolled_via_tier,\n meta: {\n credentialId: meta.credentialId,\n wrapIv: bufferToBase64(newIv),\n prfUsed,\n beFlag,\n requireSingleDevice: meta.requireSingleDevice === true,\n },\n }\n}\n\n/**\n * Check whether a `WebAuthnEnrollment` record looks well-formed.\n * Does not perform any cryptographic verification.\n */\nexport function isValidEnrollment(value: unknown): value is WebAuthnEnrollment {\n if (!value || typeof value !== 'object') return false\n const e = value as Record<string, unknown>\n return (\n e._noydb_webauthn === 1 &&\n typeof e.vault === 'string' &&\n typeof e.userId === 'string' &&\n typeof e.credentialId === 'string' &&\n typeof e.wrappedPayload === 'string' &&\n typeof e.wrapIv === 'string'\n )\n}\n"],"mappings":"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","names":["ValidationError"]}
|
package/package.json
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@noy-db/on-webauthn",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0-pre.0",
|
|
4
4
|
"description": "WebAuthn hardware-key keyrings for noy-db — Touch ID, Face ID, Windows Hello, YubiKey, FIDO2 passkeys",
|
|
5
|
-
"license": "
|
|
5
|
+
"license": "Apache-2.0",
|
|
6
6
|
"author": "vLannaAi <vicio@lanna.ai>",
|
|
7
|
-
"homepage": "https://github.com/
|
|
7
|
+
"homepage": "https://github.com/noy-db/on/tree/main/on-webauthn#readme",
|
|
8
8
|
"repository": {
|
|
9
9
|
"type": "git",
|
|
10
|
-
"url": "git+https://github.com/
|
|
11
|
-
"directory": "
|
|
10
|
+
"url": "git+https://github.com/noy-db/on.git",
|
|
11
|
+
"directory": "on-webauthn"
|
|
12
12
|
},
|
|
13
13
|
"bugs": {
|
|
14
|
-
"url": "https://github.com/
|
|
14
|
+
"url": "https://github.com/noy-db/on/issues"
|
|
15
15
|
},
|
|
16
16
|
"type": "module",
|
|
17
17
|
"sideEffects": false,
|
|
@@ -26,17 +26,19 @@
|
|
|
26
26
|
"files": [
|
|
27
27
|
"dist",
|
|
28
28
|
"README.md",
|
|
29
|
-
"LICENSE"
|
|
29
|
+
"LICENSE",
|
|
30
|
+
"NOTICE"
|
|
30
31
|
],
|
|
31
32
|
"engines": {
|
|
32
33
|
"node": ">=22.0.0"
|
|
33
34
|
},
|
|
34
35
|
"peerDependencies": {
|
|
35
|
-
"@noy-db/hub": "^0.7.0-pre.
|
|
36
|
+
"@noy-db/hub": "^0.7.0 || ^0.7.1-pre.0 || ^0.8.0-pre.0"
|
|
36
37
|
},
|
|
37
38
|
"devDependencies": {
|
|
38
|
-
"@noy-db/
|
|
39
|
-
"@noy-db/
|
|
39
|
+
"@noy-db/hub": "0.8.0-pre.0",
|
|
40
|
+
"@noy-db/test-ceremony-conformance": "0.8.0-pre.0",
|
|
41
|
+
"happy-dom": "^18.0.0"
|
|
40
42
|
},
|
|
41
43
|
"keywords": [
|
|
42
44
|
"noy-db",
|