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.
- package/LICENSE +190 -0
- package/README.md +101 -123
- package/data/common-passwords-sdn-v1.source.json +12 -0
- package/data/common-passwords-sdn-v1.txt +10000 -0
- package/dist/browser/sdn-wallet-callback.js +1 -0
- package/dist/browser/sdn-wallet-public-client.js +1 -0
- package/dist/browser/wallet-callback.html +6 -0
- package/dist/client/asset-review.d.ts +8 -0
- package/dist/client/asset-review.js +1570 -0
- package/dist/client/callback.d.ts +13 -0
- package/dist/client/callback.js +29 -0
- package/dist/client/index.d.ts +11 -0
- package/dist/client/index.js +1379 -0
- package/dist/client/sdn.d.ts +8 -0
- package/dist/client/sdn.js +1542 -0
- package/dist/client/style.css +32 -0
- package/dist/client/types.d.ts +278 -0
- package/dist/compat/index.d.ts +26 -0
- package/dist/compat/index.js +62 -0
- package/dist/wallet-origin/index.d.ts +21 -0
- package/dist/wallet-origin/index.js +4607 -0
- package/dist/wallet-origin-host/assets/wallet-origin.3d16729bf9aaca4517cc699bec526c7209be184cdbb0ba0dbf4063d36a091e1d.js +12463 -0
- package/dist/wallet-origin-host/assets/wallet-origin.e22071a3643a57055d29f2e6951c1a2f15e77c1b58b0f81bb7f2afed5bcfb0b4.wasm +0 -0
- package/dist/wallet-origin-host/assets/wallet-origin.f3273f7ea70bb494cda1fc02264b936f2e9b010dc07df15eed05fa3f7d1356de.css +1 -0
- package/dist/wallet-origin-host/index.html +10 -0
- package/dist/wallet-origin-host/integrity.json +1 -0
- package/package.json +46 -18
- package/src/address-derivation.js +0 -514
- package/src/app.js +0 -6008
- package/src/blockchain-trust.js +0 -752
- package/src/clipboard.js +0 -44
- package/src/constants.js +0 -66
- package/src/hash.js +0 -9
- package/src/lib.js +0 -66
- package/src/template.js +0 -626
- package/src/trust-ui.js +0 -745
- package/src/wallet-storage.js +0 -773
- package/styles/main.css +0 -6220
- package/styles/widget.css +0 -6212
package/src/wallet-storage.js
DELETED
|
@@ -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
|
-
};
|