namirasoft-site 1.5.2 → 1.5.3

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.
@@ -0,0 +1,67 @@
1
+ import { IStorage } from "namirasoft-core";
2
+ export interface IWebAuthnPrfOperationOptions {
3
+ rpName: string;
4
+ persistent: IStorage;
5
+ session: IStorage;
6
+ rpId?: string;
7
+ userName?: string;
8
+ userDisplayName?: string;
9
+ prfSalt?: Uint8Array;
10
+ idleTimeoutMs?: number;
11
+ ceremonyTimeoutMs?: number;
12
+ namespace?: string;
13
+ }
14
+ export interface IWebAuthnPrfRegisterResult {
15
+ credentialId: Uint8Array;
16
+ prfEnabled: boolean;
17
+ }
18
+ export interface IWebAuthnPrfStatus {
19
+ registered: boolean;
20
+ unlocked: boolean;
21
+ remainingUnlockMs: number;
22
+ }
23
+ export declare class WebAuthnPrfOperation {
24
+ private static readonly DEFAULT_SALT;
25
+ private readonly rpName;
26
+ private readonly rpId;
27
+ private readonly userName;
28
+ private readonly userDisplayName;
29
+ private readonly prfSalt;
30
+ private idleTimeoutMs;
31
+ private readonly ceremonyTimeoutMs;
32
+ private readonly persistent;
33
+ private readonly session;
34
+ private readonly credKey;
35
+ private readonly cacheKey;
36
+ private readonly blobPrefix;
37
+ private readonly indexKey;
38
+ constructor(options: IWebAuthnPrfOperationOptions);
39
+ setIdleTimeoutMs(ms: number): void;
40
+ static isSupported(): boolean;
41
+ static isPlatformAuthenticatorAvailable(): Promise<boolean>;
42
+ register(): Promise<IWebAuthnPrfRegisterResult>;
43
+ isRegistered(): boolean;
44
+ save(name: string, plaintext: string | Uint8Array): Promise<void>;
45
+ load(name: string): Promise<string>;
46
+ loadBytes(name: string): Promise<Uint8Array>;
47
+ remove(name: string): void;
48
+ list(): string[];
49
+ lock(): void;
50
+ isUnlocked(): boolean;
51
+ getRemainingUnlockMs(): number;
52
+ getStatus(): IWebAuthnPrfStatus;
53
+ reset(): void;
54
+ unlock(): Promise<void>;
55
+ private getKey;
56
+ private derivePrfKey;
57
+ private readCachedKey;
58
+ private touchCachedKey;
59
+ private readIndex;
60
+ private writeIndex;
61
+ private addToIndex;
62
+ private removeFromIndex;
63
+ private resolveRpId;
64
+ private assertSupported;
65
+ private static encodeBase64;
66
+ private static decodeBase64;
67
+ }
@@ -0,0 +1,287 @@
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
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.WebAuthnPrfOperation = void 0;
13
+ class WebAuthnPrfOperation {
14
+ constructor(options) {
15
+ var _a, _b, _c, _d, _e, _f;
16
+ this.rpName = options.rpName;
17
+ this.rpId = options.rpId;
18
+ this.userName = (_a = options.userName) !== null && _a !== void 0 ? _a : "user";
19
+ this.userDisplayName = (_b = options.userDisplayName) !== null && _b !== void 0 ? _b : "User";
20
+ this.prfSalt = (_c = options.prfSalt) !== null && _c !== void 0 ? _c : WebAuthnPrfOperation.DEFAULT_SALT;
21
+ this.idleTimeoutMs = (_d = options.idleTimeoutMs) !== null && _d !== void 0 ? _d : 2 * 60 * 1000;
22
+ this.ceremonyTimeoutMs = (_e = options.ceremonyTimeoutMs) !== null && _e !== void 0 ? _e : 60000;
23
+ this.persistent = options.persistent;
24
+ this.session = options.session;
25
+ const ns = (_f = options.namespace) !== null && _f !== void 0 ? _f : "nsp.wapr";
26
+ this.credKey = `${ns}.credential-id`;
27
+ this.cacheKey = `${ns}.cached-key`;
28
+ this.blobPrefix = `${ns}.blob.`;
29
+ this.indexKey = `${ns}.blob-index`;
30
+ }
31
+ setIdleTimeoutMs(ms) {
32
+ if (ms && ms > 0)
33
+ this.idleTimeoutMs = ms;
34
+ }
35
+ static isSupported() {
36
+ return typeof window !== "undefined" && typeof window.PublicKeyCredential !== "undefined";
37
+ }
38
+ static isPlatformAuthenticatorAvailable() {
39
+ return __awaiter(this, void 0, void 0, function* () {
40
+ if (!WebAuthnPrfOperation.isSupported())
41
+ return false;
42
+ try {
43
+ return yield PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable();
44
+ }
45
+ catch (_a) {
46
+ return false;
47
+ }
48
+ });
49
+ }
50
+ register() {
51
+ return __awaiter(this, void 0, void 0, function* () {
52
+ var _a;
53
+ this.assertSupported();
54
+ const challenge = crypto.getRandomValues(new Uint8Array(32));
55
+ const userId = crypto.getRandomValues(new Uint8Array(16));
56
+ const credential = yield navigator.credentials.create({
57
+ publicKey: {
58
+ challenge,
59
+ rp: { name: this.rpName, id: this.resolveRpId() },
60
+ user: {
61
+ id: userId,
62
+ name: this.userName,
63
+ displayName: this.userDisplayName,
64
+ },
65
+ pubKeyCredParams: [
66
+ { type: "public-key", alg: -7 },
67
+ { type: "public-key", alg: -257 },
68
+ ],
69
+ authenticatorSelection: {
70
+ residentKey: "preferred",
71
+ userVerification: "required",
72
+ },
73
+ timeout: this.ceremonyTimeoutMs,
74
+ extensions: { prf: {} },
75
+ },
76
+ });
77
+ if (!credential)
78
+ throw new Error("Credential creation was cancelled.");
79
+ const credentialId = new Uint8Array(credential.rawId);
80
+ const ext = credential.getClientExtensionResults();
81
+ const prfEnabled = ((_a = ext.prf) === null || _a === void 0 ? void 0 : _a.enabled) === true;
82
+ this.persistent.set(this.credKey, WebAuthnPrfOperation.encodeBase64(credentialId));
83
+ return { credentialId, prfEnabled };
84
+ });
85
+ }
86
+ isRegistered() {
87
+ return this.persistent.get(this.credKey, "") !== "";
88
+ }
89
+ save(name, plaintext) {
90
+ return __awaiter(this, void 0, void 0, function* () {
91
+ const key = yield this.getKey();
92
+ const data = typeof plaintext === "string" ? new TextEncoder().encode(plaintext) : plaintext;
93
+ const iv = crypto.getRandomValues(new Uint8Array(12));
94
+ const ct = yield crypto.subtle.encrypt({ name: "AES-GCM", iv }, key, data);
95
+ const blob = {
96
+ iv: Array.from(iv),
97
+ ct: Array.from(new Uint8Array(ct)),
98
+ };
99
+ this.persistent.set(this.blobPrefix + name, JSON.stringify(blob));
100
+ this.addToIndex(name);
101
+ });
102
+ }
103
+ load(name) {
104
+ return __awaiter(this, void 0, void 0, function* () {
105
+ const bytes = yield this.loadBytes(name);
106
+ return new TextDecoder().decode(bytes);
107
+ });
108
+ }
109
+ loadBytes(name) {
110
+ return __awaiter(this, void 0, void 0, function* () {
111
+ const raw = this.persistent.get(this.blobPrefix + name, "");
112
+ if (!raw)
113
+ throw new Error(`No encrypted blob named "${name}".`);
114
+ const blob = JSON.parse(raw);
115
+ const key = yield this.getKey();
116
+ const pt = yield crypto.subtle.decrypt({ name: "AES-GCM", iv: new Uint8Array(blob.iv) }, key, new Uint8Array(blob.ct));
117
+ return new Uint8Array(pt);
118
+ });
119
+ }
120
+ remove(name) {
121
+ this.persistent.del(this.blobPrefix + name);
122
+ this.removeFromIndex(name);
123
+ }
124
+ list() {
125
+ return this.readIndex();
126
+ }
127
+ lock() {
128
+ this.session.del(this.cacheKey);
129
+ }
130
+ isUnlocked() {
131
+ return this.getRemainingUnlockMs() > 0;
132
+ }
133
+ getRemainingUnlockMs() {
134
+ const raw = this.session.get(this.cacheKey, "");
135
+ if (!raw)
136
+ return 0;
137
+ const cached = JSON.parse(raw);
138
+ const remaining = cached.expiresAt - Date.now();
139
+ if (remaining <= 0) {
140
+ this.session.del(this.cacheKey);
141
+ return 0;
142
+ }
143
+ return remaining;
144
+ }
145
+ getStatus() {
146
+ return {
147
+ registered: this.isRegistered(),
148
+ unlocked: this.isUnlocked(),
149
+ remainingUnlockMs: this.getRemainingUnlockMs(),
150
+ };
151
+ }
152
+ reset() {
153
+ this.persistent.del(this.credKey);
154
+ this.session.del(this.cacheKey);
155
+ for (const name of this.readIndex())
156
+ this.persistent.del(this.blobPrefix + name);
157
+ this.persistent.del(this.indexKey);
158
+ }
159
+ unlock() {
160
+ return __awaiter(this, void 0, void 0, function* () {
161
+ this.lock();
162
+ yield this.derivePrfKey();
163
+ });
164
+ }
165
+ getKey() {
166
+ return __awaiter(this, void 0, void 0, function* () {
167
+ const cached = yield this.readCachedKey();
168
+ if (cached) {
169
+ this.touchCachedKey();
170
+ return cached;
171
+ }
172
+ return this.derivePrfKey();
173
+ });
174
+ }
175
+ derivePrfKey() {
176
+ return __awaiter(this, void 0, void 0, function* () {
177
+ var _a, _b;
178
+ this.assertSupported();
179
+ const credEncoded = this.persistent.get(this.credKey, "");
180
+ if (!credEncoded)
181
+ throw new Error("No credential registered. Call register() first.");
182
+ const credentialId = WebAuthnPrfOperation.decodeBase64(credEncoded);
183
+ const challenge = crypto.getRandomValues(new Uint8Array(32));
184
+ const assertion = yield navigator.credentials.get({
185
+ publicKey: {
186
+ challenge,
187
+ rpId: this.resolveRpId(),
188
+ allowCredentials: [{ type: "public-key", id: credentialId }],
189
+ userVerification: "required",
190
+ timeout: this.ceremonyTimeoutMs,
191
+ extensions: {
192
+ prf: { eval: { first: this.prfSalt } },
193
+ },
194
+ },
195
+ });
196
+ if (!assertion)
197
+ throw new Error("Authentication was cancelled.");
198
+ const ext = assertion.getClientExtensionResults();
199
+ const prfSecret = (_b = (_a = ext.prf) === null || _a === void 0 ? void 0 : _a.results) === null || _b === void 0 ? void 0 : _b.first;
200
+ if (!prfSecret)
201
+ throw new Error("Authenticator did not return a PRF result. The authenticator may not support the PRF extension.");
202
+ const rawBytes = ArrayBuffer.isView(prfSecret)
203
+ ? new Uint8Array(prfSecret.buffer, prfSecret.byteOffset, prfSecret.byteLength)
204
+ : new Uint8Array(prfSecret);
205
+ this.session.set(this.cacheKey, JSON.stringify({
206
+ rawKey: WebAuthnPrfOperation.encodeBase64(rawBytes),
207
+ expiresAt: Date.now() + this.idleTimeoutMs,
208
+ }));
209
+ return crypto.subtle.importKey("raw", rawBytes, { name: "AES-GCM" }, false, ["encrypt", "decrypt"]);
210
+ });
211
+ }
212
+ readCachedKey() {
213
+ return __awaiter(this, void 0, void 0, function* () {
214
+ const raw = this.session.get(this.cacheKey, "");
215
+ if (!raw)
216
+ return null;
217
+ const cached = JSON.parse(raw);
218
+ if (Date.now() > cached.expiresAt) {
219
+ this.session.del(this.cacheKey);
220
+ return null;
221
+ }
222
+ const rawBytes = WebAuthnPrfOperation.decodeBase64(cached.rawKey);
223
+ return crypto.subtle.importKey("raw", rawBytes, { name: "AES-GCM" }, false, ["encrypt", "decrypt"]);
224
+ });
225
+ }
226
+ touchCachedKey() {
227
+ const raw = this.session.get(this.cacheKey, "");
228
+ if (!raw)
229
+ return;
230
+ const cached = JSON.parse(raw);
231
+ cached.expiresAt = Date.now() + this.idleTimeoutMs;
232
+ this.session.set(this.cacheKey, JSON.stringify(cached));
233
+ }
234
+ readIndex() {
235
+ const raw = this.persistent.get(this.indexKey, "");
236
+ if (!raw)
237
+ return [];
238
+ try {
239
+ return JSON.parse(raw);
240
+ }
241
+ catch (_a) {
242
+ return [];
243
+ }
244
+ }
245
+ writeIndex(names) {
246
+ this.persistent.set(this.indexKey, JSON.stringify(names));
247
+ }
248
+ addToIndex(name) {
249
+ const names = this.readIndex();
250
+ if (!names.includes(name)) {
251
+ names.push(name);
252
+ this.writeIndex(names);
253
+ }
254
+ }
255
+ removeFromIndex(name) {
256
+ const names = this.readIndex().filter(n => n !== name);
257
+ this.writeIndex(names);
258
+ }
259
+ resolveRpId() {
260
+ if (this.rpId)
261
+ return this.rpId;
262
+ if (typeof location === "undefined")
263
+ throw new Error("rpId must be provided when `location` is not available.");
264
+ return location.hostname;
265
+ }
266
+ assertSupported() {
267
+ if (!WebAuthnPrfOperation.isSupported())
268
+ throw new Error("WebAuthn is not available in this environment.");
269
+ }
270
+ static encodeBase64(bytes) {
271
+ let s = "";
272
+ for (let i = 0; i < bytes.length; i++)
273
+ s += String.fromCharCode(bytes[i]);
274
+ return btoa(s);
275
+ }
276
+ static decodeBase64(s) {
277
+ const bin = atob(s);
278
+ const out = new Uint8Array(bin.length);
279
+ for (let i = 0; i < bin.length; i++)
280
+ out[i] = bin.charCodeAt(i);
281
+ return out;
282
+ }
283
+ }
284
+ exports.WebAuthnPrfOperation = WebAuthnPrfOperation;
285
+ WebAuthnPrfOperation.DEFAULT_SALT = new TextEncoder()
286
+ .encode("namirasoft.webauthn.prf.default.salt.v1");
287
+ //# sourceMappingURL=WebAuthnPrfOperation.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"WebAuthnPrfOperation.js","sourceRoot":"","sources":["../src/WebAuthnPrfOperation.ts"],"names":[],"mappings":"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"}
package/dist/index.d.ts CHANGED
@@ -6,3 +6,4 @@ export * from "./NSHeader";
6
6
  export * from "./NSNameParser";
