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/README.md +16 -1
- package/package.json +10 -5
- package/src/app.js +703 -182
- package/src/template.js +24 -0
- package/src/wallet-storage.js +148 -35
- package/styles/main.css +89 -0
- package/styles/widget.css +6089 -0
package/src/app.js
CHANGED
|
@@ -132,7 +132,6 @@ function bindInfoHandlers() {
|
|
|
132
132
|
|
|
133
133
|
function setTruncatedValue(el, value) {
|
|
134
134
|
if (!el) return;
|
|
135
|
-
el.dataset.fullValue = value;
|
|
136
135
|
el.textContent = middleTruncate(value, 17, 17);
|
|
137
136
|
}
|
|
138
137
|
|
|
@@ -145,6 +144,23 @@ function toBase64(arr) {
|
|
|
145
144
|
return btoa(String.fromCharCode(...arr));
|
|
146
145
|
}
|
|
147
146
|
|
|
147
|
+
function base64ToBytes(b64) {
|
|
148
|
+
const binary = atob(b64);
|
|
149
|
+
const bytes = new Uint8Array(binary.length);
|
|
150
|
+
for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
|
|
151
|
+
return bytes;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function bytesToBase64(bytes) {
|
|
155
|
+
// Avoid spreading large arrays into String.fromCharCode.
|
|
156
|
+
let binary = '';
|
|
157
|
+
const chunk = 0x8000;
|
|
158
|
+
for (let i = 0; i < bytes.length; i += chunk) {
|
|
159
|
+
binary += String.fromCharCode(...bytes.subarray(i, i + chunk));
|
|
160
|
+
}
|
|
161
|
+
return btoa(binary);
|
|
162
|
+
}
|
|
163
|
+
|
|
148
164
|
// =============================================================================
|
|
149
165
|
// SHA-256 and HKDF (WebCrypto-based)
|
|
150
166
|
// =============================================================================
|
|
@@ -164,6 +180,26 @@ async function hkdf(ikm, salt, info, length) {
|
|
|
164
180
|
return new Uint8Array(derived);
|
|
165
181
|
}
|
|
166
182
|
|
|
183
|
+
async function aesGcmEncryptJson(keyBytes, obj, aadStr) {
|
|
184
|
+
if (!(keyBytes instanceof Uint8Array)) throw new Error('Invalid AES key');
|
|
185
|
+
const iv = crypto.getRandomValues(new Uint8Array(12));
|
|
186
|
+
const cryptoKey = await crypto.subtle.importKey('raw', keyBytes, { name: 'AES-GCM' }, false, ['encrypt']);
|
|
187
|
+
const alg = { name: 'AES-GCM', iv };
|
|
188
|
+
if (aadStr) alg.additionalData = new TextEncoder().encode(aadStr);
|
|
189
|
+
const plaintext = new TextEncoder().encode(JSON.stringify(obj));
|
|
190
|
+
const ciphertext = await crypto.subtle.encrypt(alg, cryptoKey, plaintext);
|
|
191
|
+
return { iv, ciphertext: new Uint8Array(ciphertext) };
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
async function aesGcmDecryptJson(keyBytes, iv, ciphertextBytes, aadStr) {
|
|
195
|
+
if (!(keyBytes instanceof Uint8Array)) throw new Error('Invalid AES key');
|
|
196
|
+
const cryptoKey = await crypto.subtle.importKey('raw', keyBytes, { name: 'AES-GCM' }, false, ['decrypt']);
|
|
197
|
+
const alg = { name: 'AES-GCM', iv };
|
|
198
|
+
if (aadStr) alg.additionalData = new TextEncoder().encode(aadStr);
|
|
199
|
+
const plaintext = await crypto.subtle.decrypt(alg, cryptoKey, ciphertextBytes);
|
|
200
|
+
return JSON.parse(new TextDecoder().decode(plaintext));
|
|
201
|
+
}
|
|
202
|
+
|
|
167
203
|
// =============================================================================
|
|
168
204
|
// Key Generation
|
|
169
205
|
// =============================================================================
|
|
@@ -207,6 +243,10 @@ async function p384GenerateKeyPairAsync() {
|
|
|
207
243
|
// Integration callback — set via options.onLogin in createWalletUI / init
|
|
208
244
|
let _onLoginCallback = null;
|
|
209
245
|
|
|
246
|
+
// When false, login() will NOT auto-open the Account modal after authentication.
|
|
247
|
+
// Set via options.openAccountAfterLogin in createWalletUI / init (default: true).
|
|
248
|
+
let _openAccountAfterLogin = true;
|
|
249
|
+
|
|
210
250
|
const state = {
|
|
211
251
|
initialized: false,
|
|
212
252
|
loggedIn: false,
|
|
@@ -328,49 +368,52 @@ async function deriveKeysFromPassword(username, password) {
|
|
|
328
368
|
const initialHash = await sha256(new Uint8Array([...usernameSalt, ...passwordBytes]));
|
|
329
369
|
const masterKey = await hkdf(initialHash, usernameSalt, encoder.encode('master-key'), 32);
|
|
330
370
|
|
|
331
|
-
state.encryptionKey = await hkdf(masterKey, new Uint8Array(0), encoder.encode('buffer-encryption-key'), 32);
|
|
332
|
-
state.encryptionIV = await hkdf(masterKey, new Uint8Array(0), encoder.encode('buffer-encryption-iv'), 16);
|
|
333
|
-
|
|
334
371
|
// Create 64-byte seed for HD wallet (password-based, not BIP39)
|
|
335
372
|
const hdSeed = await hkdf(masterKey, new Uint8Array(0), encoder.encode('hd-wallet-seed'), 64);
|
|
336
|
-
state.masterSeed = hdSeed;
|
|
337
|
-
state.hdRoot = state.hdWalletModule.hdkey.fromSeed(hdSeed);
|
|
338
|
-
state.mnemonic = null; // Not available for password-derived wallets
|
|
339
|
-
console.log('HD wallet initialized from password, hdRoot:', !!state.hdRoot);
|
|
340
|
-
|
|
341
|
-
const keys = deriveKeysFromHDRoot(state.hdRoot);
|
|
342
|
-
// Also derive auxiliary keys for encryption / key agreement
|
|
343
|
-
keys.x25519 = generateKeyPair(Curve.X25519);
|
|
344
|
-
keys.p256 = await p256GenerateKeyPairAsync();
|
|
345
|
-
keys.p384 = await p384GenerateKeyPairAsync();
|
|
346
373
|
|
|
347
|
-
|
|
374
|
+
try {
|
|
375
|
+
// Derive session keys from the master seed so "remember wallet" can unlock without
|
|
376
|
+
// storing the user password/seed phrase at rest.
|
|
377
|
+
return await deriveKeysFromMasterSeed(hdSeed);
|
|
378
|
+
} finally {
|
|
379
|
+
// Best-effort JS-layer cleanup (strings cannot be wiped).
|
|
380
|
+
passwordBytes.fill(0);
|
|
381
|
+
initialHash.fill(0);
|
|
382
|
+
masterKey.fill(0);
|
|
383
|
+
hdSeed.fill(0);
|
|
384
|
+
}
|
|
348
385
|
}
|
|
349
386
|
|
|
350
387
|
async function deriveKeysFromSeed(seedPhrase) {
|
|
351
|
-
const seed = state.hdWalletModule.mnemonic.toSeed(seedPhrase);
|
|
352
388
|
const encoder = new TextEncoder();
|
|
389
|
+
const seed = state.hdWalletModule.mnemonic.toSeed(seedPhrase);
|
|
390
|
+
const seedBytes = seed instanceof Uint8Array ? seed : new Uint8Array(seed);
|
|
353
391
|
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
392
|
+
try {
|
|
393
|
+
return await deriveKeysFromMasterSeed(seedBytes);
|
|
394
|
+
} finally {
|
|
395
|
+
// Don't retain the seed phrase in JS state.
|
|
396
|
+
// (Seed phrase strings can't be wiped; we just avoid storing them.)
|
|
397
|
+
seedBytes.fill(0);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
async function deriveKeysFromMasterSeed(masterSeedBytes) {
|
|
402
|
+
const encoder = new TextEncoder();
|
|
360
403
|
|
|
361
|
-
state
|
|
362
|
-
state.
|
|
404
|
+
// Copy seed into state; callers can wipe their input buffer.
|
|
405
|
+
state.masterSeed = new Uint8Array(masterSeedBytes);
|
|
406
|
+
state.hdRoot = state.hdWalletModule.hdkey.fromSeed(state.masterSeed);
|
|
407
|
+
state.mnemonic = null;
|
|
363
408
|
|
|
364
|
-
|
|
365
|
-
state.
|
|
366
|
-
state.
|
|
367
|
-
console.log('HD wallet initialized from seed phrase, hdRoot:', !!state.hdRoot);
|
|
409
|
+
// Session encryption key for local encrypted blobs (PKI, etc).
|
|
410
|
+
state.encryptionKey = await hkdf(state.masterSeed, new Uint8Array(0), encoder.encode('buffer-encryption-key'), 32);
|
|
411
|
+
state.encryptionIV = await hkdf(state.masterSeed, new Uint8Array(0), encoder.encode('buffer-encryption-iv'), 16);
|
|
368
412
|
|
|
369
413
|
const keys = deriveKeysFromHDRoot(state.hdRoot);
|
|
370
414
|
keys.x25519 = generateKeyPair(Curve.X25519);
|
|
371
415
|
keys.p256 = await p256GenerateKeyPairAsync();
|
|
372
416
|
keys.p384 = await p384GenerateKeyPairAsync();
|
|
373
|
-
|
|
374
417
|
return keys;
|
|
375
418
|
}
|
|
376
419
|
|
|
@@ -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
|
-
|
|
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
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
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
|
-
|
|
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.
|
|
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(
|
|
2233
|
-
privateKey: hexToBytes(
|
|
2378
|
+
publicKey: hexToBytes(plaintext.alice.publicKey),
|
|
2379
|
+
privateKey: hexToBytes(plaintext.alice.privateKey),
|
|
2234
2380
|
};
|
|
2235
2381
|
state.pki.bob = {
|
|
2236
|
-
publicKey: hexToBytes(
|
|
2237
|
-
privateKey: hexToBytes(
|
|
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 =
|
|
2257
|
-
if (alicePublicKey) alicePublicKey.textContent =
|
|
2258
|
-
if (alicePrivateKey) alicePrivateKey.textContent =
|
|
2259
|
-
if (bobPublicKey) bobPublicKey.textContent =
|
|
2260
|
-
if (bobPrivateKey) bobPrivateKey.textContent =
|
|
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
|
|
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,
|
|
2385
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
2599
|
+
xprvEl.textContent = 'Hidden (click reveal)';
|
|
2444
2600
|
xprvEl.dataset.revealed = 'false';
|
|
2445
2601
|
}
|
|
2446
|
-
if (seedEl
|
|
2447
|
-
seedEl.textContent =
|
|
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
|
|
2504
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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: '
|
|
3876
|
+
type: 'masterSeed',
|
|
3877
|
+
source: 'password',
|
|
3695
3878
|
username,
|
|
3696
|
-
|
|
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: '
|
|
3794
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4961
|
-
|
|
4962
|
-
|
|
4963
|
-
const
|
|
4964
|
-
const
|
|
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
|
-
|
|
4972
|
-
if (senderPathEl) senderPathEl.textContent = encPath;
|
|
4973
|
-
|
|
5437
|
+
const senderAlgoEl = $('encrypt-sender-algo');
|
|
4974
5438
|
const encryptBtn = $('encrypt-btn');
|
|
4975
|
-
|
|
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
|
-
|
|
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)
|
|
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
|
|
5063
|
-
const
|
|
5064
|
-
const
|
|
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
|
-
|
|
5110
|
-
|
|
5111
|
-
|
|
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)
|
|
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
|
|
5175
|
-
|
|
5176
|
-
const
|
|
5177
|
-
const
|
|
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 =
|
|
5681
|
+
ciphertext = hexToBytesStrict(payload.ENCRYPTED_BLOB, null);
|
|
5187
5682
|
}
|
|
5188
5683
|
|
|
5189
|
-
const
|
|
5190
|
-
const
|
|
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
|
-
//
|
|
5197
|
-
const
|
|
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
|
-
|
|
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
|
-
//
|
|
5204
|
-
const
|
|
5205
|
-
const
|
|
5697
|
+
// Derive recipient private key from configured path.
|
|
5698
|
+
const derived = deriveHDKey(path);
|
|
5699
|
+
const recipientPriv = derived.privateKey();
|
|
5700
|
+
derived.wipe();
|
|
5206
5701
|
|
|
5207
|
-
|
|
5208
|
-
|
|
5209
|
-
|
|
5210
|
-
|
|
5211
|
-
|
|
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
|
-
|
|
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
|
|
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 = {}) {
|