@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.
@@ -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
+ };
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Keystore encryption module exports.
3
+ */
4
+ export * from './encryption.js';
5
+ export { default } from './encryption.js';
6
+ export * from './provider.js';