hd-wallet-ui 1.2.5 → 1.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -1
- package/package.json +10 -5
- package/src/app.js +657 -181
- package/src/template.js +25 -1
- package/src/wallet-storage.js +153 -36
- package/styles/main.css +73 -0
- package/styles/widget.css +6047 -0
package/src/app.js
CHANGED
|
@@ -132,7 +132,6 @@ function bindInfoHandlers() {
|
|
|
132
132
|
|
|
133
133
|
function setTruncatedValue(el, value) {
|
|
134
134
|
if (!el) return;
|
|
135
|
-
el.dataset.fullValue = value;
|
|
136
135
|
el.textContent = middleTruncate(value, 17, 17);
|
|
137
136
|
}
|
|
138
137
|
|
|
@@ -145,6 +144,23 @@ function toBase64(arr) {
|
|
|
145
144
|
return btoa(String.fromCharCode(...arr));
|
|
146
145
|
}
|
|
147
146
|
|
|
147
|
+
function base64ToBytes(b64) {
|
|
148
|
+
const binary = atob(b64);
|
|
149
|
+
const bytes = new Uint8Array(binary.length);
|
|
150
|
+
for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
|
|
151
|
+
return bytes;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function bytesToBase64(bytes) {
|
|
155
|
+
// Avoid spreading large arrays into String.fromCharCode.
|
|
156
|
+
let binary = '';
|
|
157
|
+
const chunk = 0x8000;
|
|
158
|
+
for (let i = 0; i < bytes.length; i += chunk) {
|
|
159
|
+
binary += String.fromCharCode(...bytes.subarray(i, i + chunk));
|
|
160
|
+
}
|
|
161
|
+
return btoa(binary);
|
|
162
|
+
}
|
|
163
|
+
|
|
148
164
|
// =============================================================================
|
|
149
165
|
// SHA-256 and HKDF (WebCrypto-based)
|
|
150
166
|
// =============================================================================
|
|
@@ -164,6 +180,26 @@ async function hkdf(ikm, salt, info, length) {
|
|
|
164
180
|
return new Uint8Array(derived);
|
|
165
181
|
}
|
|
166
182
|
|
|
183
|
+
async function aesGcmEncryptJson(keyBytes, obj, aadStr) {
|
|
184
|
+
if (!(keyBytes instanceof Uint8Array)) throw new Error('Invalid AES key');
|
|
185
|
+
const iv = crypto.getRandomValues(new Uint8Array(12));
|
|
186
|
+
const cryptoKey = await crypto.subtle.importKey('raw', keyBytes, { name: 'AES-GCM' }, false, ['encrypt']);
|
|
187
|
+
const alg = { name: 'AES-GCM', iv };
|
|
188
|
+
if (aadStr) alg.additionalData = new TextEncoder().encode(aadStr);
|
|
189
|
+
const plaintext = new TextEncoder().encode(JSON.stringify(obj));
|
|
190
|
+
const ciphertext = await crypto.subtle.encrypt(alg, cryptoKey, plaintext);
|
|
191
|
+
return { iv, ciphertext: new Uint8Array(ciphertext) };
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
async function aesGcmDecryptJson(keyBytes, iv, ciphertextBytes, aadStr) {
|
|
195
|
+
if (!(keyBytes instanceof Uint8Array)) throw new Error('Invalid AES key');
|
|
196
|
+
const cryptoKey = await crypto.subtle.importKey('raw', keyBytes, { name: 'AES-GCM' }, false, ['decrypt']);
|
|
197
|
+
const alg = { name: 'AES-GCM', iv };
|
|
198
|
+
if (aadStr) alg.additionalData = new TextEncoder().encode(aadStr);
|
|
199
|
+
const plaintext = await crypto.subtle.decrypt(alg, cryptoKey, ciphertextBytes);
|
|
200
|
+
return JSON.parse(new TextDecoder().decode(plaintext));
|
|
201
|
+
}
|
|
202
|
+
|
|
167
203
|
// =============================================================================
|
|
168
204
|
// Key Generation
|
|
169
205
|
// =============================================================================
|
|
@@ -207,6 +243,10 @@ async function p384GenerateKeyPairAsync() {
|
|
|
207
243
|
// Integration callback — set via options.onLogin in createWalletUI / init
|
|
208
244
|
let _onLoginCallback = null;
|
|
209
245
|
|
|
246
|
+
// When false, login() will NOT auto-open the Account modal after authentication.
|
|
247
|
+
// Set via options.openAccountAfterLogin in createWalletUI / init (default: true).
|
|
248
|
+
let _openAccountAfterLogin = true;
|
|
249
|
+
|
|
210
250
|
const state = {
|
|
211
251
|
initialized: false,
|
|
212
252
|
loggedIn: false,
|
|
@@ -328,49 +368,52 @@ async function deriveKeysFromPassword(username, password) {
|
|
|
328
368
|
const initialHash = await sha256(new Uint8Array([...usernameSalt, ...passwordBytes]));
|
|
329
369
|
const masterKey = await hkdf(initialHash, usernameSalt, encoder.encode('master-key'), 32);
|
|
330
370
|
|
|
331
|
-
state.encryptionKey = await hkdf(masterKey, new Uint8Array(0), encoder.encode('buffer-encryption-key'), 32);
|
|
332
|
-
state.encryptionIV = await hkdf(masterKey, new Uint8Array(0), encoder.encode('buffer-encryption-iv'), 16);
|
|
333
|
-
|
|
334
371
|
// Create 64-byte seed for HD wallet (password-based, not BIP39)
|
|
335
372
|
const hdSeed = await hkdf(masterKey, new Uint8Array(0), encoder.encode('hd-wallet-seed'), 64);
|
|
336
|
-
state.masterSeed = hdSeed;
|
|
337
|
-
state.hdRoot = state.hdWalletModule.hdkey.fromSeed(hdSeed);
|
|
338
|
-
state.mnemonic = null; // Not available for password-derived wallets
|
|
339
|
-
console.log('HD wallet initialized from password, hdRoot:', !!state.hdRoot);
|
|
340
|
-
|
|
341
|
-
const keys = deriveKeysFromHDRoot(state.hdRoot);
|
|
342
|
-
// Also derive auxiliary keys for encryption / key agreement
|
|
343
|
-
keys.x25519 = generateKeyPair(Curve.X25519);
|
|
344
|
-
keys.p256 = await p256GenerateKeyPairAsync();
|
|
345
|
-
keys.p384 = await p384GenerateKeyPairAsync();
|
|
346
373
|
|
|
347
|
-
|
|
374
|
+
try {
|
|
375
|
+
// Derive session keys from the master seed so "remember wallet" can unlock without
|
|
376
|
+
// storing the user password/seed phrase at rest.
|
|
377
|
+
return await deriveKeysFromMasterSeed(hdSeed);
|
|
378
|
+
} finally {
|
|
379
|
+
// Best-effort JS-layer cleanup (strings cannot be wiped).
|
|
380
|
+
passwordBytes.fill(0);
|
|
381
|
+
initialHash.fill(0);
|
|
382
|
+
masterKey.fill(0);
|
|
383
|
+
hdSeed.fill(0);
|
|
384
|
+
}
|
|
348
385
|
}
|
|
349
386
|
|
|
350
387
|
async function deriveKeysFromSeed(seedPhrase) {
|
|
351
|
-
const seed = state.hdWalletModule.mnemonic.toSeed(seedPhrase);
|
|
352
388
|
const encoder = new TextEncoder();
|
|
389
|
+
const seed = state.hdWalletModule.mnemonic.toSeed(seedPhrase);
|
|
390
|
+
const seedBytes = seed instanceof Uint8Array ? seed : new Uint8Array(seed);
|
|
353
391
|
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
392
|
+
try {
|
|
393
|
+
return await deriveKeysFromMasterSeed(seedBytes);
|
|
394
|
+
} finally {
|
|
395
|
+
// Don't retain the seed phrase in JS state.
|
|
396
|
+
// (Seed phrase strings can't be wiped; we just avoid storing them.)
|
|
397
|
+
seedBytes.fill(0);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
360
400
|
|
|
361
|
-
|
|
362
|
-
|
|
401
|
+
async function deriveKeysFromMasterSeed(masterSeedBytes) {
|
|
402
|
+
const encoder = new TextEncoder();
|
|
403
|
+
|
|
404
|
+
// Copy seed into state; callers can wipe their input buffer.
|
|
405
|
+
state.masterSeed = new Uint8Array(masterSeedBytes);
|
|
406
|
+
state.hdRoot = state.hdWalletModule.hdkey.fromSeed(state.masterSeed);
|
|
407
|
+
state.mnemonic = null;
|
|
363
408
|
|
|
364
|
-
|
|
365
|
-
state.
|
|
366
|
-
state.
|
|
367
|
-
console.log('HD wallet initialized from seed phrase, hdRoot:', !!state.hdRoot);
|
|
409
|
+
// Session encryption key for local encrypted blobs (PKI, etc).
|
|
410
|
+
state.encryptionKey = await hkdf(state.masterSeed, new Uint8Array(0), encoder.encode('buffer-encryption-key'), 32);
|
|
411
|
+
state.encryptionIV = await hkdf(state.masterSeed, new Uint8Array(0), encoder.encode('buffer-encryption-iv'), 16);
|
|
368
412
|
|
|
369
413
|
const keys = deriveKeysFromHDRoot(state.hdRoot);
|
|
370
414
|
keys.x25519 = generateKeyPair(Curve.X25519);
|
|
371
415
|
keys.p256 = await p256GenerateKeyPairAsync();
|
|
372
416
|
keys.p384 = await p384GenerateKeyPairAsync();
|
|
373
|
-
|
|
374
417
|
return keys;
|
|
375
418
|
}
|
|
376
419
|
|
|
@@ -2191,7 +2234,14 @@ function savePKIKeys() {
|
|
|
2191
2234
|
return;
|
|
2192
2235
|
}
|
|
2193
2236
|
|
|
2194
|
-
|
|
2237
|
+
// SECURITY: Never persist private keys in plaintext localStorage.
|
|
2238
|
+
// Persist encrypted only when a session encryption key exists (i.e., after wallet login).
|
|
2239
|
+
if (!(state.encryptionKey instanceof Uint8Array) || state.encryptionKey.length < 16) {
|
|
2240
|
+
console.warn('Skipping PKI key persistence: session encryption key not available (login required)');
|
|
2241
|
+
return;
|
|
2242
|
+
}
|
|
2243
|
+
|
|
2244
|
+
const plaintext = {
|
|
2195
2245
|
algorithm: state.pki.algorithm,
|
|
2196
2246
|
alice: {
|
|
2197
2247
|
publicKey: toHexCompact(state.pki.alice.publicKey),
|
|
@@ -2204,44 +2254,77 @@ function savePKIKeys() {
|
|
|
2204
2254
|
savedAt: new Date().toISOString(),
|
|
2205
2255
|
};
|
|
2206
2256
|
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2257
|
+
aesGcmEncryptJson(state.encryptionKey, plaintext, 'wallet-ui|pki-keys')
|
|
2258
|
+
.then(({ iv, ciphertext }) => {
|
|
2259
|
+
const stored = {
|
|
2260
|
+
v: 1,
|
|
2261
|
+
iv: bytesToBase64(iv),
|
|
2262
|
+
ciphertext: bytesToBase64(ciphertext),
|
|
2263
|
+
};
|
|
2264
|
+
localStorage.setItem(PKI_STORAGE_KEY, JSON.stringify(stored));
|
|
2265
|
+
})
|
|
2266
|
+
.catch((e) => {
|
|
2267
|
+
console.warn('Failed to encrypt+save PKI keys to localStorage:', e);
|
|
2268
|
+
});
|
|
2217
2269
|
}
|
|
2218
2270
|
|
|
2219
|
-
function loadPKIKeys() {
|
|
2271
|
+
async function loadPKIKeys() {
|
|
2220
2272
|
try {
|
|
2221
2273
|
const stored = localStorage.getItem(PKI_STORAGE_KEY);
|
|
2222
2274
|
if (!stored) return false;
|
|
2223
2275
|
|
|
2224
2276
|
const data = JSON.parse(stored);
|
|
2225
|
-
|
|
2277
|
+
const hasEncryptedShape = data && typeof data === 'object' && typeof data.iv === 'string' && typeof data.ciphertext === 'string';
|
|
2278
|
+
|
|
2279
|
+
// Legacy plaintext format (insecure): refuse to load until logged in, then upgrade.
|
|
2280
|
+
const hasLegacyPlaintextShape = data?.alice?.privateKey && data?.bob?.privateKey && data?.algorithm;
|
|
2281
|
+
|
|
2282
|
+
if (!hasEncryptedShape && !hasLegacyPlaintextShape) {
|
|
2226
2283
|
console.warn('Invalid PKI data in localStorage');
|
|
2227
2284
|
return false;
|
|
2228
2285
|
}
|
|
2229
2286
|
|
|
2230
|
-
state.
|
|
2287
|
+
if (!(state.encryptionKey instanceof Uint8Array) || state.encryptionKey.length < 16) {
|
|
2288
|
+
// Not logged in yet; don't load private keys.
|
|
2289
|
+
return false;
|
|
2290
|
+
}
|
|
2291
|
+
|
|
2292
|
+
let plaintext;
|
|
2293
|
+
if (hasEncryptedShape) {
|
|
2294
|
+
const iv = base64ToBytes(data.iv);
|
|
2295
|
+
const ciphertext = base64ToBytes(data.ciphertext);
|
|
2296
|
+
plaintext = await aesGcmDecryptJson(state.encryptionKey, iv, ciphertext, 'wallet-ui|pki-keys');
|
|
2297
|
+
} else {
|
|
2298
|
+
// Legacy plaintext: load and immediately re-encrypt on next save.
|
|
2299
|
+
plaintext = data;
|
|
2300
|
+
// Upgrade-in-place.
|
|
2301
|
+
try {
|
|
2302
|
+
const { iv, ciphertext } = await aesGcmEncryptJson(state.encryptionKey, plaintext, 'wallet-ui|pki-keys');
|
|
2303
|
+
localStorage.setItem(PKI_STORAGE_KEY, JSON.stringify({
|
|
2304
|
+
v: 1,
|
|
2305
|
+
iv: bytesToBase64(iv),
|
|
2306
|
+
ciphertext: bytesToBase64(ciphertext),
|
|
2307
|
+
}));
|
|
2308
|
+
} catch (e) {
|
|
2309
|
+
console.warn('Failed to upgrade legacy plaintext PKI storage:', e);
|
|
2310
|
+
}
|
|
2311
|
+
}
|
|
2312
|
+
|
|
2313
|
+
if (!plaintext?.alice || !plaintext?.bob || !plaintext?.algorithm) {
|
|
2314
|
+
console.warn('Invalid decrypted PKI data');
|
|
2315
|
+
return false;
|
|
2316
|
+
}
|
|
2317
|
+
|
|
2318
|
+
state.pki.algorithm = plaintext.algorithm;
|
|
2231
2319
|
state.pki.alice = {
|
|
2232
|
-
publicKey: hexToBytes(
|
|
2233
|
-
privateKey: hexToBytes(
|
|
2320
|
+
publicKey: hexToBytes(plaintext.alice.publicKey),
|
|
2321
|
+
privateKey: hexToBytes(plaintext.alice.privateKey),
|
|
2234
2322
|
};
|
|
2235
2323
|
state.pki.bob = {
|
|
2236
|
-
publicKey: hexToBytes(
|
|
2237
|
-
privateKey: hexToBytes(
|
|
2324
|
+
publicKey: hexToBytes(plaintext.bob.publicKey),
|
|
2325
|
+
privateKey: hexToBytes(plaintext.bob.privateKey),
|
|
2238
2326
|
};
|
|
2239
2327
|
|
|
2240
|
-
if (data.encryptionKey && data.encryptionIV) {
|
|
2241
|
-
state.encryptionKey = hexToBytes(data.encryptionKey);
|
|
2242
|
-
state.encryptionIV = hexToBytes(data.encryptionIV);
|
|
2243
|
-
}
|
|
2244
|
-
|
|
2245
2328
|
// Update UI
|
|
2246
2329
|
const alicePublicKey = $('alice-public-key');
|
|
2247
2330
|
const alicePrivateKey = $('alice-private-key');
|
|
@@ -2253,11 +2336,11 @@ function loadPKIKeys() {
|
|
|
2253
2336
|
const pkiClearKeys = $('pki-clear-keys');
|
|
2254
2337
|
|
|
2255
2338
|
const pkiAlgorithm = $('pki-algorithm');
|
|
2256
|
-
if (pkiAlgorithm) pkiAlgorithm.value =
|
|
2257
|
-
if (alicePublicKey) alicePublicKey.textContent =
|
|
2258
|
-
if (alicePrivateKey) alicePrivateKey.textContent =
|
|
2259
|
-
if (bobPublicKey) bobPublicKey.textContent =
|
|
2260
|
-
if (bobPrivateKey) bobPrivateKey.textContent =
|
|
2339
|
+
if (pkiAlgorithm) pkiAlgorithm.value = plaintext.algorithm;
|
|
2340
|
+
if (alicePublicKey) alicePublicKey.textContent = plaintext.alice.publicKey;
|
|
2341
|
+
if (alicePrivateKey) alicePrivateKey.textContent = plaintext.alice.privateKey;
|
|
2342
|
+
if (bobPublicKey) bobPublicKey.textContent = plaintext.bob.publicKey;
|
|
2343
|
+
if (bobPrivateKey) bobPrivateKey.textContent = plaintext.bob.privateKey;
|
|
2261
2344
|
if (pkiParties) pkiParties.style.display = 'grid';
|
|
2262
2345
|
if (pkiDemo) pkiDemo.style.display = 'block';
|
|
2263
2346
|
if (pkiSecurity) pkiSecurity.style.display = 'block';
|
|
@@ -2277,6 +2360,13 @@ function clearPKIKeys() {
|
|
|
2277
2360
|
console.warn('Failed to clear PKI keys:', e);
|
|
2278
2361
|
}
|
|
2279
2362
|
|
|
2363
|
+
try {
|
|
2364
|
+
if (state.pki?.alice?.privateKey instanceof Uint8Array) state.pki.alice.privateKey.fill(0);
|
|
2365
|
+
if (state.pki?.bob?.privateKey instanceof Uint8Array) state.pki.bob.privateKey.fill(0);
|
|
2366
|
+
} catch {
|
|
2367
|
+
// ignore
|
|
2368
|
+
}
|
|
2369
|
+
|
|
2280
2370
|
state.pki.alice = null;
|
|
2281
2371
|
state.pki.bob = null;
|
|
2282
2372
|
state.pki.algorithm = 'x25519';
|
|
@@ -2378,11 +2468,14 @@ function login(keys) {
|
|
|
2378
2468
|
state.addresses = deriveAllAddressesFromHD();
|
|
2379
2469
|
state.selectedCrypto = 'btc';
|
|
2380
2470
|
|
|
2381
|
-
// Fire onLogin callback with SDN identity (coin type
|
|
2471
|
+
// Fire onLogin callback with SDN identity (BIP-44 Bitcoin coin type 0)
|
|
2382
2472
|
if (_onLoginCallback && state.hdRoot) {
|
|
2383
2473
|
try {
|
|
2384
|
-
const sdnSigning = getSigningKey(state.hdRoot,
|
|
2385
|
-
const
|
|
2474
|
+
const sdnSigning = getSigningKey(state.hdRoot, 0, 0, 0);
|
|
2475
|
+
const sdnPrivKey = sdnSigning.privateKey;
|
|
2476
|
+
const sdnPubKey = ed25519.getPublicKey(sdnPrivKey);
|
|
2477
|
+
// Don't keep derived private key bytes around longer than needed.
|
|
2478
|
+
if (sdnPrivKey instanceof Uint8Array) sdnPrivKey.fill(0);
|
|
2386
2479
|
const xpub = state.hdRoot.toXpub();
|
|
2387
2480
|
_onLoginCallback({
|
|
2388
2481
|
xpub,
|
|
@@ -2391,7 +2484,12 @@ function login(keys) {
|
|
|
2391
2484
|
const msgBytes = typeof message === 'string'
|
|
2392
2485
|
? new TextEncoder().encode(message)
|
|
2393
2486
|
: message;
|
|
2394
|
-
|
|
2487
|
+
const signing = getSigningKey(state.hdRoot, 0, 0, 0);
|
|
2488
|
+
try {
|
|
2489
|
+
return ed25519.sign(msgBytes, signing.privateKey);
|
|
2490
|
+
} finally {
|
|
2491
|
+
if (signing?.privateKey instanceof Uint8Array) signing.privateKey.fill(0);
|
|
2492
|
+
}
|
|
2395
2493
|
},
|
|
2396
2494
|
});
|
|
2397
2495
|
} catch (err) {
|
|
@@ -2440,14 +2538,12 @@ function login(keys) {
|
|
|
2440
2538
|
}
|
|
2441
2539
|
populateAccountAddressDropdown();
|
|
2442
2540
|
if (xprvEl) {
|
|
2443
|
-
|
|
2541
|
+
xprvEl.textContent = 'Hidden (click reveal)';
|
|
2444
2542
|
xprvEl.dataset.revealed = 'false';
|
|
2445
2543
|
}
|
|
2446
|
-
if (seedEl
|
|
2447
|
-
seedEl.textContent =
|
|
2544
|
+
if (seedEl) {
|
|
2545
|
+
seedEl.textContent = 'Not retained by the app';
|
|
2448
2546
|
seedEl.dataset.revealed = 'false';
|
|
2449
|
-
} else if (seedEl) {
|
|
2450
|
-
seedEl.textContent = 'Not available (derived from password)';
|
|
2451
2547
|
}
|
|
2452
2548
|
|
|
2453
2549
|
// Load persisted wallets and active accounts
|
|
@@ -2500,15 +2596,23 @@ function login(keys) {
|
|
|
2500
2596
|
if (pkiSecurity) pkiSecurity.style.display = 'block';
|
|
2501
2597
|
const pkiClearKeys = $('pki-clear-keys');
|
|
2502
2598
|
if (pkiClearKeys) pkiClearKeys.style.display = 'inline-flex';
|
|
2503
|
-
} else
|
|
2504
|
-
|
|
2599
|
+
} else {
|
|
2600
|
+
// PKI persistence is encrypted and requires the session key (available only after login).
|
|
2601
|
+
// Kick off an async load attempt; if it fails, generate fresh keys.
|
|
2602
|
+
loadPKIKeys().then((ok) => {
|
|
2603
|
+
if (!ok) generatePKIKeyPairs();
|
|
2604
|
+
}).catch(() => {
|
|
2605
|
+
generatePKIKeyPairs();
|
|
2606
|
+
});
|
|
2505
2607
|
}
|
|
2506
2608
|
|
|
2507
2609
|
// Update wallet addresses and balances
|
|
2508
2610
|
updateAdversarialSecurity();
|
|
2509
2611
|
|
|
2510
2612
|
// Open Account modal so user can see the wallet they just loaded
|
|
2511
|
-
|
|
2613
|
+
if (_openAccountAfterLogin) {
|
|
2614
|
+
$('keys-modal')?.classList.add('active');
|
|
2615
|
+
}
|
|
2512
2616
|
|
|
2513
2617
|
// Resolve names and update title
|
|
2514
2618
|
clearNameCache();
|
|
@@ -2526,6 +2630,26 @@ function logout() {
|
|
|
2526
2630
|
const titleEl = $('account-title');
|
|
2527
2631
|
if (titleEl) titleEl.textContent = 'Account';
|
|
2528
2632
|
state.loggedIn = false;
|
|
2633
|
+
|
|
2634
|
+
// Best-effort wipe of JS buffers (strings are not wipeable).
|
|
2635
|
+
const wipe = (u8) => {
|
|
2636
|
+
if (u8 instanceof Uint8Array) u8.fill(0);
|
|
2637
|
+
};
|
|
2638
|
+
try {
|
|
2639
|
+
wipe(state.wallet?.x25519?.privateKey);
|
|
2640
|
+
wipe(state.wallet?.ed25519?.privateKey);
|
|
2641
|
+
wipe(state.wallet?.secp256k1?.privateKey);
|
|
2642
|
+
wipe(state.wallet?.p256?.privateKey);
|
|
2643
|
+
wipe(state.encryptionKey);
|
|
2644
|
+
wipe(state.encryptionIV);
|
|
2645
|
+
wipe(state.masterSeed);
|
|
2646
|
+
wipe(state.pki?.alice?.privateKey);
|
|
2647
|
+
wipe(state.pki?.bob?.privateKey);
|
|
2648
|
+
state.hdRoot?.wipe?.();
|
|
2649
|
+
} catch {
|
|
2650
|
+
// ignore
|
|
2651
|
+
}
|
|
2652
|
+
|
|
2529
2653
|
state.wallet = { x25519: null, ed25519: null, secp256k1: null, p256: null };
|
|
2530
2654
|
state.encryptionKey = null;
|
|
2531
2655
|
state.encryptionIV = null;
|
|
@@ -2586,7 +2710,7 @@ async function exportWallet(format) {
|
|
|
2586
2710
|
switch (format) {
|
|
2587
2711
|
case 'mnemonic':
|
|
2588
2712
|
if (!state.mnemonic) {
|
|
2589
|
-
alert('Seed phrase not available.
|
|
2713
|
+
alert('Seed phrase not available. For security, the app does not retain the mnemonic after login.');
|
|
2590
2714
|
return;
|
|
2591
2715
|
}
|
|
2592
2716
|
data = state.mnemonic;
|
|
@@ -3643,6 +3767,19 @@ function setupLoginHandlers() {
|
|
|
3643
3767
|
updatePasswordStrength(e.target.value);
|
|
3644
3768
|
});
|
|
3645
3769
|
|
|
3770
|
+
// Password show/hide toggle
|
|
3771
|
+
$('toggle-password-vis')?.addEventListener('click', () => {
|
|
3772
|
+
const pw = $('wallet-password');
|
|
3773
|
+
const btn = $('toggle-password-vis');
|
|
3774
|
+
if (!pw || !btn) return;
|
|
3775
|
+
const showing = pw.type === 'text';
|
|
3776
|
+
pw.type = showing ? 'password' : 'text';
|
|
3777
|
+
btn.querySelector('.eye-open').style.display = showing ? '' : 'none';
|
|
3778
|
+
btn.querySelector('.eye-closed').style.display = showing ? 'none' : '';
|
|
3779
|
+
btn.title = showing ? 'Show password' : 'Hide password';
|
|
3780
|
+
pw.focus();
|
|
3781
|
+
});
|
|
3782
|
+
|
|
3646
3783
|
$('wallet-username')?.addEventListener('input', () => {
|
|
3647
3784
|
const pw = $('wallet-password');
|
|
3648
3785
|
if (pw) updatePasswordStrength(pw.value);
|
|
@@ -3656,9 +3793,7 @@ function setupLoginHandlers() {
|
|
|
3656
3793
|
const usePasskey = rememberMethod.password === 'passkey';
|
|
3657
3794
|
const pin = $('pin-input-password')?.value;
|
|
3658
3795
|
|
|
3659
|
-
console.log('Login clicked, username:', username, 'password length:', password?.length);
|
|
3660
3796
|
if (!username || !password || password.length < 24) {
|
|
3661
|
-
console.log('Login validation failed');
|
|
3662
3797
|
return;
|
|
3663
3798
|
}
|
|
3664
3799
|
|
|
@@ -3672,16 +3807,18 @@ function setupLoginHandlers() {
|
|
|
3672
3807
|
btn.textContent = 'Logging in...';
|
|
3673
3808
|
|
|
3674
3809
|
try {
|
|
3675
|
-
console.log('Calling deriveKeysFromPassword...');
|
|
3676
3810
|
const keys = await deriveKeysFromPassword(username, password);
|
|
3677
|
-
|
|
3811
|
+
|
|
3812
|
+
// Best-effort: don't keep the password in the input field after login.
|
|
3813
|
+
const pwEl = $('wallet-password');
|
|
3814
|
+
if (pwEl) pwEl.value = '';
|
|
3678
3815
|
|
|
3679
3816
|
if (rememberWallet) {
|
|
3680
3817
|
const walletData = {
|
|
3681
|
-
type: '
|
|
3818
|
+
type: 'masterSeed',
|
|
3819
|
+
source: 'password',
|
|
3682
3820
|
username,
|
|
3683
|
-
|
|
3684
|
-
masterSeed: Array.from(state.masterSeed)
|
|
3821
|
+
masterSeed: Array.from(state.masterSeed),
|
|
3685
3822
|
};
|
|
3686
3823
|
|
|
3687
3824
|
if (usePasskey) {
|
|
@@ -3710,7 +3847,6 @@ function setupLoginHandlers() {
|
|
|
3710
3847
|
}
|
|
3711
3848
|
|
|
3712
3849
|
login(keys);
|
|
3713
|
-
console.log('Login complete, hdRoot:', !!state.hdRoot);
|
|
3714
3850
|
} catch (err) {
|
|
3715
3851
|
console.error('Login error:', err);
|
|
3716
3852
|
alert('Error: ' + err.message);
|
|
@@ -3775,11 +3911,15 @@ function setupLoginHandlers() {
|
|
|
3775
3911
|
try {
|
|
3776
3912
|
const keys = await deriveKeysFromSeed(phrase);
|
|
3777
3913
|
|
|
3914
|
+
// Best-effort: don't keep the mnemonic in the textarea after login.
|
|
3915
|
+
const seedEl = $('seed-phrase');
|
|
3916
|
+
if (seedEl) seedEl.value = '';
|
|
3917
|
+
|
|
3778
3918
|
if (rememberWallet) {
|
|
3779
3919
|
const walletData = {
|
|
3780
|
-
type: '
|
|
3781
|
-
|
|
3782
|
-
masterSeed: Array.from(state.masterSeed)
|
|
3920
|
+
type: 'masterSeed',
|
|
3921
|
+
source: 'seed',
|
|
3922
|
+
masterSeed: Array.from(state.masterSeed),
|
|
3783
3923
|
};
|
|
3784
3924
|
|
|
3785
3925
|
if (usePasskey) {
|
|
@@ -3832,12 +3972,27 @@ function setupLoginHandlers() {
|
|
|
3832
3972
|
const walletData = await WalletStorage.retrieveWithPIN(pin);
|
|
3833
3973
|
|
|
3834
3974
|
let keys;
|
|
3835
|
-
|
|
3975
|
+
const storedSeed = walletData.masterSeed || walletData.seed || walletData.hdSeed;
|
|
3976
|
+
if (storedSeed) {
|
|
3977
|
+
keys = await deriveKeysFromMasterSeed(new Uint8Array(storedSeed));
|
|
3978
|
+
} else if (walletData.type === 'password') {
|
|
3979
|
+
// Legacy format: stored password/seedPhrase (deprecated). Unlock, then upgrade storage.
|
|
3836
3980
|
keys = await deriveKeysFromPassword(walletData.username, walletData.password);
|
|
3981
|
+
await WalletStorage.storeWithPIN(pin, {
|
|
3982
|
+
type: 'masterSeed',
|
|
3983
|
+
source: 'password',
|
|
3984
|
+
username: walletData.username,
|
|
3985
|
+
masterSeed: Array.from(state.masterSeed),
|
|
3986
|
+
});
|
|
3837
3987
|
} else if (walletData.type === 'seed') {
|
|
3838
3988
|
keys = await deriveKeysFromSeed(walletData.seedPhrase);
|
|
3989
|
+
await WalletStorage.storeWithPIN(pin, {
|
|
3990
|
+
type: 'masterSeed',
|
|
3991
|
+
source: 'seed',
|
|
3992
|
+
masterSeed: Array.from(state.masterSeed),
|
|
3993
|
+
});
|
|
3839
3994
|
} else {
|
|
3840
|
-
throw new Error('Unknown wallet
|
|
3995
|
+
throw new Error('Unknown stored wallet format');
|
|
3841
3996
|
}
|
|
3842
3997
|
|
|
3843
3998
|
login(keys);
|
|
@@ -3861,12 +4016,34 @@ function setupLoginHandlers() {
|
|
|
3861
4016
|
const walletData = await WalletStorage.retrieveWithPasskey();
|
|
3862
4017
|
|
|
3863
4018
|
let keys;
|
|
3864
|
-
|
|
4019
|
+
const storedSeed = walletData.masterSeed || walletData.seed || walletData.hdSeed;
|
|
4020
|
+
if (storedSeed) {
|
|
4021
|
+
keys = await deriveKeysFromMasterSeed(new Uint8Array(storedSeed));
|
|
4022
|
+
} else if (walletData.type === 'password') {
|
|
3865
4023
|
keys = await deriveKeysFromPassword(walletData.username, walletData.password);
|
|
4024
|
+
await WalletStorage.storeWithPasskey({
|
|
4025
|
+
type: 'masterSeed',
|
|
4026
|
+
source: 'password',
|
|
4027
|
+
username: walletData.username,
|
|
4028
|
+
masterSeed: Array.from(state.masterSeed),
|
|
4029
|
+
}, {
|
|
4030
|
+
rpName: 'HD Wallet',
|
|
4031
|
+
userName: walletData.username || 'wallet-user',
|
|
4032
|
+
userDisplayName: walletData.username || 'Wallet User'
|
|
4033
|
+
});
|
|
3866
4034
|
} else if (walletData.type === 'seed') {
|
|
3867
4035
|
keys = await deriveKeysFromSeed(walletData.seedPhrase);
|
|
4036
|
+
await WalletStorage.storeWithPasskey({
|
|
4037
|
+
type: 'masterSeed',
|
|
4038
|
+
source: 'seed',
|
|
4039
|
+
masterSeed: Array.from(state.masterSeed),
|
|
4040
|
+
}, {
|
|
4041
|
+
rpName: 'HD Wallet',
|
|
4042
|
+
userName: 'seed-wallet',
|
|
4043
|
+
userDisplayName: 'Seed Phrase Wallet'
|
|
4044
|
+
});
|
|
3868
4045
|
} else {
|
|
3869
|
-
throw new Error('Unknown wallet
|
|
4046
|
+
throw new Error('Unknown stored wallet format');
|
|
3870
4047
|
}
|
|
3871
4048
|
|
|
3872
4049
|
login(keys);
|
|
@@ -4312,7 +4489,23 @@ function setupMainAppHandlers() {
|
|
|
4312
4489
|
const targetEl = $(targetId);
|
|
4313
4490
|
if (targetEl) {
|
|
4314
4491
|
const isRevealed = targetEl.dataset.revealed === 'true';
|
|
4315
|
-
|
|
4492
|
+
const nextRevealed = !isRevealed;
|
|
4493
|
+
targetEl.dataset.revealed = nextRevealed ? 'true' : 'false';
|
|
4494
|
+
|
|
4495
|
+
if (nextRevealed) {
|
|
4496
|
+
if (targetId === 'wallet-xprv') {
|
|
4497
|
+
targetEl.textContent = state.hdRoot?.toXprv?.() || 'N/A';
|
|
4498
|
+
} else if (targetId === 'wallet-seed-phrase') {
|
|
4499
|
+
targetEl.textContent = state.mnemonic || 'Not retained by the app';
|
|
4500
|
+
}
|
|
4501
|
+
} else {
|
|
4502
|
+
if (targetId === 'wallet-xprv') {
|
|
4503
|
+
targetEl.textContent = 'Hidden (click reveal)';
|
|
4504
|
+
} else if (targetId === 'wallet-seed-phrase') {
|
|
4505
|
+
targetEl.textContent = 'Not retained by the app';
|
|
4506
|
+
}
|
|
4507
|
+
}
|
|
4508
|
+
|
|
4316
4509
|
btn.innerHTML = isRevealed
|
|
4317
4510
|
? '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg>'
|
|
4318
4511
|
: '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24"/><line x1="1" y1="1" x2="23" y2="23"/></svg>';
|
|
@@ -4327,7 +4520,28 @@ function setupMainAppHandlers() {
|
|
|
4327
4520
|
const targetEl = $(targetId);
|
|
4328
4521
|
if (targetEl) {
|
|
4329
4522
|
try {
|
|
4330
|
-
|
|
4523
|
+
let value = '';
|
|
4524
|
+
if (targetId === 'wallet-xpub' || targetId === 'wallet-tab-xpub') {
|
|
4525
|
+
value = state.hdRoot?.toXpub?.() || '';
|
|
4526
|
+
} else if (targetId === 'wallet-xprv') {
|
|
4527
|
+
if (targetEl.dataset.revealed !== 'true') {
|
|
4528
|
+
alert('Reveal the xprv first to copy it.');
|
|
4529
|
+
return;
|
|
4530
|
+
}
|
|
4531
|
+
if (!confirm('Warning: copying your master private key (xprv) is extremely sensitive. Continue?')) {
|
|
4532
|
+
return;
|
|
4533
|
+
}
|
|
4534
|
+
value = state.hdRoot?.toXprv?.() || '';
|
|
4535
|
+
} else if (targetId === 'wallet-seed-phrase') {
|
|
4536
|
+
alert('Seed phrase not available. For security, the app does not retain the mnemonic after login.');
|
|
4537
|
+
return;
|
|
4538
|
+
} else {
|
|
4539
|
+
value = targetEl.textContent || '';
|
|
4540
|
+
}
|
|
4541
|
+
if (!value) {
|
|
4542
|
+
throw new Error('Nothing to copy');
|
|
4543
|
+
}
|
|
4544
|
+
await navigator.clipboard.writeText(value);
|
|
4331
4545
|
btn.classList.add('copied');
|
|
4332
4546
|
setTimeout(() => btn.classList.remove('copied'), 1500);
|
|
4333
4547
|
} catch (err) {
|
|
@@ -4943,26 +5157,250 @@ function setupTrustHandlers() {
|
|
|
4943
5157
|
// Encryption Tab Handlers (ECIES: ECDH + HKDF + AES-256-GCM)
|
|
4944
5158
|
// =========================================================================
|
|
4945
5159
|
|
|
5160
|
+
const MESSAGING_KEY_CONFIG_KEY = 'hd-wallet-messaging-key-config-v1';
|
|
5161
|
+
const messagingKeyDefaults = Object.freeze({
|
|
5162
|
+
btc: { path: "m/44'/0'/0'/1/0", algorithm: 'secp256k1', publicKeyFormat: 'compressed' },
|
|
5163
|
+
eth: { path: "m/44'/60'/0'/1/0", algorithm: 'secp256k1', publicKeyFormat: 'uncompressed' },
|
|
5164
|
+
sol: { path: "m/44'/501'/0'/1/0", algorithm: 'x25519', publicKeyFormat: 'raw' },
|
|
5165
|
+
});
|
|
5166
|
+
|
|
5167
|
+
const wipeBytes = (u8) => {
|
|
5168
|
+
if (u8 instanceof Uint8Array) u8.fill(0);
|
|
5169
|
+
};
|
|
5170
|
+
|
|
5171
|
+
function getMessagingKeyType() {
|
|
5172
|
+
const v = $('messaging-key-type')?.value;
|
|
5173
|
+
return v === 'eth' || v === 'sol' ? v : 'btc';
|
|
5174
|
+
}
|
|
5175
|
+
|
|
5176
|
+
function getMessagingDefaultPath(keyType = getMessagingKeyType()) {
|
|
5177
|
+
return messagingKeyDefaults[keyType]?.path || messagingKeyDefaults.btc.path;
|
|
5178
|
+
}
|
|
5179
|
+
|
|
5180
|
+
function getMessagingHDPath(keyType = getMessagingKeyType()) {
|
|
5181
|
+
const el = $('messaging-hd-path');
|
|
5182
|
+
const raw = el?.value || '';
|
|
5183
|
+
const path = raw.trim();
|
|
5184
|
+
return path || getMessagingDefaultPath(keyType);
|
|
5185
|
+
}
|
|
5186
|
+
|
|
5187
|
+
function setMessagingRecipientPlaceholder(keyType = getMessagingKeyType()) {
|
|
5188
|
+
const input = $('encrypt-recipient-pubkey');
|
|
5189
|
+
if (!input) return;
|
|
5190
|
+
if (keyType === 'sol') {
|
|
5191
|
+
input.placeholder = "Paste recipient's X25519 public key (hex, 32 bytes)";
|
|
5192
|
+
return;
|
|
5193
|
+
}
|
|
5194
|
+
if (keyType === 'eth') {
|
|
5195
|
+
input.placeholder = "Paste recipient's secp256k1 public key (hex, 65 bytes preferred)";
|
|
5196
|
+
return;
|
|
5197
|
+
}
|
|
5198
|
+
input.placeholder = "Paste recipient's secp256k1 public key (hex, 33 bytes preferred)";
|
|
5199
|
+
}
|
|
5200
|
+
|
|
5201
|
+
function loadMessagingKeyConfig() {
|
|
5202
|
+
try {
|
|
5203
|
+
const raw = localStorage.getItem(MESSAGING_KEY_CONFIG_KEY);
|
|
5204
|
+
if (!raw) return null;
|
|
5205
|
+
const parsed = JSON.parse(raw);
|
|
5206
|
+
if (!parsed || typeof parsed !== 'object') return null;
|
|
5207
|
+
return {
|
|
5208
|
+
keyType: parsed.keyType,
|
|
5209
|
+
path: parsed.path,
|
|
5210
|
+
};
|
|
5211
|
+
} catch {
|
|
5212
|
+
return null;
|
|
5213
|
+
}
|
|
5214
|
+
}
|
|
5215
|
+
|
|
5216
|
+
function saveMessagingKeyConfig(keyType, path) {
|
|
5217
|
+
try {
|
|
5218
|
+
localStorage.setItem(MESSAGING_KEY_CONFIG_KEY, JSON.stringify({ keyType, path }));
|
|
5219
|
+
} catch {
|
|
5220
|
+
// ignore
|
|
5221
|
+
}
|
|
5222
|
+
}
|
|
5223
|
+
|
|
5224
|
+
function initMessagingKeyControls() {
|
|
5225
|
+
const keyTypeEl = $('messaging-key-type');
|
|
5226
|
+
const pathEl = $('messaging-hd-path');
|
|
5227
|
+
const resetBtn = $('messaging-hd-path-default');
|
|
5228
|
+
if (!keyTypeEl || !pathEl) return;
|
|
5229
|
+
|
|
5230
|
+
const saved = loadMessagingKeyConfig();
|
|
5231
|
+
const hasSaved = !!saved;
|
|
5232
|
+
if (saved?.keyType === 'btc' || saved?.keyType === 'eth' || saved?.keyType === 'sol') {
|
|
5233
|
+
keyTypeEl.value = saved.keyType;
|
|
5234
|
+
} else {
|
|
5235
|
+
keyTypeEl.value = 'btc';
|
|
5236
|
+
}
|
|
5237
|
+
|
|
5238
|
+
if (typeof saved?.path === 'string' && saved.path.trim()) {
|
|
5239
|
+
pathEl.value = saved.path.trim();
|
|
5240
|
+
} else if (hasSaved || !pathEl.value?.trim()) {
|
|
5241
|
+
pathEl.value = getMessagingDefaultPath(keyTypeEl.value);
|
|
5242
|
+
}
|
|
5243
|
+
|
|
5244
|
+
setMessagingRecipientPlaceholder(keyTypeEl.value);
|
|
5245
|
+
|
|
5246
|
+
const onChange = () => {
|
|
5247
|
+
const keyType = getMessagingKeyType();
|
|
5248
|
+
const path = getMessagingHDPath(keyType);
|
|
5249
|
+
saveMessagingKeyConfig(keyType, path);
|
|
5250
|
+
setMessagingRecipientPlaceholder(keyType);
|
|
5251
|
+
if (state.hdRoot) updateEncryptionTab();
|
|
5252
|
+
};
|
|
5253
|
+
|
|
5254
|
+
keyTypeEl.addEventListener('change', () => {
|
|
5255
|
+
const prev = pathEl.value?.trim();
|
|
5256
|
+
const prevDefaults = Object.values(messagingKeyDefaults).map(v => v.path);
|
|
5257
|
+
const nextKeyType = getMessagingKeyType();
|
|
5258
|
+
const nextDefault = getMessagingDefaultPath(nextKeyType);
|
|
5259
|
+
// If user hasn't customized, keep the path in sync with key type.
|
|
5260
|
+
if (!prev || prevDefaults.includes(prev)) {
|
|
5261
|
+
pathEl.value = nextDefault;
|
|
5262
|
+
}
|
|
5263
|
+
onChange();
|
|
5264
|
+
});
|
|
5265
|
+
pathEl.addEventListener('input', onChange);
|
|
5266
|
+
resetBtn?.addEventListener('click', () => {
|
|
5267
|
+
const keyType = getMessagingKeyType();
|
|
5268
|
+
pathEl.value = getMessagingDefaultPath(keyType);
|
|
5269
|
+
onChange();
|
|
5270
|
+
});
|
|
5271
|
+
}
|
|
5272
|
+
|
|
5273
|
+
function hexToBytesStrict(hex, expectedLen = null) {
|
|
5274
|
+
if (typeof hex !== 'string') throw new Error('Expected hex string');
|
|
5275
|
+
const cleaned = hex.trim().toLowerCase().replace(/^0x/, '');
|
|
5276
|
+
if (!cleaned) throw new Error('Empty hex string');
|
|
5277
|
+
if (cleaned.length % 2 !== 0) throw new Error('Invalid hex length');
|
|
5278
|
+
if (!/^[0-9a-f]+$/.test(cleaned)) throw new Error('Invalid hex string');
|
|
5279
|
+
const bytes = new Uint8Array(cleaned.length / 2);
|
|
5280
|
+
for (let i = 0; i < cleaned.length; i += 2) {
|
|
5281
|
+
bytes[i / 2] = parseInt(cleaned.slice(i, i + 2), 16);
|
|
5282
|
+
}
|
|
5283
|
+
if (expectedLen !== null && bytes.length !== expectedLen) {
|
|
5284
|
+
throw new Error(`Expected ${expectedLen} bytes, got ${bytes.length}`);
|
|
5285
|
+
}
|
|
5286
|
+
return bytes;
|
|
5287
|
+
}
|
|
5288
|
+
|
|
5289
|
+
function deriveKeyMaterialForMessaging(w, keyType, path) {
|
|
5290
|
+
if (!state.hdRoot || !w) throw new Error('HD wallet not initialized');
|
|
5291
|
+
const derived = deriveHDKey(path);
|
|
5292
|
+
try {
|
|
5293
|
+
if (keyType === 'sol') {
|
|
5294
|
+
const priv = derived.privateKey();
|
|
5295
|
+
const pub = w.curves.x25519.publicKey(priv);
|
|
5296
|
+
return { algorithm: 'x25519', privateKey: priv, publicKey: pub, path };
|
|
5297
|
+
}
|
|
5298
|
+
|
|
5299
|
+
const priv = derived.privateKey();
|
|
5300
|
+
const pubCompressed = derived.publicKey();
|
|
5301
|
+
if (keyType === 'eth') {
|
|
5302
|
+
const pub = derived.publicKeyUncompressed();
|
|
5303
|
+
return { algorithm: 'secp256k1', privateKey: priv, publicKey: pub, path };
|
|
5304
|
+
}
|
|
5305
|
+
return { algorithm: 'secp256k1', privateKey: priv, publicKey: pubCompressed, path };
|
|
5306
|
+
} finally {
|
|
5307
|
+
derived.wipe();
|
|
5308
|
+
}
|
|
5309
|
+
}
|
|
5310
|
+
|
|
5311
|
+
function deriveMessagingPublicKey(w, keyType, path) {
|
|
5312
|
+
if (!state.hdRoot || !w) throw new Error('HD wallet not initialized');
|
|
5313
|
+
const derived = deriveHDKey(path);
|
|
5314
|
+
try {
|
|
5315
|
+
if (keyType === 'sol') {
|
|
5316
|
+
const priv = derived.privateKey();
|
|
5317
|
+
try {
|
|
5318
|
+
return w.curves.x25519.publicKey(priv);
|
|
5319
|
+
} finally {
|
|
5320
|
+
wipeBytes(priv);
|
|
5321
|
+
}
|
|
5322
|
+
}
|
|
5323
|
+
if (keyType === 'eth') {
|
|
5324
|
+
return derived.publicKeyUncompressed();
|
|
5325
|
+
}
|
|
5326
|
+
return derived.publicKey();
|
|
5327
|
+
} finally {
|
|
5328
|
+
derived.wipe();
|
|
5329
|
+
}
|
|
5330
|
+
}
|
|
5331
|
+
|
|
5332
|
+
function normalizeSecp256k1PublicKeyBytes(publicKey) {
|
|
5333
|
+
if (!(publicKey instanceof Uint8Array)) throw new Error('Invalid public key');
|
|
5334
|
+
// Ethereum public keys are sometimes provided as raw 64-byte x||y without the 0x04 prefix.
|
|
5335
|
+
if (publicKey.length === 64) {
|
|
5336
|
+
const out = new Uint8Array(65);
|
|
5337
|
+
out[0] = 0x04;
|
|
5338
|
+
out.set(publicKey, 1);
|
|
5339
|
+
return out;
|
|
5340
|
+
}
|
|
5341
|
+
if (publicKey.length !== 33 && publicKey.length !== 65) {
|
|
5342
|
+
throw new Error('secp256k1 public key must be 33 (compressed) or 65 (uncompressed) bytes');
|
|
5343
|
+
}
|
|
5344
|
+
return publicKey;
|
|
5345
|
+
}
|
|
5346
|
+
|
|
5347
|
+
function normalizeRecipientPublicKeyForAlgorithm(algorithm, publicKey) {
|
|
5348
|
+
if (algorithm === 'x25519') {
|
|
5349
|
+
if (!(publicKey instanceof Uint8Array) || publicKey.length !== 32) {
|
|
5350
|
+
throw new Error('X25519 public key must be 32 bytes');
|
|
5351
|
+
}
|
|
5352
|
+
return publicKey;
|
|
5353
|
+
}
|
|
5354
|
+
return normalizeSecp256k1PublicKeyBytes(publicKey);
|
|
5355
|
+
}
|
|
5356
|
+
|
|
5357
|
+
function eciesInfoForAlgorithm(algorithm) {
|
|
5358
|
+
const infoStr = algorithm === 'x25519'
|
|
5359
|
+
? 'ecies-x25519-aes256gcm'
|
|
5360
|
+
: 'ecies-secp256k1-aes256gcm';
|
|
5361
|
+
return new TextEncoder().encode(infoStr);
|
|
5362
|
+
}
|
|
5363
|
+
|
|
5364
|
+
function envelopeAlgorithmParameters(keyType, algorithm) {
|
|
5365
|
+
if (algorithm === 'x25519') return 'x25519';
|
|
5366
|
+
// secp256k1 modes
|
|
5367
|
+
return keyType === 'eth' ? 'secp256k1-uncompressed' : 'secp256k1-compressed';
|
|
5368
|
+
}
|
|
5369
|
+
|
|
4946
5370
|
function updateEncryptionTab() {
|
|
4947
|
-
|
|
4948
|
-
|
|
4949
|
-
|
|
4950
|
-
const
|
|
4951
|
-
const
|
|
4952
|
-
const encKey = deriveHDKey(encPath);
|
|
4953
|
-
const pubKey = encKey.publicKey();
|
|
4954
|
-
const pubHex = toHexCompact(pubKey);
|
|
5371
|
+
const w = state.hdWalletModule;
|
|
5372
|
+
if (!state.hdRoot || !w) return;
|
|
5373
|
+
|
|
5374
|
+
const keyType = getMessagingKeyType();
|
|
5375
|
+
const path = getMessagingHDPath(keyType);
|
|
4955
5376
|
|
|
4956
5377
|
const senderPubEl = $('encrypt-sender-pubkey');
|
|
4957
5378
|
const senderPathEl = $('encrypt-sender-path');
|
|
4958
|
-
|
|
4959
|
-
if (senderPathEl) senderPathEl.textContent = encPath;
|
|
4960
|
-
|
|
5379
|
+
const senderAlgoEl = $('encrypt-sender-algo');
|
|
4961
5380
|
const encryptBtn = $('encrypt-btn');
|
|
4962
|
-
|
|
5381
|
+
|
|
5382
|
+
if (senderPathEl) senderPathEl.textContent = path;
|
|
5383
|
+
const baseAlgo = messagingKeyDefaults[keyType]?.algorithm || '--';
|
|
5384
|
+
if (senderAlgoEl) {
|
|
5385
|
+
senderAlgoEl.textContent = baseAlgo === '--'
|
|
5386
|
+
? '--'
|
|
5387
|
+
: envelopeAlgorithmParameters(keyType, baseAlgo);
|
|
5388
|
+
}
|
|
5389
|
+
if (encryptBtn) encryptBtn.disabled = true;
|
|
5390
|
+
|
|
5391
|
+
try {
|
|
5392
|
+
const publicKey = deriveMessagingPublicKey(w, keyType, path);
|
|
5393
|
+
if (senderPubEl) senderPubEl.textContent = toHexCompact(publicKey);
|
|
5394
|
+
if (encryptBtn) encryptBtn.disabled = false;
|
|
5395
|
+
} catch (e) {
|
|
5396
|
+
if (senderPubEl) senderPubEl.textContent = '--';
|
|
5397
|
+
if (senderAlgoEl) senderAlgoEl.textContent = 'invalid path';
|
|
5398
|
+
}
|
|
4963
5399
|
}
|
|
4964
5400
|
|
|
4965
|
-
|
|
5401
|
+
initMessagingKeyControls();
|
|
5402
|
+
|
|
5403
|
+
// Update encryption tab when it becomes active
|
|
4966
5404
|
$qa('.modal-tab[data-modal-tab="messaging-tab-content"]').forEach(tab => {
|
|
4967
5405
|
tab.addEventListener('click', () => {
|
|
4968
5406
|
if (state.hdRoot) updateEncryptionTab();
|
|
@@ -5036,7 +5474,10 @@ function setupTrustHandlers() {
|
|
|
5036
5474
|
// Encrypt button
|
|
5037
5475
|
$('encrypt-btn')?.addEventListener('click', () => {
|
|
5038
5476
|
const w = state.hdWalletModule;
|
|
5039
|
-
if (!w || !state.hdRoot)
|
|
5477
|
+
if (!w || !state.hdRoot) {
|
|
5478
|
+
alert('Please login first.');
|
|
5479
|
+
return;
|
|
5480
|
+
}
|
|
5040
5481
|
|
|
5041
5482
|
const recipientHex = $('encrypt-recipient-pubkey')?.value?.trim();
|
|
5042
5483
|
const plainStr = $('encrypt-plaintext')?.value;
|
|
@@ -5046,56 +5487,64 @@ function setupTrustHandlers() {
|
|
|
5046
5487
|
}
|
|
5047
5488
|
|
|
5048
5489
|
try {
|
|
5049
|
-
const
|
|
5050
|
-
const
|
|
5051
|
-
const
|
|
5052
|
-
const encPath = buildEncryptionPath(coin, account, index);
|
|
5053
|
-
const senderKey = deriveHDKey(encPath);
|
|
5054
|
-
const senderPriv = senderKey.privateKey();
|
|
5055
|
-
const senderPub = senderKey.publicKey();
|
|
5056
|
-
|
|
5057
|
-
// Parse recipient public key from hex
|
|
5058
|
-
const recipientPub = new Uint8Array(recipientHex.match(/.{1,2}/g).map(b => parseInt(b, 16)));
|
|
5059
|
-
|
|
5060
|
-
// 1. ECDH shared secret
|
|
5061
|
-
const shared = w.curves.secp256k1.ecdh(senderPriv, recipientPub);
|
|
5062
|
-
|
|
5063
|
-
// 2. HKDF: derive 32-byte AES key from shared secret
|
|
5064
|
-
const salt = w.utils.getRandomBytes(32);
|
|
5065
|
-
const info = new TextEncoder().encode('ecies-secp256k1-aes256gcm');
|
|
5066
|
-
const aesKey = w.utils.hkdf(shared, salt, info, 32);
|
|
5067
|
-
|
|
5068
|
-
// 3. AES-256-GCM encrypt
|
|
5069
|
-
const iv = w.utils.generateIv();
|
|
5070
|
-
const plaintext = new TextEncoder().encode(plainStr);
|
|
5071
|
-
const { ciphertext, tag } = w.utils.aesGcm.encrypt(aesKey, plaintext, iv);
|
|
5072
|
-
|
|
5073
|
-
// Display field-level results
|
|
5074
|
-
$('encrypt-out-ciphertext').textContent = toHexCompact(ciphertext);
|
|
5075
|
-
$('encrypt-out-tag').textContent = toHexCompact(tag);
|
|
5076
|
-
$('encrypt-out-iv').textContent = toHexCompact(iv);
|
|
5077
|
-
$('encrypt-out-salt').textContent = toHexCompact(salt);
|
|
5078
|
-
$('encrypt-out-sender-pub').textContent = toHexCompact(senderPub);
|
|
5079
|
-
// Build EME (Encrypted Message Envelope) standard object
|
|
5080
|
-
currentEME = new EMET(
|
|
5081
|
-
Array.from(ciphertext), // ENCRYPTED_BLOB
|
|
5082
|
-
toHexCompact(senderPub), // EPHEMERAL_PUBLIC_KEY
|
|
5083
|
-
null, // MAC (not used, tag covers it)
|
|
5084
|
-
null, // NONCE (we use IV field instead)
|
|
5085
|
-
toHexCompact(tag), // TAG
|
|
5086
|
-
toHexCompact(iv), // IV
|
|
5087
|
-
toHexCompact(salt), // SALT
|
|
5088
|
-
null, // PUBLIC_KEY_IDENTIFIER
|
|
5089
|
-
'aes-256-gcm', // CIPHER_SUITE
|
|
5090
|
-
'hkdf-sha256', // KDF_PARAMETERS
|
|
5091
|
-
'secp256k1', // ENCRYPTION_ALGORITHM_PARAMETERS
|
|
5092
|
-
);
|
|
5093
|
-
|
|
5094
|
-
updateBundleDisplay();
|
|
5490
|
+
const keyType = getMessagingKeyType();
|
|
5491
|
+
const path = getMessagingHDPath(keyType);
|
|
5492
|
+
const { algorithm, privateKey: senderPriv, publicKey: senderPub } = deriveKeyMaterialForMessaging(w, keyType, path);
|
|
5095
5493
|
|
|
5096
|
-
|
|
5097
|
-
|
|
5098
|
-
|
|
5494
|
+
let shared = null;
|
|
5495
|
+
let aesKey = null;
|
|
5496
|
+
try {
|
|
5497
|
+
// Parse recipient public key from hex
|
|
5498
|
+
const recipientPubRaw = hexToBytesStrict(recipientHex);
|
|
5499
|
+
const recipientPub = normalizeRecipientPublicKeyForAlgorithm(algorithm, recipientPubRaw);
|
|
5500
|
+
|
|
5501
|
+
// 1. ECDH shared secret
|
|
5502
|
+
shared = algorithm === 'x25519'
|
|
5503
|
+
? w.curves.x25519.ecdh(senderPriv, recipientPub)
|
|
5504
|
+
: w.curves.secp256k1.ecdh(senderPriv, recipientPub);
|
|
5505
|
+
|
|
5506
|
+
// 2. HKDF: derive 32-byte AES key from shared secret
|
|
5507
|
+
const salt = w.utils.getRandomBytes(32);
|
|
5508
|
+
const info = eciesInfoForAlgorithm(algorithm);
|
|
5509
|
+
aesKey = w.utils.hkdf(shared, salt, info, 32);
|
|
5510
|
+
|
|
5511
|
+
// 3. AES-256-GCM encrypt
|
|
5512
|
+
const iv = w.utils.generateIv();
|
|
5513
|
+
const plaintext = new TextEncoder().encode(plainStr);
|
|
5514
|
+
const { ciphertext, tag } = w.utils.aesGcm.encrypt(aesKey, plaintext, iv);
|
|
5515
|
+
|
|
5516
|
+
// Display field-level results
|
|
5517
|
+
$('encrypt-out-ciphertext').textContent = toHexCompact(ciphertext);
|
|
5518
|
+
$('encrypt-out-tag').textContent = toHexCompact(tag);
|
|
5519
|
+
$('encrypt-out-iv').textContent = toHexCompact(iv);
|
|
5520
|
+
$('encrypt-out-salt').textContent = toHexCompact(salt);
|
|
5521
|
+
$('encrypt-out-sender-pub').textContent = toHexCompact(senderPub);
|
|
5522
|
+
|
|
5523
|
+
// Build EME (Encrypted Message Envelope) standard object
|
|
5524
|
+
currentEME = new EMET(
|
|
5525
|
+
Array.from(ciphertext), // ENCRYPTED_BLOB
|
|
5526
|
+
toHexCompact(senderPub), // EPHEMERAL_PUBLIC_KEY
|
|
5527
|
+
null, // MAC (not used, tag covers it)
|
|
5528
|
+
null, // NONCE (we use IV field instead)
|
|
5529
|
+
toHexCompact(tag), // TAG
|
|
5530
|
+
toHexCompact(iv), // IV
|
|
5531
|
+
toHexCompact(salt), // SALT
|
|
5532
|
+
null, // PUBLIC_KEY_IDENTIFIER
|
|
5533
|
+
'aes-256-gcm', // CIPHER_SUITE
|
|
5534
|
+
'hkdf-sha256', // KDF_PARAMETERS
|
|
5535
|
+
envelopeAlgorithmParameters(keyType, algorithm), // ENCRYPTION_ALGORITHM_PARAMETERS
|
|
5536
|
+
);
|
|
5537
|
+
|
|
5538
|
+
updateBundleDisplay();
|
|
5539
|
+
|
|
5540
|
+
// Switch to result step
|
|
5541
|
+
$('encrypt-step-compose').style.display = 'none';
|
|
5542
|
+
$('encrypt-step-result').style.display = 'block';
|
|
5543
|
+
} finally {
|
|
5544
|
+
wipeBytes(senderPriv);
|
|
5545
|
+
wipeBytes(shared);
|
|
5546
|
+
wipeBytes(aesKey);
|
|
5547
|
+
}
|
|
5099
5548
|
} catch (err) {
|
|
5100
5549
|
console.error('Encryption failed:', err);
|
|
5101
5550
|
alert('Encryption failed: ' + err.message);
|
|
@@ -5148,7 +5597,10 @@ function setupTrustHandlers() {
|
|
|
5148
5597
|
// Decrypt button
|
|
5149
5598
|
$('decrypt-btn')?.addEventListener('click', () => {
|
|
5150
5599
|
const w = state.hdWalletModule;
|
|
5151
|
-
if (!w || !state.hdRoot)
|
|
5600
|
+
if (!w || !state.hdRoot) {
|
|
5601
|
+
alert('Please login first.');
|
|
5602
|
+
return;
|
|
5603
|
+
}
|
|
5152
5604
|
|
|
5153
5605
|
const payloadStr = $('decrypt-payload')?.value?.trim();
|
|
5154
5606
|
if (!payloadStr) {
|
|
@@ -5158,44 +5610,63 @@ function setupTrustHandlers() {
|
|
|
5158
5610
|
|
|
5159
5611
|
try {
|
|
5160
5612
|
const payload = parseEMEPayload(payloadStr);
|
|
5161
|
-
const
|
|
5162
|
-
|
|
5163
|
-
const
|
|
5164
|
-
const
|
|
5165
|
-
const iv = fromHex(payload.IV);
|
|
5166
|
-
const salt = fromHex(payload.SALT);
|
|
5613
|
+
const senderPubRaw = hexToBytesStrict(payload.EPHEMERAL_PUBLIC_KEY, null);
|
|
5614
|
+
const tag = hexToBytesStrict(payload.TAG, 16);
|
|
5615
|
+
const iv = hexToBytesStrict(payload.IV, 12);
|
|
5616
|
+
const salt = hexToBytesStrict(payload.SALT, 32);
|
|
5167
5617
|
|
|
5168
5618
|
// ENCRYPTED_BLOB can be a number array (from EMET) or hex string
|
|
5169
5619
|
let ciphertext;
|
|
5170
5620
|
if (Array.isArray(payload.ENCRYPTED_BLOB)) {
|
|
5171
5621
|
ciphertext = new Uint8Array(payload.ENCRYPTED_BLOB);
|
|
5172
5622
|
} else {
|
|
5173
|
-
ciphertext =
|
|
5623
|
+
ciphertext = hexToBytesStrict(payload.ENCRYPTED_BLOB, null);
|
|
5174
5624
|
}
|
|
5175
5625
|
|
|
5176
|
-
const
|
|
5177
|
-
const
|
|
5178
|
-
const index = $('hd-index')?.value || '0';
|
|
5179
|
-
const encPath = buildEncryptionPath(coin, account, index);
|
|
5180
|
-
const recipientKey = deriveHDKey(encPath);
|
|
5181
|
-
const recipientPriv = recipientKey.privateKey();
|
|
5182
|
-
|
|
5183
|
-
// 1. ECDH shared secret (using sender's public key)
|
|
5184
|
-
const shared = w.curves.secp256k1.ecdh(recipientPriv, senderPub);
|
|
5626
|
+
const keyType = getMessagingKeyType();
|
|
5627
|
+
const path = getMessagingHDPath(keyType);
|
|
5185
5628
|
|
|
5186
|
-
//
|
|
5187
|
-
const
|
|
5188
|
-
|
|
5629
|
+
// Prefer payload algorithm; fall back to current UI selection.
|
|
5630
|
+
const algoParams = typeof payload.ENCRYPTION_ALGORITHM_PARAMETERS === 'string'
|
|
5631
|
+
? payload.ENCRYPTION_ALGORITHM_PARAMETERS.toLowerCase()
|
|
5632
|
+
: '';
|
|
5633
|
+
const algorithm = algoParams.includes('x25519')
|
|
5634
|
+
? 'x25519'
|
|
5635
|
+
: (messagingKeyDefaults[keyType]?.algorithm || 'secp256k1');
|
|
5189
5636
|
|
|
5190
|
-
|
|
5191
|
-
const decrypted = w.utils.aesGcm.decrypt(aesKey, ciphertext, tag, iv);
|
|
5192
|
-
const decStr = new TextDecoder().decode(decrypted);
|
|
5637
|
+
const senderPub = normalizeRecipientPublicKeyForAlgorithm(algorithm, senderPubRaw);
|
|
5193
5638
|
|
|
5194
|
-
|
|
5639
|
+
// Derive recipient private key from configured path.
|
|
5640
|
+
const derived = deriveHDKey(path);
|
|
5641
|
+
const recipientPriv = derived.privateKey();
|
|
5642
|
+
derived.wipe();
|
|
5195
5643
|
|
|
5196
|
-
|
|
5197
|
-
|
|
5198
|
-
|
|
5644
|
+
let shared = null;
|
|
5645
|
+
let aesKey = null;
|
|
5646
|
+
try {
|
|
5647
|
+
// 1. ECDH shared secret (using sender's public key)
|
|
5648
|
+
shared = algorithm === 'x25519'
|
|
5649
|
+
? w.curves.x25519.ecdh(recipientPriv, senderPub)
|
|
5650
|
+
: w.curves.secp256k1.ecdh(recipientPriv, senderPub);
|
|
5651
|
+
|
|
5652
|
+
// 2. HKDF: derive same AES key
|
|
5653
|
+
const info = eciesInfoForAlgorithm(algorithm);
|
|
5654
|
+
aesKey = w.utils.hkdf(shared, salt, info, 32);
|
|
5655
|
+
|
|
5656
|
+
// 3. AES-256-GCM decrypt
|
|
5657
|
+
const decrypted = w.utils.aesGcm.decrypt(aesKey, ciphertext, tag, iv);
|
|
5658
|
+
const decStr = new TextDecoder().decode(decrypted);
|
|
5659
|
+
|
|
5660
|
+
$('decrypt-result-value').textContent = decStr;
|
|
5661
|
+
|
|
5662
|
+
// Switch to result step
|
|
5663
|
+
$('decrypt-step-input').style.display = 'none';
|
|
5664
|
+
$('decrypt-step-result').style.display = 'block';
|
|
5665
|
+
} finally {
|
|
5666
|
+
wipeBytes(recipientPriv);
|
|
5667
|
+
wipeBytes(shared);
|
|
5668
|
+
wipeBytes(aesKey);
|
|
5669
|
+
}
|
|
5199
5670
|
} catch (err) {
|
|
5200
5671
|
console.error('Decryption failed:', err);
|
|
5201
5672
|
alert('Decryption failed: ' + err.message);
|
|
@@ -5254,10 +5725,11 @@ function setupHomepageHandlers() {
|
|
|
5254
5725
|
// =============================================================================
|
|
5255
5726
|
|
|
5256
5727
|
export async function init(rootElement, options = {}) {
|
|
5257
|
-
const { autoOpenWallet = false, onLogin = null } = typeof rootElement === 'object' && !(rootElement instanceof Node)
|
|
5728
|
+
const { autoOpenWallet = false, onLogin = null, openAccountAfterLogin = true } = typeof rootElement === 'object' && !(rootElement instanceof Node)
|
|
5258
5729
|
? (options = rootElement, {}) : options;
|
|
5259
5730
|
if (rootElement && rootElement instanceof Node) _root = rootElement;
|
|
5260
5731
|
if (typeof onLogin === 'function') _onLoginCallback = onLogin;
|
|
5732
|
+
_openAccountAfterLogin = openAccountAfterLogin;
|
|
5261
5733
|
|
|
5262
5734
|
// Inject modal HTML if not already present in the DOM
|
|
5263
5735
|
if (!document.getElementById('keys-modal')) {
|
|
@@ -5282,8 +5754,9 @@ export async function init(rootElement, options = {}) {
|
|
|
5282
5754
|
if (status) status.textContent = 'Loading HD wallet module...';
|
|
5283
5755
|
state.hdWalletModule = await initHDWallet();
|
|
5284
5756
|
|
|
5285
|
-
// Load saved PKI keys if available
|
|
5286
|
-
|
|
5757
|
+
// Load saved PKI keys if available.
|
|
5758
|
+
// (If not logged in yet, this will return false since encrypted keys require the session key.)
|
|
5759
|
+
const hasSavedKeys = await loadPKIKeys();
|
|
5287
5760
|
|
|
5288
5761
|
state.initialized = true;
|
|
5289
5762
|
|
|
@@ -5369,7 +5842,10 @@ export async function init(rootElement, options = {}) {
|
|
|
5369
5842
|
* @param {Node} [rootElement] - Optional root element for DOM queries
|
|
5370
5843
|
* @param {Object} [options] - Options passed to init()
|
|
5371
5844
|
* @param {Function} [options.onLogin] - Callback fired after successful login with
|
|
5372
|
-
* `{ xpub, signingPublicKey, sign(message) }` for SDN identity (coin type
|
|
5845
|
+
* `{ xpub, signingPublicKey, sign(message) }` for SDN identity (BIP-44 coin type 0)
|
|
5846
|
+
* @param {boolean} [options.openAccountAfterLogin=true] - When false, the Account
|
|
5847
|
+
* modal will NOT auto-open after login. Useful for integrations that handle
|
|
5848
|
+
* post-login UX themselves (e.g. challenge-response auth flows).
|
|
5373
5849
|
* @returns {Promise<{openLogin: Function, openAccount: Function, destroy: Function}>}
|
|
5374
5850
|
*/
|
|
5375
5851
|
export async function createWalletUI(rootElement, options = {}) {
|