hd-wallet-ui 2.0.20 → 2.0.26

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.
Files changed (39) hide show
  1. package/LICENSE +190 -0
  2. package/README.md +101 -123
  3. package/data/common-passwords-sdn-v1.source.json +12 -0
  4. package/data/common-passwords-sdn-v1.txt +10000 -0
  5. package/dist/browser/sdn-wallet-callback.js +1 -0
  6. package/dist/browser/sdn-wallet-public-client.js +1 -0
  7. package/dist/browser/wallet-callback.html +6 -0
  8. package/dist/client/asset-review.d.ts +8 -0
  9. package/dist/client/asset-review.js +1570 -0
  10. package/dist/client/callback.d.ts +13 -0
  11. package/dist/client/callback.js +29 -0
  12. package/dist/client/index.d.ts +11 -0
  13. package/dist/client/index.js +1379 -0
  14. package/dist/client/sdn.d.ts +8 -0
  15. package/dist/client/sdn.js +1542 -0
  16. package/dist/client/style.css +32 -0
  17. package/dist/client/types.d.ts +278 -0
  18. package/dist/compat/index.d.ts +26 -0
  19. package/dist/compat/index.js +62 -0
  20. package/dist/wallet-origin/index.d.ts +21 -0
  21. package/dist/wallet-origin/index.js +4607 -0
  22. package/dist/wallet-origin-host/assets/wallet-origin.3d16729bf9aaca4517cc699bec526c7209be184cdbb0ba0dbf4063d36a091e1d.js +12463 -0
  23. package/dist/wallet-origin-host/assets/wallet-origin.e22071a3643a57055d29f2e6951c1a2f15e77c1b58b0f81bb7f2afed5bcfb0b4.wasm +0 -0
  24. package/dist/wallet-origin-host/assets/wallet-origin.f3273f7ea70bb494cda1fc02264b936f2e9b010dc07df15eed05fa3f7d1356de.css +1 -0
  25. package/dist/wallet-origin-host/index.html +10 -0
  26. package/dist/wallet-origin-host/integrity.json +1 -0
  27. package/package.json +46 -18
  28. package/src/address-derivation.js +0 -514
  29. package/src/app.js +0 -6008
  30. package/src/blockchain-trust.js +0 -752
  31. package/src/clipboard.js +0 -44
  32. package/src/constants.js +0 -66
  33. package/src/hash.js +0 -9
  34. package/src/lib.js +0 -66
  35. package/src/template.js +0 -626
  36. package/src/trust-ui.js +0 -745
  37. package/src/wallet-storage.js +0 -773
  38. package/styles/main.css +0 -6220
  39. package/styles/widget.css +0 -6212
