hd-wallet-ui 1.2.6 → 1.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -1
- package/package.json +10 -5
- package/src/app.js +644 -181
- package/src/template.js +24 -0
- package/src/wallet-storage.js +153 -36
- package/styles/main.css +47 -0
- package/styles/widget.css +6047 -0
package/src/app.js
CHANGED
|
@@ -132,7 +132,6 @@ function bindInfoHandlers() {
|
|
|
132
132
|
|
|
133
133
|
function setTruncatedValue(el, value) {
|
|
134
134
|
if (!el) return;
|
|
135
|
-
el.dataset.fullValue = value;
|
|
136
135
|
el.textContent = middleTruncate(value, 17, 17);
|
|
137
136
|
}
|
|
138
137
|
|
|
@@ -145,6 +144,23 @@ function toBase64(arr) {
|
|
|
145
144
|
return btoa(String.fromCharCode(...arr));
|
|
146
145
|
}
|
|
147
146
|
|
|
147
|
+
function base64ToBytes(b64) {
|
|
148
|
+
const binary = atob(b64);
|
|
149
|
+
const bytes = new Uint8Array(binary.length);
|
|
150
|
+
for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
|
|
151
|
+
return bytes;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function bytesToBase64(bytes) {
|
|
155
|
+
// Avoid spreading large arrays into String.fromCharCode.
|
|
156
|
+
let binary = '';
|
|
157
|
+
const chunk = 0x8000;
|
|
158
|
+
for (let i = 0; i < bytes.length; i += chunk) {
|
|
159
|
+
binary += String.fromCharCode(...bytes.subarray(i, i + chunk));
|
|
160
|
+
}
|
|
161
|
+
return btoa(binary);
|
|
162
|
+
}
|
|
163
|
+
|
|
148
164
|
// =============================================================================
|
|
149
165
|
// SHA-256 and HKDF (WebCrypto-based)
|
|
150
166
|
// =============================================================================
|
|
@@ -164,6 +180,26 @@ async function hkdf(ikm, salt, info, length) {
|
|
|
164
180
|
return new Uint8Array(derived);
|
|
165
181
|
}
|
|
166
182
|
|
|
183
|
+
async function aesGcmEncryptJson(keyBytes, obj, aadStr) {
|
|
184
|
+
if (!(keyBytes instanceof Uint8Array)) throw new Error('Invalid AES key');
|
|
185
|
+
const iv = crypto.getRandomValues(new Uint8Array(12));
|
|
186
|
+
const cryptoKey = await crypto.subtle.importKey('raw', keyBytes, { name: 'AES-GCM' }, false, ['encrypt']);
|
|
187
|
+
const alg = { name: 'AES-GCM', iv };
|
|
188
|
+
if (aadStr) alg.additionalData = new TextEncoder().encode(aadStr);
|
|
189
|
+
const plaintext = new TextEncoder().encode(JSON.stringify(obj));
|
|
190
|
+
const ciphertext = await crypto.subtle.encrypt(alg, cryptoKey, plaintext);
|
|
191
|
+
return { iv, ciphertext: new Uint8Array(ciphertext) };
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
async function aesGcmDecryptJson(keyBytes, iv, ciphertextBytes, aadStr) {
|
|
195
|
+
if (!(keyBytes instanceof Uint8Array)) throw new Error('Invalid AES key');
|
|
196
|
+
const cryptoKey = await crypto.subtle.importKey('raw', keyBytes, { name: 'AES-GCM' }, false, ['decrypt']);
|
|
197
|
+
const alg = { name: 'AES-GCM', iv };
|
|
198
|
+
if (aadStr) alg.additionalData = new TextEncoder().encode(aadStr);
|
|
199
|
+
const plaintext = await crypto.subtle.decrypt(alg, cryptoKey, ciphertextBytes);
|
|
200
|
+
return JSON.parse(new TextDecoder().decode(plaintext));
|
|
201
|
+
}
|
|
202
|
+
|
|
167
203
|
// =============================================================================
|
|
168
204
|
// Key Generation
|
|
169
205
|
// =============================================================================
|
|
@@ -207,6 +243,10 @@ async function p384GenerateKeyPairAsync() {
|
|
|
207
243
|
// Integration callback — set via options.onLogin in createWalletUI / init
|
|
208
244
|
let _onLoginCallback = null;
|
|
209
245
|
|
|
246
|
+
// When false, login() will NOT auto-open the Account modal after authentication.
|
|
247
|
+
// Set via options.openAccountAfterLogin in createWalletUI / init (default: true).
|
|
248
|
+
let _openAccountAfterLogin = true;
|
|
249
|
+
|
|
210
250
|
const state = {
|
|
211
251
|
initialized: false,
|
|
212
252
|
loggedIn: false,
|
|
@@ -328,49 +368,52 @@ async function deriveKeysFromPassword(username, password) {
|
|
|
328
368
|
const initialHash = await sha256(new Uint8Array([...usernameSalt, ...passwordBytes]));
|
|
329
369
|
const masterKey = await hkdf(initialHash, usernameSalt, encoder.encode('master-key'), 32);
|
|
330
370
|
|
|
331
|
-
state.encryptionKey = await hkdf(masterKey, new Uint8Array(0), encoder.encode('buffer-encryption-key'), 32);
|
|
332
|
-
state.encryptionIV = await hkdf(masterKey, new Uint8Array(0), encoder.encode('buffer-encryption-iv'), 16);
|
|
333
|
-
|
|
334
371
|
// Create 64-byte seed for HD wallet (password-based, not BIP39)
|
|
335
372
|
const hdSeed = await hkdf(masterKey, new Uint8Array(0), encoder.encode('hd-wallet-seed'), 64);
|
|
336
|
-
state.masterSeed = hdSeed;
|
|
337
|
-
state.hdRoot = state.hdWalletModule.hdkey.fromSeed(hdSeed);
|
|
338
|
-
state.mnemonic = null; // Not available for password-derived wallets
|
|
339
|
-
console.log('HD wallet initialized from password, hdRoot:', !!state.hdRoot);
|
|
340
|
-
|
|
341
|
-
const keys = deriveKeysFromHDRoot(state.hdRoot);
|
|
342
|
-
// Also derive auxiliary keys for encryption / key agreement
|
|
343
|
-
keys.x25519 = generateKeyPair(Curve.X25519);
|
|
344
|
-
keys.p256 = await p256GenerateKeyPairAsync();
|
|
345
|
-
keys.p384 = await p384GenerateKeyPairAsync();
|
|
346
373
|
|
|
347
|
-
|
|
374
|
+
try {
|
|
375
|
+
// Derive session keys from the master seed so "remember wallet" can unlock without
|
|
376
|
+
// storing the user password/seed phrase at rest.
|
|
377
|
+
return await deriveKeysFromMasterSeed(hdSeed);
|
|
378
|
+
} finally {
|
|
379
|
+
// Best-effort JS-layer cleanup (strings cannot be wiped).
|
|
380
|
+
passwordBytes.fill(0);
|
|
381
|
+
initialHash.fill(0);
|
|
382
|
+
masterKey.fill(0);
|
|
383
|
+
hdSeed.fill(0);
|
|
384
|
+
}
|
|
348
385
|
}
|
|
349
386
|
|
|
350
387
|
async function deriveKeysFromSeed(seedPhrase) {
|
|
351
|
-
const seed = state.hdWalletModule.mnemonic.toSeed(seedPhrase);
|
|
352
388
|
const encoder = new TextEncoder();
|
|
389
|
+
const seed = state.hdWalletModule.mnemonic.toSeed(seedPhrase);
|
|
390
|
+
const seedBytes = seed instanceof Uint8Array ? seed : new Uint8Array(seed);
|
|
353
391
|
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
392
|
+
try {
|
|
393
|
+
return await deriveKeysFromMasterSeed(seedBytes);
|
|
394
|
+
} finally {
|
|
395
|
+
// Don't retain the seed phrase in JS state.
|
|
396
|
+
// (Seed phrase strings can't be wiped; we just avoid storing them.)
|
|
397
|
+
seedBytes.fill(0);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
360
400
|
|
|
361
|
-
|
|
362
|
-
|
|
401
|
+
async function deriveKeysFromMasterSeed(masterSeedBytes) {
|
|
402
|
+
const encoder = new TextEncoder();
|
|
403
|
+
|
|
404
|
+
// Copy seed into state; callers can wipe their input buffer.
|
|
405
|
+
state.masterSeed = new Uint8Array(masterSeedBytes);
|
|
406
|
+
state.hdRoot = state.hdWalletModule.hdkey.fromSeed(state.masterSeed);
|
|
407
|
+
state.mnemonic = null;
|
|
363
408
|
|
|
364
|
-
|
|
365
|
-
state.
|
|
366
|
-
state.
|
|
367
|
-
console.log('HD wallet initialized from seed phrase, hdRoot:', !!state.hdRoot);
|
|
409
|
+
// Session encryption key for local encrypted blobs (PKI, etc).
|
|
410
|
+
state.encryptionKey = await hkdf(state.masterSeed, new Uint8Array(0), encoder.encode('buffer-encryption-key'), 32);
|
|
411
|
+
state.encryptionIV = await hkdf(state.masterSeed, new Uint8Array(0), encoder.encode('buffer-encryption-iv'), 16);
|
|
368
412
|
|
|
369
413
|
const keys = deriveKeysFromHDRoot(state.hdRoot);
|
|
370
414
|
keys.x25519 = generateKeyPair(Curve.X25519);
|
|
371
415
|
keys.p256 = await p256GenerateKeyPairAsync();
|
|
372
416
|
keys.p384 = await p384GenerateKeyPairAsync();
|
|
373
|
-
|
|
374
417
|
return keys;
|
|
375
418
|
}
|
|
376
419
|
|
|
@@ -2191,7 +2234,14 @@ function savePKIKeys() {
|
|
|
2191
2234
|
return;
|
|
2192
2235
|
}
|
|
2193
2236
|
|
|
2194
|
-
|
|
2237
|
+
// SECURITY: Never persist private keys in plaintext localStorage.
|
|
2238
|
+
// Persist encrypted only when a session encryption key exists (i.e., after wallet login).
|
|
2239
|
+
if (!(state.encryptionKey instanceof Uint8Array) || state.encryptionKey.length < 16) {
|
|
2240
|
+
console.warn('Skipping PKI key persistence: session encryption key not available (login required)');
|
|
2241
|
+
return;
|
|
2242
|
+
}
|
|
2243
|
+
|
|
2244
|
+
const plaintext = {
|
|
2195
2245
|
algorithm: state.pki.algorithm,
|
|
2196
2246
|
alice: {
|
|
2197
2247
|
publicKey: toHexCompact(state.pki.alice.publicKey),
|
|
@@ -2204,44 +2254,77 @@ function savePKIKeys() {
|
|
|
2204
2254
|
savedAt: new Date().toISOString(),
|
|
2205
2255
|
};
|
|
2206
2256
|
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2257
|
+
aesGcmEncryptJson(state.encryptionKey, plaintext, 'wallet-ui|pki-keys')
|
|
2258
|
+
.then(({ iv, ciphertext }) => {
|
|
2259
|
+
const stored = {
|
|
2260
|
+
v: 1,
|
|
2261
|
+
iv: bytesToBase64(iv),
|
|
2262
|
+
ciphertext: bytesToBase64(ciphertext),
|
|
2263
|
+
};
|
|
2264
|
+
localStorage.setItem(PKI_STORAGE_KEY, JSON.stringify(stored));
|
|
2265
|
+
})
|
|
2266
|
+
.catch((e) => {
|
|
2267
|
+
console.warn('Failed to encrypt+save PKI keys to localStorage:', e);
|
|
2268
|
+
});
|
|
2217
2269
|
}
|
|
2218
2270
|
|
|
2219
|
-
function loadPKIKeys() {
|
|
2271
|
+
async function loadPKIKeys() {
|
|
2220
2272
|
try {
|
|
2221
2273
|
const stored = localStorage.getItem(PKI_STORAGE_KEY);
|
|
2222
2274
|
if (!stored) return false;
|
|
2223
2275
|
|
|
2224
2276
|
const data = JSON.parse(stored);
|
|
2225
|
-
|
|
2277
|
+
const hasEncryptedShape = data && typeof data === 'object' && typeof data.iv === 'string' && typeof data.ciphertext === 'string';
|
|
2278
|
+
|
|
2279
|
+
// Legacy plaintext format (insecure): refuse to load until logged in, then upgrade.
|
|
2280
|
+
const hasLegacyPlaintextShape = data?.alice?.privateKey && data?.bob?.privateKey && data?.algorithm;
|
|
2281
|
+
|
|
2282
|
+
if (!hasEncryptedShape && !hasLegacyPlaintextShape) {
|
|
2226
2283
|
console.warn('Invalid PKI data in localStorage');
|
|
2227
2284
|
return false;
|
|
2228
2285
|
}
|
|
2229
2286
|
|
|
2230
|
-
state.
|
|
2287
|
+
if (!(state.encryptionKey instanceof Uint8Array) || state.encryptionKey.length < 16) {
|
|
2288
|
+
// Not logged in yet; don't load private keys.
|
|
2289
|
+
return false;
|
|
2290
|
+
}
|
|
2291
|
+
|
|
2292
|
+
let plaintext;
|
|
2293
|
+
if (hasEncryptedShape) {
|
|
2294
|
+
const iv = base64ToBytes(data.iv);
|
|
2295
|
+
const ciphertext = base64ToBytes(data.ciphertext);
|
|
2296
|
+
plaintext = await aesGcmDecryptJson(state.encryptionKey, iv, ciphertext, 'wallet-ui|pki-keys');
|
|
2297
|
+
} else {
|
|
2298
|
+
// Legacy plaintext: load and immediately re-encrypt on next save.
|
|
2299
|
+
plaintext = data;
|
|
2300
|
+
// Upgrade-in-place.
|
|
2301
|
+
try {
|
|
2302
|
+
const { iv, ciphertext } = await aesGcmEncryptJson(state.encryptionKey, plaintext, 'wallet-ui|pki-keys');
|
|
2303
|
+
localStorage.setItem(PKI_STORAGE_KEY, JSON.stringify({
|
|
2304
|
+
v: 1,
|
|
2305
|
+
iv: bytesToBase64(iv),
|
|
2306
|
+
ciphertext: bytesToBase64(ciphertext),
|
|
2307
|
+
}));
|
|
2308
|
+
} catch (e) {
|
|
2309
|
+
console.warn('Failed to upgrade legacy plaintext PKI storage:', e);
|
|
2310
|
+
}
|
|
2311
|
+
}
|
|
2312
|
+
|
|
2313
|
+
if (!plaintext?.alice || !plaintext?.bob || !plaintext?.algorithm) {
|
|
2314
|
+
console.warn('Invalid decrypted PKI data');
|
|
2315
|
+
return false;
|
|
2316
|
+
}
|
|
2317
|
+
|
|
2318
|
+
state.pki.algorithm = plaintext.algorithm;
|
|
2231
2319
|
state.pki.alice = {
|
|
2232
|
-
publicKey: hexToBytes(
|
|
2233
|
-
privateKey: hexToBytes(
|
|
2320
|
+
publicKey: hexToBytes(plaintext.alice.publicKey),
|
|
2321
|
+
privateKey: hexToBytes(plaintext.alice.privateKey),
|
|
2234
2322
|
};
|
|
2235
2323
|
state.pki.bob = {
|
|
2236
|
-
publicKey: hexToBytes(
|
|
2237
|
-
privateKey: hexToBytes(
|
|
2324
|
+
publicKey: hexToBytes(plaintext.bob.publicKey),
|
|
2325
|
+
privateKey: hexToBytes(plaintext.bob.privateKey),
|
|
2238
2326
|
};
|
|
2239
2327
|
|
|
2240
|
-
if (data.encryptionKey && data.encryptionIV) {
|
|
2241
|
-
state.encryptionKey = hexToBytes(data.encryptionKey);
|
|
2242
|
-
state.encryptionIV = hexToBytes(data.encryptionIV);
|
|
2243
|
-
}
|
|
2244
|
-
|
|
2245
2328
|
// Update UI
|
|
2246
2329
|
const alicePublicKey = $('alice-public-key');
|
|
2247
2330
|
const alicePrivateKey = $('alice-private-key');
|
|
@@ -2253,11 +2336,11 @@ function loadPKIKeys() {
|
|
|
2253
2336
|
const pkiClearKeys = $('pki-clear-keys');
|
|
2254
2337
|
|
|
2255
2338
|
const pkiAlgorithm = $('pki-algorithm');
|
|
2256
|
-
if (pkiAlgorithm) pkiAlgorithm.value =
|
|
2257
|
-
if (alicePublicKey) alicePublicKey.textContent =
|
|
2258
|
-
if (alicePrivateKey) alicePrivateKey.textContent =
|
|
2259
|
-
if (bobPublicKey) bobPublicKey.textContent =
|
|
2260
|
-
if (bobPrivateKey) bobPrivateKey.textContent =
|
|
2339
|
+
if (pkiAlgorithm) pkiAlgorithm.value = plaintext.algorithm;
|
|
2340
|
+
if (alicePublicKey) alicePublicKey.textContent = plaintext.alice.publicKey;
|
|
2341
|
+
if (alicePrivateKey) alicePrivateKey.textContent = plaintext.alice.privateKey;
|
|
2342
|
+
if (bobPublicKey) bobPublicKey.textContent = plaintext.bob.publicKey;
|
|
2343
|
+
if (bobPrivateKey) bobPrivateKey.textContent = plaintext.bob.privateKey;
|
|
2261
2344
|
if (pkiParties) pkiParties.style.display = 'grid';
|
|
2262
2345
|
if (pkiDemo) pkiDemo.style.display = 'block';
|
|
2263
2346
|
if (pkiSecurity) pkiSecurity.style.display = 'block';
|
|
@@ -2277,6 +2360,13 @@ function clearPKIKeys() {
|
|
|
2277
2360
|
console.warn('Failed to clear PKI keys:', e);
|
|
2278
2361
|
}
|
|
2279
2362
|
|
|
2363
|
+
try {
|
|
2364
|
+
if (state.pki?.alice?.privateKey instanceof Uint8Array) state.pki.alice.privateKey.fill(0);
|
|
2365
|
+
if (state.pki?.bob?.privateKey instanceof Uint8Array) state.pki.bob.privateKey.fill(0);
|
|
2366
|
+
} catch {
|
|
2367
|
+
// ignore
|
|
2368
|
+
}
|
|
2369
|
+
|
|
2280
2370
|
state.pki.alice = null;
|
|
2281
2371
|
state.pki.bob = null;
|
|
2282
2372
|
state.pki.algorithm = 'x25519';
|
|
@@ -2378,11 +2468,14 @@ function login(keys) {
|
|
|
2378
2468
|
state.addresses = deriveAllAddressesFromHD();
|
|
2379
2469
|
state.selectedCrypto = 'btc';
|
|
2380
2470
|
|
|
2381
|
-
// Fire onLogin callback with SDN identity (coin type
|
|
2471
|
+
// Fire onLogin callback with SDN identity (BIP-44 Bitcoin coin type 0)
|
|
2382
2472
|
if (_onLoginCallback && state.hdRoot) {
|
|
2383
2473
|
try {
|
|
2384
|
-
const sdnSigning = getSigningKey(state.hdRoot,
|
|
2385
|
-
const
|
|
2474
|
+
const sdnSigning = getSigningKey(state.hdRoot, 0, 0, 0);
|
|
2475
|
+
const sdnPrivKey = sdnSigning.privateKey;
|
|
2476
|
+
const sdnPubKey = ed25519.getPublicKey(sdnPrivKey);
|
|
2477
|
+
// Don't keep derived private key bytes around longer than needed.
|
|
2478
|
+
if (sdnPrivKey instanceof Uint8Array) sdnPrivKey.fill(0);
|
|
2386
2479
|
const xpub = state.hdRoot.toXpub();
|
|
2387
2480
|
_onLoginCallback({
|
|
2388
2481
|
xpub,
|
|
@@ -2391,7 +2484,12 @@ function login(keys) {
|
|
|
2391
2484
|
const msgBytes = typeof message === 'string'
|
|
2392
2485
|
? new TextEncoder().encode(message)
|
|
2393
2486
|
: message;
|
|
2394
|
-
|
|
2487
|
+
const signing = getSigningKey(state.hdRoot, 0, 0, 0);
|
|
2488
|
+
try {
|
|
2489
|
+
return ed25519.sign(msgBytes, signing.privateKey);
|
|
2490
|
+
} finally {
|
|
2491
|
+
if (signing?.privateKey instanceof Uint8Array) signing.privateKey.fill(0);
|
|
2492
|
+
}
|
|
2395
2493
|
},
|
|
2396
2494
|
});
|
|
2397
2495
|
} catch (err) {
|
|
@@ -2440,14 +2538,12 @@ function login(keys) {
|
|
|
2440
2538
|
}
|
|
2441
2539
|
populateAccountAddressDropdown();
|
|
2442
2540
|
if (xprvEl) {
|
|
2443
|
-
|
|
2541
|
+
xprvEl.textContent = 'Hidden (click reveal)';
|
|
2444
2542
|
xprvEl.dataset.revealed = 'false';
|
|
2445
2543
|
}
|
|
2446
|
-
if (seedEl
|
|
2447
|
-
seedEl.textContent =
|
|
2544
|
+
if (seedEl) {
|
|
2545
|
+
seedEl.textContent = 'Not retained by the app';
|
|
2448
2546
|
seedEl.dataset.revealed = 'false';
|
|
2449
|
-
} else if (seedEl) {
|
|
2450
|
-
seedEl.textContent = 'Not available (derived from password)';
|
|
2451
2547
|
}
|
|
2452
2548
|
|
|
2453
2549
|
// Load persisted wallets and active accounts
|
|
@@ -2500,15 +2596,23 @@ function login(keys) {
|
|
|
2500
2596
|
if (pkiSecurity) pkiSecurity.style.display = 'block';
|
|
2501
2597
|
const pkiClearKeys = $('pki-clear-keys');
|
|
2502
2598
|
if (pkiClearKeys) pkiClearKeys.style.display = 'inline-flex';
|
|
2503
|
-
} else
|
|
2504
|
-
|
|
2599
|
+
} else {
|
|
2600
|
+
// PKI persistence is encrypted and requires the session key (available only after login).
|
|
2601
|
+
// Kick off an async load attempt; if it fails, generate fresh keys.
|
|
2602
|
+
loadPKIKeys().then((ok) => {
|
|
2603
|
+
if (!ok) generatePKIKeyPairs();
|
|
2604
|
+
}).catch(() => {
|
|
2605
|
+
generatePKIKeyPairs();
|
|
2606
|
+
});
|
|
2505
2607
|
}
|
|
2506
2608
|
|
|
2507
2609
|
// Update wallet addresses and balances
|
|
2508
2610
|
updateAdversarialSecurity();
|
|
2509
2611
|
|
|
2510
2612
|
// Open Account modal so user can see the wallet they just loaded
|
|
2511
|
-
|
|
2613
|
+
if (_openAccountAfterLogin) {
|
|
2614
|
+
$('keys-modal')?.classList.add('active');
|
|
2615
|
+
}
|
|
2512
2616
|
|
|
2513
2617
|
// Resolve names and update title
|
|
2514
2618
|
clearNameCache();
|
|
@@ -2526,6 +2630,26 @@ function logout() {
|
|
|
2526
2630
|
const titleEl = $('account-title');
|
|
2527
2631
|
if (titleEl) titleEl.textContent = 'Account';
|
|
2528
2632
|
state.loggedIn = false;
|
|
2633
|
+
|
|
2634
|
+
// Best-effort wipe of JS buffers (strings are not wipeable).
|
|
2635
|
+
const wipe = (u8) => {
|
|
2636
|
+
if (u8 instanceof Uint8Array) u8.fill(0);
|
|
2637
|
+
};
|
|
2638
|
+
try {
|
|
2639
|
+
wipe(state.wallet?.x25519?.privateKey);
|
|
2640
|
+
wipe(state.wallet?.ed25519?.privateKey);
|
|
2641
|
+
wipe(state.wallet?.secp256k1?.privateKey);
|
|
2642
|
+
wipe(state.wallet?.p256?.privateKey);
|
|
2643
|
+
wipe(state.encryptionKey);
|
|
2644
|
+
wipe(state.encryptionIV);
|
|
2645
|
+
wipe(state.masterSeed);
|
|
2646
|
+
wipe(state.pki?.alice?.privateKey);
|
|
2647
|
+
wipe(state.pki?.bob?.privateKey);
|
|
2648
|
+
state.hdRoot?.wipe?.();
|
|
2649
|
+
} catch {
|
|
2650
|
+
// ignore
|
|
2651
|
+
}
|
|
2652
|
+
|
|
2529
2653
|
state.wallet = { x25519: null, ed25519: null, secp256k1: null, p256: null };
|
|
2530
2654
|
state.encryptionKey = null;
|
|
2531
2655
|
state.encryptionIV = null;
|
|
@@ -2586,7 +2710,7 @@ async function exportWallet(format) {
|
|
|
2586
2710
|
switch (format) {
|
|
2587
2711
|
case 'mnemonic':
|
|
2588
2712
|
if (!state.mnemonic) {
|
|
2589
|
-
alert('Seed phrase not available.
|
|
2713
|
+
alert('Seed phrase not available. For security, the app does not retain the mnemonic after login.');
|
|
2590
2714
|
return;
|
|
2591
2715
|
}
|
|
2592
2716
|
data = state.mnemonic;
|
|
@@ -3669,9 +3793,7 @@ function setupLoginHandlers() {
|
|
|
3669
3793
|
const usePasskey = rememberMethod.password === 'passkey';
|
|
3670
3794
|
const pin = $('pin-input-password')?.value;
|
|
3671
3795
|
|
|
3672
|
-
console.log('Login clicked, username:', username, 'password length:', password?.length);
|
|
3673
3796
|
if (!username || !password || password.length < 24) {
|
|
3674
|
-
console.log('Login validation failed');
|
|
3675
3797
|
return;
|
|
3676
3798
|
}
|
|
3677
3799
|
|
|
@@ -3685,16 +3807,18 @@ function setupLoginHandlers() {
|
|
|
3685
3807
|
btn.textContent = 'Logging in...';
|
|
3686
3808
|
|
|
3687
3809
|
try {
|
|
3688
|
-
console.log('Calling deriveKeysFromPassword...');
|
|
3689
3810
|
const keys = await deriveKeysFromPassword(username, password);
|
|
3690
|
-
|
|
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 = '';
|
|
3691
3815
|
|
|
3692
3816
|
if (rememberWallet) {
|
|
3693
3817
|
const walletData = {
|
|
3694
|
-
type: '
|
|
3818
|
+
type: 'masterSeed',
|
|
3819
|
+
source: 'password',
|
|
3695
3820
|
username,
|
|
3696
|
-
|
|
3697
|
-
masterSeed: Array.from(state.masterSeed)
|
|
3821
|
+
masterSeed: Array.from(state.masterSeed),
|
|
3698
3822
|
};
|
|
3699
3823
|
|
|
3700
3824
|
if (usePasskey) {
|
|
@@ -3723,7 +3847,6 @@ function setupLoginHandlers() {
|
|
|
3723
3847
|
}
|
|
3724
3848
|
|
|
3725
3849
|
login(keys);
|
|
3726
|
-
console.log('Login complete, hdRoot:', !!state.hdRoot);
|
|
3727
3850
|
} catch (err) {
|
|
3728
3851
|
console.error('Login error:', err);
|
|
3729
3852
|
alert('Error: ' + err.message);
|
|
@@ -3788,11 +3911,15 @@ function setupLoginHandlers() {
|
|
|
3788
3911
|
try {
|
|
3789
3912
|
const keys = await deriveKeysFromSeed(phrase);
|
|
3790
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
|
+
|
|
3791
3918
|
if (rememberWallet) {
|
|
3792
3919
|
const walletData = {
|
|
3793
|
-
type: '
|
|
3794
|
-
|
|
3795
|
-
masterSeed: Array.from(state.masterSeed)
|
|
3920
|
+
type: 'masterSeed',
|
|
3921
|
+
source: 'seed',
|
|
3922
|
+
masterSeed: Array.from(state.masterSeed),
|
|
3796
3923
|
};
|
|
3797
3924
|
|
|
3798
3925
|
if (usePasskey) {
|
|
@@ -3845,12 +3972,27 @@ function setupLoginHandlers() {
|
|
|
3845
3972
|
const walletData = await WalletStorage.retrieveWithPIN(pin);
|
|
3846
3973
|
|
|
3847
3974
|
let keys;
|
|
3848
|
-
|
|
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.
|
|
3849
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
|
+
});
|
|
3850
3987
|
} else if (walletData.type === 'seed') {
|
|
3851
3988
|
keys = await deriveKeysFromSeed(walletData.seedPhrase);
|
|
3989
|
+
await WalletStorage.storeWithPIN(pin, {
|
|
3990
|
+
type: 'masterSeed',
|
|
3991
|
+
source: 'seed',
|
|
3992
|
+
masterSeed: Array.from(state.masterSeed),
|
|
3993
|
+
});
|
|
3852
3994
|
} else {
|
|
3853
|
-
throw new Error('Unknown wallet
|
|
3995
|
+
throw new Error('Unknown stored wallet format');
|
|
3854
3996
|
}
|
|
3855
3997
|
|
|
3856
3998
|
login(keys);
|
|
@@ -3874,12 +4016,34 @@ function setupLoginHandlers() {
|
|
|
3874
4016
|
const walletData = await WalletStorage.retrieveWithPasskey();
|
|
3875
4017
|
|
|
3876
4018
|
let keys;
|
|
3877
|
-
|
|
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') {
|
|
3878
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
|
+
});
|
|
3879
4034
|
} else if (walletData.type === 'seed') {
|
|
3880
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
|
+
});
|
|
3881
4045
|
} else {
|
|
3882
|
-
throw new Error('Unknown wallet
|
|
4046
|
+
throw new Error('Unknown stored wallet format');
|
|
3883
4047
|
}
|
|
3884
4048
|
|
|
3885
4049
|
login(keys);
|
|
@@ -4325,7 +4489,23 @@ function setupMainAppHandlers() {
|
|
|
4325
4489
|
const targetEl = $(targetId);
|
|
4326
4490
|
if (targetEl) {
|
|
4327
4491
|
const isRevealed = targetEl.dataset.revealed === 'true';
|
|
4328
|
-
|
|
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
|
+
|
|
4329
4509
|
btn.innerHTML = isRevealed
|
|
4330
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>'
|
|
4331
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>';
|
|
@@ -4340,7 +4520,28 @@ function setupMainAppHandlers() {
|
|
|
4340
4520
|
const targetEl = $(targetId);
|
|
4341
4521
|
if (targetEl) {
|
|
4342
4522
|
try {
|
|
4343
|
-
|
|
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);
|
|
4344
4545
|
btn.classList.add('copied');
|
|
4345
4546
|
setTimeout(() => btn.classList.remove('copied'), 1500);
|
|
4346
4547
|
} catch (err) {
|
|
@@ -4956,26 +5157,250 @@ function setupTrustHandlers() {
|
|
|
4956
5157
|
// Encryption Tab Handlers (ECIES: ECDH + HKDF + AES-256-GCM)
|
|
4957
5158
|
// =========================================================================
|
|
4958
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
|
+
|
|
4959
5370
|
function updateEncryptionTab() {
|
|
4960
|
-
|
|
4961
|
-
|
|
4962
|
-
|
|
4963
|
-
const
|
|
4964
|
-
const
|
|
4965
|
-
const encKey = deriveHDKey(encPath);
|
|
4966
|
-
const pubKey = encKey.publicKey();
|
|
4967
|
-
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);
|
|
4968
5376
|
|
|
4969
5377
|
const senderPubEl = $('encrypt-sender-pubkey');
|
|
4970
5378
|
const senderPathEl = $('encrypt-sender-path');
|
|
4971
|
-
|
|
4972
|
-
if (senderPathEl) senderPathEl.textContent = encPath;
|
|
4973
|
-
|
|
5379
|
+
const senderAlgoEl = $('encrypt-sender-algo');
|
|
4974
5380
|
const encryptBtn = $('encrypt-btn');
|
|
4975
|
-
|
|
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
|
+
}
|
|
4976
5399
|
}
|
|
4977
5400
|
|
|
4978
|
-
|
|
5401
|
+
initMessagingKeyControls();
|
|
5402
|
+
|
|
5403
|
+
// Update encryption tab when it becomes active
|
|
4979
5404
|
$qa('.modal-tab[data-modal-tab="messaging-tab-content"]').forEach(tab => {
|
|
4980
5405
|
tab.addEventListener('click', () => {
|
|
4981
5406
|
if (state.hdRoot) updateEncryptionTab();
|
|
@@ -5049,7 +5474,10 @@ function setupTrustHandlers() {
|
|
|
5049
5474
|
// Encrypt button
|
|
5050
5475
|
$('encrypt-btn')?.addEventListener('click', () => {
|
|
5051
5476
|
const w = state.hdWalletModule;
|
|
5052
|
-
if (!w || !state.hdRoot)
|
|
5477
|
+
if (!w || !state.hdRoot) {
|
|
5478
|
+
alert('Please login first.');
|
|
5479
|
+
return;
|
|
5480
|
+
}
|
|
5053
5481
|
|
|
5054
5482
|
const recipientHex = $('encrypt-recipient-pubkey')?.value?.trim();
|
|
5055
5483
|
const plainStr = $('encrypt-plaintext')?.value;
|
|
@@ -5059,56 +5487,64 @@ function setupTrustHandlers() {
|
|
|
5059
5487
|
}
|
|
5060
5488
|
|
|
5061
5489
|
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();
|
|
5490
|
+
const keyType = getMessagingKeyType();
|
|
5491
|
+
const path = getMessagingHDPath(keyType);
|
|
5492
|
+
const { algorithm, privateKey: senderPriv, publicKey: senderPub } = deriveKeyMaterialForMessaging(w, keyType, path);
|
|
5108
5493
|
|
|
5109
|
-
|
|
5110
|
-
|
|
5111
|
-
|
|
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
|
+
}
|
|
5112
5548
|
} catch (err) {
|
|
5113
5549
|
console.error('Encryption failed:', err);
|
|
5114
5550
|
alert('Encryption failed: ' + err.message);
|
|
@@ -5161,7 +5597,10 @@ function setupTrustHandlers() {
|
|
|
5161
5597
|
// Decrypt button
|
|
5162
5598
|
$('decrypt-btn')?.addEventListener('click', () => {
|
|
5163
5599
|
const w = state.hdWalletModule;
|
|
5164
|
-
if (!w || !state.hdRoot)
|
|
5600
|
+
if (!w || !state.hdRoot) {
|
|
5601
|
+
alert('Please login first.');
|
|
5602
|
+
return;
|
|
5603
|
+
}
|
|
5165
5604
|
|
|
5166
5605
|
const payloadStr = $('decrypt-payload')?.value?.trim();
|
|
5167
5606
|
if (!payloadStr) {
|
|
@@ -5171,44 +5610,63 @@ function setupTrustHandlers() {
|
|
|
5171
5610
|
|
|
5172
5611
|
try {
|
|
5173
5612
|
const payload = parseEMEPayload(payloadStr);
|
|
5174
|
-
const
|
|
5175
|
-
|
|
5176
|
-
const
|
|
5177
|
-
const
|
|
5178
|
-
const iv = fromHex(payload.IV);
|
|
5179
|
-
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);
|
|
5180
5617
|
|
|
5181
5618
|
// ENCRYPTED_BLOB can be a number array (from EMET) or hex string
|
|
5182
5619
|
let ciphertext;
|
|
5183
5620
|
if (Array.isArray(payload.ENCRYPTED_BLOB)) {
|
|
5184
5621
|
ciphertext = new Uint8Array(payload.ENCRYPTED_BLOB);
|
|
5185
5622
|
} else {
|
|
5186
|
-
ciphertext =
|
|
5623
|
+
ciphertext = hexToBytesStrict(payload.ENCRYPTED_BLOB, null);
|
|
5187
5624
|
}
|
|
5188
5625
|
|
|
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();
|
|
5195
|
-
|
|
5196
|
-
// 1. ECDH shared secret (using sender's public key)
|
|
5197
|
-
const shared = w.curves.secp256k1.ecdh(recipientPriv, senderPub);
|
|
5626
|
+
const keyType = getMessagingKeyType();
|
|
5627
|
+
const path = getMessagingHDPath(keyType);
|
|
5198
5628
|
|
|
5199
|
-
//
|
|
5200
|
-
const
|
|
5201
|
-
|
|
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');
|
|
5202
5636
|
|
|
5203
|
-
|
|
5204
|
-
const decrypted = w.utils.aesGcm.decrypt(aesKey, ciphertext, tag, iv);
|
|
5205
|
-
const decStr = new TextDecoder().decode(decrypted);
|
|
5637
|
+
const senderPub = normalizeRecipientPublicKeyForAlgorithm(algorithm, senderPubRaw);
|
|
5206
5638
|
|
|
5207
|
-
|
|
5639
|
+
// Derive recipient private key from configured path.
|
|
5640
|
+
const derived = deriveHDKey(path);
|
|
5641
|
+
const recipientPriv = derived.privateKey();
|
|
5642
|
+
derived.wipe();
|
|
5208
5643
|
|
|
5209
|
-
|
|
5210
|
-
|
|
5211
|
-
|
|
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
|
+
}
|
|
5212
5670
|
} catch (err) {
|
|
5213
5671
|
console.error('Decryption failed:', err);
|
|
5214
5672
|
alert('Decryption failed: ' + err.message);
|
|
@@ -5267,10 +5725,11 @@ function setupHomepageHandlers() {
|
|
|
5267
5725
|
// =============================================================================
|
|
5268
5726
|
|
|
5269
5727
|
export async function init(rootElement, options = {}) {
|
|
5270
|
-
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)
|
|
5271
5729
|
? (options = rootElement, {}) : options;
|
|
5272
5730
|
if (rootElement && rootElement instanceof Node) _root = rootElement;
|
|
5273
5731
|
if (typeof onLogin === 'function') _onLoginCallback = onLogin;
|
|
5732
|
+
_openAccountAfterLogin = openAccountAfterLogin;
|
|
5274
5733
|
|
|
5275
5734
|
// Inject modal HTML if not already present in the DOM
|
|
5276
5735
|
if (!document.getElementById('keys-modal')) {
|
|
@@ -5295,8 +5754,9 @@ export async function init(rootElement, options = {}) {
|
|
|
5295
5754
|
if (status) status.textContent = 'Loading HD wallet module...';
|
|
5296
5755
|
state.hdWalletModule = await initHDWallet();
|
|
5297
5756
|
|
|
5298
|
-
// Load saved PKI keys if available
|
|
5299
|
-
|
|
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();
|
|
5300
5760
|
|
|
5301
5761
|
state.initialized = true;
|
|
5302
5762
|
|
|
@@ -5382,7 +5842,10 @@ export async function init(rootElement, options = {}) {
|
|
|
5382
5842
|
* @param {Node} [rootElement] - Optional root element for DOM queries
|
|
5383
5843
|
* @param {Object} [options] - Options passed to init()
|
|
5384
5844
|
* @param {Function} [options.onLogin] - Callback fired after successful login with
|
|
5385
|
-
* `{ xpub, signingPublicKey, sign(message) }` for SDN identity (coin type
|
|
5845
|
+
* `{ xpub, signingPublicKey, sign(message) }` for SDN identity (BIP-44 coin type 0)
|
|
5846
|
+
* @param {boolean} [options.openAccountAfterLogin=true] - When false, the Account
|
|
5847
|
+
* modal will NOT auto-open after login. Useful for integrations that handle
|
|
5848
|
+
* post-login UX themselves (e.g. challenge-response auth flows).
|
|
5386
5849
|
* @returns {Promise<{openLogin: Function, openAccount: Function, destroy: Function}>}
|
|
5387
5850
|
*/
|
|
5388
5851
|
export async function createWalletUI(rootElement, options = {}) {
|