7
7
  export * from "./NSReqHeader";
8
8
  export * from "./NSResHeader";
9
+ export * from "./WebAuthnPrfOperation";
package/dist/index.js CHANGED
@@ -22,4 +22,5 @@ __exportStar(require("./NSHeader"), exports);
22
22
  __exportStar(require("./NSNameParser"), exports);
23
23
  __exportStar(require("./NSReqHeader"), exports);
24
24
  __exportStar(require("./NSResHeader"), exports);
25
+ __exportStar(require("./WebAuthnPrfOperation"), exports);
25
26
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;AAAA,iDAA+B;AAC/B,uDAAqC;AACrC,oDAAkC;AAClC,oDAAkC;AAClC,6CAA2B;AAC3B,iDAA+B;AAC/B,gDAA8B;AAC9B,gDAA8B"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;AAAA,iDAA+B;AAC/B,uDAAqC;AACrC,oDAAkC;AAClC,oDAAkC;AAClC,6CAA2B;AAC3B,iDAA+B;AAC/B,gDAA8B;AAC9B,gDAA8B;AAC9B,yDAAuC"}
package/package.json CHANGED
@@ -8,7 +8,7 @@
8
8
  "framework": "npm",
9
9
  "application": "package",
10
10
  "private": false,
11
- "version": "1.5.2",
11
+ "version": "1.5.3",
12
12
  "author": "Amir Abolhasani",
