hd-wallet-ui 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.
@@ -0,0 +1,696 @@
1
+ /**
2
+ * Wallet Storage Module
3
+ *
4
+ * Industry-standard implementation for securely storing wallet credentials
5
+ * using WebAuthn PRF extension or PIN-based encryption.
6
+ *
7
+ * Based on best practices from:
8
+ * - Yubico WebAuthn PRF Developer Guide
9
+ * - wwWallet FUNKE implementation
10
+ * - W3C WebAuthn PRF Extension specification
11
+ *
12
+ * @module wallet-storage
13
+ */
14
+
15
+ // =============================================================================
16
+ // Storage Keys
17
+ // =============================================================================
18
+
19
+ const STORAGE_PREFIX = 'wallet_storage_';
20
+ const METADATA_KEY = `${STORAGE_PREFIX}metadata`;
21
+ const ENCRYPTED_DATA_KEY = `${STORAGE_PREFIX}encrypted`;
22
+ const PASSKEY_CREDENTIAL_KEY = `${STORAGE_PREFIX}passkey_credential`;
23
+
24
+ // Version for future migrations
25
+ const STORAGE_VERSION = 2;
26
+
27
+ // =============================================================================
28
+ // Storage Method Enum
29
+ // =============================================================================
30
+
31
+ export const StorageMethod = {
32
+ NONE: 'none',
33
+ PIN: 'pin',
34
+ PASSKEY: 'passkey'
35
+ };
36
+
37
+ // =============================================================================
38
+ // Utility Functions
39
+ // =============================================================================
40
+
41
+ /**
42
+ * Convert ArrayBuffer to base64 string
43
+ */
44
+ function arrayBufferToBase64(buffer) {
45
+ const bytes = new Uint8Array(buffer);
46
+ let binary = '';
47
+ for (let i = 0; i < bytes.byteLength; i++) {
48
+ binary += String.fromCharCode(bytes[i]);
49
+ }
50
+ return btoa(binary);
51
+ }
52
+
53
+ /**
54
+ * Convert base64 string to Uint8Array
55
+ */
56
+ function base64ToUint8Array(base64) {
57
+ const binary = atob(base64);
58
+ const bytes = new Uint8Array(binary.length);
59
+ for (let i = 0; i < binary.length; i++) {
60
+ bytes[i] = binary.charCodeAt(i);
61
+ }
62
+ return bytes;
63
+ }
64
+
65
+ /**
66
+ * Generate cryptographically secure random bytes
67
+ */
68
+ function generateRandomBytes(length) {
69
+ const bytes = new Uint8Array(length);
70
+ crypto.getRandomValues(bytes);
71
+ return bytes;
72
+ }
73
+
74
+ // =============================================================================
75
+ // Key Derivation (HKDF - Industry Standard)
76
+ // =============================================================================
77
+
78
+ /**
79
+ * Derive encryption key using HKDF (HMAC-based Key Derivation Function)
80
+ * This is the industry-standard approach recommended by Yubico and others.
81
+ *
82
+ * @param {Uint8Array} inputKeyMaterial - The input key material (from PRF or PIN hash)
83
+ * @param {Uint8Array} salt - Salt for HKDF
84
+ * @param {string} info - Context info string
85
+ * @param {number} length - Desired key length in bytes
86
+ * @returns {Promise<Uint8Array>} Derived key
87
+ */
88
+ async function hkdfDerive(inputKeyMaterial, salt, info, length) {
89
+ const encoder = new TextEncoder();
90
+
91
+ // Import the input key material
92
+ const keyMaterial = await crypto.subtle.importKey(
93
+ 'raw',
94
+ inputKeyMaterial,
95
+ 'HKDF',
96
+ false,
97
+ ['deriveBits']
98
+ );
99
+
100
+ // Derive bits using HKDF
101
+ const derivedBits = await crypto.subtle.deriveBits(
102
+ {
103
+ name: 'HKDF',
104
+ hash: 'SHA-256',
105
+ salt: salt,
106
+ info: encoder.encode(info)
107
+ },
108
+ keyMaterial,
109
+ length * 8
110
+ );
111
+
112
+ return new Uint8Array(derivedBits);
113
+ }
114
+
115
+ /**
116
+ * Derive encryption key and IV from key material
117
+ */
118
+ async function deriveKeyAndIV(keyMaterial, context) {
119
+ const salt = new TextEncoder().encode(`wallet-storage-v${STORAGE_VERSION}`);
120
+
121
+ const encryptionKey = await hkdfDerive(
122
+ keyMaterial,
123
+ salt,
124
+ `${context}-encryption-key`,
125
+ 32
126
+ );
127
+
128
+ const iv = await hkdfDerive(
129
+ keyMaterial,
130
+ salt,
131
+ `${context}-encryption-iv`,
132
+ 12 // AES-GCM standard IV size
133
+ );
134
+
135
+ return { encryptionKey, iv };
136
+ }
137
+
138
+ // =============================================================================
139
+ // PIN-Based Encryption
140
+ // =============================================================================
141
+
142
+ /**
143
+ * Derive key material from a 6-digit PIN
144
+ * Uses PBKDF2 for additional security against brute-force attacks
145
+ */
146
+ async function deriveKeyFromPIN(pin, storedSalt) {
147
+ if (!/^\d{6}$/.test(pin)) {
148
+ throw new Error('PIN must be exactly 6 digits');
149
+ }
150
+
151
+ const encoder = new TextEncoder();
152
+ const pinBytes = encoder.encode(pin);
153
+
154
+ // Use stored salt or generate new one
155
+ const salt = storedSalt || generateRandomBytes(16);
156
+
157
+ // Import PIN as key material
158
+ const keyMaterial = await crypto.subtle.importKey(
159
+ 'raw',
160
+ pinBytes,
161
+ 'PBKDF2',
162
+ false,
163
+ ['deriveBits']
164
+ );
165
+
166
+ // Use PBKDF2 with high iteration count for PIN (since PINs have low entropy)
167
+ const derivedBits = await crypto.subtle.deriveBits(
168
+ {
169
+ name: 'PBKDF2',
170
+ hash: 'SHA-256',
171
+ salt: salt,
172
+ iterations: 100000 // High iteration count for brute-force resistance
173
+ },
174
+ keyMaterial,
175
+ 256
176
+ );
177
+
178
+ return {
179
+ keyMaterial: new Uint8Array(derivedBits),
180
+ salt
181
+ };
182
+ }
183
+
184
+ // =============================================================================
185
+ // WebAuthn PRF Extension
186
+ // =============================================================================
187
+
188
+ /**
189
+ * Check if WebAuthn/Passkeys are supported
190
+ */
191
+ export function isPasskeySupported() {
192
+ return !!(
193
+ window.PublicKeyCredential &&
194
+ typeof window.PublicKeyCredential === 'function'
195
+ );
196
+ }
197
+
198
+ /**
199
+ * Check if PRF extension is likely supported
200
+ * Note: Full support detection requires actually creating a credential
201
+ */
202
+ export async function isPRFLikelySupported() {
203
+ if (!isPasskeySupported()) return false;
204
+
205
+ try {
206
+ // Check if platform authenticator is available
207
+ const available = await PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable();
208
+ return available;
209
+ } catch {
210
+ return false;
211
+ }
212
+ }
213
+
214
+ /**
215
+ * Generate WebAuthn challenge
216
+ */
217
+ function generateChallenge() {
218
+ return generateRandomBytes(32);
219
+ }
220
+
221
+ /**
222
+ * Create PRF input salts for key derivation
223
+ * Following the recommended pattern from Yubico for key rotation support
224
+ */
225
+ function createPRFInputs() {
226
+ const encoder = new TextEncoder();
227
+ return {
228
+ // Primary key derivation salt
229
+ first: encoder.encode('wallet-storage-prf-v2-primary'),
230
+ // Secondary salt for future key rotation support
231
+ second: encoder.encode('wallet-storage-prf-v2-secondary')
232
+ };
233
+ }
234
+
235
+ /**
236
+ * Register a new passkey and derive encryption key material
237
+ *
238
+ * @param {Object} options - Registration options
239
+ * @param {string} options.rpName - Relying party name (e.g., 'My App')
240
+ * @param {string} options.userName - User identifier
241
+ * @param {string} options.userDisplayName - User display name
242
+ * @returns {Promise<{credentialId: string, keyMaterial: Uint8Array, hasPRF: boolean}>}
243
+ */
244
+ export async function registerPasskey(options = {}) {
245
+ if (!isPasskeySupported()) {
246
+ throw new Error('Passkeys are not supported on this device');
247
+ }
248
+
249
+ const {
250
+ rpName = 'Wallet Storage',
251
+ userName = 'wallet-user',
252
+ userDisplayName = 'Wallet User'
253
+ } = options;
254
+
255
+ const challenge = generateChallenge();
256
+ const userId = generateRandomBytes(16);
257
+ const prfInputs = createPRFInputs();
258
+
259
+ const publicKeyCredentialCreationOptions = {
260
+ challenge,
261
+ rp: {
262
+ name: rpName,
263
+ id: window.location.hostname
264
+ },
265
+ user: {
266
+ id: userId,
267
+ name: userName,
268
+ displayName: userDisplayName
269
+ },
270
+ pubKeyCredParams: [
271
+ { alg: -7, type: 'public-key' }, // ES256 (P-256)
272
+ { alg: -257, type: 'public-key' } // RS256
273
+ ],
274
+ authenticatorSelection: {
275
+ authenticatorAttachment: 'platform',
276
+ userVerification: 'required',
277
+ residentKey: 'preferred' // Changed from 'required' for broader compatibility
278
+ },
279
+ timeout: 60000,
280
+ attestation: 'none',
281
+ extensions: {
282
+ prf: {
283
+ eval: {
284
+ first: prfInputs.first
285
+ }
286
+ }
287
+ }
288
+ };
289
+
290
+ const credential = await navigator.credentials.create({
291
+ publicKey: publicKeyCredentialCreationOptions
292
+ });
293
+
294
+ // Extract PRF result or fall back to credential ID
295
+ const extensionResults = credential.getClientExtensionResults();
296
+ const prfResult = extensionResults?.prf?.results?.first;
297
+
298
+ let keyMaterial;
299
+ let hasPRF = false;
300
+
301
+ if (prfResult && prfResult.byteLength > 0) {
302
+ // PRF is supported - use the PRF output
303
+ keyMaterial = new Uint8Array(prfResult);
304
+ hasPRF = true;
305
+ } else {
306
+ // PRF not supported - derive key from credential ID
307
+ // This is less secure but provides fallback functionality
308
+ const rawId = new Uint8Array(credential.rawId);
309
+ const hash = await crypto.subtle.digest('SHA-256', rawId);
310
+ keyMaterial = new Uint8Array(hash);
311
+ }
312
+
313
+ return {
314
+ credentialId: arrayBufferToBase64(credential.rawId),
315
+ keyMaterial,
316
+ hasPRF
317
+ };
318
+ }
319
+
320
+ /**
321
+ * Authenticate with existing passkey and derive encryption key material
322
+ *
323
+ * @param {string} credentialId - Base64-encoded credential ID
324
+ * @returns {Promise<{keyMaterial: Uint8Array, hasPRF: boolean}>}
325
+ */
326
+ export async function authenticatePasskey(credentialId) {
327
+ if (!isPasskeySupported()) {
328
+ throw new Error('Passkeys are not supported on this device');
329
+ }
330
+
331
+ const challenge = generateChallenge();
332
+ const prfInputs = createPRFInputs();
333
+ const credentialIdBytes = base64ToUint8Array(credentialId);
334
+
335
+ const publicKeyCredentialRequestOptions = {
336
+ challenge,
337
+ allowCredentials: [{
338
+ id: credentialIdBytes,
339
+ type: 'public-key',
340
+ transports: ['internal', 'hybrid'] // Support both platform and cross-device
341
+ }],
342
+ userVerification: 'required',
343
+ timeout: 60000,
344
+ extensions: {
345
+ prf: {
346
+ eval: {
347
+ first: prfInputs.first
348
+ }
349
+ }
350
+ }
351
+ };
352
+
353
+ const assertion = await navigator.credentials.get({
354
+ publicKey: publicKeyCredentialRequestOptions
355
+ });
356
+
357
+ // Extract PRF result or fall back to credential ID
358
+ const extensionResults = assertion.getClientExtensionResults();
359
+ const prfResult = extensionResults?.prf?.results?.first;
360
+
361
+ let keyMaterial;
362
+ let hasPRF = false;
363
+
364
+ if (prfResult && prfResult.byteLength > 0) {
365
+ keyMaterial = new Uint8Array(prfResult);
366
+ hasPRF = true;
367
+ } else {
368
+ // Fallback: derive from credential ID
369
+ const rawId = new Uint8Array(assertion.rawId);
370
+ const hash = await crypto.subtle.digest('SHA-256', rawId);
371
+ keyMaterial = new Uint8Array(hash);
372
+ }
373
+
374
+ return { keyMaterial, hasPRF };
375
+ }
376
+
377
+ // =============================================================================
378
+ // Encryption/Decryption
379
+ // =============================================================================
380
+
381
+ /**
382
+ * Encrypt data using AES-256-GCM
383
+ */
384
+ async function encryptData(data, encryptionKey, iv) {
385
+ const encoder = new TextEncoder();
386
+ const plaintext = encoder.encode(JSON.stringify(data));
387
+
388
+ const cryptoKey = await crypto.subtle.importKey(
389
+ 'raw',
390
+ encryptionKey,
391
+ { name: 'AES-GCM' },
392
+ false,
393
+ ['encrypt']
394
+ );
395
+
396
+ const ciphertext = await crypto.subtle.encrypt(
397
+ { name: 'AES-GCM', iv },
398
+ cryptoKey,
399
+ plaintext
400
+ );
401
+
402
+ return new Uint8Array(ciphertext);
403
+ }
404
+
405
+ /**
406
+ * Decrypt data using AES-256-GCM
407
+ */
408
+ async function decryptData(ciphertext, encryptionKey, iv) {
409
+ const cryptoKey = await crypto.subtle.importKey(
410
+ 'raw',
411
+ encryptionKey,
412
+ { name: 'AES-GCM' },
413
+ false,
414
+ ['decrypt']
415
+ );
416
+
417
+ const plaintext = await crypto.subtle.decrypt(
418
+ { name: 'AES-GCM', iv },
419
+ cryptoKey,
420
+ ciphertext
421
+ );
422
+
423
+ const decoder = new TextDecoder();
424
+ return JSON.parse(decoder.decode(plaintext));
425
+ }
426
+
427
+ // =============================================================================
428
+ // High-Level Storage API
429
+ // =============================================================================
430
+
431
+ /**
432
+ * Get storage metadata
433
+ * @returns {Object|null} Storage metadata or null if no wallet stored
434
+ */
435
+ export function getStorageMetadata() {
436
+ try {
437
+ const metadataJson = localStorage.getItem(METADATA_KEY);
438
+ if (!metadataJson) return null;
439
+
440
+ const metadata = JSON.parse(metadataJson);
441
+ return {
442
+ method: metadata.method || StorageMethod.NONE,
443
+ timestamp: metadata.timestamp,
444
+ date: new Date(metadata.timestamp).toLocaleDateString(),
445
+ version: metadata.version,
446
+ hasPRF: metadata.hasPRF || false
447
+ };
448
+ } catch {
449
+ return null;
450
+ }
451
+ }
452
+
453
+ /**
454
+ * Check if a wallet is stored
455
+ * @returns {boolean}
456
+ */
457
+ export function hasStoredWallet() {
458
+ return getStorageMetadata() !== null;
459
+ }
460
+
461
+ /**
462
+ * Get the storage method used
463
+ * @returns {string} StorageMethod value
464
+ */
465
+ export function getStorageMethod() {
466
+ const metadata = getStorageMetadata();
467
+ return metadata?.method || StorageMethod.NONE;
468
+ }
469
+
470
+ /**
471
+ * Store wallet data with PIN encryption
472
+ *
473
+ * @param {string} pin - 6-digit PIN
474
+ * @param {Object} walletData - Data to encrypt and store
475
+ * @returns {Promise<boolean>}
476
+ */
477
+ export async function storeWithPIN(pin, walletData) {
478
+ // Derive key from PIN
479
+ const { keyMaterial, salt } = await deriveKeyFromPIN(pin);
480
+ const { encryptionKey, iv } = await deriveKeyAndIV(keyMaterial, 'pin');
481
+
482
+ // Encrypt wallet data
483
+ const ciphertext = await encryptData(walletData, encryptionKey, iv);
484
+
485
+ // Store encrypted data
486
+ const encryptedData = {
487
+ ciphertext: arrayBufferToBase64(ciphertext),
488
+ salt: arrayBufferToBase64(salt)
489
+ };
490
+ localStorage.setItem(ENCRYPTED_DATA_KEY, JSON.stringify(encryptedData));
491
+
492
+ // Store metadata
493
+ const metadata = {
494
+ method: StorageMethod.PIN,
495
+ timestamp: Date.now(),
496
+ version: STORAGE_VERSION
497
+ };
498
+ localStorage.setItem(METADATA_KEY, JSON.stringify(metadata));
499
+
500
+ return true;
501
+ }
502
+
503
+ /**
504
+ * Retrieve wallet data with PIN
505
+ *
506
+ * @param {string} pin - 6-digit PIN
507
+ * @returns {Promise<Object>} Decrypted wallet data
508
+ */
509
+ export async function retrieveWithPIN(pin) {
510
+ const metadata = getStorageMetadata();
511
+ if (!metadata || metadata.method !== StorageMethod.PIN) {
512
+ throw new Error('No PIN-encrypted wallet found');
513
+ }
514
+
515
+ const encryptedJson = localStorage.getItem(ENCRYPTED_DATA_KEY);
516
+ if (!encryptedJson) {
517
+ throw new Error('Encrypted data not found');
518
+ }
519
+
520
+ const encryptedData = JSON.parse(encryptedJson);
521
+ const salt = base64ToUint8Array(encryptedData.salt);
522
+ const ciphertext = base64ToUint8Array(encryptedData.ciphertext);
523
+
524
+ // Derive key from PIN with stored salt
525
+ const { keyMaterial } = await deriveKeyFromPIN(pin, salt);
526
+ const { encryptionKey, iv } = await deriveKeyAndIV(keyMaterial, 'pin');
527
+
528
+ try {
529
+ return await decryptData(ciphertext, encryptionKey, iv);
530
+ } catch (e) {
531
+ throw new Error('Invalid PIN or corrupted data');
532
+ }
533
+ }
534
+
535
+ /**
536
+ * Store wallet data with passkey encryption
537
+ *
538
+ * @param {Object} walletData - Data to encrypt and store
539
+ * @param {Object} options - Passkey options
540
+ * @returns {Promise<boolean>}
541
+ */
542
+ export async function storeWithPasskey(walletData, options = {}) {
543
+ // Register passkey and get key material
544
+ const { credentialId, keyMaterial, hasPRF } = await registerPasskey(options);
545
+
546
+ // Derive encryption key
547
+ const { encryptionKey, iv } = await deriveKeyAndIV(keyMaterial, 'passkey');
548
+
549
+ // Encrypt wallet data
550
+ const ciphertext = await encryptData(walletData, encryptionKey, iv);
551
+
552
+ // Store credential info
553
+ const credentialData = {
554
+ id: credentialId,
555
+ hasPRF
556
+ };
557
+ localStorage.setItem(PASSKEY_CREDENTIAL_KEY, JSON.stringify(credentialData));
558
+
559
+ // Store encrypted data
560
+ const encryptedData = {
561
+ ciphertext: arrayBufferToBase64(ciphertext)
562
+ };
563
+ localStorage.setItem(ENCRYPTED_DATA_KEY, JSON.stringify(encryptedData));
564
+
565
+ // Store metadata
566
+ const metadata = {
567
+ method: StorageMethod.PASSKEY,
568
+ timestamp: Date.now(),
569
+ version: STORAGE_VERSION,
570
+ hasPRF
571
+ };
572
+ localStorage.setItem(METADATA_KEY, JSON.stringify(metadata));
573
+
574
+ return true;
575
+ }
576
+
577
+ /**
578
+ * Retrieve wallet data with passkey
579
+ *
580
+ * @returns {Promise<Object>} Decrypted wallet data
581
+ */
582
+ export async function retrieveWithPasskey() {
583
+ const metadata = getStorageMetadata();
584
+ if (!metadata || metadata.method !== StorageMethod.PASSKEY) {
585
+ throw new Error('No passkey-encrypted wallet found');
586
+ }
587
+
588
+ const credentialJson = localStorage.getItem(PASSKEY_CREDENTIAL_KEY);
589
+ if (!credentialJson) {
590
+ throw new Error('Passkey credential not found');
591
+ }
592
+
593
+ const credentialData = JSON.parse(credentialJson);
594
+
595
+ const encryptedJson = localStorage.getItem(ENCRYPTED_DATA_KEY);
596
+ if (!encryptedJson) {
597
+ throw new Error('Encrypted data not found');
598
+ }
599
+
600
+ const encryptedData = JSON.parse(encryptedJson);
601
+ const ciphertext = base64ToUint8Array(encryptedData.ciphertext);
602
+
603
+ // Authenticate with passkey and get key material
604
+ const { keyMaterial } = await authenticatePasskey(credentialData.id);
605
+
606
+ // Derive encryption key
607
+ const { encryptionKey, iv } = await deriveKeyAndIV(keyMaterial, 'passkey');
608
+
609
+ try {
610
+ return await decryptData(ciphertext, encryptionKey, iv);
611
+ } catch (e) {
612
+ throw new Error('Passkey authentication failed or data corrupted');
613
+ }
614
+ }
615
+
616
+ /**
617
+ * Clear all stored wallet data
618
+ */
619
+ export function clearStorage() {
620
+ localStorage.removeItem(METADATA_KEY);
621
+ localStorage.removeItem(ENCRYPTED_DATA_KEY);
622
+ localStorage.removeItem(PASSKEY_CREDENTIAL_KEY);
623
+ }
624
+
625
+ /**
626
+ * Migrate from old storage format (v1) to new format (v2)
627
+ * Call this on app initialization
628
+ */
629
+ export function migrateStorage() {
630
+ // Check for old v1 keys
631
+ const oldPinWallet = localStorage.getItem('encrypted_wallet');
632
+ const oldPasskeyCredential = localStorage.getItem('passkey_credential');
633
+ const oldPasskeyWallet = localStorage.getItem('passkey_wallet');
634
+
635
+ // Already migrated or no old data
636
+ if (getStorageMetadata() !== null) return;
637
+ if (!oldPinWallet && !oldPasskeyCredential) return;
638
+
639
+ console.log('Migrating wallet storage from v1 to v2...');
640
+
641
+ if (oldPasskeyCredential && oldPasskeyWallet) {
642
+ // Migrate passkey storage
643
+ try {
644
+ const credential = JSON.parse(oldPasskeyCredential);
645
+ const wallet = JSON.parse(oldPasskeyWallet);
646
+
647
+ localStorage.setItem(PASSKEY_CREDENTIAL_KEY, JSON.stringify({
648
+ id: credential.id,
649
+ hasPRF: credential.hasPRF || false
650
+ }));
651
+
652
+ localStorage.setItem(ENCRYPTED_DATA_KEY, JSON.stringify({
653
+ ciphertext: wallet.ciphertext
654
+ }));
655
+
656
+ localStorage.setItem(METADATA_KEY, JSON.stringify({
657
+ method: StorageMethod.PASSKEY,
658
+ timestamp: credential.timestamp || wallet.timestamp || Date.now(),
659
+ version: STORAGE_VERSION,
660
+ hasPRF: credential.hasPRF || false
661
+ }));
662
+
663
+ // Clean up old keys
664
+ localStorage.removeItem('passkey_credential');
665
+ localStorage.removeItem('passkey_wallet');
666
+
667
+ console.log('Passkey storage migrated successfully');
668
+ } catch (e) {
669
+ console.error('Failed to migrate passkey storage:', e);
670
+ }
671
+ } else if (oldPinWallet) {
672
+ // Migrate PIN storage - can't fully migrate since we need the salt
673
+ // User will need to re-enter their wallet
674
+ console.log('PIN storage detected but cannot be migrated - user will need to re-login');
675
+ localStorage.removeItem('encrypted_wallet');
676
+ }
677
+ }
678
+
679
+ // =============================================================================
680
+ // Export default object for convenience
681
+ // =============================================================================
682
+
683
+ export default {
684
+ StorageMethod,
685
+ isPasskeySupported,
686
+ isPRFLikelySupported,
687
+ getStorageMetadata,
688
+ hasStoredWallet,
689
+ getStorageMethod,
690
+ storeWithPIN,
691
+ retrieveWithPIN,
692
+ storeWithPasskey,
693
+ retrieveWithPasskey,
694
+ clearStorage,
695
+ migrateStorage
696
+ };