@le-space/orbitdb-identity-provider-webauthn-did 0.1.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +132 -410
- package/package.json +35 -5
- package/src/index.js +58 -503
- package/src/keystore/encryption.js +579 -0
- package/src/keystore/index.js +6 -0
- package/src/keystore/provider.js +555 -0
- package/src/keystore-encryption.js +6 -0
- package/src/varsig/assertion.js +205 -0
- package/src/varsig/credential.js +144 -0
- package/src/varsig/domain.js +11 -0
- package/src/varsig/identity.js +161 -0
- package/src/varsig/index.js +6 -0
- package/src/varsig/provider.js +78 -0
- package/src/varsig/storage.js +46 -0
- package/src/varsig/utils.js +43 -0
- package/src/verification.js +33 -33
- package/src/webauthn/provider.js +542 -0
- package/verification.js +1 -0
|
@@ -0,0 +1,579 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Keystore Encryption Utilities
|
|
3
|
+
*
|
|
4
|
+
* Provides AES-GCM encryption for OrbitDB keystore private keys,
|
|
5
|
+
* protected by WebAuthn credentials using PRF, largeBlob, or hmac-secret extensions.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { logger } from '@libp2p/logger';
|
|
9
|
+
|
|
10
|
+
const log = logger('orbitdb-identity-provider-webauthn-did:keystore-encryption');
|
|
11
|
+
const PRF_INFO = new TextEncoder().encode('orbitdb/keystore-prf');
|
|
12
|
+
|
|
13
|
+
async function deriveKeyFromPrfSeed(prfSeed) {
|
|
14
|
+
const saltHash = await crypto.subtle.digest('SHA-256', prfSeed);
|
|
15
|
+
const salt = new Uint8Array(saltHash).slice(0, 16);
|
|
16
|
+
const baseKey = await crypto.subtle.importKey(
|
|
17
|
+
'raw',
|
|
18
|
+
prfSeed,
|
|
19
|
+
'HKDF',
|
|
20
|
+
false,
|
|
21
|
+
['deriveBits']
|
|
22
|
+
);
|
|
23
|
+
const bits = await crypto.subtle.deriveBits(
|
|
24
|
+
{
|
|
25
|
+
name: 'HKDF',
|
|
26
|
+
hash: 'SHA-256',
|
|
27
|
+
salt,
|
|
28
|
+
info: PRF_INFO
|
|
29
|
+
},
|
|
30
|
+
baseKey,
|
|
31
|
+
256
|
|
32
|
+
);
|
|
33
|
+
return new Uint8Array(bits);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function getPrfSeed(credential, rawCredentialId) {
|
|
37
|
+
if (credential) {
|
|
38
|
+
try {
|
|
39
|
+
const extensions = credential.getClientExtensionResults();
|
|
40
|
+
const prfResults = extensions?.prf;
|
|
41
|
+
if (prfResults?.results?.first) {
|
|
42
|
+
log('Using WebAuthn PRF extension for key derivation');
|
|
43
|
+
return { seed: new Uint8Array(prfResults.results.first), source: 'prf' };
|
|
44
|
+
}
|
|
45
|
+
} catch (error) {
|
|
46
|
+
log('Error reading PRF extension results: %s', error.message);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
log('PRF extension not available, using rawCredentialId for key derivation');
|
|
51
|
+
return { seed: rawCredentialId, source: 'credentialId' };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Generate a random AES-GCM secret key (256-bit)
|
|
56
|
+
* @returns {Uint8Array} Secret key bytes.
|
|
57
|
+
*/
|
|
58
|
+
export function generateSecretKey() {
|
|
59
|
+
return crypto.getRandomValues(new Uint8Array(32));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Encrypt data with AES-GCM
|
|
64
|
+
* @param {Uint8Array} data - Data to encrypt
|
|
65
|
+
* @param {Uint8Array} sk - Secret key (32 bytes)
|
|
66
|
+
* @returns {Promise<{ciphertext: Uint8Array, iv: Uint8Array}>}
|
|
67
|
+
*/
|
|
68
|
+
export async function encryptWithAESGCM(data, sk) {
|
|
69
|
+
log('Encrypting data with AES-GCM');
|
|
70
|
+
|
|
71
|
+
// Generate random IV (12 bytes for GCM)
|
|
72
|
+
const iv = crypto.getRandomValues(new Uint8Array(12));
|
|
73
|
+
|
|
74
|
+
// Import secret key
|
|
75
|
+
const cryptoKey = await crypto.subtle.importKey(
|
|
76
|
+
'raw',
|
|
77
|
+
sk,
|
|
78
|
+
{ name: 'AES-GCM', length: 256 },
|
|
79
|
+
false,
|
|
80
|
+
['encrypt']
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
// Encrypt
|
|
84
|
+
const ciphertext = await crypto.subtle.encrypt(
|
|
85
|
+
{ name: 'AES-GCM', iv },
|
|
86
|
+
cryptoKey,
|
|
87
|
+
data
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
log('Encryption successful, ciphertext length: %d', ciphertext.byteLength);
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
ciphertext: new Uint8Array(ciphertext),
|
|
94
|
+
iv
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Decrypt data with AES-GCM
|
|
100
|
+
* @param {Uint8Array} ciphertext - Encrypted data
|
|
101
|
+
* @param {Uint8Array} sk - Secret key (32 bytes)
|
|
102
|
+
* @param {Uint8Array} iv - Initialization vector
|
|
103
|
+
* @returns {Promise<Uint8Array>}
|
|
104
|
+
*/
|
|
105
|
+
export async function decryptWithAESGCM(ciphertext, sk, iv) {
|
|
106
|
+
log('Decrypting data with AES-GCM');
|
|
107
|
+
|
|
108
|
+
try {
|
|
109
|
+
// Import secret key
|
|
110
|
+
const cryptoKey = await crypto.subtle.importKey(
|
|
111
|
+
'raw',
|
|
112
|
+
sk,
|
|
113
|
+
{ name: 'AES-GCM', length: 256 },
|
|
114
|
+
false,
|
|
115
|
+
['decrypt']
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
// Decrypt
|
|
119
|
+
const plaintext = await crypto.subtle.decrypt(
|
|
120
|
+
{ name: 'AES-GCM', iv },
|
|
121
|
+
cryptoKey,
|
|
122
|
+
ciphertext
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
log('Decryption successful, plaintext length: %d', plaintext.byteLength);
|
|
126
|
+
|
|
127
|
+
return new Uint8Array(plaintext);
|
|
128
|
+
} catch (error) {
|
|
129
|
+
log.error('Decryption failed: %s', error.message);
|
|
130
|
+
throw new Error(`Failed to decrypt data: ${error.message}`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Store secret key in WebAuthn credential using largeBlob extension
|
|
136
|
+
* @param {Object} credentialOptions - WebAuthn credential creation options
|
|
137
|
+
* @param {Uint8Array} sk - Secret key to store
|
|
138
|
+
* @returns {Promise<Object>} Enhanced credential options with largeBlob
|
|
139
|
+
*/
|
|
140
|
+
export function addLargeBlobToCredentialOptions(credentialOptions, sk) {
|
|
141
|
+
log('Adding largeBlob extension to credential options');
|
|
142
|
+
|
|
143
|
+
return {
|
|
144
|
+
...credentialOptions,
|
|
145
|
+
extensions: {
|
|
146
|
+
...credentialOptions.extensions,
|
|
147
|
+
largeBlob: {
|
|
148
|
+
support: 'required',
|
|
149
|
+
write: sk
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Add PRF extension to credential options
|
|
157
|
+
* @param {Object} credentialOptions - WebAuthn credential creation options
|
|
158
|
+
* @param {Uint8Array} [prfInput] - Optional PRF input (salt)
|
|
159
|
+
* @returns {{credentialOptions: Object, prfInput: Uint8Array}}
|
|
160
|
+
*/
|
|
161
|
+
export function addPRFToCredentialOptions(credentialOptions, prfInput = crypto.getRandomValues(new Uint8Array(32))) {
|
|
162
|
+
log('Adding PRF extension to credential options');
|
|
163
|
+
|
|
164
|
+
return {
|
|
165
|
+
credentialOptions: {
|
|
166
|
+
...credentialOptions,
|
|
167
|
+
extensions: {
|
|
168
|
+
...credentialOptions.extensions,
|
|
169
|
+
prf: {
|
|
170
|
+
eval: { first: prfInput }
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
},
|
|
174
|
+
prfInput
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Retrieve secret key from WebAuthn credential using largeBlob extension
|
|
180
|
+
* @param {Uint8Array} credentialId - WebAuthn credential ID
|
|
181
|
+
* @param {string} rpId - Relying party ID (domain)
|
|
182
|
+
* @returns {Promise<Uint8Array>} Secret key
|
|
183
|
+
*/
|
|
184
|
+
export async function retrieveSKFromLargeBlob(credentialId, rpId) {
|
|
185
|
+
log('Retrieving secret key from largeBlob');
|
|
186
|
+
|
|
187
|
+
try {
|
|
188
|
+
const assertion = await navigator.credentials.get({
|
|
189
|
+
publicKey: {
|
|
190
|
+
challenge: crypto.getRandomValues(new Uint8Array(32)),
|
|
191
|
+
allowCredentials: [{
|
|
192
|
+
id: credentialId,
|
|
193
|
+
type: 'public-key'
|
|
194
|
+
}],
|
|
195
|
+
rpId: rpId,
|
|
196
|
+
userVerification: 'required',
|
|
197
|
+
extensions: {
|
|
198
|
+
largeBlob: {
|
|
199
|
+
read: true
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
const extensions = assertion.getClientExtensionResults();
|
|
206
|
+
|
|
207
|
+
if (!extensions.largeBlob || !extensions.largeBlob.blob) {
|
|
208
|
+
throw new Error('No largeBlob data found in credential');
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const sk = new Uint8Array(extensions.largeBlob.blob);
|
|
212
|
+
log('Retrieved secret key from largeBlob, length: %d', sk.length);
|
|
213
|
+
|
|
214
|
+
return sk;
|
|
215
|
+
} catch (error) {
|
|
216
|
+
log.error('Failed to retrieve secret key from largeBlob: %s', error.message);
|
|
217
|
+
throw new Error(`Failed to retrieve secret key: ${error.message}`);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Add hmac-secret extension to credential options
|
|
223
|
+
* @param {Object} credentialOptions - WebAuthn credential creation options
|
|
224
|
+
* @returns {Object} Enhanced credential options with hmac-secret
|
|
225
|
+
*/
|
|
226
|
+
export function addHmacSecretToCredentialOptions(credentialOptions) {
|
|
227
|
+
log('Adding hmac-secret extension to credential options');
|
|
228
|
+
|
|
229
|
+
return {
|
|
230
|
+
...credentialOptions,
|
|
231
|
+
extensions: {
|
|
232
|
+
...credentialOptions.extensions,
|
|
233
|
+
hmacCreateSecret: true
|
|
234
|
+
}
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Wrap secret key using hmac-secret extension
|
|
240
|
+
* @param {Uint8Array} credentialId - WebAuthn credential ID
|
|
241
|
+
* @param {Uint8Array} sk - Secret key to wrap
|
|
242
|
+
* @param {string} rpId - Relying party ID (domain)
|
|
243
|
+
* @returns {Promise<{wrappedSK: Uint8Array, salt: Uint8Array}>}
|
|
244
|
+
*/
|
|
245
|
+
export async function wrapSKWithHmacSecret(credentialId, sk, rpId) {
|
|
246
|
+
log('Wrapping secret key with hmac-secret');
|
|
247
|
+
|
|
248
|
+
const salt = crypto.getRandomValues(new Uint8Array(32));
|
|
249
|
+
|
|
250
|
+
try {
|
|
251
|
+
const assertion = await navigator.credentials.get({
|
|
252
|
+
publicKey: {
|
|
253
|
+
challenge: crypto.getRandomValues(new Uint8Array(32)),
|
|
254
|
+
allowCredentials: [{
|
|
255
|
+
id: credentialId,
|
|
256
|
+
type: 'public-key'
|
|
257
|
+
}],
|
|
258
|
+
rpId: rpId,
|
|
259
|
+
userVerification: 'required',
|
|
260
|
+
extensions: {
|
|
261
|
+
hmacGetSecret: {
|
|
262
|
+
salt1: salt
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
const extensions = assertion.getClientExtensionResults();
|
|
269
|
+
|
|
270
|
+
if (!extensions.hmacGetSecret || !extensions.hmacGetSecret.output1) {
|
|
271
|
+
throw new Error('No hmac-secret output from credential');
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const hmacOutput = new Uint8Array(extensions.hmacGetSecret.output1);
|
|
275
|
+
|
|
276
|
+
// Use HMAC output as wrapping key
|
|
277
|
+
const wrappedSK = await encryptWithAESGCM(sk, hmacOutput.slice(0, 32));
|
|
278
|
+
|
|
279
|
+
log('Secret key wrapped with hmac-secret');
|
|
280
|
+
|
|
281
|
+
return {
|
|
282
|
+
wrappedSK: wrappedSK.ciphertext,
|
|
283
|
+
wrappingIV: wrappedSK.iv,
|
|
284
|
+
salt
|
|
285
|
+
};
|
|
286
|
+
} catch (error) {
|
|
287
|
+
log.error('Failed to wrap secret key with hmac-secret: %s', error.message);
|
|
288
|
+
throw new Error(`Failed to wrap secret key: ${error.message}`);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Wrap secret key using PRF extension
|
|
294
|
+
* @param {Uint8Array} credentialId - WebAuthn credential ID
|
|
295
|
+
* @param {Uint8Array} sk - Secret key to wrap
|
|
296
|
+
* @param {string} rpId - Relying party ID (domain)
|
|
297
|
+
* @param {Uint8Array} [prfInput] - PRF input (salt) for deterministic output
|
|
298
|
+
* @returns {Promise<{wrappedSK: Uint8Array, wrappingIV: Uint8Array, salt: Uint8Array, prfSource: string}>}
|
|
299
|
+
*/
|
|
300
|
+
export async function wrapSKWithPRF(credentialId, sk, rpId, prfInput) {
|
|
301
|
+
log('Wrapping secret key with PRF');
|
|
302
|
+
|
|
303
|
+
const prfEval = prfInput || crypto.getRandomValues(new Uint8Array(32));
|
|
304
|
+
|
|
305
|
+
try {
|
|
306
|
+
const assertion = await navigator.credentials.get({
|
|
307
|
+
publicKey: {
|
|
308
|
+
challenge: crypto.getRandomValues(new Uint8Array(32)),
|
|
309
|
+
allowCredentials: [{
|
|
310
|
+
id: credentialId,
|
|
311
|
+
type: 'public-key'
|
|
312
|
+
}],
|
|
313
|
+
rpId: rpId,
|
|
314
|
+
userVerification: 'required',
|
|
315
|
+
extensions: {
|
|
316
|
+
prf: {
|
|
317
|
+
eval: { first: prfEval }
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
const { seed, source } = getPrfSeed(assertion, credentialId);
|
|
324
|
+
const prfKey = await deriveKeyFromPrfSeed(seed);
|
|
325
|
+
const wrapped = await encryptWithAESGCM(sk, prfKey);
|
|
326
|
+
|
|
327
|
+
log('Secret key wrapped with PRF (%s)', source);
|
|
328
|
+
|
|
329
|
+
return {
|
|
330
|
+
wrappedSK: wrapped.ciphertext,
|
|
331
|
+
wrappingIV: wrapped.iv,
|
|
332
|
+
salt: prfEval,
|
|
333
|
+
prfSource: source
|
|
334
|
+
};
|
|
335
|
+
} catch (error) {
|
|
336
|
+
log.error('Failed to wrap secret key with PRF: %s', error.message);
|
|
337
|
+
throw new Error(`Failed to wrap secret key: ${error.message}`);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Unwrap secret key using hmac-secret extension
|
|
343
|
+
* @param {Uint8Array} credentialId - WebAuthn credential ID
|
|
344
|
+
* @param {Uint8Array} wrappedSK - Wrapped secret key
|
|
345
|
+
* @param {Uint8Array} wrappingIV - IV used for wrapping
|
|
346
|
+
* @param {Uint8Array} salt - Salt used for HMAC
|
|
347
|
+
* @param {string} rpId - Relying party ID (domain)
|
|
348
|
+
* @returns {Promise<Uint8Array>} Unwrapped secret key
|
|
349
|
+
*/
|
|
350
|
+
export async function unwrapSKWithHmacSecret(credentialId, wrappedSK, wrappingIV, salt, rpId) {
|
|
351
|
+
log('Unwrapping secret key with hmac-secret');
|
|
352
|
+
|
|
353
|
+
try {
|
|
354
|
+
const assertion = await navigator.credentials.get({
|
|
355
|
+
publicKey: {
|
|
356
|
+
challenge: crypto.getRandomValues(new Uint8Array(32)),
|
|
357
|
+
allowCredentials: [{
|
|
358
|
+
id: credentialId,
|
|
359
|
+
type: 'public-key'
|
|
360
|
+
}],
|
|
361
|
+
rpId: rpId,
|
|
362
|
+
userVerification: 'required',
|
|
363
|
+
extensions: {
|
|
364
|
+
hmacGetSecret: {
|
|
365
|
+
salt1: salt
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
const extensions = assertion.getClientExtensionResults();
|
|
372
|
+
|
|
373
|
+
if (!extensions.hmacGetSecret || !extensions.hmacGetSecret.output1) {
|
|
374
|
+
throw new Error('No hmac-secret output from credential');
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
const hmacOutput = new Uint8Array(extensions.hmacGetSecret.output1);
|
|
378
|
+
|
|
379
|
+
// Unwrap with HMAC output
|
|
380
|
+
const sk = await decryptWithAESGCM(wrappedSK, hmacOutput.slice(0, 32), wrappingIV);
|
|
381
|
+
|
|
382
|
+
log('Secret key unwrapped with hmac-secret');
|
|
383
|
+
|
|
384
|
+
return sk;
|
|
385
|
+
} catch (error) {
|
|
386
|
+
log.error('Failed to unwrap secret key with hmac-secret: %s', error.message);
|
|
387
|
+
throw new Error(`Failed to unwrap secret key: ${error.message}`);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* Unwrap secret key using PRF extension
|
|
393
|
+
* @param {Uint8Array} credentialId - WebAuthn credential ID
|
|
394
|
+
* @param {Uint8Array} wrappedSK - Wrapped secret key
|
|
395
|
+
* @param {Uint8Array} wrappingIV - IV used for wrapping
|
|
396
|
+
* @param {Uint8Array} salt - PRF input (salt)
|
|
397
|
+
* @param {string} rpId - Relying party ID (domain)
|
|
398
|
+
* @returns {Promise<Uint8Array>} Unwrapped secret key
|
|
399
|
+
*/
|
|
400
|
+
export async function unwrapSKWithPRF(credentialId, wrappedSK, wrappingIV, salt, rpId) {
|
|
401
|
+
log('Unwrapping secret key with PRF');
|
|
402
|
+
|
|
403
|
+
const prfEval = salt || crypto.getRandomValues(new Uint8Array(32));
|
|
404
|
+
|
|
405
|
+
try {
|
|
406
|
+
const assertion = await navigator.credentials.get({
|
|
407
|
+
publicKey: {
|
|
408
|
+
challenge: crypto.getRandomValues(new Uint8Array(32)),
|
|
409
|
+
allowCredentials: [{
|
|
410
|
+
id: credentialId,
|
|
411
|
+
type: 'public-key'
|
|
412
|
+
}],
|
|
413
|
+
rpId: rpId,
|
|
414
|
+
userVerification: 'required',
|
|
415
|
+
extensions: {
|
|
416
|
+
prf: {
|
|
417
|
+
eval: { first: prfEval }
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
const { seed, source } = getPrfSeed(assertion, credentialId);
|
|
424
|
+
const prfKey = await deriveKeyFromPrfSeed(seed);
|
|
425
|
+
const sk = await decryptWithAESGCM(wrappedSK, prfKey, wrappingIV);
|
|
426
|
+
|
|
427
|
+
log('Secret key unwrapped with PRF (%s)', source);
|
|
428
|
+
return sk;
|
|
429
|
+
} catch (error) {
|
|
430
|
+
log.error('Failed to unwrap secret key with PRF: %s', error.message);
|
|
431
|
+
throw new Error(`Failed to unwrap secret key: ${error.message}`);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* Store encrypted keystore data in IndexedDB
|
|
437
|
+
* @param {Object} data - Encrypted keystore data
|
|
438
|
+
* @param {string} credentialId - WebAuthn credential ID (used as key)
|
|
439
|
+
*/
|
|
440
|
+
export async function storeEncryptedKeystore(data, credentialId) {
|
|
441
|
+
log('Storing encrypted keystore in IndexedDB');
|
|
442
|
+
|
|
443
|
+
const storageKey = `encrypted-keystore-${credentialId}`;
|
|
444
|
+
|
|
445
|
+
const serializedData = {
|
|
446
|
+
ciphertext: Array.from(data.ciphertext),
|
|
447
|
+
iv: Array.from(data.iv),
|
|
448
|
+
credentialId: data.credentialId,
|
|
449
|
+
publicKey: data.publicKey ? {
|
|
450
|
+
...data.publicKey,
|
|
451
|
+
x: data.publicKey.x ? Array.from(data.publicKey.x) : undefined,
|
|
452
|
+
y: data.publicKey.y ? Array.from(data.publicKey.y) : undefined
|
|
453
|
+
} : undefined,
|
|
454
|
+
wrappedSK: data.wrappedSK ? Array.from(data.wrappedSK) : undefined,
|
|
455
|
+
wrappingIV: data.wrappingIV ? Array.from(data.wrappingIV) : undefined,
|
|
456
|
+
salt: data.salt ? Array.from(data.salt) : undefined,
|
|
457
|
+
encryptionMethod: data.encryptionMethod || 'largeBlob',
|
|
458
|
+
keyType: data.keyType,
|
|
459
|
+
timestamp: Date.now()
|
|
460
|
+
};
|
|
461
|
+
|
|
462
|
+
try {
|
|
463
|
+
localStorage.setItem(storageKey, JSON.stringify(serializedData));
|
|
464
|
+
log('Encrypted keystore stored successfully');
|
|
465
|
+
} catch (error) {
|
|
466
|
+
log.error('Failed to store encrypted keystore: %s', error.message);
|
|
467
|
+
throw new Error(`Failed to store encrypted keystore: ${error.message}`);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
/**
|
|
472
|
+
* Load encrypted keystore data from IndexedDB
|
|
473
|
+
* @param {string} credentialId - WebAuthn credential ID
|
|
474
|
+
* @returns {Promise<Object>} Encrypted keystore data
|
|
475
|
+
*/
|
|
476
|
+
export async function loadEncryptedKeystore(credentialId) {
|
|
477
|
+
log('Loading encrypted keystore from IndexedDB');
|
|
478
|
+
|
|
479
|
+
const storageKey = `encrypted-keystore-${credentialId}`;
|
|
480
|
+
|
|
481
|
+
try {
|
|
482
|
+
const stored = localStorage.getItem(storageKey);
|
|
483
|
+
|
|
484
|
+
if (!stored) {
|
|
485
|
+
throw new Error('No encrypted keystore found for this credential');
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
const data = JSON.parse(stored);
|
|
489
|
+
|
|
490
|
+
const deserialized = {
|
|
491
|
+
ciphertext: new Uint8Array(data.ciphertext),
|
|
492
|
+
iv: new Uint8Array(data.iv),
|
|
493
|
+
credentialId: data.credentialId,
|
|
494
|
+
publicKey: data.publicKey ? {
|
|
495
|
+
...data.publicKey,
|
|
496
|
+
x: data.publicKey.x ? new Uint8Array(data.publicKey.x) : undefined,
|
|
497
|
+
y: data.publicKey.y ? new Uint8Array(data.publicKey.y) : undefined
|
|
498
|
+
} : undefined,
|
|
499
|
+
wrappedSK: data.wrappedSK ? new Uint8Array(data.wrappedSK) : undefined,
|
|
500
|
+
wrappingIV: data.wrappingIV ? new Uint8Array(data.wrappingIV) : undefined,
|
|
501
|
+
salt: data.salt ? new Uint8Array(data.salt) : undefined,
|
|
502
|
+
encryptionMethod: data.encryptionMethod || 'largeBlob',
|
|
503
|
+
keyType: data.keyType,
|
|
504
|
+
timestamp: data.timestamp
|
|
505
|
+
};
|
|
506
|
+
|
|
507
|
+
log('Encrypted keystore loaded successfully');
|
|
508
|
+
|
|
509
|
+
return deserialized;
|
|
510
|
+
} catch (error) {
|
|
511
|
+
log.error('Failed to load encrypted keystore: %s', error.message);
|
|
512
|
+
throw new Error(`Failed to load encrypted keystore: ${error.message}`);
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
/**
|
|
517
|
+
* Clear encrypted keystore from storage
|
|
518
|
+
* @param {string} credentialId - WebAuthn credential ID
|
|
519
|
+
*/
|
|
520
|
+
export async function clearEncryptedKeystore(credentialId) {
|
|
521
|
+
log('Clearing encrypted keystore from storage');
|
|
522
|
+
|
|
523
|
+
const storageKey = `encrypted-keystore-${credentialId}`;
|
|
524
|
+
|
|
525
|
+
try {
|
|
526
|
+
localStorage.removeItem(storageKey);
|
|
527
|
+
log('Encrypted keystore cleared successfully');
|
|
528
|
+
} catch (error) {
|
|
529
|
+
log.error('Failed to clear encrypted keystore: %s', error.message);
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
/**
|
|
534
|
+
* Check if browser supports WebAuthn extensions
|
|
535
|
+
* @returns {Promise<Object>} Support status for largeBlob and hmac-secret
|
|
536
|
+
*/
|
|
537
|
+
export async function checkExtensionSupport() {
|
|
538
|
+
const support = {
|
|
539
|
+
largeBlob: false,
|
|
540
|
+
hmacSecret: false
|
|
541
|
+
};
|
|
542
|
+
|
|
543
|
+
if (!window.PublicKeyCredential) {
|
|
544
|
+
return support;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
try {
|
|
548
|
+
// Check largeBlob support
|
|
549
|
+
if (window.PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable) {
|
|
550
|
+
const available = await window.PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable();
|
|
551
|
+
// largeBlob is available in Chrome 106+, Edge 106+
|
|
552
|
+
support.largeBlob = available && 'largeBlob' in PublicKeyCredential.prototype;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
// hmac-secret support cannot be reliably detected without a real credential.
|
|
556
|
+
// Keep it false by default and let users opt-in explicitly.
|
|
557
|
+
support.hmacSecret = false;
|
|
558
|
+
|
|
559
|
+
} catch (error) {
|
|
560
|
+
log.error('Failed to check extension support: %s', error.message);
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
return support;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
export default {
|
|
567
|
+
generateSecretKey,
|
|
568
|
+
encryptWithAESGCM,
|
|
569
|
+
decryptWithAESGCM,
|
|
570
|
+
addLargeBlobToCredentialOptions,
|
|
571
|
+
retrieveSKFromLargeBlob,
|
|
572
|
+
addHmacSecretToCredentialOptions,
|
|
573
|
+
wrapSKWithHmacSecret,
|
|
574
|
+
unwrapSKWithHmacSecret,
|
|
575
|
+
storeEncryptedKeystore,
|
|
576
|
+
loadEncryptedKeystore,
|
|
577
|
+
clearEncryptedKeystore,
|
|
578
|
+
checkExtensionSupport
|
|
579
|
+
};
|