13
13
  "license": "MIT",
14
14
  "main": "./dist/index.js",
@@ -18,6 +18,6 @@
18
18
  },
19
19
  "dependencies": {
20
20
  "axios": "^1.20.0",
21
- "namirasoft-core": "^1.5.12"
21
+ "namirasoft-core": "^1.5.15"
22
22
  }
23
23
  }
@@ -0,0 +1,403 @@
1
+ // WebAuthn PRF-based secret storage.
2
+ //
3
+ // Uses the WebAuthn `prf` extension (CTAP2 hmac-secret under the hood) to
4
+ // derive a deterministic 32-byte AES key from a platform or roaming
5
+ // authenticator, gated by user verification. The derived key encrypts/decrypts
6
+ // user-supplied blobs. Raw key material is cached in a session-scoped IStorage
7
+ // with a sliding idle timeout so the authenticator prompt only appears when
8
+ // the cache is empty or expired.
9
+ //
10
+ // Nothing persistent ever contains key material: only the public credentialId
11
+ // and AES-GCM ciphertext.
12
+ //
13
+ // Storage model — two IStorage instances, both sync string KV:
14
+ // - persistent → survives browser restart. Holds credentialId + ciphertexts.
15
+ // Caller passes e.g. IStorageLocal.
16
+ // - session → ideally RAM-only / tab-scoped. Holds the derived AES key.
17
+ // Caller passes e.g. IStorageSession.
18
+
19
+ import { IStorage } from "namirasoft-core";
20
+
21
+ export interface IWebAuthnPrfOperationOptions
22
+ {
23
+ rpName: string;
24
+ persistent: IStorage;
25
+ session: IStorage;
26
+ rpId?: string;
27
+ userName?: string;
28
+ userDisplayName?: string;
29
+ prfSalt?: Uint8Array;
30
+ idleTimeoutMs?: number;
31
+ ceremonyTimeoutMs?: number;
32
+ namespace?: string;
33
+ }
34
+
35
+ export interface IWebAuthnPrfRegisterResult
36
+ {
37
+ credentialId: Uint8Array;
38
+ prfEnabled: boolean;
39
+ }
40
+
41
+ export interface IWebAuthnPrfStatus
42
+ {
43
+ registered: boolean;
44
+ unlocked: boolean;
45
+ remainingUnlockMs: number;
46
+ }
47
+
48
+ // Narrow local type for the PRF extension outputs. Declared inline so the code
49
+ // compiles against DOM lib versions that predate the PRF addition.
50
+ interface IPrfExtensionOutputs
51
+ {
52
+ prf?: {
53
+ enabled?: boolean;
54
+ results?: { first?: ArrayBuffer | ArrayBufferView; second?: ArrayBuffer | ArrayBufferView };
55
+ };
56
+ }
57
+
58
+ export class WebAuthnPrfOperation
59
+ {
60
+ private static readonly DEFAULT_SALT: Uint8Array = new TextEncoder()
61
+ .encode("namirasoft.webauthn.prf.default.salt.v1");
62
+
63
+ private readonly rpName: string;
64
+ private readonly rpId: string | undefined;
65
+ private readonly userName: string;
66
+ private readonly userDisplayName: string;
67
+ private readonly prfSalt: Uint8Array;
68
+ private idleTimeoutMs: number;
69
+ private readonly ceremonyTimeoutMs: number;
70
+
71
+ private readonly persistent: IStorage;
72
+ private readonly session: IStorage;
73
+
74
+ private readonly credKey: string;
75
+ private readonly cacheKey: string;
76
+ private readonly blobPrefix: string;
77
+ private readonly indexKey: string;
78
+
79
+ constructor(options: IWebAuthnPrfOperationOptions)
80
+ {
81
+ this.rpName = options.rpName;
82
+ this.rpId = options.rpId;
83
+ this.userName = options.userName ?? "user";
84
+ this.userDisplayName = options.userDisplayName ?? "User";
85
+ this.prfSalt = options.prfSalt ?? WebAuthnPrfOperation.DEFAULT_SALT;
86
+ this.idleTimeoutMs = options.idleTimeoutMs ?? 2 * 60 * 1000;
87
+ this.ceremonyTimeoutMs = options.ceremonyTimeoutMs ?? 60000;
88
+
89
+ this.persistent = options.persistent;
90
+ this.session = options.session;
91
+
92
+ const ns = options.namespace ?? "nsp.wapr";
93
+ this.credKey = `${ns}.credential-id`;
94
+ this.cacheKey = `${ns}.cached-key`;
95
+ this.blobPrefix = `${ns}.blob.`;
96
+ this.indexKey = `${ns}.blob-index`;
97
+ }
98
+
99
+ setIdleTimeoutMs(ms: number): void
100
+ {
101
+ if (ms && ms > 0) this.idleTimeoutMs = ms;
102
+ }
103
+
104
+ static isSupported(): boolean
105
+ {
106
+ return typeof window !== "undefined" && typeof window.PublicKeyCredential !== "undefined";
107
+ }
108
+
109
+ static async isPlatformAuthenticatorAvailable(): Promise<boolean>
110
+ {
111
+ if (!WebAuthnPrfOperation.isSupported()) return false;
112
+ try { return await PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable(); }
113
+ catch { return false; }
114
+ }
115
+
116
+ async register(): Promise<IWebAuthnPrfRegisterResult>
117
+ {
118
+ this.assertSupported();
119
+
120
+ const challenge = crypto.getRandomValues(new Uint8Array(32));
121
+ const userId = crypto.getRandomValues(new Uint8Array(16));
122
+
123
+ const credential = await navigator.credentials.create({
124
+ publicKey: {
125
+ challenge,
126
+ rp: { name: this.rpName, id: this.resolveRpId() },
127
+ user: {
128
+ id: userId,
129
+ name: this.userName,
130
+ displayName: this.userDisplayName,
131
+ },
132
+ pubKeyCredParams: [
133
+ { type: "public-key", alg: -7 }, // ES256
134
+ { type: "public-key", alg: -257 }, // RS256
135
+ ],
136
+ authenticatorSelection: {
137
+ residentKey: "preferred",
138
+ userVerification: "required",
139
+ },
140
+ timeout: this.ceremonyTimeoutMs,
141
+ extensions: { prf: {} } as AuthenticationExtensionsClientInputs,
142
+ },
143
+ }) as PublicKeyCredential | null;
144
+
145
+ if (!credential) throw new Error("Credential creation was cancelled.");
146
+
147
+ const credentialId = new Uint8Array(credential.rawId);
148
+ const ext = credential.getClientExtensionResults() as IPrfExtensionOutputs;
149
+ const prfEnabled = ext.prf?.enabled === true;
150
+
151
+ this.persistent.set(this.credKey, WebAuthnPrfOperation.encodeBase64(credentialId));
152
+
153
+ return { credentialId, prfEnabled };
154
+ }
155
+
156
+ isRegistered(): boolean
157
+ {
158
+ return this.persistent.get(this.credKey, "") !== "";
159
+ }
160
+
161
+ async save(name: string, plaintext: string | Uint8Array): Promise<void>
162
+ {
163
+ const key = await this.getKey();
164
+ const data = typeof plaintext === "string" ? new TextEncoder().encode(plaintext) : plaintext;
165
+ const iv = crypto.getRandomValues(new Uint8Array(12));
166
+ const ct = await crypto.subtle.encrypt({ name: "AES-GCM", iv }, key, data as BufferSource);
167
+ const blob = {
168
+ iv: Array.from(iv),
169
+ ct: Array.from(new Uint8Array(ct)),
170
+ };
171
+ this.persistent.set(this.blobPrefix + name, JSON.stringify(blob));
172
+ this.addToIndex(name);
173
+ }
174
+
175
+ async load(name: string): Promise<string>
176
+ {
177
+ const bytes = await this.loadBytes(name);
178
+ return new TextDecoder().decode(bytes);
179
+ }
180
+
181
+ async loadBytes(name: string): Promise<Uint8Array>
182
+ {
183
+ const raw = this.persistent.get(this.blobPrefix + name, "");
184
+ if (!raw) throw new Error(`No encrypted blob named "${name}".`);
185
+ const blob = JSON.parse(raw) as { iv: number[]; ct: number[] };
186
+ const key = await this.getKey();
187
+ const pt = await crypto.subtle.decrypt(
188
+ { name: "AES-GCM", iv: new Uint8Array(blob.iv) },
189
+ key,
190
+ new Uint8Array(blob.ct),
191
+ );
192
+ return new Uint8Array(pt);
193
+ }
194
+
195
+ remove(name: string): void
196
+ {
197
+ this.persistent.del(this.blobPrefix + name);
198
+ this.removeFromIndex(name);
199
+ }
200
+
201
+ list(): string[]
202
+ {
203
+ return this.readIndex();
204
+ }
205
+
206
+ lock(): void
207
+ {
208
+ this.session.del(this.cacheKey);
209
+ }
210
+
211
+ isUnlocked(): boolean
212
+ {
213
+ return this.getRemainingUnlockMs() > 0;
214
+ }
215
+
216
+ getRemainingUnlockMs(): number
217
+ {
218
+ const raw = this.session.get(this.cacheKey, "");
219
+ if (!raw) return 0;
220
+ const cached = JSON.parse(raw) as { expiresAt: number };
221
+ const remaining = cached.expiresAt - Date.now();
222
+ if (remaining <= 0)
223
+ {
224
+ this.session.del(this.cacheKey);
225
+ return 0;
226
+ }
227
+ return remaining;
228
+ }
229
+
230
+ getStatus(): IWebAuthnPrfStatus
231
+ {
232
+ return {
233
+ registered: this.isRegistered(),
234
+ unlocked: this.isUnlocked(),
235
+ remainingUnlockMs: this.getRemainingUnlockMs(),
236
+ };
237
+ }
238
+
239
+ // Clears all local state (credentialId, blobs, cached key). The underlying
240
+ // passkey on the authenticator is NOT removed — the user must delete it
241
+ // from their OS passkey settings.
242
+ reset(): void
243
+ {
244
+ this.persistent.del(this.credKey);
245
+ this.session.del(this.cacheKey);
246
+ for (const name of this.readIndex())
247
+ this.persistent.del(this.blobPrefix + name);
248
+ this.persistent.del(this.indexKey);
249
+ }
250
+
251
+ // Forces a fresh PRF ceremony, bypassing the cache.
252
+ async unlock(): Promise<void>
253
+ {
254
+ this.lock();
255
+ await this.derivePrfKey();
256
+ }
257
+
258
+ private async getKey(): Promise<CryptoKey>
259
+ {
260
+ const cached = await this.readCachedKey();
261
+ if (cached)
262
+ {
263
+ this.touchCachedKey();
264
+ return cached;
265
+ }
266
+ return this.derivePrfKey();
267
+ }
268
+
269
+ private async derivePrfKey(): Promise<CryptoKey>
270
+ {
271
+ this.assertSupported();
272
+
273
+ const credEncoded = this.persistent.get(this.credKey, "");
274
+ if (!credEncoded) throw new Error("No credential registered. Call register() first.");
275
+ const credentialId = WebAuthnPrfOperation.decodeBase64(credEncoded);
276
+
277
+ const challenge = crypto.getRandomValues(new Uint8Array(32));
278
+
279
+ const assertion = await navigator.credentials.get({
280
+ publicKey: {
281
+ challenge,
282
+ rpId: this.resolveRpId(),
283
+ allowCredentials: [{ type: "public-key", id: credentialId as BufferSource }],
284
+ userVerification: "required",
285
+ timeout: this.ceremonyTimeoutMs,
286
+ extensions: {
287
+ prf: { eval: { first: this.prfSalt } },
288
+ } as AuthenticationExtensionsClientInputs,
289
+ },
290
+ }) as PublicKeyCredential | null;
291
+
292
+ if (!assertion) throw new Error("Authentication was cancelled.");
293
+
294
+ const ext = assertion.getClientExtensionResults() as IPrfExtensionOutputs;
295
+ const prfSecret = ext.prf?.results?.first;
296
+ if (!prfSecret)
297
+ throw new Error("Authenticator did not return a PRF result. The authenticator may not support the PRF extension.");
298
+
299
+ const rawBytes = ArrayBuffer.isView(prfSecret)
300
+ ? new Uint8Array(prfSecret.buffer, prfSecret.byteOffset, prfSecret.byteLength)
301
+ : new Uint8Array(prfSecret);
302
+
303
+ this.session.set(this.cacheKey, JSON.stringify({
304
+ rawKey: WebAuthnPrfOperation.encodeBase64(rawBytes),
305
+ expiresAt: Date.now() + this.idleTimeoutMs,
306
+ }));
307
+
308
+ return crypto.subtle.importKey(
309
+ "raw",
310
+ rawBytes as BufferSource,
311
+ { name: "AES-GCM" },
312
+ false,
313
+ ["encrypt", "decrypt"],
314
+ );
315
+ }
316
+
317
+ private async readCachedKey(): Promise<CryptoKey | null>
318
+ {
319
+ const raw = this.session.get(this.cacheKey, "");
320
+ if (!raw) return null;
321
+ const cached = JSON.parse(raw) as { rawKey: string; expiresAt: number };
322
+ if (Date.now() > cached.expiresAt)
323
+ {
324
+ this.session.del(this.cacheKey);
325
+ return null;
326
+ }
327
+ const rawBytes = WebAuthnPrfOperation.decodeBase64(cached.rawKey);
328
+ return crypto.subtle.importKey(
329
+ "raw",
330
+ rawBytes as BufferSource,
331
+ { name: "AES-GCM" },
332
+ false,
333
+ ["encrypt", "decrypt"],
334
+ );
335
+ }
336
+
337
+ private touchCachedKey(): void
338
+ {
339
+ const raw = this.session.get(this.cacheKey, "");
340
+ if (!raw) return;
341
+ const cached = JSON.parse(raw) as { rawKey: string; expiresAt: number };
342
+ cached.expiresAt = Date.now() + this.idleTimeoutMs;
343
+ this.session.set(this.cacheKey, JSON.stringify(cached));
344
+ }
345
+
346
+ private readIndex(): string[]
347
+ {
348
+ const raw = this.persistent.get(this.indexKey, "");
349
+ if (!raw) return [];
350
+ try { return JSON.parse(raw) as string[]; }
351
+ catch { return []; }
352
+ }
353
+
354
+ private writeIndex(names: string[]): void
355
+ {
356
+ this.persistent.set(this.indexKey, JSON.stringify(names));
357
+ }
358
+
359
+ private addToIndex(name: string): void
360
+ {
361
+ const names = this.readIndex();
362
+ if (!names.includes(name))
363
+ {
364
+ names.push(name);
365
+ this.writeIndex(names);
366
+ }
367
+ }
368
+
369
+ private removeFromIndex(name: string): void
370
+ {
371
+ const names = this.readIndex().filter(n => n !== name);
372
+ this.writeIndex(names);
373
+ }
374
+
375
+ private resolveRpId(): string
376
+ {
377
+ if (this.rpId) return this.rpId;
378
+ if (typeof location === "undefined")
379
+ throw new Error("rpId must be provided when `location` is not available.");
380
+ return location.hostname;
381
+ }
382
+
383
+ private assertSupported(): void
384
+ {
385
+ if (!WebAuthnPrfOperation.isSupported())
386
+ throw new Error("WebAuthn is not available in this environment.");
387
+ }
388
+
389
+ private static encodeBase64(bytes: Uint8Array): string
390
+ {
391
+ let s = "";
392
+ for (let i = 0; i < bytes.length; i++) s += String.fromCharCode(bytes[i]);
393
+ return btoa(s);
394
+ }
395
+
396
+ private static decodeBase64(s: string): Uint8Array
397
+ {
398
+ const bin = atob(s);
399
+ const out = new Uint8Array(bin.length);
400
+ for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
401
+ return out;
402
+ }
403
+ }
package/src/index.ts CHANGED
@@ -5,4 +5,5 @@ export * from "./NSCookiesHeader";
5
5
  export * from "./NSHeader";
6
6
  export * from "./NSNameParser";
7
7
  export * from "./NSReqHeader";
8
- export * from "./NSResHeader";
8
+ export * from "./NSResHeader";
9
+ export * from "./WebAuthnPrfOperation";