@@ -1,773 +0,0 @@
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
- import initHDWallet from 'hd-wallet-wasm';
16
-
17
- // =============================================================================
18
- // Storage Keys
19
- // =============================================================================
20
-
21
- const STORAGE_PREFIX = 'wallet_storage_';
22
- const METADATA_KEY = `${STORAGE_PREFIX}metadata`;
23
- const ENCRYPTED_DATA_KEY = `${STORAGE_PREFIX}encrypted`;
24
- const PASSKEY_CREDENTIAL_KEY = `${STORAGE_PREFIX}passkey_credential`;
25
-
26
- // Version for future migrations
27
- const STORAGE_VERSION = 3;
28
-
29
- // AES-GCM standard IV size (96-bit)
30
- const AES_GCM_IV_LENGTH = 12;
31
-
32
- // 6-digit PINs have low entropy; PBKDF2 must be expensive to slow offline brute force.
33
- // (This is still not a substitute for rate-limiting when an attacker can query online.)
34
- const PIN_PBKDF2_ITERATIONS = 600000;
35
- const LEGACY_PIN_PBKDF2_ITERATIONS = 100000;
36
- const AES_GCM_TAG_LENGTH = 16;
37
-
38
- let walletPromise = null;
39
-
40
- function getWallet() {
41
- if (!walletPromise) walletPromise = initHDWallet();
42
- return walletPromise;
43
- }
44
-
45
- function asUint8Array(value) {
46
- if (value instanceof Uint8Array) return value;
47
- if (ArrayBuffer.isView(value)) {
48
- return new Uint8Array(value.buffer, value.byteOffset, value.byteLength);
49
- }
50
- if (value instanceof ArrayBuffer) return new Uint8Array(value);
51
- throw new Error('Expected byte array');
52
- }
53
-
54
- // =============================================================================
55
- // Storage Method Enum
56
- // =============================================================================
57
-
58
- export const StorageMethod = {
59
- NONE: 'none',
60
- PIN: 'pin',
61
- PASSKEY: 'passkey'
62
- };
63
-
64
- // =============================================================================
65
- // Utility Functions
66
- // =============================================================================
67
-
68
- /**
69
- * Convert ArrayBuffer to base64 string
70
- */
71
- function arrayBufferToBase64(buffer) {
72
- const bytes = new Uint8Array(buffer);
73
- let binary = '';
74
- for (let i = 0; i < bytes.byteLength; i++) {
75
- binary += String.fromCharCode(bytes[i]);
76
- }
77
- return btoa(binary);
78
- }
79
-
80
- /**
81
- * Convert base64 string to Uint8Array
82
- */
83
- function base64ToUint8Array(base64) {
84
- const binary = atob(base64);
85
- const bytes = new Uint8Array(binary.length);
86
- for (let i = 0; i < binary.length; i++) {
87
- bytes[i] = binary.charCodeAt(i);
88
- }
89
- return bytes;
90
- }
91
-
92
- /**
93
- * Generate cryptographically secure random bytes
94
- */
95
- async function generateRandomBytes(length) {
96
- const wallet = await getWallet();
97
- return wallet.utils.getRandomBytes(length);
98
- }
99
-
100
- // =============================================================================
101
- // Key Derivation (HKDF - Industry Standard)
102
- // =============================================================================
103
-
104
- /**
105
- * Derive encryption key using HKDF (HMAC-based Key Derivation Function)
106
- * This is the industry-standard approach recommended by Yubico and others.
107
- *
108
- * @param {Uint8Array} inputKeyMaterial - The input key material (from PRF or PIN hash)
109
- * @param {Uint8Array} salt - Salt for HKDF
110
- * @param {string} info - Context info string
111
- * @param {number} length - Desired key length in bytes
112
- * @returns {Promise<Uint8Array>} Derived key
113
- */
114
- async function hkdfDerive(inputKeyMaterial, salt, info, length) {
115
- const encoder = new TextEncoder();
116
- const wallet = await getWallet();
117
- const infoBytes = typeof info === 'string' ? encoder.encode(info) : asUint8Array(info);
118
- return wallet.utils.hkdf(asUint8Array(inputKeyMaterial), asUint8Array(salt), infoBytes, length);
119
- }
120
-
121
- /**
122
- * Derive encryption key from key material (HKDF).
123
- */
124
- async function deriveEncryptionKey(keyMaterial, context) {
125
- const salt = new TextEncoder().encode(`wallet-storage-v${STORAGE_VERSION}`);
126
-
127
- const encryptionKey = await hkdfDerive(
128
- keyMaterial,
129
- salt,
130
- `${context}-encryption-key`,
131
- 32
132
- );
133
-
134
- return encryptionKey;
135
- }
136
-
137
- /**
138
- * Legacy: v1/v2 storage used a deterministic IV derived via HKDF.
139
- * This exists only to decrypt and upgrade old stored blobs.
140
- */
141
- async function deriveLegacyKeyAndIV(keyMaterial, context, version) {
142
- const salt = new TextEncoder().encode(`wallet-storage-v${version}`);
143
- const encryptionKey = await hkdfDerive(
144
- keyMaterial,
145
- salt,
146
- `${context}-encryption-key`,
147
- 32
148
- );
149
- const iv = await hkdfDerive(
150
- keyMaterial,
151
- salt,
152
- `${context}-encryption-iv`,
153
- AES_GCM_IV_LENGTH
154
- );
155
- return { encryptionKey, iv };
156
- }
157
-
158
- // =============================================================================
159
- // PIN-Based Encryption
160
- // =============================================================================
161
-
162
- /**
163
- * Derive key material from a 6-digit PIN
164
- * Uses PBKDF2 for additional security against brute-force attacks
165
- */
166
- async function deriveKeyFromPIN(pin, storedSalt, iterations = PIN_PBKDF2_ITERATIONS) {
167
- if (!/^\d{6}$/.test(pin)) {
168
- throw new Error('PIN must be exactly 6 digits');
169
- }
170
-
171
- const encoder = new TextEncoder();
172
- const pinBytes = encoder.encode(pin);
173
-
174
- // Use stored salt or generate new one
175
- const salt = storedSalt || await generateRandomBytes(16);
176
- const wallet = await getWallet();
177
-
178
- return {
179
- keyMaterial: wallet.utils.pbkdf2(pinBytes, salt, iterations, 32),
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
- async 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 = await generateChallenge();
256
- const userId = await 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
- keyMaterial = new Uint8Array(prfResult);
303
- hasPRF = true;
304
- } else {
305
- // PRF not available — derive key material from credential ID + a fixed salt.
306
- const rawId = new Uint8Array(credential.rawId);
307
- const salt = new TextEncoder().encode('wallet-storage-credid-fallback-v1');
308
- keyMaterial = await hkdfDerive(rawId, salt, new Uint8Array(0), 32);
309
- }
310
-
311
- return {
312
- credentialId: arrayBufferToBase64(credential.rawId),
313
- keyMaterial,
314
- hasPRF
315
- };
316
- }
317
-
318
- /**
319
- * Authenticate with existing passkey and derive encryption key material
320
- *
321
- * @param {string} credentialId - Base64-encoded credential ID
322
- * @returns {Promise<{keyMaterial: Uint8Array, hasPRF: boolean}>}
323
- */
324
- export async function authenticatePasskey(credentialId) {
325
- if (!isPasskeySupported()) {
326
- throw new Error('Passkeys are not supported on this device');
327
- }
328
-
329
- const challenge = await generateChallenge();
330
- const prfInputs = createPRFInputs();
331
- const credentialIdBytes = base64ToUint8Array(credentialId);
332
-
333
- const publicKeyCredentialRequestOptions = {
334
- challenge,
335
- allowCredentials: [{
336
- id: credentialIdBytes,
337
- type: 'public-key',
338
- transports: ['internal', 'hybrid'] // Support both platform and cross-device
339
- }],
340
- userVerification: 'required',
341
- timeout: 60000,
342
- extensions: {
343
- prf: {
344
- eval: {
345
- first: prfInputs.first
346
- }
347
- }
348
- }
349
- };
350
-
351
- const assertion = await navigator.credentials.get({
352
- publicKey: publicKeyCredentialRequestOptions
353
- });
354
-
355
- // Extract PRF result or fall back to credential ID
356
- const extensionResults = assertion.getClientExtensionResults();
357
- const prfResult = extensionResults?.prf?.results?.first;
358
-
359
- let keyMaterial;
360
- let hasPRF = false;
361
-
362
- if (prfResult && prfResult.byteLength > 0) {
363
- keyMaterial = new Uint8Array(prfResult);
364
- hasPRF = true;
365
- } else {
366
- // PRF not available — derive key material from credential ID + a fixed salt.
367
- const rawId = new Uint8Array(assertion.rawId);
368
- const salt = new TextEncoder().encode('wallet-storage-credid-fallback-v1');
369
- keyMaterial = await hkdfDerive(rawId, salt, new Uint8Array(0), 32);
370
- }
371
-
372
- return { keyMaterial, hasPRF };
373
- }
374
-
375
- // =============================================================================
376
- // Encryption/Decryption
377
- // =============================================================================
378
-
379
- /**
380
- * Encrypt data using AES-256-GCM
381
- */
382
- async function encryptData(data, encryptionKey, aad) {
383
- const encoder = new TextEncoder();
384
- const plaintext = encoder.encode(JSON.stringify(data));
385
- const iv = await generateRandomBytes(AES_GCM_IV_LENGTH);
386
- const wallet = await getWallet();
387
- const { ciphertext, tag } = wallet.utils.aesGcm.encrypt(
388
- asUint8Array(encryptionKey),
389
- plaintext,
390
- iv,
391
- aad ? asUint8Array(aad) : new Uint8Array(0)
392
- );
393
- const sealed = new Uint8Array(ciphertext.length + tag.length);
394
- sealed.set(ciphertext, 0);
395
- sealed.set(tag, ciphertext.length);
396
-
397
- return { iv, ciphertext: sealed };
398
- }
399
-
400
- /**
401
- * Decrypt data using AES-256-GCM
402
- */
403
- async function decryptData(ciphertext, encryptionKey, iv, aad) {
404
- const sealed = asUint8Array(ciphertext);
405
- if (sealed.length <= AES_GCM_TAG_LENGTH) {
406
- throw new Error('Ciphertext is too short');
407
- }
408
-
409
- const wallet = await getWallet();
410
- const plaintext = wallet.utils.aesGcm.decrypt(
411
- asUint8Array(encryptionKey),
412
- sealed.subarray(0, sealed.length - AES_GCM_TAG_LENGTH),
413
- sealed.subarray(sealed.length - AES_GCM_TAG_LENGTH),
414
- asUint8Array(iv),
415
- aad ? asUint8Array(aad) : new Uint8Array(0)
416
- );
417
-
418
- const decoder = new TextDecoder();
419
- return JSON.parse(decoder.decode(plaintext));
420
- }
421
-
422
- // =============================================================================
423
- // High-Level Storage API
424
- // =============================================================================
425
-
426
- function getAadForMethod(method) {
427
- // Small AAD to bind ciphertexts to this module + method.
428
- // (Replay protection against localStorage rollback is not achievable without an external monotonic anchor.)
429
- return new TextEncoder().encode(`wallet-storage|v${STORAGE_VERSION}|${method}`);
430
- }
431
-
432
- /**
433
- * Get storage metadata
434
- * @returns {Object|null} Storage metadata or null if no wallet stored
435
- */
436
- export function getStorageMetadata() {
437
- try {
438
- const metadataJson = localStorage.getItem(METADATA_KEY);
439
- if (!metadataJson) return null;
440
-
441
- const metadata = JSON.parse(metadataJson);
442
- return {
443
- method: metadata.method || StorageMethod.NONE,
444
- timestamp: metadata.timestamp,
445
- date: new Date(metadata.timestamp).toLocaleDateString(),
446
- version: metadata.version,
447
- hasPRF: metadata.hasPRF || false
448
- };
449
- } catch {
450
- return null;
451
- }
452
- }
453
-
454
- /**
455
- * Check if a wallet is stored
456
- * @returns {boolean}
457
- */
458
- export function hasStoredWallet() {
459
- return getStorageMetadata() !== null;
460
- }
461
-
462
- /**
463
- * Get the storage method used
464
- * @returns {string} StorageMethod value
465
- */
466
- export function getStorageMethod() {
467
- const metadata = getStorageMetadata();
468
- return metadata?.method || StorageMethod.NONE;
469
- }
470
-
471
- /**
472
- * Store wallet data with PIN encryption
473
- *
474
- * @param {string} pin - 6-digit PIN
475
- * @param {Object} walletData - Data to encrypt and store
476
- * @returns {Promise<boolean>}
477
- */
478
- export async function storeWithPIN(pin, walletData) {
479
- // Derive key from PIN
480
- const { keyMaterial, salt } = await deriveKeyFromPIN(pin);
481
- const encryptionKey = await deriveEncryptionKey(keyMaterial, 'pin');
482
-
483
- // Encrypt wallet data
484
- const aad = getAadForMethod(StorageMethod.PIN);
485
- const { ciphertext, iv } = await encryptData(walletData, encryptionKey, aad);
486
-
487
- // Store encrypted data
488
- const encryptedData = {
489
- ciphertext: arrayBufferToBase64(ciphertext),
490
- iv: arrayBufferToBase64(iv),
491
- salt: arrayBufferToBase64(salt)
492
- };
493
- localStorage.setItem(ENCRYPTED_DATA_KEY, JSON.stringify(encryptedData));
494
-
495
- // Store metadata
496
- const metadata = {
497
- method: StorageMethod.PIN,
498
- timestamp: Date.now(),
499
- version: STORAGE_VERSION
500
- };
501
- localStorage.setItem(METADATA_KEY, JSON.stringify(metadata));
502
-
503
- return true;
504
- }
505
-
506
- /**
507
- * Retrieve wallet data with PIN
508
- *
509
- * @param {string} pin - 6-digit PIN
510
- * @returns {Promise<Object>} Decrypted wallet data
511
- */
512
- export async function retrieveWithPIN(pin) {
513
- const metadata = getStorageMetadata();
514
- if (!metadata || metadata.method !== StorageMethod.PIN) {
515
- throw new Error('No PIN-encrypted wallet found');
516
- }
517
-
518
- const encryptedJson = localStorage.getItem(ENCRYPTED_DATA_KEY);
519
- if (!encryptedJson) {
520
- throw new Error('Encrypted data not found');
521
- }
522
-
523
- const encryptedData = JSON.parse(encryptedJson);
524
- const salt = base64ToUint8Array(encryptedData.salt);
525
- const ciphertext = base64ToUint8Array(encryptedData.ciphertext);
526
- const iv = encryptedData.iv ? base64ToUint8Array(encryptedData.iv) : null;
527
-
528
- // Derive key from PIN with stored salt
529
- const { keyMaterial } = await deriveKeyFromPIN(pin, salt, iv ? PIN_PBKDF2_ITERATIONS : LEGACY_PIN_PBKDF2_ITERATIONS);
530
- const aad = getAadForMethod(StorageMethod.PIN);
531
-
532
- try {
533
- let walletData;
534
- if (iv) {
535
- const encryptionKey = await deriveEncryptionKey(keyMaterial, 'pin');
536
- walletData = await decryptData(ciphertext, encryptionKey, iv, aad);
537
- } else {
538
- // Legacy v1/v2 deterministic-IV storage. Decrypt and upgrade in-place.
539
- const legacyVersion = metadata.version || 2;
540
- const { encryptionKey: legacyKey, iv: legacyIv } = await deriveLegacyKeyAndIV(keyMaterial, 'pin', legacyVersion);
541
- walletData = await decryptData(ciphertext, legacyKey, legacyIv);
542
-
543
- // Upgrade stored blob to v3 (random IV) after successful decrypt.
544
- const newKey = await deriveEncryptionKey(keyMaterial, 'pin');
545
- const { ciphertext: newCt, iv: newIv } = await encryptData(walletData, newKey, aad);
546
- localStorage.setItem(ENCRYPTED_DATA_KEY, JSON.stringify({
547
- ciphertext: arrayBufferToBase64(newCt),
548
- iv: arrayBufferToBase64(newIv),
549
- salt: arrayBufferToBase64(salt)
550
- }));
551
- localStorage.setItem(METADATA_KEY, JSON.stringify({
552
- ...metadata,
553
- version: STORAGE_VERSION
554
- }));
555
- }
556
- return walletData;
557
- } catch (e) {
558
- throw new Error('Invalid PIN or corrupted data');
559
- }
560
- }
561
-
562
- /**
563
- * Store wallet data with passkey encryption
564
- *
565
- * @param {Object} walletData - Data to encrypt and store
566
- * @param {Object} options - Passkey options
567
- * @returns {Promise<boolean>}
568
- */
569
- export async function storeWithPasskey(walletData, options = {}) {
570
- // Prefer reusing an existing stored passkey credential to avoid creating duplicates.
571
- let credentialId = null;
572
- let keyMaterial = null;
573
- let hasPRF = false;
574
-
575
- try {
576
- const existing = localStorage.getItem(PASSKEY_CREDENTIAL_KEY);
577
- if (existing) {
578
- const parsed = JSON.parse(existing);
579
- if (parsed?.id) {
580
- credentialId = parsed.id;
581
- const auth = await authenticatePasskey(credentialId);
582
- keyMaterial = auth.keyMaterial;
583
- hasPRF = auth.hasPRF;
584
- }
585
- }
586
- } catch {
587
- // Ignore and fall back to registration below.
588
- }
589
-
590
- if (!keyMaterial) {
591
- const reg = await registerPasskey(options);
592
- credentialId = reg.credentialId;
593
- keyMaterial = reg.keyMaterial;
594
- hasPRF = reg.hasPRF;
595
- }
596
-
597
- // Derive encryption key
598
- const encryptionKey = await deriveEncryptionKey(keyMaterial, 'passkey');
599
-
600
- // Encrypt wallet data
601
- const aad = getAadForMethod(StorageMethod.PASSKEY);
602
- const { ciphertext, iv } = await encryptData(walletData, encryptionKey, aad);
603
-
604
- // Store credential info
605
- const credentialData = {
606
- id: credentialId,
607
- hasPRF
608
- };
609
- localStorage.setItem(PASSKEY_CREDENTIAL_KEY, JSON.stringify(credentialData));
610
-
611
- // Store encrypted data
612
- const encryptedData = {
613
- ciphertext: arrayBufferToBase64(ciphertext),
614
- iv: arrayBufferToBase64(iv)
615
- };
616
- localStorage.setItem(ENCRYPTED_DATA_KEY, JSON.stringify(encryptedData));
617
-
618
- // Store metadata
619
- const metadata = {
620
- method: StorageMethod.PASSKEY,
621
- timestamp: Date.now(),
622
- version: STORAGE_VERSION,
623
- hasPRF
624
- };
625
- localStorage.setItem(METADATA_KEY, JSON.stringify(metadata));
626
-
627
- return true;
628
- }
629
-
630
- /**
631
- * Retrieve wallet data with passkey
632
- *
633
- * @returns {Promise<Object>} Decrypted wallet data
634
- */
635
- export async function retrieveWithPasskey() {
636
- const metadata = getStorageMetadata();
637
- if (!metadata || metadata.method !== StorageMethod.PASSKEY) {
638
- throw new Error('No passkey-encrypted wallet found');
639
- }
640
- const credentialJson = localStorage.getItem(PASSKEY_CREDENTIAL_KEY);
641
- if (!credentialJson) {
642
- throw new Error('Passkey credential not found');
643
- }
644
-
645
- const credentialData = JSON.parse(credentialJson);
646
-
647
- const encryptedJson = localStorage.getItem(ENCRYPTED_DATA_KEY);
648
- if (!encryptedJson) {
649
- throw new Error('Encrypted data not found');
650
- }
651
-
652
- const encryptedData = JSON.parse(encryptedJson);
653
- const ciphertext = base64ToUint8Array(encryptedData.ciphertext);
654
- const iv = encryptedData.iv ? base64ToUint8Array(encryptedData.iv) : null;
655
-
656
- // Authenticate with passkey and get key material
657
- const { keyMaterial } = await authenticatePasskey(credentialData.id);
658
-
659
- // Derive encryption key
660
- const aad = getAadForMethod(StorageMethod.PASSKEY);
661
-
662
- try {
663
- let walletData;
664
- if (iv) {
665
- const encryptionKey = await deriveEncryptionKey(keyMaterial, 'passkey');
666
- walletData = await decryptData(ciphertext, encryptionKey, iv, aad);
667
- } else {
668
- // Legacy v1/v2 deterministic-IV storage. Decrypt and upgrade in-place.
669
- const legacyVersion = metadata.version || 2;
670
- const { encryptionKey: legacyKey, iv: legacyIv } = await deriveLegacyKeyAndIV(keyMaterial, 'passkey', legacyVersion);
671
- walletData = await decryptData(ciphertext, legacyKey, legacyIv);
672
-
673
- const newKey = await deriveEncryptionKey(keyMaterial, 'passkey');
674
- const { ciphertext: newCt, iv: newIv } = await encryptData(walletData, newKey, aad);
675
- localStorage.setItem(ENCRYPTED_DATA_KEY, JSON.stringify({
676
- ciphertext: arrayBufferToBase64(newCt),
677
- iv: arrayBufferToBase64(newIv)
678
- }));
679
- localStorage.setItem(METADATA_KEY, JSON.stringify({
680
- ...metadata,
681
- version: STORAGE_VERSION,
682
- hasPRF: true
683
- }));
684
- }
685
- return walletData;
686
- } catch (e) {
687
- throw new Error('Passkey authentication failed or data corrupted');
688
- }
689
- }
690
-
691
- /**
692
- * Clear all stored wallet data
693
- */
694
- export function clearStorage() {
695
- localStorage.removeItem(METADATA_KEY);
696
- localStorage.removeItem(ENCRYPTED_DATA_KEY);
697
- localStorage.removeItem(PASSKEY_CREDENTIAL_KEY);
698
- }
699
-
700
- /**
701
- * Migrate from old storage format (v1) to new format (v2)
702
- * Call this on app initialization
703
- */
704
- export function migrateStorage() {
705
- // Check for old v1 keys
706
- const oldPinWallet = localStorage.getItem('encrypted_wallet');
707
- const oldPasskeyCredential = localStorage.getItem('passkey_credential');
708
- const oldPasskeyWallet = localStorage.getItem('passkey_wallet');
709
-
710
- // Already migrated or no old data
711
- if (getStorageMetadata() !== null) return;
712
- if (!oldPinWallet && !oldPasskeyCredential) return;
713
-
714
- console.log('Migrating wallet storage from legacy format...');
715
-
716
- if (oldPasskeyCredential && oldPasskeyWallet) {
717
- // Migrate passkey storage
718
- try {
719
- const credential = JSON.parse(oldPasskeyCredential);
720
- const wallet = JSON.parse(oldPasskeyWallet);
721
-
722
- localStorage.setItem(PASSKEY_CREDENTIAL_KEY, JSON.stringify({
723
- id: credential.id,
724
- hasPRF: credential.hasPRF || false
725
- }));
726
-
727
- localStorage.setItem(ENCRYPTED_DATA_KEY, JSON.stringify({
728
- ciphertext: wallet.ciphertext
729
- }));
730
-
731
- localStorage.setItem(METADATA_KEY, JSON.stringify({
732
- method: StorageMethod.PASSKEY,
733
- timestamp: credential.timestamp || wallet.timestamp || Date.now(),
734
- // Preserve legacy version so decrypt can derive the correct legacy HKDF salt/IV,
735
- // then upgrade in-place on first successful retrieval.
736
- version: credential.version || wallet.version || 2,
737
- hasPRF: credential.hasPRF || false
738
- }));
739
-
740
- // Clean up old keys
741
- localStorage.removeItem('passkey_credential');
742
- localStorage.removeItem('passkey_wallet');
743
-
744
- console.log('Passkey storage migrated successfully');
745
- } catch (e) {
746
- console.error('Failed to migrate passkey storage:', e);
747
- }
748
- } else if (oldPinWallet) {
749
- // Migrate PIN storage - can't fully migrate since we need the salt
750
- // User will need to re-enter their wallet
751
- console.log('PIN storage detected but cannot be migrated - user will need to re-login');
752
- localStorage.removeItem('encrypted_wallet');
753
- }
754
- }
755
-
756
- // =============================================================================
757
- // Export default object for convenience
758
- // =============================================================================
759
-
760
- export default {
761
- StorageMethod,
762
- isPasskeySupported,
763
- isPRFLikelySupported,
764
- getStorageMetadata,
765
- hasStoredWallet,
766
- getStorageMethod,
767
- storeWithPIN,
768
- retrieveWithPIN,
769
- storeWithPasskey,
770
- retrieveWithPasskey,
771
- clearStorage,
772
- migrateStorage
773
- };