hd-wallet-ui 1.2.6 → 1.4.3

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/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
- return keys;
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
- const masterKey = await hkdf(
355
- new Uint8Array(seed.slice(0, 32)),
356
- new Uint8Array(0),
357
- encoder.encode('wallet-master'),
358
- 32
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
+ }
400
+
401
+ async function deriveKeysFromMasterSeed(masterSeedBytes) {
402
+ const encoder = new TextEncoder();
360
403
 
361
- state.encryptionKey = await hkdf(masterKey, new Uint8Array(0), encoder.encode('buffer-encryption-key'), 32);
362
- state.encryptionIV = await hkdf(masterKey, new Uint8Array(0), encoder.encode('buffer-encryption-iv'), 16);
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
- state.masterSeed = new Uint8Array(seed);
365
- state.hdRoot = state.hdWalletModule.hdkey.fromSeed(new Uint8Array(seed));
366
- state.mnemonic = seedPhrase;
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
 
@@ -454,6 +497,30 @@ function deriveHDKey(path) {
454
497
  }
455
498
  }
456
499
 
500
+ function derivePeerInfo(acct) {
501
+ if (!state.hdRoot || !state.hdWalletModule) return null;
502
+ try {
503
+ const path = acct.path || buildSigningPath(acct.coinType, acct.account, acct.index);
504
+ const derived = state.hdRoot.derivePath(path);
505
+ let pubKey, curve;
506
+ if (acct.coinType === 501) {
507
+ pubKey = ed25519.getPublicKey(derived.privateKey());
508
+ curve = Curve.ED25519;
509
+ } else {
510
+ pubKey = derived.publicKey();
511
+ curve = Curve.SECP256K1;
512
+ }
513
+ const peerIdBytes = state.hdWalletModule.libp2p.peerIdFromPublicKey(pubKey, curve);
514
+ return {
515
+ peerIdStr: state.hdWalletModule.libp2p.peerIdToString(peerIdBytes),
516
+ ipnsHash: state.hdWalletModule.libp2p.ipnsHash(peerIdBytes),
517
+ };
518
+ } catch (e) {
519
+ console.warn('Failed to derive peer info:', e);
520
+ return null;
521
+ }
522
+ }
523
+
457
524
  function updatePathDisplay() {
458
525
  const coin = $('hd-coin')?.value;
459
526
  const account = $('hd-account')?.value || '0';
@@ -1562,8 +1629,20 @@ async function showReceiveModal(acct) {
1562
1629
  <h4 id="wallet-receive-title" class="section-label"></h4>
1563
1630
  <canvas id="wallet-receive-qr"></canvas>
1564
1631
  <code id="wallet-receive-address" class="wallet-receive-address"></code>
1632
+ <div id="wallet-receive-peer-section" class="wallet-receive-peer-section" style="display:none">
1633
+ <div class="wallet-receive-field">
1634
+ <span class="wallet-receive-field-label">PeerID</span>
1635
+ <code id="wallet-receive-peerid" class="wallet-receive-field-value"></code>
1636
+ <button id="wallet-receive-copy-peerid" class="glass-btn small">Copy</button>
1637
+ </div>
1638
+ <div class="wallet-receive-field">
1639
+ <span class="wallet-receive-field-label">IPNS</span>
1640
+ <code id="wallet-receive-ipns" class="wallet-receive-field-value"></code>
1641
+ <button id="wallet-receive-copy-ipns" class="glass-btn small">Copy</button>
1642
+ </div>
1643
+ </div>
1565
1644
  <div class="wallet-receive-actions">
1566
- <button id="wallet-receive-copy" class="glass-btn small">Copy</button>
1645
+ <button id="wallet-receive-copy" class="glass-btn small">Copy Address</button>
1567
1646
  <button id="wallet-receive-close" class="glass-btn small">Close</button>
1568
1647
  </div>
1569
1648
  </div>
@@ -1576,6 +1655,18 @@ async function showReceiveModal(acct) {
1576
1655
  if (titleEl) titleEl.textContent = `Receive ${acct.name}`;
1577
1656
  if (addrEl) addrEl.textContent = acct.address;
1578
1657
 
1658
+ const peerSection = overlay.querySelector('#wallet-receive-peer-section');
1659
+ const peerIdEl = overlay.querySelector('#wallet-receive-peerid');
1660
+ const ipnsEl = overlay.querySelector('#wallet-receive-ipns');
1661
+ const peerInfo = derivePeerInfo(acct);
1662
+ if (peerInfo && peerSection) {
1663
+ peerSection.style.display = '';
1664
+ if (peerIdEl) peerIdEl.textContent = peerInfo.peerIdStr;
1665
+ if (ipnsEl) ipnsEl.textContent = peerInfo.ipnsHash;
1666
+ } else if (peerSection) {
1667
+ peerSection.style.display = 'none';
1668
+ }
1669
+
1579
1670
  try {
1580
1671
  const qrCanvas = overlay.querySelector('#wallet-receive-qr');
1581
1672
  if (qrCanvas) {
@@ -1598,6 +1689,16 @@ async function showReceiveModal(acct) {
1598
1689
  overlay.querySelector('#wallet-receive-close')?.addEventListener('click', () => {
1599
1690
  overlay.style.display = 'none';
1600
1691
  }, { once: true });
1692
+
1693
+ overlay.querySelector('#wallet-receive-copy-peerid')?.addEventListener('click', () => {
1694
+ const val = overlay.querySelector('#wallet-receive-peerid')?.textContent;
1695
+ if (val) navigator.clipboard.writeText(val).catch(() => {});
1696
+ }, { once: true });
1697
+
1698
+ overlay.querySelector('#wallet-receive-copy-ipns')?.addEventListener('click', () => {
1699
+ const val = overlay.querySelector('#wallet-receive-ipns')?.textContent;
1700
+ if (val) navigator.clipboard.writeText(val).catch(() => {});
1701
+ }, { once: true });
1601
1702
  }
1602
1703
 
1603
1704
  // =============================================================================
@@ -2191,7 +2292,14 @@ function savePKIKeys() {
2191
2292
  return;
2192
2293
  }
2193
2294
 
2194
- const data = {
2295
+ // SECURITY: Never persist private keys in plaintext localStorage.
2296
+ // Persist encrypted only when a session encryption key exists (i.e., after wallet login).
2297
+ if (!(state.encryptionKey instanceof Uint8Array) || state.encryptionKey.length < 16) {
2298
+ console.warn('Skipping PKI key persistence: session encryption key not available (login required)');
2299
+ return;
2300
+ }
2301
+
2302
+ const plaintext = {
2195
2303
  algorithm: state.pki.algorithm,
2196
2304
  alice: {
2197
2305
  publicKey: toHexCompact(state.pki.alice.publicKey),
@@ -2204,44 +2312,77 @@ function savePKIKeys() {
2204
2312
  savedAt: new Date().toISOString(),
2205
2313
  };
2206
2314
 
2207
- if (state.encryptionKey && state.encryptionIV) {
2208
- data.encryptionKey = toHexCompact(state.encryptionKey);
2209
- data.encryptionIV = toHexCompact(state.encryptionIV);
2210
- }
2211
-
2212
- try {
2213
- localStorage.setItem(PKI_STORAGE_KEY, JSON.stringify(data));
2214
- } catch (e) {
2215
- console.warn('Failed to save PKI keys to localStorage:', e);
2216
- }
2315
+ aesGcmEncryptJson(state.encryptionKey, plaintext, 'wallet-ui|pki-keys')
2316
+ .then(({ iv, ciphertext }) => {
2317
+ const stored = {
2318
+ v: 1,
2319
+ iv: bytesToBase64(iv),
2320
+ ciphertext: bytesToBase64(ciphertext),
2321
+ };
2322
+ localStorage.setItem(PKI_STORAGE_KEY, JSON.stringify(stored));
2323
+ })
2324
+ .catch((e) => {
2325
+ console.warn('Failed to encrypt+save PKI keys to localStorage:', e);
2326
+ });
2217
2327
  }
2218
2328
 
2219
- function loadPKIKeys() {
2329
+ async function loadPKIKeys() {
2220
2330
  try {
2221
2331
  const stored = localStorage.getItem(PKI_STORAGE_KEY);
2222
2332
  if (!stored) return false;
2223
2333
 
2224
2334
  const data = JSON.parse(stored);
2225
- if (!data.alice || !data.bob || !data.algorithm) {
2335
+ const hasEncryptedShape = data && typeof data === 'object' && typeof data.iv === 'string' && typeof data.ciphertext === 'string';
2336
+
2337
+ // Legacy plaintext format (insecure): refuse to load until logged in, then upgrade.
2338
+ const hasLegacyPlaintextShape = data?.alice?.privateKey && data?.bob?.privateKey && data?.algorithm;
2339
+
2340
+ if (!hasEncryptedShape && !hasLegacyPlaintextShape) {
2226
2341
  console.warn('Invalid PKI data in localStorage');
2227
2342
  return false;
2228
2343
  }
2229
2344
 
2230
- state.pki.algorithm = data.algorithm;
2345
+ if (!(state.encryptionKey instanceof Uint8Array) || state.encryptionKey.length < 16) {
2346
+ // Not logged in yet; don't load private keys.
2347
+ return false;
2348
+ }
2349
+
2350
+ let plaintext;
2351
+ if (hasEncryptedShape) {
2352
+ const iv = base64ToBytes(data.iv);
2353
+ const ciphertext = base64ToBytes(data.ciphertext);
2354
+ plaintext = await aesGcmDecryptJson(state.encryptionKey, iv, ciphertext, 'wallet-ui|pki-keys');
2355
+ } else {
2356
+ // Legacy plaintext: load and immediately re-encrypt on next save.
2357
+ plaintext = data;
2358
+ // Upgrade-in-place.
2359
+ try {
2360
+ const { iv, ciphertext } = await aesGcmEncryptJson(state.encryptionKey, plaintext, 'wallet-ui|pki-keys');
2361
+ localStorage.setItem(PKI_STORAGE_KEY, JSON.stringify({
2362
+ v: 1,
2363
+ iv: bytesToBase64(iv),
2364
+ ciphertext: bytesToBase64(ciphertext),
2365
+ }));
2366
+ } catch (e) {
2367
+ console.warn('Failed to upgrade legacy plaintext PKI storage:', e);
2368
+ }
2369
+ }
2370
+
2371
+ if (!plaintext?.alice || !plaintext?.bob || !plaintext?.algorithm) {
2372
+ console.warn('Invalid decrypted PKI data');
2373
+ return false;
2374
+ }
2375
+
2376
+ state.pki.algorithm = plaintext.algorithm;
2231
2377
  state.pki.alice = {
2232
- publicKey: hexToBytes(data.alice.publicKey),
2233
- privateKey: hexToBytes(data.alice.privateKey),
2378
+ publicKey: hexToBytes(plaintext.alice.publicKey),
2379
+ privateKey: hexToBytes(plaintext.alice.privateKey),
2234
2380
  };
2235
2381
  state.pki.bob = {
2236
- publicKey: hexToBytes(data.bob.publicKey),
2237
- privateKey: hexToBytes(data.bob.privateKey),
2382
+ publicKey: hexToBytes(plaintext.bob.publicKey),
2383
+ privateKey: hexToBytes(plaintext.bob.privateKey),
2238
2384
  };
2239
2385
 
2240
- if (data.encryptionKey && data.encryptionIV) {
2241
- state.encryptionKey = hexToBytes(data.encryptionKey);
2242
- state.encryptionIV = hexToBytes(data.encryptionIV);
2243
- }
2244
-
2245
2386
  // Update UI
2246
2387
  const alicePublicKey = $('alice-public-key');
2247
2388
  const alicePrivateKey = $('alice-private-key');
@@ -2253,11 +2394,11 @@ function loadPKIKeys() {
2253
2394
  const pkiClearKeys = $('pki-clear-keys');
2254
2395
 
2255
2396
  const pkiAlgorithm = $('pki-algorithm');
2256
- if (pkiAlgorithm) pkiAlgorithm.value = data.algorithm;
2257
- if (alicePublicKey) alicePublicKey.textContent = data.alice.publicKey;
2258
- if (alicePrivateKey) alicePrivateKey.textContent = data.alice.privateKey;
2259
- if (bobPublicKey) bobPublicKey.textContent = data.bob.publicKey;
2260
- if (bobPrivateKey) bobPrivateKey.textContent = data.bob.privateKey;
2397
+ if (pkiAlgorithm) pkiAlgorithm.value = plaintext.algorithm;
2398
+ if (alicePublicKey) alicePublicKey.textContent = plaintext.alice.publicKey;
2399
+ if (alicePrivateKey) alicePrivateKey.textContent = plaintext.alice.privateKey;
2400
+ if (bobPublicKey) bobPublicKey.textContent = plaintext.bob.publicKey;
2401
+ if (bobPrivateKey) bobPrivateKey.textContent = plaintext.bob.privateKey;
2261
2402
  if (pkiParties) pkiParties.style.display = 'grid';
2262
2403
  if (pkiDemo) pkiDemo.style.display = 'block';
2263
2404
  if (pkiSecurity) pkiSecurity.style.display = 'block';
@@ -2277,6 +2418,13 @@ function clearPKIKeys() {
2277
2418
  console.warn('Failed to clear PKI keys:', e);
2278
2419
  }
2279
2420
 
2421
+ try {
2422
+ if (state.pki?.alice?.privateKey instanceof Uint8Array) state.pki.alice.privateKey.fill(0);
2423
+ if (state.pki?.bob?.privateKey instanceof Uint8Array) state.pki.bob.privateKey.fill(0);
2424
+ } catch {
2425
+ // ignore
2426
+ }
2427
+
2280
2428
  state.pki.alice = null;
2281
2429
  state.pki.bob = null;
2282
2430
  state.pki.algorithm = 'x25519';
@@ -2378,11 +2526,14 @@ function login(keys) {
2378
2526
  state.addresses = deriveAllAddressesFromHD();
2379
2527
  state.selectedCrypto = 'btc';
2380
2528
 
2381
- // Fire onLogin callback with SDN identity (coin type 1957 — Sputnik)
2529
+ // Fire onLogin callback with SDN identity (BIP-44 Bitcoin coin type 0)
2382
2530
  if (_onLoginCallback && state.hdRoot) {
2383
2531
  try {
2384
- const sdnSigning = getSigningKey(state.hdRoot, 1957, 0, 0);
2385
- const sdnPubKey = ed25519.getPublicKey(sdnSigning.privateKey);
2532
+ const sdnSigning = getSigningKey(state.hdRoot, 0, 0, 0);
2533
+ const sdnPrivKey = sdnSigning.privateKey;
2534
+ const sdnPubKey = ed25519.getPublicKey(sdnPrivKey);
2535
+ // Don't keep derived private key bytes around longer than needed.
2536
+ if (sdnPrivKey instanceof Uint8Array) sdnPrivKey.fill(0);
2386
2537
  const xpub = state.hdRoot.toXpub();
2387
2538
  _onLoginCallback({
2388
2539
  xpub,
@@ -2391,7 +2542,12 @@ function login(keys) {
2391
2542
  const msgBytes = typeof message === 'string'
2392
2543
  ? new TextEncoder().encode(message)
2393
2544
  : message;
2394
- return ed25519.sign(msgBytes, sdnSigning.privateKey);
2545
+ const signing = getSigningKey(state.hdRoot, 0, 0, 0);
2546
+ try {
2547
+ return ed25519.sign(msgBytes, signing.privateKey);
2548
+ } finally {
2549
+ if (signing?.privateKey instanceof Uint8Array) signing.privateKey.fill(0);
2550
+ }
2395
2551
  },
2396
2552
  });
2397
2553
  } catch (err) {
@@ -2440,14 +2596,12 @@ function login(keys) {
2440
2596
  }
2441
2597
  populateAccountAddressDropdown();
2442
2598
  if (xprvEl) {
2443
- setTruncatedValue(xprvEl, state.hdRoot.toXprv() || 'N/A');
2599
+ xprvEl.textContent = 'Hidden (click reveal)';
2444
2600
  xprvEl.dataset.revealed = 'false';
2445
2601
  }
2446
- if (seedEl && state.mnemonic) {
2447
- seedEl.textContent = state.mnemonic;
2602
+ if (seedEl) {
2603
+ seedEl.textContent = 'Not retained by the app';
2448
2604
  seedEl.dataset.revealed = 'false';
2449
- } else if (seedEl) {
2450
- seedEl.textContent = 'Not available (derived from password)';
2451
2605
  }
2452
2606
 
2453
2607
  // Load persisted wallets and active accounts
@@ -2500,15 +2654,23 @@ function login(keys) {
2500
2654
  if (pkiSecurity) pkiSecurity.style.display = 'block';
2501
2655
  const pkiClearKeys = $('pki-clear-keys');
2502
2656
  if (pkiClearKeys) pkiClearKeys.style.display = 'inline-flex';
2503
- } else if (!loadPKIKeys()) {
2504
- generatePKIKeyPairs();
2657
+ } else {
2658
+ // PKI persistence is encrypted and requires the session key (available only after login).
2659
+ // Kick off an async load attempt; if it fails, generate fresh keys.
2660
+ loadPKIKeys().then((ok) => {
2661
+ if (!ok) generatePKIKeyPairs();
2662
+ }).catch(() => {
2663
+ generatePKIKeyPairs();
2664
+ });
2505
2665
  }
2506
2666
 
2507
2667
  // Update wallet addresses and balances
2508
2668
  updateAdversarialSecurity();
2509
2669
 
2510
2670
  // Open Account modal so user can see the wallet they just loaded
2511
- $('keys-modal')?.classList.add('active');
2671
+ if (_openAccountAfterLogin) {
2672
+ $('keys-modal')?.classList.add('active');
2673
+ }
2512
2674
 
2513
2675
  // Resolve names and update title
2514
2676
  clearNameCache();
@@ -2526,6 +2688,26 @@ function logout() {
2526
2688
  const titleEl = $('account-title');
2527
2689
  if (titleEl) titleEl.textContent = 'Account';
2528
2690
  state.loggedIn = false;
2691
+
2692
+ // Best-effort wipe of JS buffers (strings are not wipeable).
2693
+ const wipe = (u8) => {
2694
+ if (u8 instanceof Uint8Array) u8.fill(0);
2695
+ };
2696
+ try {
2697
+ wipe(state.wallet?.x25519?.privateKey);
2698
+ wipe(state.wallet?.ed25519?.privateKey);
2699
+ wipe(state.wallet?.secp256k1?.privateKey);
2700
+ wipe(state.wallet?.p256?.privateKey);
2701
+ wipe(state.encryptionKey);
2702
+ wipe(state.encryptionIV);
2703
+ wipe(state.masterSeed);
2704
+ wipe(state.pki?.alice?.privateKey);
2705
+ wipe(state.pki?.bob?.privateKey);
2706
+ state.hdRoot?.wipe?.();
2707
+ } catch {
2708
+ // ignore
2709
+ }
2710
+
2529
2711
  state.wallet = { x25519: null, ed25519: null, secp256k1: null, p256: null };
2530
2712
  state.encryptionKey = null;
2531
2713
  state.encryptionIV = null;
@@ -2586,7 +2768,7 @@ async function exportWallet(format) {
2586
2768
  switch (format) {
2587
2769
  case 'mnemonic':
2588
2770
  if (!state.mnemonic) {
2589
- alert('Seed phrase not available. This wallet was derived from a password.');
2771
+ alert('Seed phrase not available. For security, the app does not retain the mnemonic after login.');
2590
2772
  return;
2591
2773
  }
2592
2774
  data = state.mnemonic;
@@ -3669,9 +3851,7 @@ function setupLoginHandlers() {
3669
3851
  const usePasskey = rememberMethod.password === 'passkey';
3670
3852
  const pin = $('pin-input-password')?.value;
3671
3853
 
3672
- console.log('Login clicked, username:', username, 'password length:', password?.length);
3673
3854
  if (!username || !password || password.length < 24) {
3674
- console.log('Login validation failed');
3675
3855
  return;
3676
3856
  }
3677
3857
 
@@ -3685,16 +3865,18 @@ function setupLoginHandlers() {
3685
3865
  btn.textContent = 'Logging in...';
3686
3866
 
3687
3867
  try {
3688
- console.log('Calling deriveKeysFromPassword...');
3689
3868
  const keys = await deriveKeysFromPassword(username, password);
3690
- console.log('Keys derived, hdRoot after derivation:', !!state.hdRoot);
3869
+
3870
+ // Best-effort: don't keep the password in the input field after login.
3871
+ const pwEl = $('wallet-password');
3872
+ if (pwEl) pwEl.value = '';
3691
3873
 
3692
3874
  if (rememberWallet) {
3693
3875
  const walletData = {
3694
- type: 'password',
3876
+ type: 'masterSeed',
3877
+ source: 'password',
3695
3878
  username,
3696
- password,
3697
- masterSeed: Array.from(state.masterSeed)
3879
+ masterSeed: Array.from(state.masterSeed),
3698
3880
  };
3699
3881
 
3700
3882
  if (usePasskey) {
@@ -3723,7 +3905,6 @@ function setupLoginHandlers() {
3723
3905
  }
3724
3906
 
3725
3907
  login(keys);
3726
- console.log('Login complete, hdRoot:', !!state.hdRoot);
3727
3908
  } catch (err) {
3728
3909
  console.error('Login error:', err);
3729
3910
  alert('Error: ' + err.message);
@@ -3788,11 +3969,15 @@ function setupLoginHandlers() {
3788
3969
  try {
3789
3970
  const keys = await deriveKeysFromSeed(phrase);
3790
3971
 
3972
+ // Best-effort: don't keep the mnemonic in the textarea after login.
3973
+ const seedEl = $('seed-phrase');
3974
+ if (seedEl) seedEl.value = '';
3975
+
3791
3976
  if (rememberWallet) {
3792
3977
  const walletData = {
3793
- type: 'seed',
3794
- seedPhrase: phrase,
3795
- masterSeed: Array.from(state.masterSeed)
3978
+ type: 'masterSeed',
3979
+ source: 'seed',
3980
+ masterSeed: Array.from(state.masterSeed),
3796
3981
  };
3797
3982
 
3798
3983
  if (usePasskey) {
@@ -3845,12 +4030,27 @@ function setupLoginHandlers() {
3845
4030
  const walletData = await WalletStorage.retrieveWithPIN(pin);
3846
4031
 
3847
4032
  let keys;
3848
- if (walletData.type === 'password') {
4033
+ const storedSeed = walletData.masterSeed || walletData.seed || walletData.hdSeed;
4034
+ if (storedSeed) {
4035
+ keys = await deriveKeysFromMasterSeed(new Uint8Array(storedSeed));
4036
+ } else if (walletData.type === 'password') {
4037
+ // Legacy format: stored password/seedPhrase (deprecated). Unlock, then upgrade storage.
3849
4038
  keys = await deriveKeysFromPassword(walletData.username, walletData.password);
4039
+ await WalletStorage.storeWithPIN(pin, {
4040
+ type: 'masterSeed',
4041
+ source: 'password',
4042
+ username: walletData.username,
4043
+ masterSeed: Array.from(state.masterSeed),
4044
+ });
3850
4045
  } else if (walletData.type === 'seed') {
3851
4046
  keys = await deriveKeysFromSeed(walletData.seedPhrase);
4047
+ await WalletStorage.storeWithPIN(pin, {
4048
+ type: 'masterSeed',
4049
+ source: 'seed',
4050
+ masterSeed: Array.from(state.masterSeed),
4051
+ });
3852
4052
  } else {
3853
- throw new Error('Unknown wallet type');
4053
+ throw new Error('Unknown stored wallet format');
3854
4054
  }
3855
4055
 
3856
4056
  login(keys);
@@ -3874,12 +4074,34 @@ function setupLoginHandlers() {
3874
4074
  const walletData = await WalletStorage.retrieveWithPasskey();
3875
4075
 
3876
4076
  let keys;
3877
- if (walletData.type === 'password') {
4077
+ const storedSeed = walletData.masterSeed || walletData.seed || walletData.hdSeed;
4078
+ if (storedSeed) {
4079
+ keys = await deriveKeysFromMasterSeed(new Uint8Array(storedSeed));
4080
+ } else if (walletData.type === 'password') {
3878
4081
  keys = await deriveKeysFromPassword(walletData.username, walletData.password);
4082
+ await WalletStorage.storeWithPasskey({
4083
+ type: 'masterSeed',
4084
+ source: 'password',
4085
+ username: walletData.username,
4086
+ masterSeed: Array.from(state.masterSeed),
4087
+ }, {
4088
+ rpName: 'HD Wallet',
4089
+ userName: walletData.username || 'wallet-user',
4090
+ userDisplayName: walletData.username || 'Wallet User'
4091
+ });
3879
4092
  } else if (walletData.type === 'seed') {
3880
4093
  keys = await deriveKeysFromSeed(walletData.seedPhrase);
4094
+ await WalletStorage.storeWithPasskey({
4095
+ type: 'masterSeed',
4096
+ source: 'seed',
4097
+ masterSeed: Array.from(state.masterSeed),
4098
+ }, {
4099
+ rpName: 'HD Wallet',
4100
+ userName: 'seed-wallet',
4101
+ userDisplayName: 'Seed Phrase Wallet'
4102
+ });
3881
4103
  } else {
3882
- throw new Error('Unknown wallet type');
4104
+ throw new Error('Unknown stored wallet format');
3883
4105
  }
3884
4106
 
3885
4107
  login(keys);
@@ -4325,7 +4547,23 @@ function setupMainAppHandlers() {
4325
4547
  const targetEl = $(targetId);
4326
4548
  if (targetEl) {
4327
4549
  const isRevealed = targetEl.dataset.revealed === 'true';
4328
- targetEl.dataset.revealed = isRevealed ? 'false' : 'true';
4550
+ const nextRevealed = !isRevealed;
4551
+ targetEl.dataset.revealed = nextRevealed ? 'true' : 'false';
4552
+
4553
+ if (nextRevealed) {
4554
+ if (targetId === 'wallet-xprv') {
4555
+ targetEl.textContent = state.hdRoot?.toXprv?.() || 'N/A';
4556
+ } else if (targetId === 'wallet-seed-phrase') {
4557
+ targetEl.textContent = state.mnemonic || 'Not retained by the app';
4558
+ }
4559
+ } else {
4560
+ if (targetId === 'wallet-xprv') {
4561
+ targetEl.textContent = 'Hidden (click reveal)';
4562
+ } else if (targetId === 'wallet-seed-phrase') {
4563
+ targetEl.textContent = 'Not retained by the app';
4564
+ }
4565
+ }
4566
+
4329
4567
  btn.innerHTML = isRevealed
4330
4568
  ? '<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>'
4331
4569
  : '<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>';
@@ -4340,7 +4578,28 @@ function setupMainAppHandlers() {
4340
4578
  const targetEl = $(targetId);
4341
4579
  if (targetEl) {
4342
4580
  try {
4343
- await navigator.clipboard.writeText(targetEl.dataset.fullValue || targetEl.textContent);
4581
+ let value = '';
4582
+ if (targetId === 'wallet-xpub' || targetId === 'wallet-tab-xpub') {
4583
+ value = state.hdRoot?.toXpub?.() || '';
4584
+ } else if (targetId === 'wallet-xprv') {
4585
+ if (targetEl.dataset.revealed !== 'true') {
4586
+ alert('Reveal the xprv first to copy it.');
4587
+ return;
4588
+ }
4589
+ if (!confirm('Warning: copying your master private key (xprv) is extremely sensitive. Continue?')) {
4590
+ return;
4591
+ }
4592
+ value = state.hdRoot?.toXprv?.() || '';
4593
+ } else if (targetId === 'wallet-seed-phrase') {
4594
+ alert('Seed phrase not available. For security, the app does not retain the mnemonic after login.');
4595
+ return;
4596
+ } else {
4597
+ value = targetEl.textContent || '';
4598
+ }
4599
+ if (!value) {
4600
+ throw new Error('Nothing to copy');
4601
+ }
4602
+ await navigator.clipboard.writeText(value);
4344
4603
  btn.classList.add('copied');
4345
4604
  setTimeout(() => btn.classList.remove('copied'), 1500);
4346
4605
  } catch (err) {
@@ -4956,26 +5215,250 @@ function setupTrustHandlers() {
4956
5215
  // Encryption Tab Handlers (ECIES: ECDH + HKDF + AES-256-GCM)
4957
5216
  // =========================================================================
4958
5217
 
5218
+ const MESSAGING_KEY_CONFIG_KEY = 'hd-wallet-messaging-key-config-v1';
5219
+ const messagingKeyDefaults = Object.freeze({
5220
+ btc: { path: "m/44'/0'/0'/1/0", algorithm: 'secp256k1', publicKeyFormat: 'compressed' },
5221
+ eth: { path: "m/44'/60'/0'/1/0", algorithm: 'secp256k1', publicKeyFormat: 'uncompressed' },
5222
+ sol: { path: "m/44'/501'/0'/1/0", algorithm: 'x25519', publicKeyFormat: 'raw' },
5223
+ });
5224
+
5225
+ const wipeBytes = (u8) => {
5226
+ if (u8 instanceof Uint8Array) u8.fill(0);
5227
+ };
5228
+
5229
+ function getMessagingKeyType() {
5230
+ const v = $('messaging-key-type')?.value;
5231
+ return v === 'eth' || v === 'sol' ? v : 'btc';
5232
+ }
5233
+
5234
+ function getMessagingDefaultPath(keyType = getMessagingKeyType()) {
5235
+ return messagingKeyDefaults[keyType]?.path || messagingKeyDefaults.btc.path;
5236
+ }
5237
+
5238
+ function getMessagingHDPath(keyType = getMessagingKeyType()) {
5239
+ const el = $('messaging-hd-path');
5240
+ const raw = el?.value || '';
5241
+ const path = raw.trim();
5242
+ return path || getMessagingDefaultPath(keyType);
5243
+ }
5244
+
5245
+ function setMessagingRecipientPlaceholder(keyType = getMessagingKeyType()) {
5246
+ const input = $('encrypt-recipient-pubkey');
5247
+ if (!input) return;
5248
+ if (keyType === 'sol') {
5249
+ input.placeholder = "Paste recipient's X25519 public key (hex, 32 bytes)";
5250
+ return;
5251
+ }
5252
+ if (keyType === 'eth') {
5253
+ input.placeholder = "Paste recipient's secp256k1 public key (hex, 65 bytes preferred)";
5254
+ return;
5255
+ }
5256
+ input.placeholder = "Paste recipient's secp256k1 public key (hex, 33 bytes preferred)";
5257
+ }
5258
+
5259
+ function loadMessagingKeyConfig() {
5260
+ try {
5261
+ const raw = localStorage.getItem(MESSAGING_KEY_CONFIG_KEY);
5262
+ if (!raw) return null;
5263
+ const parsed = JSON.parse(raw);
5264
+ if (!parsed || typeof parsed !== 'object') return null;
5265
+ return {
5266
+ keyType: parsed.keyType,
5267
+ path: parsed.path,
5268
+ };
5269
+ } catch {
5270
+ return null;
5271
+ }
5272
+ }
5273
+
5274
+ function saveMessagingKeyConfig(keyType, path) {
5275
+ try {
5276
+ localStorage.setItem(MESSAGING_KEY_CONFIG_KEY, JSON.stringify({ keyType, path }));
5277
+ } catch {
5278
+ // ignore
5279
+ }
5280
+ }
5281
+
5282
+ function initMessagingKeyControls() {
5283
+ const keyTypeEl = $('messaging-key-type');
5284
+ const pathEl = $('messaging-hd-path');
5285
+ const resetBtn = $('messaging-hd-path-default');
5286
+ if (!keyTypeEl || !pathEl) return;
5287
+
5288
+ const saved = loadMessagingKeyConfig();
5289
+ const hasSaved = !!saved;
5290
+ if (saved?.keyType === 'btc' || saved?.keyType === 'eth' || saved?.keyType === 'sol') {
5291
+ keyTypeEl.value = saved.keyType;
5292
+ } else {
5293
+ keyTypeEl.value = 'btc';
5294
+ }
5295
+
5296
+ if (typeof saved?.path === 'string' && saved.path.trim()) {
5297
+ pathEl.value = saved.path.trim();
5298
+ } else if (hasSaved || !pathEl.value?.trim()) {
5299
+ pathEl.value = getMessagingDefaultPath(keyTypeEl.value);
5300
+ }
5301
+
5302
+ setMessagingRecipientPlaceholder(keyTypeEl.value);
5303
+
5304
+ const onChange = () => {
5305
+ const keyType = getMessagingKeyType();
5306
+ const path = getMessagingHDPath(keyType);
5307
+ saveMessagingKeyConfig(keyType, path);
5308
+ setMessagingRecipientPlaceholder(keyType);
5309
+ if (state.hdRoot) updateEncryptionTab();
5310
+ };
5311
+
5312
+ keyTypeEl.addEventListener('change', () => {
5313
+ const prev = pathEl.value?.trim();
5314
+ const prevDefaults = Object.values(messagingKeyDefaults).map(v => v.path);
5315
+ const nextKeyType = getMessagingKeyType();
5316
+ const nextDefault = getMessagingDefaultPath(nextKeyType);
5317
+ // If user hasn't customized, keep the path in sync with key type.
5318
+ if (!prev || prevDefaults.includes(prev)) {
5319
+ pathEl.value = nextDefault;
5320
+ }
5321
+ onChange();
5322
+ });
5323
+ pathEl.addEventListener('input', onChange);
5324
+ resetBtn?.addEventListener('click', () => {
5325
+ const keyType = getMessagingKeyType();
5326
+ pathEl.value = getMessagingDefaultPath(keyType);
5327
+ onChange();
5328
+ });
5329
+ }
5330
+
5331
+ function hexToBytesStrict(hex, expectedLen = null) {
5332
+ if (typeof hex !== 'string') throw new Error('Expected hex string');
5333
+ const cleaned = hex.trim().toLowerCase().replace(/^0x/, '');
5334
+ if (!cleaned) throw new Error('Empty hex string');
5335
+ if (cleaned.length % 2 !== 0) throw new Error('Invalid hex length');
5336
+ if (!/^[0-9a-f]+$/.test(cleaned)) throw new Error('Invalid hex string');
5337
+ const bytes = new Uint8Array(cleaned.length / 2);
5338
+ for (let i = 0; i < cleaned.length; i += 2) {
5339
+ bytes[i / 2] = parseInt(cleaned.slice(i, i + 2), 16);
5340
+ }
5341
+ if (expectedLen !== null && bytes.length !== expectedLen) {
5342
+ throw new Error(`Expected ${expectedLen} bytes, got ${bytes.length}`);
5343
+ }
5344
+ return bytes;
5345
+ }
5346
+
5347
+ function deriveKeyMaterialForMessaging(w, keyType, path) {
5348
+ if (!state.hdRoot || !w) throw new Error('HD wallet not initialized');
5349
+ const derived = deriveHDKey(path);
5350
+ try {
5351
+ if (keyType === 'sol') {
5352
+ const priv = derived.privateKey();
5353
+ const pub = w.curves.x25519.publicKey(priv);
5354
+ return { algorithm: 'x25519', privateKey: priv, publicKey: pub, path };
5355
+ }
5356
+
5357
+ const priv = derived.privateKey();
5358
+ const pubCompressed = derived.publicKey();
5359
+ if (keyType === 'eth') {
5360
+ const pub = derived.publicKeyUncompressed();
5361
+ return { algorithm: 'secp256k1', privateKey: priv, publicKey: pub, path };
5362
+ }
5363
+ return { algorithm: 'secp256k1', privateKey: priv, publicKey: pubCompressed, path };
5364
+ } finally {
5365
+ derived.wipe();
5366
+ }
5367
+ }
5368
+
5369
+ function deriveMessagingPublicKey(w, keyType, path) {
5370
+ if (!state.hdRoot || !w) throw new Error('HD wallet not initialized');
5371
+ const derived = deriveHDKey(path);
5372
+ try {
5373
+ if (keyType === 'sol') {
5374
+ const priv = derived.privateKey();
5375
+ try {
5376
+ return w.curves.x25519.publicKey(priv);
5377
+ } finally {
5378
+ wipeBytes(priv);
5379
+ }
5380
+ }
5381
+ if (keyType === 'eth') {
5382
+ return derived.publicKeyUncompressed();
5383
+ }
5384
+ return derived.publicKey();
5385
+ } finally {
5386
+ derived.wipe();
5387
+ }
5388
+ }
5389
+
5390
+ function normalizeSecp256k1PublicKeyBytes(publicKey) {
5391
+ if (!(publicKey instanceof Uint8Array)) throw new Error('Invalid public key');
5392
+ // Ethereum public keys are sometimes provided as raw 64-byte x||y without the 0x04 prefix.
5393
+ if (publicKey.length === 64) {
5394
+ const out = new Uint8Array(65);
5395
+ out[0] = 0x04;
5396
+ out.set(publicKey, 1);
5397
+ return out;
5398
+ }
5399
+ if (publicKey.length !== 33 && publicKey.length !== 65) {
5400
+ throw new Error('secp256k1 public key must be 33 (compressed) or 65 (uncompressed) bytes');
5401
+ }
5402
+ return publicKey;
5403
+ }
5404
+
5405
+ function normalizeRecipientPublicKeyForAlgorithm(algorithm, publicKey) {
5406
+ if (algorithm === 'x25519') {
5407
+ if (!(publicKey instanceof Uint8Array) || publicKey.length !== 32) {
5408
+ throw new Error('X25519 public key must be 32 bytes');
5409
+ }
5410
+ return publicKey;
5411
+ }
5412
+ return normalizeSecp256k1PublicKeyBytes(publicKey);
5413
+ }
5414
+
5415
+ function eciesInfoForAlgorithm(algorithm) {
5416
+ const infoStr = algorithm === 'x25519'
5417
+ ? 'ecies-x25519-aes256gcm'
5418
+ : 'ecies-secp256k1-aes256gcm';
5419
+ return new TextEncoder().encode(infoStr);
5420
+ }
5421
+
5422
+ function envelopeAlgorithmParameters(keyType, algorithm) {
5423
+ if (algorithm === 'x25519') return 'x25519';
5424
+ // secp256k1 modes
5425
+ return keyType === 'eth' ? 'secp256k1-uncompressed' : 'secp256k1-compressed';
5426
+ }
5427
+
4959
5428
  function updateEncryptionTab() {
4960
- if (!state.hdRoot || !state.hdWalletModule) return;
4961
- const coin = $('hd-coin')?.value || '0';
4962
- const account = $('hd-account')?.value || '0';
4963
- const index = $('hd-index')?.value || '0';
4964
- const encPath = buildEncryptionPath(coin, account, index);
4965
- const encKey = deriveHDKey(encPath);
4966
- const pubKey = encKey.publicKey();
4967
- const pubHex = toHexCompact(pubKey);
5429
+ const w = state.hdWalletModule;
5430
+ if (!state.hdRoot || !w) return;
5431
+
5432
+ const keyType = getMessagingKeyType();
5433
+ const path = getMessagingHDPath(keyType);
4968
5434
 
4969
5435
  const senderPubEl = $('encrypt-sender-pubkey');
4970
5436
  const senderPathEl = $('encrypt-sender-path');
4971
- if (senderPubEl) senderPubEl.textContent = pubHex;
4972
- if (senderPathEl) senderPathEl.textContent = encPath;
4973
-
5437
+ const senderAlgoEl = $('encrypt-sender-algo');
4974
5438
  const encryptBtn = $('encrypt-btn');
4975
- if (encryptBtn) encryptBtn.disabled = false;
5439
+
5440
+ if (senderPathEl) senderPathEl.textContent = path;
5441
+ const baseAlgo = messagingKeyDefaults[keyType]?.algorithm || '--';
5442
+ if (senderAlgoEl) {
5443
+ senderAlgoEl.textContent = baseAlgo === '--'
5444
+ ? '--'
5445
+ : envelopeAlgorithmParameters(keyType, baseAlgo);
5446
+ }
5447
+ if (encryptBtn) encryptBtn.disabled = true;
5448
+
5449
+ try {
5450
+ const publicKey = deriveMessagingPublicKey(w, keyType, path);
5451
+ if (senderPubEl) senderPubEl.textContent = toHexCompact(publicKey);
5452
+ if (encryptBtn) encryptBtn.disabled = false;
5453
+ } catch (e) {
5454
+ if (senderPubEl) senderPubEl.textContent = '--';
5455
+ if (senderAlgoEl) senderAlgoEl.textContent = 'invalid path';
5456
+ }
4976
5457
  }
4977
5458
 
4978
- // Update encryption tab when it becomes active or HD controls change
5459
+ initMessagingKeyControls();
5460
+
5461
+ // Update encryption tab when it becomes active
4979
5462
  $qa('.modal-tab[data-modal-tab="messaging-tab-content"]').forEach(tab => {
4980
5463
  tab.addEventListener('click', () => {
4981
5464
  if (state.hdRoot) updateEncryptionTab();
@@ -5049,7 +5532,10 @@ function setupTrustHandlers() {
5049
5532
  // Encrypt button
5050
5533
  $('encrypt-btn')?.addEventListener('click', () => {
5051
5534
  const w = state.hdWalletModule;
5052
- if (!w || !state.hdRoot) return;
5535
+ if (!w || !state.hdRoot) {
5536
+ alert('Please login first.');
5537
+ return;
5538
+ }
5053
5539
 
5054
5540
  const recipientHex = $('encrypt-recipient-pubkey')?.value?.trim();
5055
5541
  const plainStr = $('encrypt-plaintext')?.value;
@@ -5059,56 +5545,64 @@ function setupTrustHandlers() {
5059
5545
  }
5060
5546
 
5061
5547
  try {
5062
- const coin = $('hd-coin')?.value || '0';
5063
- const account = $('hd-account')?.value || '0';
5064
- const index = $('hd-index')?.value || '0';
5065
- const encPath = buildEncryptionPath(coin, account, index);
5066
- const senderKey = deriveHDKey(encPath);
5067
- const senderPriv = senderKey.privateKey();
5068
- const senderPub = senderKey.publicKey();
5069
-
5070
- // Parse recipient public key from hex
5071
- const recipientPub = new Uint8Array(recipientHex.match(/.{1,2}/g).map(b => parseInt(b, 16)));
5072
-
5073
- // 1. ECDH shared secret
5074
- const shared = w.curves.secp256k1.ecdh(senderPriv, recipientPub);
5075
-
5076
- // 2. HKDF: derive 32-byte AES key from shared secret
5077
- const salt = w.utils.getRandomBytes(32);
5078
- const info = new TextEncoder().encode('ecies-secp256k1-aes256gcm');
5079
- const aesKey = w.utils.hkdf(shared, salt, info, 32);
5080
-
5081
- // 3. AES-256-GCM encrypt
5082
- const iv = w.utils.generateIv();
5083
- const plaintext = new TextEncoder().encode(plainStr);
5084
- const { ciphertext, tag } = w.utils.aesGcm.encrypt(aesKey, plaintext, iv);
5085
-
5086
- // Display field-level results
5087
- $('encrypt-out-ciphertext').textContent = toHexCompact(ciphertext);
5088
- $('encrypt-out-tag').textContent = toHexCompact(tag);
5089
- $('encrypt-out-iv').textContent = toHexCompact(iv);
5090
- $('encrypt-out-salt').textContent = toHexCompact(salt);
5091
- $('encrypt-out-sender-pub').textContent = toHexCompact(senderPub);
5092
- // Build EME (Encrypted Message Envelope) standard object
5093
- currentEME = new EMET(
5094
- Array.from(ciphertext), // ENCRYPTED_BLOB
5095
- toHexCompact(senderPub), // EPHEMERAL_PUBLIC_KEY
5096
- null, // MAC (not used, tag covers it)
5097
- null, // NONCE (we use IV field instead)
5098
- toHexCompact(tag), // TAG
5099
- toHexCompact(iv), // IV
5100
- toHexCompact(salt), // SALT
5101
- null, // PUBLIC_KEY_IDENTIFIER
5102
- 'aes-256-gcm', // CIPHER_SUITE
5103
- 'hkdf-sha256', // KDF_PARAMETERS
5104
- 'secp256k1', // ENCRYPTION_ALGORITHM_PARAMETERS
5105
- );
5106
-
5107
- updateBundleDisplay();
5548
+ const keyType = getMessagingKeyType();
5549
+ const path = getMessagingHDPath(keyType);
5550
+ const { algorithm, privateKey: senderPriv, publicKey: senderPub } = deriveKeyMaterialForMessaging(w, keyType, path);
5108
5551
 
5109
- // Switch to result step
5110
- $('encrypt-step-compose').style.display = 'none';
5111
- $('encrypt-step-result').style.display = 'block';
5552
+ let shared = null;
5553
+ let aesKey = null;
5554
+ try {
5555
+ // Parse recipient public key from hex
5556
+ const recipientPubRaw = hexToBytesStrict(recipientHex);
5557
+ const recipientPub = normalizeRecipientPublicKeyForAlgorithm(algorithm, recipientPubRaw);
5558
+
5559
+ // 1. ECDH shared secret
5560
+ shared = algorithm === 'x25519'
5561
+ ? w.curves.x25519.ecdh(senderPriv, recipientPub)
5562
+ : w.curves.secp256k1.ecdh(senderPriv, recipientPub);
5563
+
5564
+ // 2. HKDF: derive 32-byte AES key from shared secret
5565
+ const salt = w.utils.getRandomBytes(32);
5566
+ const info = eciesInfoForAlgorithm(algorithm);
5567
+ aesKey = w.utils.hkdf(shared, salt, info, 32);
5568
+
5569
+ // 3. AES-256-GCM encrypt
5570
+ const iv = w.utils.generateIv();
5571
+ const plaintext = new TextEncoder().encode(plainStr);
5572
+ const { ciphertext, tag } = w.utils.aesGcm.encrypt(aesKey, plaintext, iv);
5573
+
5574
+ // Display field-level results
5575
+ $('encrypt-out-ciphertext').textContent = toHexCompact(ciphertext);
5576
+ $('encrypt-out-tag').textContent = toHexCompact(tag);
5577
+ $('encrypt-out-iv').textContent = toHexCompact(iv);
5578
+ $('encrypt-out-salt').textContent = toHexCompact(salt);
5579
+ $('encrypt-out-sender-pub').textContent = toHexCompact(senderPub);
5580
+
5581
+ // Build EME (Encrypted Message Envelope) standard object
5582
+ currentEME = new EMET(
5583
+ Array.from(ciphertext), // ENCRYPTED_BLOB
5584
+ toHexCompact(senderPub), // EPHEMERAL_PUBLIC_KEY
5585
+ null, // MAC (not used, tag covers it)
5586
+ null, // NONCE (we use IV field instead)
5587
+ toHexCompact(tag), // TAG
5588
+ toHexCompact(iv), // IV
5589
+ toHexCompact(salt), // SALT
5590
+ null, // PUBLIC_KEY_IDENTIFIER
5591
+ 'aes-256-gcm', // CIPHER_SUITE
5592
+ 'hkdf-sha256', // KDF_PARAMETERS
5593
+ envelopeAlgorithmParameters(keyType, algorithm), // ENCRYPTION_ALGORITHM_PARAMETERS
5594
+ );
5595
+
5596
+ updateBundleDisplay();
5597
+
5598
+ // Switch to result step
5599
+ $('encrypt-step-compose').style.display = 'none';
5600
+ $('encrypt-step-result').style.display = 'block';
5601
+ } finally {
5602
+ wipeBytes(senderPriv);
5603
+ wipeBytes(shared);
5604
+ wipeBytes(aesKey);
5605
+ }
5112
5606
  } catch (err) {
5113
5607
  console.error('Encryption failed:', err);
5114
5608
  alert('Encryption failed: ' + err.message);
@@ -5161,7 +5655,10 @@ function setupTrustHandlers() {
5161
5655
  // Decrypt button
5162
5656
  $('decrypt-btn')?.addEventListener('click', () => {
5163
5657
  const w = state.hdWalletModule;
5164
- if (!w || !state.hdRoot) return;
5658
+ if (!w || !state.hdRoot) {
5659
+ alert('Please login first.');
5660
+ return;
5661
+ }
5165
5662
 
5166
5663
  const payloadStr = $('decrypt-payload')?.value?.trim();
5167
5664
  if (!payloadStr) {
@@ -5171,44 +5668,63 @@ function setupTrustHandlers() {
5171
5668
 
5172
5669
  try {
5173
5670
  const payload = parseEMEPayload(payloadStr);
5174
- const fromHex = (h) => new Uint8Array(h.match(/.{1,2}/g).map(b => parseInt(b, 16)));
5175
-
5176
- const senderPub = fromHex(payload.EPHEMERAL_PUBLIC_KEY);
5177
- const tag = fromHex(payload.TAG);
5178
- const iv = fromHex(payload.IV);
5179
- const salt = fromHex(payload.SALT);
5671
+ const senderPubRaw = hexToBytesStrict(payload.EPHEMERAL_PUBLIC_KEY, null);
5672
+ const tag = hexToBytesStrict(payload.TAG, 16);
5673
+ const iv = hexToBytesStrict(payload.IV, 12);
5674
+ const salt = hexToBytesStrict(payload.SALT, 32);
5180
5675
 
5181
5676
  // ENCRYPTED_BLOB can be a number array (from EMET) or hex string
5182
5677
  let ciphertext;
5183
5678
  if (Array.isArray(payload.ENCRYPTED_BLOB)) {
5184
5679
  ciphertext = new Uint8Array(payload.ENCRYPTED_BLOB);
5185
5680
  } else {
5186
- ciphertext = fromHex(payload.ENCRYPTED_BLOB);
5681
+ ciphertext = hexToBytesStrict(payload.ENCRYPTED_BLOB, null);
5187
5682
  }
5188
5683
 
5189
- const coin = $('hd-coin')?.value || '0';
5190
- const account = $('hd-account')?.value || '0';
5191
- const index = $('hd-index')?.value || '0';
5192
- const encPath = buildEncryptionPath(coin, account, index);
5193
- const recipientKey = deriveHDKey(encPath);
5194
- const recipientPriv = recipientKey.privateKey();
5684
+ const keyType = getMessagingKeyType();
5685
+ const path = getMessagingHDPath(keyType);
5195
5686
 
5196
- // 1. ECDH shared secret (using sender's public key)
5197
- const shared = w.curves.secp256k1.ecdh(recipientPriv, senderPub);
5687
+ // Prefer payload algorithm; fall back to current UI selection.
5688
+ const algoParams = typeof payload.ENCRYPTION_ALGORITHM_PARAMETERS === 'string'
5689
+ ? payload.ENCRYPTION_ALGORITHM_PARAMETERS.toLowerCase()
5690
+ : '';
5691
+ const algorithm = algoParams.includes('x25519')
5692
+ ? 'x25519'
5693
+ : (messagingKeyDefaults[keyType]?.algorithm || 'secp256k1');
5198
5694
 
5199
- // 2. HKDF: derive same AES key
5200
- const info = new TextEncoder().encode('ecies-secp256k1-aes256gcm');
5201
- const aesKey = w.utils.hkdf(shared, salt, info, 32);
5695
+ const senderPub = normalizeRecipientPublicKeyForAlgorithm(algorithm, senderPubRaw);
5202
5696
 
5203
- // 3. AES-256-GCM decrypt
5204
- const decrypted = w.utils.aesGcm.decrypt(aesKey, ciphertext, tag, iv);
5205
- const decStr = new TextDecoder().decode(decrypted);
5697
+ // Derive recipient private key from configured path.
5698
+ const derived = deriveHDKey(path);
5699
+ const recipientPriv = derived.privateKey();
5700
+ derived.wipe();
5206
5701
 
5207
- $('decrypt-result-value').textContent = decStr;
5208
-
5209
- // Switch to result step
5210
- $('decrypt-step-input').style.display = 'none';
5211
- $('decrypt-step-result').style.display = 'block';
5702
+ let shared = null;
5703
+ let aesKey = null;
5704
+ try {
5705
+ // 1. ECDH shared secret (using sender's public key)
5706
+ shared = algorithm === 'x25519'
5707
+ ? w.curves.x25519.ecdh(recipientPriv, senderPub)
5708
+ : w.curves.secp256k1.ecdh(recipientPriv, senderPub);
5709
+
5710
+ // 2. HKDF: derive same AES key
5711
+ const info = eciesInfoForAlgorithm(algorithm);
5712
+ aesKey = w.utils.hkdf(shared, salt, info, 32);
5713
+
5714
+ // 3. AES-256-GCM decrypt
5715
+ const decrypted = w.utils.aesGcm.decrypt(aesKey, ciphertext, tag, iv);
5716
+ const decStr = new TextDecoder().decode(decrypted);
5717
+
5718
+ $('decrypt-result-value').textContent = decStr;
5719
+
5720
+ // Switch to result step
5721
+ $('decrypt-step-input').style.display = 'none';
5722
+ $('decrypt-step-result').style.display = 'block';
5723
+ } finally {
5724
+ wipeBytes(recipientPriv);
5725
+ wipeBytes(shared);
5726
+ wipeBytes(aesKey);
5727
+ }
5212
5728
  } catch (err) {
5213
5729
  console.error('Decryption failed:', err);
5214
5730
  alert('Decryption failed: ' + err.message);
@@ -5267,10 +5783,11 @@ function setupHomepageHandlers() {
5267
5783
  // =============================================================================
5268
5784
 
5269
5785
  export async function init(rootElement, options = {}) {
5270
- const { autoOpenWallet = false, onLogin = null } = typeof rootElement === 'object' && !(rootElement instanceof Node)
5786
+ const { autoOpenWallet = false, onLogin = null, openAccountAfterLogin = true } = typeof rootElement === 'object' && !(rootElement instanceof Node)
5271
5787
  ? (options = rootElement, {}) : options;
5272
5788
  if (rootElement && rootElement instanceof Node) _root = rootElement;
5273
5789
  if (typeof onLogin === 'function') _onLoginCallback = onLogin;
5790
+ _openAccountAfterLogin = openAccountAfterLogin;
5274
5791
 
5275
5792
  // Inject modal HTML if not already present in the DOM
5276
5793
  if (!document.getElementById('keys-modal')) {
@@ -5295,8 +5812,9 @@ export async function init(rootElement, options = {}) {
5295
5812
  if (status) status.textContent = 'Loading HD wallet module...';
5296
5813
  state.hdWalletModule = await initHDWallet();
5297
5814
 
5298
- // Load saved PKI keys if available
5299
- const hasSavedKeys = loadPKIKeys();
5815
+ // Load saved PKI keys if available.
5816
+ // (If not logged in yet, this will return false since encrypted keys require the session key.)
5817
+ const hasSavedKeys = await loadPKIKeys();
5300
5818
 
5301
5819
  state.initialized = true;
5302
5820
 
@@ -5382,7 +5900,10 @@ export async function init(rootElement, options = {}) {
5382
5900
  * @param {Node} [rootElement] - Optional root element for DOM queries
5383
5901
  * @param {Object} [options] - Options passed to init()
5384
5902
  * @param {Function} [options.onLogin] - Callback fired after successful login with
5385
- * `{ xpub, signingPublicKey, sign(message) }` for SDN identity (coin type 1957)
5903
+ * `{ xpub, signingPublicKey, sign(message) }` for SDN identity (BIP-44 coin type 0)
5904
+ * @param {boolean} [options.openAccountAfterLogin=true] - When false, the Account
5905
+ * modal will NOT auto-open after login. Useful for integrations that handle
5906
+ * post-login UX themselves (e.g. challenge-response auth flows).
5386
5907
  * @returns {Promise<{openLogin: Function, openAccount: Function, destroy: Function}>}
5387
5908
  */
5388
5909
  export async function createWalletUI(rootElement, options = {}) {