@virtonetwork/authenticators-webauthn 1.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 ADDED
@@ -0,0 +1,91 @@
1
+ # WebAuthn Authenticator for Virto Network
2
+
3
+ A TypeScript helper that wires **passkeys** (WebAuthn resident credentials) to the [@virtonetwork/signer](https://github.com/virto-network/papi-signers) stack. It exposes a single class, `WebAuthn`, that fulfils the `Authenticator<number>` interface used by `PassSigner`.
4
+ The implementation is **browser‑only** and keeps all credential mapping in the caller’s hands — perfect for SPAs or wallet extensions that already manage users.
5
+
6
+ ## ✨ Features
7
+
8
+ * **One‑line setup** → `await new WebAuthn(user).setup()`
9
+ * **Kreivo‑compatible challenges** for secure on‑chain attestations
10
+ * Deterministic `deviceId = Blake2‑256(credentialId)`
11
+ * Produces SCALE‑encoded `Attestation` / `PassAuthenticate` objects
12
+ * Zero persistence: inject or register credentials as you see fit
13
+
14
+ ## 📦 Installation
15
+
16
+ ```bash
17
+ npm i @virtonetwork/authenticators-webauthn
18
+ ```
19
+
20
+ ## 🚀 Quick start
21
+
22
+ ```ts
23
+ import { WebAuthn } from "@virtonetwork/authenticators-webauthn";
24
+ import { PassSigner } from "@virtonetwork/signer";
25
+
26
+ // 1️⃣ Restore user → credential mapping (from DB, localStorage…)
27
+ const savedId = await db.getCredentialId("alice@example.com");
28
+
29
+ // 2️⃣ Bootstrap helper
30
+ const wa = await new WebAuthn("alice@example.com", savedId).setup();
31
+
32
+ // 3️⃣ Enrol a new pass‑key if needed
33
+ if (!savedId) {
34
+ const att = await wa.register(blockNumber, blockHash);
35
+ await db.saveCredentialId("alice@example.com", att.credentialId);
36
+ }
37
+
38
+ // 4️⃣ Sign any runtime challenge
39
+ await passSigner.credentials(
40
+ await wa.authenticate(challenge, blockNumber),
41
+ );
42
+ ```
43
+
44
+ ## 🛠️ API
45
+
46
+ | Method | Returns | Notes |
47
+ | --------------------------------------------- | ------------------------------- | -------------------------------------------------------------------------------------- |
48
+ | `setup()` | `Promise< this >` | Computes `hashedUserId`. Call once. |
49
+ | `register(blockNo, blockHash, [displayName])` | `Promise<TAttestation<number>>` | Generates a WebAuthn credential and attestation. Throws if `credentialId` already set. |
50
+ | `authenticate(challenge, context)` | `Promise<TPassAuthenticate>` | Signs an arbitrary 32‑byte challenge. Requires `credentialId`. |
51
+ | `getDeviceId(webAuthn) | `Promise<DeviceId>` | `Blake2‑256(credentialId)` wrapped in `Binary`. |
52
+ | `setCredentialId(id)` | `void` | Inject credential id after construction. |
53
+
54
+ > **Type parameter** `<number>` → `context` inside attestations/assertions is the **block number**.
55
+
56
+ ## 📝 Persistence Strategy
57
+
58
+ This package **does not** store credential ids. A typical strategy is:
59
+
60
+ 1. During **registration**, persist `attestation.publicKey.bytes` keyed by `userId`.
61
+ 2. On next load, feed that id into the `WebAuthn` constructor.
62
+ 3. For multiple devices per account, maintain an *array* of ids and pick one UI‑side.
63
+
64
+ ## ⚠️ Error Handling
65
+
66
+ | Error message | Cause | Fix |
67
+ | ------------------------------ | -------------------------------------------------------- | --------------------------------------------- |
68
+ | `Already have a credentialId…` | Called `register()` when id already present | Skip registration or call with a new instance |
69
+ | `credentialId unknown…` | Tried to authenticate/get device id without a credential | Inject stored id or call `register()` |
70
+ | `DOMException: …` | User dismissed the WebAuthn prompt | Ask user to retry |
71
+
72
+ ## 🧳 Dependencies
73
+
74
+ * **@virtonetwork/signer** ≥ 0.10 — interfaces, `KreivoBlockChallenger`, `PassSigner`
75
+ * **@polkadot-api/substrate-bindings** — `Binary`, `Blake2256`
76
+ * Browser with WebAuthn (Chrome ≥ 109, Firefox ≥ 106, Safari ≥ 16)
77
+
78
+ ## 🩹 Development
79
+
80
+ ```bash
81
+ # lint & type‑check
82
+ npm run lint && npm run typecheck
83
+ ```
84
+
85
+ ### Tests
86
+
87
+ Go to `tests/test.ts` to check out our tests.
88
+
89
+ ## 📄 License
90
+
91
+ MIT © Virto Network contributors
@@ -0,0 +1,261 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ var __generator = (this && this.__generator) || function (thisArg, body) {
12
+ var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype);
13
+ return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
14
+ function verb(n) { return function (v) { return step([n, v]); }; }
15
+ function step(op) {
16
+ if (f) throw new TypeError("Generator is already executing.");
17
+ while (g && (g = 0, op[0] && (_ = 0)), _) try {
18
+ if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
19
+ if (y = 0, t) op = [op[0] & 2, t.value];
20
+ switch (op[0]) {
21
+ case 0: case 1: t = op; break;
22
+ case 4: _.label++; return { value: op[1], done: false };
23
+ case 5: _.label++; y = op[1]; op = [0]; continue;
24
+ case 7: op = _.ops.pop(); _.trys.pop(); continue;
25
+ default:
26
+ if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
27
+ if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
28
+ if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
29
+ if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
30
+ if (t[2]) _.ops.pop();
31
+ _.trys.pop(); continue;
32
+ }
33
+ op = body.call(thisArg, _);
34
+ } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
35
+ if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
36
+ }
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.WebAuthn = exports.KREIVO_AUTHORITY_ID = void 0;
40
+ /**
41
+ * WebAuthn pass‑key authenticator for Virto Network.
42
+ *
43
+ * Exposes a browser‑side implementation of {@link Authenticator} that creates,
44
+ * stores, and uses WebAuthn resident credentials ("passkeys") while producing
45
+ * SCALE‑encoded data structures understood by the Kreivo signer pallet.
46
+ *
47
+ * Responsibilities
48
+ * ─────────────────────────────────────────────────────────
49
+ * • Derive a deterministic `deviceId` from the raw credential id
50
+ * • Emit `TAttestation<number>` during registration
51
+ * • Emit `TPassAuthenticate` during authentication
52
+ * • Never persist the credential mapping; that is delegated to the caller
53
+ *
54
+ * @module WebAuthn
55
+ */
56
+ var signer_1 = require("@virtonetwork/signer");
57
+ var substrate_bindings_1 = require("@polkadot-api/substrate-bindings");
58
+ var types_ts_1 = require("./types.js");
59
+ var utils_1 = require("polkadot-api/utils");
60
+ /** Fixed authority id for Kreivo pass‑key attestors. */
61
+ exports.KREIVO_AUTHORITY_ID = substrate_bindings_1.Binary.fromText("kreivo_p".padEnd(32, "\0"));
62
+ /**
63
+ * Browser‑side Authenticator that wraps the WebAuthn API.
64
+ *
65
+ * The generic type parameter `<number>` indicates that the **context**
66
+ * carried inside attestations and assertions is the block number that
67
+ * originated the challenge.
68
+ *
69
+ * @implements {Authenticator<number>}
70
+ */
71
+ var WebAuthn = /** @class */ (function () {
72
+ /**
73
+ * Creates a new WebAuthn helper.
74
+ *
75
+ * @param userId - Logical user identifier (e‑mail, DID, etc.).
76
+ * @param [credentialId] - Raw credential id obtained from a previous
77
+ * registration flow; omit it if the user must enrol a new pass‑key.
78
+ */
79
+ function WebAuthn(userId, credentialId) {
80
+ this.userId = userId;
81
+ this.credentialId = credentialId;
82
+ /**
83
+ * SHA‑256 hash of {@link userId}. Filled once by {@link setup} and reused
84
+ * for all WebAuthn operations.
85
+ */
86
+ this.hashedUserId = new Uint8Array(32);
87
+ }
88
+ /**
89
+ * Deterministic identifier of the hardware/software authenticator
90
+ * (`deviceId = Blake2‑256(credentialId)`).
91
+ *
92
+ * @returns DeviceId suitable for on‑chain storage.
93
+ * @throws Error If this instance does not yet know a credential id.
94
+ */
95
+ WebAuthn.getDeviceId = function (wa) {
96
+ return __awaiter(this, void 0, void 0, function () {
97
+ return __generator(this, function (_a) {
98
+ if (!wa.credentialId) {
99
+ throw new Error("credentialId unknown – call register() first or inject it via constructor/setCredentialId()");
100
+ }
101
+ return [2 /*return*/, substrate_bindings_1.Binary.fromBytes((0, substrate_bindings_1.Blake2256)(wa.credentialId))];
102
+ });
103
+ });
104
+ };
105
+ /**
106
+ * Injects a credential id discovered by the caller after construction.
107
+ *
108
+ * @param id - Raw credential id obtained from storage or backend.
109
+ */
110
+ WebAuthn.prototype.setCredentialId = function (id) {
111
+ this.credentialId = id;
112
+ };
113
+ /**
114
+ * Pre‑computes {@link hashedUserId}.
115
+ *
116
+ * Must be awaited **once** before any other interaction.
117
+ * Returns `this` for fluent chaining.
118
+ */
119
+ WebAuthn.prototype.setup = function () {
120
+ return __awaiter(this, void 0, void 0, function () {
121
+ var _a, _b;
122
+ return __generator(this, function (_c) {
123
+ switch (_c.label) {
124
+ case 0:
125
+ _a = this;
126
+ _b = Uint8Array.bind;
127
+ return [4 /*yield*/, crypto.subtle.digest("SHA-256", new TextEncoder().encode(this.userId))];
128
+ case 1:
129
+ _a.hashedUserId = new (_b.apply(Uint8Array, [void 0, _c.sent()]))();
130
+ return [2 /*return*/, this];
131
+ }
132
+ });
133
+ });
134
+ };
135
+ /**
136
+ * Registers a **new** resident credential (pass‑key) with the user’s
137
+ * authenticator and returns a SCALE‑ready attestation.
138
+ *
139
+ * @param blockNumber - The number of the block whose hash seeds the challenge.
140
+ * @param blockHash - The block hash used to derive a deterministic challenge.
141
+ * @param [displayName=this.userId] - Friendly name shown by the authenticator.
142
+ *
143
+ * @throws Error If this instance already has a credential id.
144
+ * @returns {Promise<TAttestation<number>>} SCALE‑encoded attestation object.
145
+ */
146
+ WebAuthn.prototype.register = function (blockNumber_1, blockHash_1) {
147
+ return __awaiter(this, arguments, void 0, function (blockNumber, blockHash, displayName) {
148
+ var challenger, challenge, credentials, _a, attestationObject, clientDataJSON, getPublicKey, publicKey;
149
+ var _b, _c;
150
+ if (displayName === void 0) { displayName = this.userId; }
151
+ return __generator(this, function (_d) {
152
+ switch (_d.label) {
153
+ case 0:
154
+ if (this.credentialId) {
155
+ throw new Error("Already have a credentialId; no need to register");
156
+ }
157
+ challenger = new signer_1.KreivoBlockChallenger();
158
+ challenge = challenger.generate((0, utils_1.fromHex)(blockHash), new Uint8Array());
159
+ return [4 /*yield*/, navigator.credentials.create({
160
+ publicKey: {
161
+ challenge: challenge,
162
+ rp: { name: "Virto Passkeys" },
163
+ user: {
164
+ id: this.hashedUserId,
165
+ name: this.userId,
166
+ displayName: displayName,
167
+ },
168
+ pubKeyCredParams: [{ type: "public-key", alg: -7 /* ES256 */ }],
169
+ authenticatorSelection: { userVerification: "preferred" },
170
+ attestation: "none",
171
+ timeout: 60000,
172
+ },
173
+ })];
174
+ case 1:
175
+ credentials = (_d.sent());
176
+ _a = credentials.response, attestationObject = _a.attestationObject, clientDataJSON = _a.clientDataJSON, getPublicKey = _a.getPublicKey;
177
+ // Save raw credential id for future auth calls
178
+ this.credentialId = new Uint8Array(credentials.rawId);
179
+ publicKey = getPublicKey();
180
+ if (!publicKey) {
181
+ throw new Error("The credentials don't expose a public key. Please use another authenticator device.");
182
+ }
183
+ _b = {};
184
+ _c = {
185
+ authorityId: exports.KREIVO_AUTHORITY_ID
186
+ };
187
+ return [4 /*yield*/, WebAuthn.getDeviceId(this)];
188
+ case 2: return [2 /*return*/, (_b.meta = (_c.deviceId = _d.sent(),
189
+ _c.context = blockNumber,
190
+ _c),
191
+ _b.authenticatorData = substrate_bindings_1.Binary.fromBytes(new Uint8Array(attestationObject)),
192
+ _b.clientData = substrate_bindings_1.Binary.fromBytes(new Uint8Array(clientDataJSON)),
193
+ _b.publicKey = substrate_bindings_1.Binary.fromBytes(new Uint8Array(publicKey)),
194
+ _b)];
195
+ }
196
+ });
197
+ });
198
+ };
199
+ /**
200
+ * Signs an arbitrary challenge with the pass‑key and produces a
201
+ * {@link TPassAuthenticate} payload understood by `PassSigner`.
202
+ *
203
+ * @param challenge - 32‑byte buffer supplied by the runtime.
204
+ * @param context - Block number (or any numeric context expected by the pallet).
205
+ *
206
+ * @returns SCALE‑encoded authentication payload.
207
+ * @throws Error If no credential id is available.
208
+ */
209
+ WebAuthn.prototype.authenticate = function (challenge, context) {
210
+ return __awaiter(this, void 0, void 0, function () {
211
+ var publicKey, cred, _a, authenticatorData, clientDataJSON, signature, assertion;
212
+ var _b;
213
+ return __generator(this, function (_c) {
214
+ switch (_c.label) {
215
+ case 0:
216
+ if (!this.credentialId) {
217
+ throw new Error("credentialId unknown – call register() first or inject it via constructor/setCredentialId()");
218
+ }
219
+ publicKey = {
220
+ challenge: challenge,
221
+ allowCredentials: [
222
+ {
223
+ id: this.credentialId.buffer,
224
+ type: "public-key",
225
+ transports: ["usb", "ble", "nfc", "internal"],
226
+ },
227
+ ],
228
+ userVerification: "preferred",
229
+ timeout: 60000,
230
+ };
231
+ return [4 /*yield*/, navigator.credentials.get({
232
+ publicKey: publicKey,
233
+ })];
234
+ case 1:
235
+ cred = (_c.sent());
236
+ _a = cred.response, authenticatorData = _a.authenticatorData, clientDataJSON = _a.clientDataJSON, signature = _a.signature;
237
+ assertion = {
238
+ meta: {
239
+ authorityId: exports.KREIVO_AUTHORITY_ID,
240
+ userId: substrate_bindings_1.Binary.fromBytes(this.hashedUserId),
241
+ context: context,
242
+ },
243
+ authenticatorData: substrate_bindings_1.Binary.fromBytes(new Uint8Array(authenticatorData)),
244
+ clientData: substrate_bindings_1.Binary.fromBytes(new Uint8Array(clientDataJSON)),
245
+ signature: substrate_bindings_1.Binary.fromBytes(new Uint8Array(signature)),
246
+ };
247
+ _b = {};
248
+ return [4 /*yield*/, WebAuthn.getDeviceId(this)];
249
+ case 2: return [2 /*return*/, (_b.deviceId = _c.sent(),
250
+ _b.credentials = {
251
+ tag: "WebAuthn",
252
+ value: types_ts_1.Assertion.enc(assertion),
253
+ },
254
+ _b)];
255
+ }
256
+ });
257
+ });
258
+ };
259
+ return WebAuthn;
260
+ }());
261
+ exports.WebAuthn = WebAuthn;
@@ -0,0 +1,27 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.Assertion = exports.Attestation = void 0;
4
+ var substrate_bindings_1 = require("@polkadot-api/substrate-bindings");
5
+ var scale_ts_1 = require("scale-ts");
6
+ var AttestationMeta = (0, scale_ts_1.Struct)({
7
+ authorityId: (0, substrate_bindings_1.Bin)(32),
8
+ deviceId: (0, substrate_bindings_1.Bin)(32),
9
+ context: scale_ts_1.u32,
10
+ });
11
+ exports.Attestation = (0, scale_ts_1.Struct)({
12
+ meta: AttestationMeta,
13
+ authenticatorData: (0, substrate_bindings_1.Bin)(),
14
+ clientData: (0, substrate_bindings_1.Bin)(),
15
+ publicKey: (0, substrate_bindings_1.Bin)(),
16
+ });
17
+ var AssertionMeta = (0, scale_ts_1.Struct)({
18
+ authorityId: (0, substrate_bindings_1.Bin)(32),
19
+ userId: (0, substrate_bindings_1.Bin)(32),
20
+ context: scale_ts_1.u32,
21
+ });
22
+ exports.Assertion = (0, scale_ts_1.Struct)({
23
+ meta: AssertionMeta,
24
+ authenticatorData: (0, substrate_bindings_1.Bin)(),
25
+ clientData: (0, substrate_bindings_1.Bin)(),
26
+ signature: (0, substrate_bindings_1.Bin)(),
27
+ });
@@ -0,0 +1,92 @@
1
+ /**
2
+ * WebAuthn pass‑key authenticator for Virto Network.
3
+ *
4
+ * Exposes a browser‑side implementation of {@link Authenticator} that creates,
5
+ * stores, and uses WebAuthn resident credentials ("passkeys") while producing
6
+ * SCALE‑encoded data structures understood by the Kreivo signer pallet.
7
+ *
8
+ * Responsibilities
9
+ * ─────────────────────────────────────────────────────────
10
+ * • Derive a deterministic `deviceId` from the raw credential id
11
+ * • Emit `TAttestation<number>` during registration
12
+ * • Emit `TPassAuthenticate` during authentication
13
+ * • Never persist the credential mapping; that is delegated to the caller
14
+ *
15
+ * @module WebAuthn
16
+ */
17
+ import { Authenticator, DeviceId } from "@virtonetwork/signer";
18
+ import { Binary } from "@polkadot-api/substrate-bindings";
19
+ import type { BlockHash, TAttestation } from "./types.ts";
20
+ import type { TPassAuthenticate } from "@virtonetwork/signer";
21
+ /** Fixed authority id for Kreivo pass‑key attestors. */
22
+ export declare const KREIVO_AUTHORITY_ID: Binary;
23
+ /**
24
+ * Browser‑side Authenticator that wraps the WebAuthn API.
25
+ *
26
+ * The generic type parameter `<number>` indicates that the **context**
27
+ * carried inside attestations and assertions is the block number that
28
+ * originated the challenge.
29
+ *
30
+ * @implements {Authenticator<number>}
31
+ */
32
+ export declare class WebAuthn implements Authenticator<number> {
33
+ readonly userId: string;
34
+ credentialId?: Uint8Array | undefined;
35
+ /**
36
+ * SHA‑256 hash of {@link userId}. Filled once by {@link setup} and reused
37
+ * for all WebAuthn operations.
38
+ */
39
+ hashedUserId: Uint8Array;
40
+ /**
41
+ * Creates a new WebAuthn helper.
42
+ *
43
+ * @param userId - Logical user identifier (e‑mail, DID, etc.).
44
+ * @param [credentialId] - Raw credential id obtained from a previous
45
+ * registration flow; omit it if the user must enrol a new pass‑key.
46
+ */
47
+ constructor(userId: string, credentialId?: Uint8Array | undefined);
48
+ /**
49
+ * Deterministic identifier of the hardware/software authenticator
50
+ * (`deviceId = Blake2‑256(credentialId)`).
51
+ *
52
+ * @returns DeviceId suitable for on‑chain storage.
53
+ * @throws Error If this instance does not yet know a credential id.
54
+ */
55
+ static getDeviceId(wa: WebAuthn): Promise<DeviceId>;
56
+ /**
57
+ * Injects a credential id discovered by the caller after construction.
58
+ *
59
+ * @param id - Raw credential id obtained from storage or backend.
60
+ */
61
+ setCredentialId(id: Uint8Array): void;
62
+ /**
63
+ * Pre‑computes {@link hashedUserId}.
64
+ *
65
+ * Must be awaited **once** before any other interaction.
66
+ * Returns `this` for fluent chaining.
67
+ */
68
+ setup(): Promise<this>;
69
+ /**
70
+ * Registers a **new** resident credential (pass‑key) with the user’s
71
+ * authenticator and returns a SCALE‑ready attestation.
72
+ *
73
+ * @param blockNumber - The number of the block whose hash seeds the challenge.
74
+ * @param blockHash - The block hash used to derive a deterministic challenge.
75
+ * @param [displayName=this.userId] - Friendly name shown by the authenticator.
76
+ *
77
+ * @throws Error If this instance already has a credential id.
78
+ * @returns {Promise<TAttestation<number>>} SCALE‑encoded attestation object.
79
+ */
80
+ register(blockNumber: number, blockHash: BlockHash, displayName?: string): Promise<TAttestation<number>>;
81
+ /**
82
+ * Signs an arbitrary challenge with the pass‑key and produces a
83
+ * {@link TPassAuthenticate} payload understood by `PassSigner`.
84
+ *
85
+ * @param challenge - 32‑byte buffer supplied by the runtime.
86
+ * @param context - Block number (or any numeric context expected by the pallet).
87
+ *
88
+ * @returns SCALE‑encoded authentication payload.
89
+ * @throws Error If no credential id is available.
90
+ */
91
+ authenticate(challenge: Uint8Array, context: number): Promise<TPassAuthenticate>;
92
+ }
@@ -0,0 +1,181 @@
1
+ /**
2
+ * WebAuthn pass‑key authenticator for Virto Network.
3
+ *
4
+ * Exposes a browser‑side implementation of {@link Authenticator} that creates,
5
+ * stores, and uses WebAuthn resident credentials ("passkeys") while producing
6
+ * SCALE‑encoded data structures understood by the Kreivo signer pallet.
7
+ *
8
+ * Responsibilities
9
+ * ─────────────────────────────────────────────────────────
10
+ * • Derive a deterministic `deviceId` from the raw credential id
11
+ * • Emit `TAttestation<number>` during registration
12
+ * • Emit `TPassAuthenticate` during authentication
13
+ * • Never persist the credential mapping; that is delegated to the caller
14
+ *
15
+ * @module WebAuthn
16
+ */
17
+ import { KreivoBlockChallenger, } from "@virtonetwork/signer";
18
+ import { Binary, Blake2256 } from "@polkadot-api/substrate-bindings";
19
+ import { Assertion } from "./types.js";
20
+ import { fromHex } from "polkadot-api/utils";
21
+ /** Fixed authority id for Kreivo pass‑key attestors. */
22
+ export const KREIVO_AUTHORITY_ID = Binary.fromText("kreivo_p".padEnd(32, "\0"));
23
+ /**
24
+ * Browser‑side Authenticator that wraps the WebAuthn API.
25
+ *
26
+ * The generic type parameter `<number>` indicates that the **context**
27
+ * carried inside attestations and assertions is the block number that
28
+ * originated the challenge.
29
+ *
30
+ * @implements {Authenticator<number>}
31
+ */
32
+ export class WebAuthn {
33
+ userId;
34
+ credentialId;
35
+ /**
36
+ * SHA‑256 hash of {@link userId}. Filled once by {@link setup} and reused
37
+ * for all WebAuthn operations.
38
+ */
39
+ hashedUserId = new Uint8Array(32);
40
+ /**
41
+ * Creates a new WebAuthn helper.
42
+ *
43
+ * @param userId - Logical user identifier (e‑mail, DID, etc.).
44
+ * @param [credentialId] - Raw credential id obtained from a previous
45
+ * registration flow; omit it if the user must enrol a new pass‑key.
46
+ */
47
+ constructor(userId, credentialId) {
48
+ this.userId = userId;
49
+ this.credentialId = credentialId;
50
+ }
51
+ /**
52
+ * Deterministic identifier of the hardware/software authenticator
53
+ * (`deviceId = Blake2‑256(credentialId)`).
54
+ *
55
+ * @returns DeviceId suitable for on‑chain storage.
56
+ * @throws Error If this instance does not yet know a credential id.
57
+ */
58
+ static async getDeviceId(wa) {
59
+ if (!wa.credentialId) {
60
+ throw new Error("credentialId unknown – call register() first or inject it via constructor/setCredentialId()");
61
+ }
62
+ return Binary.fromBytes(Blake2256(wa.credentialId));
63
+ }
64
+ /**
65
+ * Injects a credential id discovered by the caller after construction.
66
+ *
67
+ * @param id - Raw credential id obtained from storage or backend.
68
+ */
69
+ setCredentialId(id) {
70
+ this.credentialId = id;
71
+ }
72
+ /**
73
+ * Pre‑computes {@link hashedUserId}.
74
+ *
75
+ * Must be awaited **once** before any other interaction.
76
+ * Returns `this` for fluent chaining.
77
+ */
78
+ async setup() {
79
+ this.hashedUserId = new Uint8Array(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(this.userId)));
80
+ return this;
81
+ }
82
+ /**
83
+ * Registers a **new** resident credential (pass‑key) with the user’s
84
+ * authenticator and returns a SCALE‑ready attestation.
85
+ *
86
+ * @param blockNumber - The number of the block whose hash seeds the challenge.
87
+ * @param blockHash - The block hash used to derive a deterministic challenge.
88
+ * @param [displayName=this.userId] - Friendly name shown by the authenticator.
89
+ *
90
+ * @throws Error If this instance already has a credential id.
91
+ * @returns {Promise<TAttestation<number>>} SCALE‑encoded attestation object.
92
+ */
93
+ async register(blockNumber, blockHash, displayName = this.userId) {
94
+ if (this.credentialId) {
95
+ throw new Error("Already have a credentialId; no need to register");
96
+ }
97
+ const challenger = new KreivoBlockChallenger();
98
+ const challenge = challenger.generate(fromHex(blockHash), new Uint8Array());
99
+ const credentials = (await navigator.credentials.create({
100
+ publicKey: {
101
+ challenge,
102
+ rp: { name: "Virto Passkeys" },
103
+ user: {
104
+ id: this.hashedUserId,
105
+ name: this.userId,
106
+ displayName,
107
+ },
108
+ pubKeyCredParams: [{ type: "public-key", alg: -7 /* ES256 */ }],
109
+ authenticatorSelection: { userVerification: "preferred" },
110
+ attestation: "none",
111
+ timeout: 60_000,
112
+ },
113
+ }));
114
+ const { attestationObject, clientDataJSON, getPublicKey } = credentials.response;
115
+ // Save raw credential id for future auth calls
116
+ this.credentialId = new Uint8Array(credentials.rawId);
117
+ // Ensure publicKey is obtained in the registration process.
118
+ const publicKey = getPublicKey();
119
+ if (!publicKey) {
120
+ throw new Error("The credentials don't expose a public key. Please use another authenticator device.");
121
+ }
122
+ return {
123
+ meta: {
124
+ authorityId: KREIVO_AUTHORITY_ID,
125
+ deviceId: await WebAuthn.getDeviceId(this),
126
+ context: blockNumber,
127
+ },
128
+ authenticatorData: Binary.fromBytes(new Uint8Array(attestationObject)),
129
+ clientData: Binary.fromBytes(new Uint8Array(clientDataJSON)),
130
+ publicKey: Binary.fromBytes(new Uint8Array(publicKey)),
131
+ };
132
+ }
133
+ /**
134
+ * Signs an arbitrary challenge with the pass‑key and produces a
135
+ * {@link TPassAuthenticate} payload understood by `PassSigner`.
136
+ *
137
+ * @param challenge - 32‑byte buffer supplied by the runtime.
138
+ * @param context - Block number (or any numeric context expected by the pallet).
139
+ *
140
+ * @returns SCALE‑encoded authentication payload.
141
+ * @throws Error If no credential id is available.
142
+ */
143
+ async authenticate(challenge, context) {
144
+ if (!this.credentialId) {
145
+ throw new Error("credentialId unknown – call register() first or inject it via constructor/setCredentialId()");
146
+ }
147
+ const publicKey = {
148
+ challenge,
149
+ allowCredentials: [
150
+ {
151
+ id: this.credentialId.buffer,
152
+ type: "public-key",
153
+ transports: ["usb", "ble", "nfc", "internal"],
154
+ },
155
+ ],
156
+ userVerification: "preferred",
157
+ timeout: 60_000,
158
+ };
159
+ const cred = (await navigator.credentials.get({
160
+ publicKey,
161
+ }));
162
+ const { authenticatorData, clientDataJSON, signature } = cred.response;
163
+ const assertion = {
164
+ meta: {
165
+ authorityId: KREIVO_AUTHORITY_ID,
166
+ userId: Binary.fromBytes(this.hashedUserId),
167
+ context,
168
+ },
169
+ authenticatorData: Binary.fromBytes(new Uint8Array(authenticatorData)),
170
+ clientData: Binary.fromBytes(new Uint8Array(clientDataJSON)),
171
+ signature: Binary.fromBytes(new Uint8Array(signature)),
172
+ };
173
+ return {
174
+ deviceId: await WebAuthn.getDeviceId(this),
175
+ credentials: {
176
+ tag: "WebAuthn",
177
+ value: Assertion.enc(assertion),
178
+ },
179
+ };
180
+ }
181
+ }
@@ -0,0 +1,28 @@
1
+ import { AuthorityId, DeviceId, HashedUserId } from "@virtonetwork/signer";
2
+ import { Binary, HexString } from "@polkadot-api/substrate-bindings";
3
+ import { Codec } from "scale-ts";
4
+ export type BlockHash = HexString;
5
+ export type TAttestationMeta<Cx> = {
6
+ authorityId: AuthorityId;
7
+ deviceId: DeviceId;
8
+ context: Cx;
9
+ };
10
+ export type TAttestation<Cx> = {
11
+ meta: TAttestationMeta<Cx>;
12
+ authenticatorData: Binary;
13
+ clientData: Binary;
14
+ publicKey: Binary;
15
+ };
16
+ export declare const Attestation: Codec<TAttestation<number>>;
17
+ export type TAssertionMeta<Cx> = {
18
+ authorityId: AuthorityId;
19
+ userId: HashedUserId;
20
+ context: Cx;
21
+ };
22
+ export type TAssertion<Cx> = {
23
+ meta: TAssertionMeta<Cx>;
24
+ authenticatorData: Binary;
25
+ clientData: Binary;
26
+ signature: Binary;
27
+ };
28
+ export declare const Assertion: Codec<TAssertion<number>>;
@@ -0,0 +1,24 @@
1
+ import { Bin } from "@polkadot-api/substrate-bindings";
2
+ import { Struct, u32 } from "scale-ts";
3
+ const AttestationMeta = Struct({
4
+ authorityId: Bin(32),
5
+ deviceId: Bin(32),
6
+ context: u32,
7
+ });
8
+ export const Attestation = Struct({
9
+ meta: AttestationMeta,
10
+ authenticatorData: Bin(),
11
+ clientData: Bin(),
12
+ publicKey: Bin(),
13
+ });
14
+ const AssertionMeta = Struct({
15
+ authorityId: Bin(32),
16
+ userId: Bin(32),
17
+ context: u32,
18
+ });
19
+ export const Assertion = Struct({
20
+ meta: AssertionMeta,
21
+ authenticatorData: Bin(),
22
+ clientData: Bin(),
23
+ signature: Bin(),
24
+ });
package/package.json ADDED
@@ -0,0 +1,55 @@
1
+ {
2
+ "name": "@virtonetwork/authenticators-webauthn",
3
+ "description": "An Authenticator compatible with KreivoPassSigner that uses the WebAuthn standard",
4
+ "version": "1.0.0",
5
+ "type": "module",
6
+ "files": [
7
+ "dist"
8
+ ],
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/esm/index.d.ts",
12
+ "module": "./dist/esm/index.js",
13
+ "import": "./dist/esm/index.js",
14
+ "require": "./dist/cjs/index.js"
15
+ },
16
+ "./package.json": "./package.json"
17
+ },
18
+ "main": "./dist/cjs/index.js",
19
+ "module": "./dist/esm/index.js",
20
+ "browser": "./dist/esm/index.js",
21
+ "types": "./dist/esm/index.d.ts",
22
+ "scripts": {
23
+ "test": "node --loader ts-node/esm test/test.ts",
24
+ "build": "tsc && tsc -p tsconfig.cjs.json",
25
+ "prepack": "npm run build"
26
+ },
27
+ "author": "Virto Network <contact@virto.networks>",
28
+ "license": "MIT",
29
+ "keywords": [
30
+ "virto-sdk",
31
+ "signer",
32
+ "papi",
33
+ "scale",
34
+ "polkadot.js",
35
+ "polkadot"
36
+ ],
37
+ "dependencies": {
38
+ "@simplewebauthn/server": "^13.1.1",
39
+ "@virtonetwork/signer": "^1.0.4",
40
+ "nid-webauthn-emulator": "^0.2.4"
41
+ },
42
+ "devDependencies": {
43
+ "esmock": "^2.7.0",
44
+ "sinon": "^20.0.0",
45
+ "ts-node": "^10.9.2"
46
+ },
47
+ "repository": {
48
+ "url": "https://github.com/virto-network/papi-signers",
49
+ "directory": "authenticators/webauthn"
50
+ },
51
+ "publishConfig": {
52
+ "registry": "https://registry.npmjs.org/",
53
+ "access": "public"
54
+ }
55
+ }