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/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
+ }
360
400
 
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);
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
- 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
 
@@ -2191,7 +2234,14 @@ function savePKIKeys() {
2191
2234
  return;
2192
2235
  }
2193
2236
 
2194
- const data = {
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
- 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
- }
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
- if (!data.alice || !data.bob || !data.algorithm) {
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.pki.algorithm = data.algorithm;
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(data.alice.publicKey),
2233
- privateKey: hexToBytes(data.alice.privateKey),
2320
+ publicKey: hexToBytes(plaintext.alice.publicKey),
2321
+ privateKey: hexToBytes(plaintext.alice.privateKey),
2234
2322
  };
2235
2323
  state.pki.bob = {
2236
- publicKey: hexToBytes(data.bob.publicKey),
2237
- privateKey: hexToBytes(data.bob.privateKey),
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 = 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;
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 1957 — Sputnik)
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, 1957, 0, 0);
2385
- const sdnPubKey = ed25519.getPublicKey(sdnSigning.privateKey);
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
- return ed25519.sign(msgBytes, sdnSigning.privateKey);
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
- setTruncatedValue(xprvEl, state.hdRoot.toXprv() || 'N/A');
2541
+ xprvEl.textContent = 'Hidden (click reveal)';
2444
2542
  xprvEl.dataset.revealed = 'false';
2445
2543
  }
2446
- if (seedEl && state.mnemonic) {
2447
- seedEl.textContent = state.mnemonic;
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 if (!loadPKIKeys()) {
2504
- generatePKIKeyPairs();
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
- $('keys-modal')?.classList.add('active');
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. This wallet was derived from a password.');
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
- console.log('Keys derived, hdRoot after derivation:', !!state.hdRoot);
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: 'password',
3818
+ type: 'masterSeed',
3819
+ source: 'password',
3682
3820
  username,
3683
- password,
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: 'seed',
3781
- seedPhrase: phrase,
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
- if (walletData.type === 'password') {
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 type');
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
- if (walletData.type === 'password') {
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 type');
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
- targetEl.dataset.revealed = isRevealed ? 'false' : 'true';
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
- await navigator.clipboard.writeText(targetEl.dataset.fullValue || targetEl.textContent);
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
- if (!state.hdRoot || !state.hdWalletModule) return;
4948
- const coin = $('hd-coin')?.value || '0';
4949
- const account = $('hd-account')?.value || '0';
4950
- const index = $('hd-index')?.value || '0';
4951
- const encPath = buildEncryptionPath(coin, account, index);
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
- if (senderPubEl) senderPubEl.textContent = pubHex;
4959
- if (senderPathEl) senderPathEl.textContent = encPath;
4960
-
5379
+ const senderAlgoEl = $('encrypt-sender-algo');
4961
5380
  const encryptBtn = $('encrypt-btn');
4962
- if (encryptBtn) encryptBtn.disabled = false;
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
- // Update encryption tab when it becomes active or HD controls change
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) return;
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 coin = $('hd-coin')?.value || '0';
5050
- const account = $('hd-account')?.value || '0';
5051
- const index = $('hd-index')?.value || '0';
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
- // Switch to result step
5097
- $('encrypt-step-compose').style.display = 'none';
5098
- $('encrypt-step-result').style.display = 'block';
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) return;
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 fromHex = (h) => new Uint8Array(h.match(/.{1,2}/g).map(b => parseInt(b, 16)));
5162
-
5163
- const senderPub = fromHex(payload.EPHEMERAL_PUBLIC_KEY);
5164
- const tag = fromHex(payload.TAG);
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 = fromHex(payload.ENCRYPTED_BLOB);
5623
+ ciphertext = hexToBytesStrict(payload.ENCRYPTED_BLOB, null);
5174
5624
  }
5175
5625
 
5176
- const coin = $('hd-coin')?.value || '0';
5177
- const account = $('hd-account')?.value || '0';
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
- // 2. HKDF: derive same AES key
5187
- const info = new TextEncoder().encode('ecies-secp256k1-aes256gcm');
5188
- const aesKey = w.utils.hkdf(shared, salt, info, 32);
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
- // 3. AES-256-GCM decrypt
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
- $('decrypt-result-value').textContent = decStr;
5639
+ // Derive recipient private key from configured path.
5640
+ const derived = deriveHDKey(path);
5641
+ const recipientPriv = derived.privateKey();
5642
+ derived.wipe();
5195
5643
 
5196
- // Switch to result step
5197
- $('decrypt-step-input').style.display = 'none';
5198
- $('decrypt-step-result').style.display = 'block';
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
- const hasSavedKeys = loadPKIKeys();
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 1957)
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 = {}) {