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/template.js
CHANGED
|
@@ -410,6 +410,26 @@ export function getModalHTML() {
|
|
|
410
410
|
|
|
411
411
|
<!-- Messaging Tab (Encrypt + Decrypt) -->
|
|
412
412
|
<div id="messaging-tab-content" class="modal-tab-content">
|
|
413
|
+
<div class="glass-card messaging-key-config">
|
|
414
|
+
<div class="messaging-key-config-grid">
|
|
415
|
+
<div class="messaging-key-config-item">
|
|
416
|
+
<label>Key Type</label>
|
|
417
|
+
<select id="messaging-key-type" class="glass-input compact">
|
|
418
|
+
<option value="btc">Bitcoin (BTC) - secp256k1</option>
|
|
419
|
+
<option value="eth">Ethereum (ETH) - secp256k1</option>
|
|
420
|
+
<option value="sol">Solana (SOL) - X25519</option>
|
|
421
|
+
</select>
|
|
422
|
+
</div>
|
|
423
|
+
<div class="messaging-key-config-item">
|
|
424
|
+
<label>HD Path</label>
|
|
425
|
+
<div class="messaging-path-row">
|
|
426
|
+
<input type="text" id="messaging-hd-path" class="glass-input compact" value="m/44'/0'/0'/1/0" spellcheck="false" autocomplete="off">
|
|
427
|
+
<button id="messaging-hd-path-default" class="glass-btn small" title="Reset to default path">Default</button>
|
|
428
|
+
</div>
|
|
429
|
+
<div class="messaging-key-hint">Example: m/44'/60'/0'/1/0</div>
|
|
430
|
+
</div>
|
|
431
|
+
</div>
|
|
432
|
+
</div>
|
|
413
433
|
<div class="messaging-sub-tabs">
|
|
414
434
|
<button class="messaging-sub-tab active" data-messaging-sub="encrypt-sub">Encrypt</button>
|
|
415
435
|
<button class="messaging-sub-tab" data-messaging-sub="decrypt-sub">Decrypt</button>
|
|
@@ -435,6 +455,10 @@ export function getModalHTML() {
|
|
|
435
455
|
<label>Derivation Path</label>
|
|
436
456
|
<code id="encrypt-sender-path">--</code>
|
|
437
457
|
</div>
|
|
458
|
+
<div class="encrypt-key-detail">
|
|
459
|
+
<label>Key Algorithm</label>
|
|
460
|
+
<code id="encrypt-sender-algo">--</code>
|
|
461
|
+
</div>
|
|
438
462
|
</div>
|
|
439
463
|
<div class="encrypt-key-card glass-card">
|
|
440
464
|
<div class="encrypt-key-header">
|
package/src/wallet-storage.js
CHANGED
|
@@ -22,7 +22,15 @@ const ENCRYPTED_DATA_KEY = `${STORAGE_PREFIX}encrypted`;
|
|
|
22
22
|
const PASSKEY_CREDENTIAL_KEY = `${STORAGE_PREFIX}passkey_credential`;
|
|
23
23
|
|
|
24
24
|
// Version for future migrations
|
|
25
|
-
const STORAGE_VERSION =
|
|
25
|
+
const STORAGE_VERSION = 3;
|
|
26
|
+
|
|
27
|
+
// AES-GCM standard IV size (96-bit)
|
|
28
|
+
const AES_GCM_IV_LENGTH = 12;
|
|
29
|
+
|
|
30
|
+
// 6-digit PINs have low entropy; PBKDF2 must be expensive to slow offline brute force.
|
|
31
|
+
// (This is still not a substitute for rate-limiting when an attacker can query online.)
|
|
32
|
+
const PIN_PBKDF2_ITERATIONS = 600000;
|
|
33
|
+
const LEGACY_PIN_PBKDF2_ITERATIONS = 100000;
|
|
26
34
|
|
|
27
35
|
// =============================================================================
|
|
28
36
|
// Storage Method Enum
|
|
@@ -113,9 +121,9 @@ async function hkdfDerive(inputKeyMaterial, salt, info, length) {
|
|
|
113
121
|
}
|
|
114
122
|
|
|
115
123
|
/**
|
|
116
|
-
* Derive encryption key
|
|
124
|
+
* Derive encryption key from key material (HKDF).
|
|
117
125
|
*/
|
|
118
|
-
async function
|
|
126
|
+
async function deriveEncryptionKey(keyMaterial, context) {
|
|
119
127
|
const salt = new TextEncoder().encode(`wallet-storage-v${STORAGE_VERSION}`);
|
|
120
128
|
|
|
121
129
|
const encryptionKey = await hkdfDerive(
|
|
@@ -125,13 +133,27 @@ async function deriveKeyAndIV(keyMaterial, context) {
|
|
|
125
133
|
32
|
|
126
134
|
);
|
|
127
135
|
|
|
136
|
+
return encryptionKey;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Legacy: v1/v2 storage used a deterministic IV derived via HKDF.
|
|
141
|
+
* This exists only to decrypt and upgrade old stored blobs.
|
|
142
|
+
*/
|
|
143
|
+
async function deriveLegacyKeyAndIV(keyMaterial, context, version) {
|
|
144
|
+
const salt = new TextEncoder().encode(`wallet-storage-v${version}`);
|
|
145
|
+
const encryptionKey = await hkdfDerive(
|
|
146
|
+
keyMaterial,
|
|
147
|
+
salt,
|
|
148
|
+
`${context}-encryption-key`,
|
|
149
|
+
32
|
|
150
|
+
);
|
|
128
151
|
const iv = await hkdfDerive(
|
|
129
152
|
keyMaterial,
|
|
130
153
|
salt,
|
|
131
154
|
`${context}-encryption-iv`,
|
|
132
|
-
|
|
155
|
+
AES_GCM_IV_LENGTH
|
|
133
156
|
);
|
|
134
|
-
|
|
135
157
|
return { encryptionKey, iv };
|
|
136
158
|
}
|
|
137
159
|
|
|
@@ -143,7 +165,7 @@ async function deriveKeyAndIV(keyMaterial, context) {
|
|
|
143
165
|
* Derive key material from a 6-digit PIN
|
|
144
166
|
* Uses PBKDF2 for additional security against brute-force attacks
|
|
145
167
|
*/
|
|
146
|
-
async function deriveKeyFromPIN(pin, storedSalt) {
|
|
168
|
+
async function deriveKeyFromPIN(pin, storedSalt, iterations = PIN_PBKDF2_ITERATIONS) {
|
|
147
169
|
if (!/^\d{6}$/.test(pin)) {
|
|
148
170
|
throw new Error('PIN must be exactly 6 digits');
|
|
149
171
|
}
|
|
@@ -169,7 +191,7 @@ async function deriveKeyFromPIN(pin, storedSalt) {
|
|
|
169
191
|
name: 'PBKDF2',
|
|
170
192
|
hash: 'SHA-256',
|
|
171
193
|
salt: salt,
|
|
172
|
-
iterations
|
|
194
|
+
iterations
|
|
173
195
|
},
|
|
174
196
|
keyMaterial,
|
|
175
197
|
256
|
|
@@ -303,11 +325,9 @@ export async function registerPasskey(options = {}) {
|
|
|
303
325
|
keyMaterial = new Uint8Array(prfResult);
|
|
304
326
|
hasPRF = true;
|
|
305
327
|
} else {
|
|
306
|
-
//
|
|
307
|
-
//
|
|
308
|
-
|
|
309
|
-
const hash = await crypto.subtle.digest('SHA-256', rawId);
|
|
310
|
-
keyMaterial = new Uint8Array(hash);
|
|
328
|
+
// SECURITY: Never derive encryption keys from public credential IDs.
|
|
329
|
+
// Without PRF, we cannot get stable secret key material from passkeys in a secure way.
|
|
330
|
+
throw new Error('Passkey PRF extension is required for secure wallet encryption on this device/browser');
|
|
311
331
|
}
|
|
312
332
|
|
|
313
333
|
return {
|
|
@@ -365,10 +385,7 @@ export async function authenticatePasskey(credentialId) {
|
|
|
365
385
|
keyMaterial = new Uint8Array(prfResult);
|
|
366
386
|
hasPRF = true;
|
|
367
387
|
} else {
|
|
368
|
-
|
|
369
|
-
const rawId = new Uint8Array(assertion.rawId);
|
|
370
|
-
const hash = await crypto.subtle.digest('SHA-256', rawId);
|
|
371
|
-
keyMaterial = new Uint8Array(hash);
|
|
388
|
+
throw new Error('Passkey PRF extension is required for secure wallet decryption on this device/browser');
|
|
372
389
|
}
|
|
373
390
|
|
|
374
391
|
return { keyMaterial, hasPRF };
|
|
@@ -381,9 +398,10 @@ export async function authenticatePasskey(credentialId) {
|
|
|
381
398
|
/**
|
|
382
399
|
* Encrypt data using AES-256-GCM
|
|
383
400
|
*/
|
|
384
|
-
async function encryptData(data, encryptionKey,
|
|
401
|
+
async function encryptData(data, encryptionKey, aad) {
|
|
385
402
|
const encoder = new TextEncoder();
|
|
386
403
|
const plaintext = encoder.encode(JSON.stringify(data));
|
|
404
|
+
const iv = generateRandomBytes(AES_GCM_IV_LENGTH);
|
|
387
405
|
|
|
388
406
|
const cryptoKey = await crypto.subtle.importKey(
|
|
389
407
|
'raw',
|
|
@@ -393,19 +411,22 @@ async function encryptData(data, encryptionKey, iv) {
|
|
|
393
411
|
['encrypt']
|
|
394
412
|
);
|
|
395
413
|
|
|
414
|
+
const alg = { name: 'AES-GCM', iv };
|
|
415
|
+
if (aad) alg.additionalData = aad;
|
|
416
|
+
|
|
396
417
|
const ciphertext = await crypto.subtle.encrypt(
|
|
397
|
-
|
|
418
|
+
alg,
|
|
398
419
|
cryptoKey,
|
|
399
420
|
plaintext
|
|
400
421
|
);
|
|
401
422
|
|
|
402
|
-
return new Uint8Array(ciphertext);
|
|
423
|
+
return { iv, ciphertext: new Uint8Array(ciphertext) };
|
|
403
424
|
}
|
|
404
425
|
|
|
405
426
|
/**
|
|
406
427
|
* Decrypt data using AES-256-GCM
|
|
407
428
|
*/
|
|
408
|
-
async function decryptData(ciphertext, encryptionKey, iv) {
|
|
429
|
+
async function decryptData(ciphertext, encryptionKey, iv, aad) {
|
|
409
430
|
const cryptoKey = await crypto.subtle.importKey(
|
|
410
431
|
'raw',
|
|
411
432
|
encryptionKey,
|
|
@@ -414,8 +435,11 @@ async function decryptData(ciphertext, encryptionKey, iv) {
|
|
|
414
435
|
['decrypt']
|
|
415
436
|
);
|
|
416
437
|
|
|
438
|
+
const alg = { name: 'AES-GCM', iv };
|
|
439
|
+
if (aad) alg.additionalData = aad;
|
|
440
|
+
|
|
417
441
|
const plaintext = await crypto.subtle.decrypt(
|
|
418
|
-
|
|
442
|
+
alg,
|
|
419
443
|
cryptoKey,
|
|
420
444
|
ciphertext
|
|
421
445
|
);
|
|
@@ -428,6 +452,12 @@ async function decryptData(ciphertext, encryptionKey, iv) {
|
|
|
428
452
|
// High-Level Storage API
|
|
429
453
|
// =============================================================================
|
|
430
454
|
|
|
455
|
+
function getAadForMethod(method) {
|
|
456
|
+
// Small AAD to bind ciphertexts to this module + method.
|
|
457
|
+
// (Replay protection against localStorage rollback is not achievable without an external monotonic anchor.)
|
|
458
|
+
return new TextEncoder().encode(`wallet-storage|v${STORAGE_VERSION}|${method}`);
|
|
459
|
+
}
|
|
460
|
+
|
|
431
461
|
/**
|
|
432
462
|
* Get storage metadata
|
|
433
463
|
* @returns {Object|null} Storage metadata or null if no wallet stored
|
|
@@ -477,14 +507,16 @@ export function getStorageMethod() {
|
|
|
477
507
|
export async function storeWithPIN(pin, walletData) {
|
|
478
508
|
// Derive key from PIN
|
|
479
509
|
const { keyMaterial, salt } = await deriveKeyFromPIN(pin);
|
|
480
|
-
const
|
|
510
|
+
const encryptionKey = await deriveEncryptionKey(keyMaterial, 'pin');
|
|
481
511
|
|
|
482
512
|
// Encrypt wallet data
|
|
483
|
-
const
|
|
513
|
+
const aad = getAadForMethod(StorageMethod.PIN);
|
|
514
|
+
const { ciphertext, iv } = await encryptData(walletData, encryptionKey, aad);
|
|
484
515
|
|
|
485
516
|
// Store encrypted data
|
|
486
517
|
const encryptedData = {
|
|
487
518
|
ciphertext: arrayBufferToBase64(ciphertext),
|
|
519
|
+
iv: arrayBufferToBase64(iv),
|
|
488
520
|
salt: arrayBufferToBase64(salt)
|
|
489
521
|
};
|
|
490
522
|
localStorage.setItem(ENCRYPTED_DATA_KEY, JSON.stringify(encryptedData));
|
|
@@ -520,13 +552,37 @@ export async function retrieveWithPIN(pin) {
|
|
|
520
552
|
const encryptedData = JSON.parse(encryptedJson);
|
|
521
553
|
const salt = base64ToUint8Array(encryptedData.salt);
|
|
522
554
|
const ciphertext = base64ToUint8Array(encryptedData.ciphertext);
|
|
555
|
+
const iv = encryptedData.iv ? base64ToUint8Array(encryptedData.iv) : null;
|
|
523
556
|
|
|
524
557
|
// Derive key from PIN with stored salt
|
|
525
|
-
const { keyMaterial } = await deriveKeyFromPIN(pin, salt);
|
|
526
|
-
const
|
|
558
|
+
const { keyMaterial } = await deriveKeyFromPIN(pin, salt, iv ? PIN_PBKDF2_ITERATIONS : LEGACY_PIN_PBKDF2_ITERATIONS);
|
|
559
|
+
const aad = getAadForMethod(StorageMethod.PIN);
|
|
527
560
|
|
|
528
561
|
try {
|
|
529
|
-
|
|
562
|
+
let walletData;
|
|
563
|
+
if (iv) {
|
|
564
|
+
const encryptionKey = await deriveEncryptionKey(keyMaterial, 'pin');
|
|
565
|
+
walletData = await decryptData(ciphertext, encryptionKey, iv, aad);
|
|
566
|
+
} else {
|
|
567
|
+
// Legacy v1/v2 deterministic-IV storage. Decrypt and upgrade in-place.
|
|
568
|
+
const legacyVersion = metadata.version || 2;
|
|
569
|
+
const { encryptionKey: legacyKey, iv: legacyIv } = await deriveLegacyKeyAndIV(keyMaterial, 'pin', legacyVersion);
|
|
570
|
+
walletData = await decryptData(ciphertext, legacyKey, legacyIv);
|
|
571
|
+
|
|
572
|
+
// Upgrade stored blob to v3 (random IV) after successful decrypt.
|
|
573
|
+
const newKey = await deriveEncryptionKey(keyMaterial, 'pin');
|
|
574
|
+
const { ciphertext: newCt, iv: newIv } = await encryptData(walletData, newKey, aad);
|
|
575
|
+
localStorage.setItem(ENCRYPTED_DATA_KEY, JSON.stringify({
|
|
576
|
+
ciphertext: arrayBufferToBase64(newCt),
|
|
577
|
+
iv: arrayBufferToBase64(newIv),
|
|
578
|
+
salt: arrayBufferToBase64(salt)
|
|
579
|
+
}));
|
|
580
|
+
localStorage.setItem(METADATA_KEY, JSON.stringify({
|
|
581
|
+
...metadata,
|
|
582
|
+
version: STORAGE_VERSION
|
|
583
|
+
}));
|
|
584
|
+
}
|
|
585
|
+
return walletData;
|
|
530
586
|
} catch (e) {
|
|
531
587
|
throw new Error('Invalid PIN or corrupted data');
|
|
532
588
|
}
|
|
@@ -540,14 +596,43 @@ export async function retrieveWithPIN(pin) {
|
|
|
540
596
|
* @returns {Promise<boolean>}
|
|
541
597
|
*/
|
|
542
598
|
export async function storeWithPasskey(walletData, options = {}) {
|
|
543
|
-
//
|
|
544
|
-
|
|
599
|
+
// Prefer reusing an existing stored passkey credential to avoid creating duplicates.
|
|
600
|
+
let credentialId = null;
|
|
601
|
+
let keyMaterial = null;
|
|
602
|
+
let hasPRF = false;
|
|
603
|
+
|
|
604
|
+
try {
|
|
605
|
+
const existing = localStorage.getItem(PASSKEY_CREDENTIAL_KEY);
|
|
606
|
+
if (existing) {
|
|
607
|
+
const parsed = JSON.parse(existing);
|
|
608
|
+
if (parsed?.id) {
|
|
609
|
+
credentialId = parsed.id;
|
|
610
|
+
const auth = await authenticatePasskey(credentialId);
|
|
611
|
+
keyMaterial = auth.keyMaterial;
|
|
612
|
+
hasPRF = auth.hasPRF;
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
} catch {
|
|
616
|
+
// Ignore and fall back to registration below.
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
if (!keyMaterial) {
|
|
620
|
+
const reg = await registerPasskey(options);
|
|
621
|
+
credentialId = reg.credentialId;
|
|
622
|
+
keyMaterial = reg.keyMaterial;
|
|
623
|
+
hasPRF = reg.hasPRF;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
if (!hasPRF) {
|
|
627
|
+
throw new Error('Passkey PRF extension is required to store wallet data securely');
|
|
628
|
+
}
|
|
545
629
|
|
|
546
630
|
// Derive encryption key
|
|
547
|
-
const
|
|
631
|
+
const encryptionKey = await deriveEncryptionKey(keyMaterial, 'passkey');
|
|
548
632
|
|
|
549
633
|
// Encrypt wallet data
|
|
550
|
-
const
|
|
634
|
+
const aad = getAadForMethod(StorageMethod.PASSKEY);
|
|
635
|
+
const { ciphertext, iv } = await encryptData(walletData, encryptionKey, aad);
|
|
551
636
|
|
|
552
637
|
// Store credential info
|
|
553
638
|
const credentialData = {
|
|
@@ -558,7 +643,8 @@ export async function storeWithPasskey(walletData, options = {}) {
|
|
|
558
643
|
|
|
559
644
|
// Store encrypted data
|
|
560
645
|
const encryptedData = {
|
|
561
|
-
ciphertext: arrayBufferToBase64(ciphertext)
|
|
646
|
+
ciphertext: arrayBufferToBase64(ciphertext),
|
|
647
|
+
iv: arrayBufferToBase64(iv)
|
|
562
648
|
};
|
|
563
649
|
localStorage.setItem(ENCRYPTED_DATA_KEY, JSON.stringify(encryptedData));
|
|
564
650
|
|
|
@@ -584,6 +670,9 @@ export async function retrieveWithPasskey() {
|
|
|
584
670
|
if (!metadata || metadata.method !== StorageMethod.PASSKEY) {
|
|
585
671
|
throw new Error('No passkey-encrypted wallet found');
|
|
586
672
|
}
|
|
673
|
+
if (!metadata.hasPRF) {
|
|
674
|
+
throw new Error('Stored passkey wallet was created without PRF support and is insecure. Please forget it and use PIN storage.');
|
|
675
|
+
}
|
|
587
676
|
|
|
588
677
|
const credentialJson = localStorage.getItem(PASSKEY_CREDENTIAL_KEY);
|
|
589
678
|
if (!credentialJson) {
|
|
@@ -599,15 +688,41 @@ export async function retrieveWithPasskey() {
|
|
|
599
688
|
|
|
600
689
|
const encryptedData = JSON.parse(encryptedJson);
|
|
601
690
|
const ciphertext = base64ToUint8Array(encryptedData.ciphertext);
|
|
691
|
+
const iv = encryptedData.iv ? base64ToUint8Array(encryptedData.iv) : null;
|
|
602
692
|
|
|
603
693
|
// Authenticate with passkey and get key material
|
|
604
|
-
const { keyMaterial } = await authenticatePasskey(credentialData.id);
|
|
694
|
+
const { keyMaterial, hasPRF } = await authenticatePasskey(credentialData.id);
|
|
695
|
+
if (!hasPRF) {
|
|
696
|
+
throw new Error('Passkey PRF extension is required to decrypt wallet data securely');
|
|
697
|
+
}
|
|
605
698
|
|
|
606
699
|
// Derive encryption key
|
|
607
|
-
const
|
|
700
|
+
const aad = getAadForMethod(StorageMethod.PASSKEY);
|
|
608
701
|
|
|
609
702
|
try {
|
|
610
|
-
|
|
703
|
+
let walletData;
|
|
704
|
+
if (iv) {
|
|
705
|
+
const encryptionKey = await deriveEncryptionKey(keyMaterial, 'passkey');
|
|
706
|
+
walletData = await decryptData(ciphertext, encryptionKey, iv, aad);
|
|
707
|
+
} else {
|
|
708
|
+
// Legacy v1/v2 deterministic-IV storage. Decrypt and upgrade in-place.
|
|
709
|
+
const legacyVersion = metadata.version || 2;
|
|
710
|
+
const { encryptionKey: legacyKey, iv: legacyIv } = await deriveLegacyKeyAndIV(keyMaterial, 'passkey', legacyVersion);
|
|
711
|
+
walletData = await decryptData(ciphertext, legacyKey, legacyIv);
|
|
712
|
+
|
|
713
|
+
const newKey = await deriveEncryptionKey(keyMaterial, 'passkey');
|
|
714
|
+
const { ciphertext: newCt, iv: newIv } = await encryptData(walletData, newKey, aad);
|
|
715
|
+
localStorage.setItem(ENCRYPTED_DATA_KEY, JSON.stringify({
|
|
716
|
+
ciphertext: arrayBufferToBase64(newCt),
|
|
717
|
+
iv: arrayBufferToBase64(newIv)
|
|
718
|
+
}));
|
|
719
|
+
localStorage.setItem(METADATA_KEY, JSON.stringify({
|
|
720
|
+
...metadata,
|
|
721
|
+
version: STORAGE_VERSION,
|
|
722
|
+
hasPRF: true
|
|
723
|
+
}));
|
|
724
|
+
}
|
|
725
|
+
return walletData;
|
|
611
726
|
} catch (e) {
|
|
612
727
|
throw new Error('Passkey authentication failed or data corrupted');
|
|
613
728
|
}
|
|
@@ -636,7 +751,7 @@ export function migrateStorage() {
|
|
|
636
751
|
if (getStorageMetadata() !== null) return;
|
|
637
752
|
if (!oldPinWallet && !oldPasskeyCredential) return;
|
|
638
753
|
|
|
639
|
-
console.log('Migrating wallet storage from
|
|
754
|
+
console.log('Migrating wallet storage from legacy format...');
|
|
640
755
|
|
|
641
756
|
if (oldPasskeyCredential && oldPasskeyWallet) {
|
|
642
757
|
// Migrate passkey storage
|
|
@@ -656,7 +771,9 @@ export function migrateStorage() {
|
|
|
656
771
|
localStorage.setItem(METADATA_KEY, JSON.stringify({
|
|
657
772
|
method: StorageMethod.PASSKEY,
|
|
658
773
|
timestamp: credential.timestamp || wallet.timestamp || Date.now(),
|
|
659
|
-
version
|
|
774
|
+
// Preserve legacy version so decrypt can derive the correct legacy HKDF salt/IV,
|
|
775
|
+
// then upgrade in-place on first successful retrieval.
|
|
776
|
+
version: credential.version || wallet.version || 2,
|
|
660
777
|
hasPRF: credential.hasPRF || false
|
|
661
778
|
}));
|
|
662
779
|
|
package/styles/main.css
CHANGED
|
@@ -5247,6 +5247,53 @@ body:has(.modal.active) .nav-bar {
|
|
|
5247
5247
|
display: block;
|
|
5248
5248
|
}
|
|
5249
5249
|
|
|
5250
|
+
/* Messaging key config (derivation path + algorithm selection) */
|
|
5251
|
+
.messaging-key-config {
|
|
5252
|
+
padding: 14px;
|
|
5253
|
+
margin-bottom: 16px;
|
|
5254
|
+
}
|
|
5255
|
+
|
|
5256
|
+
.messaging-key-config-grid {
|
|
5257
|
+
display: grid;
|
|
5258
|
+
grid-template-columns: 1fr 2fr;
|
|
5259
|
+
gap: 12px;
|
|
5260
|
+
align-items: start;
|
|
5261
|
+
}
|
|
5262
|
+
|
|
5263
|
+
.messaging-key-config-item label {
|
|
5264
|
+
display: block;
|
|
5265
|
+
font-size: 0.7rem;
|
|
5266
|
+
text-transform: uppercase;
|
|
5267
|
+
letter-spacing: 0.06em;
|
|
5268
|
+
color: var(--white-40);
|
|
5269
|
+
margin-bottom: 6px;
|
|
5270
|
+
}
|
|
5271
|
+
|
|
5272
|
+
.messaging-path-row {
|
|
5273
|
+
display: flex;
|
|
5274
|
+
gap: 8px;
|
|
5275
|
+
align-items: center;
|
|
5276
|
+
}
|
|
5277
|
+
|
|
5278
|
+
.messaging-path-row input {
|
|
5279
|
+
flex: 1;
|
|
5280
|
+
min-width: 0;
|
|
5281
|
+
font-family: var(--font-mono);
|
|
5282
|
+
}
|
|
5283
|
+
|
|
5284
|
+
.messaging-key-hint {
|
|
5285
|
+
margin-top: 6px;
|
|
5286
|
+
font-size: 0.7rem;
|
|
5287
|
+
color: var(--white-40);
|
|
5288
|
+
font-family: var(--font-mono);
|
|
5289
|
+
}
|
|
5290
|
+
|
|
5291
|
+
@media (max-width: 640px) {
|
|
5292
|
+
.messaging-key-config-grid {
|
|
5293
|
+
grid-template-columns: 1fr;
|
|
5294
|
+
}
|
|
5295
|
+
}
|
|
5296
|
+
|
|
5250
5297
|
@media (max-width: 640px) {
|
|
5251
5298
|
.identity-card {
|
|
5252
5299
|
grid-template-columns: 1fr;
|