hd-wallet-ui 1.0.0 → 1.1.1
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/package.json +11 -3
- package/src/app.js +285 -0
- package/src/template.js +95 -6
- package/styles/main.css +248 -34
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hd-wallet-ui",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.1",
|
|
4
4
|
"description": "HD Wallet modal UI — login, keys, identity, trust map, and security bond. Attach to any button in your app.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/app.js",
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
"bip39": "^3.1.0",
|
|
35
35
|
"buffer": "^6.0.3",
|
|
36
36
|
"flatbuffers": "^25.9.23",
|
|
37
|
-
"hd-wallet-wasm": "
|
|
37
|
+
"hd-wallet-wasm": "^1.1.1",
|
|
38
38
|
"qrcode": "^1.5.3",
|
|
39
39
|
"vcard-cryptoperson": "^1.1.10"
|
|
40
40
|
},
|
|
@@ -55,5 +55,13 @@
|
|
|
55
55
|
"ethereum",
|
|
56
56
|
"solana"
|
|
57
57
|
],
|
|
58
|
-
"license": "Apache-2.0"
|
|
58
|
+
"license": "Apache-2.0",
|
|
59
|
+
"repository": {
|
|
60
|
+
"type": "git",
|
|
61
|
+
"url": "git+https://github.com/DigitalArsenal/hd-wallet-wasm.git"
|
|
62
|
+
},
|
|
63
|
+
"bugs": {
|
|
64
|
+
"url": "https://github.com/DigitalArsenal/hd-wallet-wasm/issues"
|
|
65
|
+
},
|
|
66
|
+
"homepage": "https://digitalarsenal.github.io/hd-wallet-wasm/"
|
|
59
67
|
}
|
package/src/app.js
CHANGED
|
@@ -19,6 +19,10 @@ import QRCode from 'qrcode';
|
|
|
19
19
|
import { Buffer } from 'buffer';
|
|
20
20
|
import { createV3 } from 'vcard-cryptoperson';
|
|
21
21
|
|
|
22
|
+
// SpaceDataStandards EME (Encrypted Message Envelope)
|
|
23
|
+
import { EME, EMET } from '@sds/lib/js/ts/EME/EME.js';
|
|
24
|
+
import * as flatbuffers from 'flatbuffers';
|
|
25
|
+
|
|
22
26
|
// Make Buffer available globally for various crypto libraries
|
|
23
27
|
window.Buffer = Buffer;
|
|
24
28
|
|
|
@@ -2772,14 +2776,17 @@ function setupMainAppHandlers() {
|
|
|
2772
2776
|
$('hd-coin')?.addEventListener('change', () => {
|
|
2773
2777
|
updatePathDisplay();
|
|
2774
2778
|
deriveAndDisplayAddress();
|
|
2779
|
+
updateEncryptionTab();
|
|
2775
2780
|
});
|
|
2776
2781
|
$('hd-account')?.addEventListener('input', () => {
|
|
2777
2782
|
updatePathDisplay();
|
|
2778
2783
|
deriveAndDisplayAddress();
|
|
2784
|
+
updateEncryptionTab();
|
|
2779
2785
|
});
|
|
2780
2786
|
$('hd-index')?.addEventListener('input', () => {
|
|
2781
2787
|
updatePathDisplay();
|
|
2782
2788
|
deriveAndDisplayAddress();
|
|
2789
|
+
updateEncryptionTab();
|
|
2783
2790
|
});
|
|
2784
2791
|
|
|
2785
2792
|
// PKI clear keys
|
|
@@ -3129,6 +3136,284 @@ function setupTrustHandlers() {
|
|
|
3129
3136
|
// Expose start/stop for login/logout
|
|
3130
3137
|
state._startTrustScanning = startTrustScanning;
|
|
3131
3138
|
state._stopTrustScanning = stopTrustScanning;
|
|
3139
|
+
|
|
3140
|
+
// =========================================================================
|
|
3141
|
+
// Encryption Tab Handlers (ECIES: ECDH + HKDF + AES-256-GCM)
|
|
3142
|
+
// =========================================================================
|
|
3143
|
+
|
|
3144
|
+
function updateEncryptionTab() {
|
|
3145
|
+
if (!state.hdRoot || !state.hdWalletModule) return;
|
|
3146
|
+
const coin = $('hd-coin')?.value || '0';
|
|
3147
|
+
const account = $('hd-account')?.value || '0';
|
|
3148
|
+
const index = $('hd-index')?.value || '0';
|
|
3149
|
+
const encPath = buildEncryptionPath(coin, account, index);
|
|
3150
|
+
const encKey = deriveHDKey(encPath);
|
|
3151
|
+
const pubKey = encKey.publicKey();
|
|
3152
|
+
const pubHex = toHexCompact(pubKey);
|
|
3153
|
+
|
|
3154
|
+
const senderPubEl = $('encrypt-sender-pubkey');
|
|
3155
|
+
const senderPathEl = $('encrypt-sender-path');
|
|
3156
|
+
if (senderPubEl) senderPubEl.textContent = pubHex;
|
|
3157
|
+
if (senderPathEl) senderPathEl.textContent = encPath;
|
|
3158
|
+
|
|
3159
|
+
const encryptBtn = $('encrypt-btn');
|
|
3160
|
+
if (encryptBtn) encryptBtn.disabled = false;
|
|
3161
|
+
}
|
|
3162
|
+
|
|
3163
|
+
// Update encryption tab when it becomes active or HD controls change
|
|
3164
|
+
$qa('.modal-tab[data-modal-tab="encryption-tab-content"]').forEach(tab => {
|
|
3165
|
+
tab.addEventListener('click', () => {
|
|
3166
|
+
if (state.hdRoot) updateEncryptionTab();
|
|
3167
|
+
});
|
|
3168
|
+
});
|
|
3169
|
+
|
|
3170
|
+
// "Self" button - fill recipient with own public key for testing
|
|
3171
|
+
$('encrypt-use-self')?.addEventListener('click', () => {
|
|
3172
|
+
const senderPub = $('encrypt-sender-pubkey')?.textContent;
|
|
3173
|
+
if (senderPub && senderPub !== '--') {
|
|
3174
|
+
$('encrypt-recipient-pubkey').value = senderPub;
|
|
3175
|
+
}
|
|
3176
|
+
});
|
|
3177
|
+
|
|
3178
|
+
// ---- EME state for current encryption result ----
|
|
3179
|
+
let currentEME = null; // EMET instance
|
|
3180
|
+
let currentFormat = 'json';
|
|
3181
|
+
|
|
3182
|
+
function emeToJSON(eme) {
|
|
3183
|
+
return JSON.stringify({
|
|
3184
|
+
ENCRYPTED_BLOB: eme.ENCRYPTED_BLOB,
|
|
3185
|
+
EPHEMERAL_PUBLIC_KEY: eme.EPHEMERAL_PUBLIC_KEY,
|
|
3186
|
+
MAC: eme.MAC,
|
|
3187
|
+
NONCE: eme.NONCE,
|
|
3188
|
+
TAG: eme.TAG,
|
|
3189
|
+
IV: eme.IV,
|
|
3190
|
+
SALT: eme.SALT,
|
|
3191
|
+
PUBLIC_KEY_IDENTIFIER: eme.PUBLIC_KEY_IDENTIFIER,
|
|
3192
|
+
CIPHER_SUITE: eme.CIPHER_SUITE,
|
|
3193
|
+
KDF_PARAMETERS: eme.KDF_PARAMETERS,
|
|
3194
|
+
ENCRYPTION_ALGORITHM_PARAMETERS: eme.ENCRYPTION_ALGORITHM_PARAMETERS,
|
|
3195
|
+
}, null, 2);
|
|
3196
|
+
}
|
|
3197
|
+
|
|
3198
|
+
function emeToFlatBuffer(eme) {
|
|
3199
|
+
const builder = new flatbuffers.Builder(1);
|
|
3200
|
+
const packed = eme.pack(builder);
|
|
3201
|
+
builder.finishSizePrefixed(packed, '$EME');
|
|
3202
|
+
return builder.asUint8Array();
|
|
3203
|
+
}
|
|
3204
|
+
|
|
3205
|
+
function emeToFlatBufferBase64(eme) {
|
|
3206
|
+
return Buffer.from(emeToFlatBuffer(eme)).toString('base64');
|
|
3207
|
+
}
|
|
3208
|
+
|
|
3209
|
+
function updateBundleDisplay() {
|
|
3210
|
+
if (!currentEME) return;
|
|
3211
|
+
const textarea = $('encrypt-bundle');
|
|
3212
|
+
if (!textarea) return;
|
|
3213
|
+
if (currentFormat === 'json') {
|
|
3214
|
+
textarea.value = emeToJSON(currentEME);
|
|
3215
|
+
} else {
|
|
3216
|
+
textarea.value = emeToFlatBufferBase64(currentEME);
|
|
3217
|
+
}
|
|
3218
|
+
}
|
|
3219
|
+
|
|
3220
|
+
// Format toggle buttons
|
|
3221
|
+
$qa('.encrypt-fmt-btn').forEach(btn => {
|
|
3222
|
+
btn.addEventListener('click', () => {
|
|
3223
|
+
$qa('.encrypt-fmt-btn').forEach(b => b.classList.remove('active'));
|
|
3224
|
+
btn.classList.add('active');
|
|
3225
|
+
currentFormat = btn.dataset.format;
|
|
3226
|
+
const label = $('encrypt-format-label');
|
|
3227
|
+
if (label) label.textContent = currentFormat === 'json'
|
|
3228
|
+
? 'EME (Encrypted Message Envelope) — SpaceDataStandards.org'
|
|
3229
|
+
: 'EME FlatBuffer binary (base64-encoded)';
|
|
3230
|
+
updateBundleDisplay();
|
|
3231
|
+
});
|
|
3232
|
+
});
|
|
3233
|
+
|
|
3234
|
+
// Encrypt button
|
|
3235
|
+
$('encrypt-btn')?.addEventListener('click', () => {
|
|
3236
|
+
const w = state.hdWalletModule;
|
|
3237
|
+
if (!w || !state.hdRoot) return;
|
|
3238
|
+
|
|
3239
|
+
const recipientHex = $('encrypt-recipient-pubkey')?.value?.trim();
|
|
3240
|
+
const plainStr = $('encrypt-plaintext')?.value;
|
|
3241
|
+
if (!recipientHex || !plainStr) {
|
|
3242
|
+
alert('Please enter both a recipient public key and a message.');
|
|
3243
|
+
return;
|
|
3244
|
+
}
|
|
3245
|
+
|
|
3246
|
+
try {
|
|
3247
|
+
const coin = $('hd-coin')?.value || '0';
|
|
3248
|
+
const account = $('hd-account')?.value || '0';
|
|
3249
|
+
const index = $('hd-index')?.value || '0';
|
|
3250
|
+
const encPath = buildEncryptionPath(coin, account, index);
|
|
3251
|
+
const senderKey = deriveHDKey(encPath);
|
|
3252
|
+
const senderPriv = senderKey.privateKey();
|
|
3253
|
+
const senderPub = senderKey.publicKey();
|
|
3254
|
+
|
|
3255
|
+
// Parse recipient public key from hex
|
|
3256
|
+
const recipientPub = new Uint8Array(recipientHex.match(/.{1,2}/g).map(b => parseInt(b, 16)));
|
|
3257
|
+
|
|
3258
|
+
// 1. ECDH shared secret
|
|
3259
|
+
const shared = w.curves.secp256k1.ecdh(senderPriv, recipientPub);
|
|
3260
|
+
|
|
3261
|
+
// 2. HKDF: derive 32-byte AES key from shared secret
|
|
3262
|
+
const salt = w.utils.getRandomBytes(32);
|
|
3263
|
+
const info = new TextEncoder().encode('ecies-secp256k1-aes256gcm');
|
|
3264
|
+
const aesKey = w.utils.hkdf(shared, salt, info, 32);
|
|
3265
|
+
|
|
3266
|
+
// 3. AES-256-GCM encrypt
|
|
3267
|
+
const iv = w.utils.generateIv();
|
|
3268
|
+
const plaintext = new TextEncoder().encode(plainStr);
|
|
3269
|
+
const { ciphertext, tag } = w.utils.aesGcm.encrypt(aesKey, plaintext, iv);
|
|
3270
|
+
|
|
3271
|
+
// Display field-level results
|
|
3272
|
+
$('encrypt-out-ciphertext').textContent = toHexCompact(ciphertext);
|
|
3273
|
+
$('encrypt-out-tag').textContent = toHexCompact(tag);
|
|
3274
|
+
$('encrypt-out-iv').textContent = toHexCompact(iv);
|
|
3275
|
+
$('encrypt-out-salt').textContent = toHexCompact(salt);
|
|
3276
|
+
$('encrypt-out-sender-pub').textContent = toHexCompact(senderPub);
|
|
3277
|
+
$('encrypt-output-section').style.display = 'block';
|
|
3278
|
+
|
|
3279
|
+
// Build EME (Encrypted Message Envelope) standard object
|
|
3280
|
+
currentEME = new EMET(
|
|
3281
|
+
Array.from(ciphertext), // ENCRYPTED_BLOB
|
|
3282
|
+
toHexCompact(senderPub), // EPHEMERAL_PUBLIC_KEY
|
|
3283
|
+
null, // MAC (not used, tag covers it)
|
|
3284
|
+
null, // NONCE (we use IV field instead)
|
|
3285
|
+
toHexCompact(tag), // TAG
|
|
3286
|
+
toHexCompact(iv), // IV
|
|
3287
|
+
toHexCompact(salt), // SALT
|
|
3288
|
+
null, // PUBLIC_KEY_IDENTIFIER
|
|
3289
|
+
'aes-256-gcm', // CIPHER_SUITE
|
|
3290
|
+
'hkdf-sha256', // KDF_PARAMETERS
|
|
3291
|
+
'secp256k1', // ENCRYPTION_ALGORITHM_PARAMETERS
|
|
3292
|
+
);
|
|
3293
|
+
|
|
3294
|
+
updateBundleDisplay();
|
|
3295
|
+
|
|
3296
|
+
// Clear any previous decrypt result
|
|
3297
|
+
$('decrypt-result').style.display = 'none';
|
|
3298
|
+
} catch (err) {
|
|
3299
|
+
console.error('Encryption failed:', err);
|
|
3300
|
+
alert('Encryption failed: ' + err.message);
|
|
3301
|
+
}
|
|
3302
|
+
});
|
|
3303
|
+
|
|
3304
|
+
// Copy bundle
|
|
3305
|
+
$('encrypt-copy-bundle')?.addEventListener('click', () => {
|
|
3306
|
+
const bundle = $('encrypt-bundle')?.value;
|
|
3307
|
+
if (bundle) {
|
|
3308
|
+
navigator.clipboard.writeText(bundle).catch(() => {});
|
|
3309
|
+
}
|
|
3310
|
+
});
|
|
3311
|
+
|
|
3312
|
+
// Download bundle
|
|
3313
|
+
$('encrypt-download-bundle')?.addEventListener('click', () => {
|
|
3314
|
+
if (!currentEME) return;
|
|
3315
|
+
let blob, filename;
|
|
3316
|
+
if (currentFormat === 'json') {
|
|
3317
|
+
blob = new Blob([emeToJSON(currentEME)], { type: 'application/json' });
|
|
3318
|
+
filename = 'message.eme.json';
|
|
3319
|
+
} else {
|
|
3320
|
+
const buf = emeToFlatBuffer(currentEME);
|
|
3321
|
+
blob = new Blob([buf], { type: 'application/octet-stream' });
|
|
3322
|
+
filename = 'message.eme.fbs';
|
|
3323
|
+
}
|
|
3324
|
+
const url = URL.createObjectURL(blob);
|
|
3325
|
+
const a = document.createElement('a');
|
|
3326
|
+
a.href = url;
|
|
3327
|
+
a.download = filename;
|
|
3328
|
+
a.click();
|
|
3329
|
+
URL.revokeObjectURL(url);
|
|
3330
|
+
});
|
|
3331
|
+
|
|
3332
|
+
// Parse EME from input (JSON or base64 FlatBuffer)
|
|
3333
|
+
function parseEMEPayload(input) {
|
|
3334
|
+
const trimmed = input.trim();
|
|
3335
|
+
// Try JSON first
|
|
3336
|
+
if (trimmed.startsWith('{')) {
|
|
3337
|
+
return JSON.parse(trimmed);
|
|
3338
|
+
}
|
|
3339
|
+
// Try base64 FlatBuffer (size-prefixed)
|
|
3340
|
+
const buf = new Uint8Array(Buffer.from(trimmed, 'base64'));
|
|
3341
|
+
const bb = new flatbuffers.ByteBuffer(buf);
|
|
3342
|
+
const root = EME.getSizePrefixedRootAsEME(bb);
|
|
3343
|
+
const eme = root.unpack();
|
|
3344
|
+
return eme;
|
|
3345
|
+
}
|
|
3346
|
+
|
|
3347
|
+
// Decrypt button
|
|
3348
|
+
$('decrypt-btn')?.addEventListener('click', () => {
|
|
3349
|
+
const w = state.hdWalletModule;
|
|
3350
|
+
if (!w || !state.hdRoot) return;
|
|
3351
|
+
|
|
3352
|
+
const payloadStr = $('decrypt-payload')?.value?.trim();
|
|
3353
|
+
if (!payloadStr) {
|
|
3354
|
+
alert('Paste an EME payload to decrypt.');
|
|
3355
|
+
return;
|
|
3356
|
+
}
|
|
3357
|
+
|
|
3358
|
+
try {
|
|
3359
|
+
const payload = parseEMEPayload(payloadStr);
|
|
3360
|
+
const fromHex = (h) => new Uint8Array(h.match(/.{1,2}/g).map(b => parseInt(b, 16)));
|
|
3361
|
+
|
|
3362
|
+
const senderPub = fromHex(payload.EPHEMERAL_PUBLIC_KEY);
|
|
3363
|
+
const tag = fromHex(payload.TAG);
|
|
3364
|
+
const iv = fromHex(payload.IV);
|
|
3365
|
+
const salt = fromHex(payload.SALT);
|
|
3366
|
+
|
|
3367
|
+
// ENCRYPTED_BLOB can be a number array (from EMET) or hex string
|
|
3368
|
+
let ciphertext;
|
|
3369
|
+
if (Array.isArray(payload.ENCRYPTED_BLOB)) {
|
|
3370
|
+
ciphertext = new Uint8Array(payload.ENCRYPTED_BLOB);
|
|
3371
|
+
} else {
|
|
3372
|
+
ciphertext = fromHex(payload.ENCRYPTED_BLOB);
|
|
3373
|
+
}
|
|
3374
|
+
|
|
3375
|
+
const coin = $('hd-coin')?.value || '0';
|
|
3376
|
+
const account = $('hd-account')?.value || '0';
|
|
3377
|
+
const index = $('hd-index')?.value || '0';
|
|
3378
|
+
const encPath = buildEncryptionPath(coin, account, index);
|
|
3379
|
+
const recipientKey = deriveHDKey(encPath);
|
|
3380
|
+
const recipientPriv = recipientKey.privateKey();
|
|
3381
|
+
|
|
3382
|
+
// 1. ECDH shared secret (using sender's public key)
|
|
3383
|
+
const shared = w.curves.secp256k1.ecdh(recipientPriv, senderPub);
|
|
3384
|
+
|
|
3385
|
+
// 2. HKDF: derive same AES key
|
|
3386
|
+
const info = new TextEncoder().encode('ecies-secp256k1-aes256gcm');
|
|
3387
|
+
const aesKey = w.utils.hkdf(shared, salt, info, 32);
|
|
3388
|
+
|
|
3389
|
+
// 3. AES-256-GCM decrypt
|
|
3390
|
+
const decrypted = w.utils.aesGcm.decrypt(aesKey, ciphertext, tag, iv);
|
|
3391
|
+
const decStr = new TextDecoder().decode(decrypted);
|
|
3392
|
+
|
|
3393
|
+
$('decrypt-result-value').textContent = decStr;
|
|
3394
|
+
$('decrypt-result').style.display = 'block';
|
|
3395
|
+
} catch (err) {
|
|
3396
|
+
console.error('Decryption failed:', err);
|
|
3397
|
+
alert('Decryption failed: ' + err.message);
|
|
3398
|
+
}
|
|
3399
|
+
});
|
|
3400
|
+
|
|
3401
|
+
// Enable decrypt button when payload is pasted
|
|
3402
|
+
$('decrypt-payload')?.addEventListener('input', () => {
|
|
3403
|
+
const btn = $('decrypt-btn');
|
|
3404
|
+
if (btn) btn.disabled = !$('decrypt-payload')?.value?.trim();
|
|
3405
|
+
});
|
|
3406
|
+
|
|
3407
|
+
// Clear button
|
|
3408
|
+
$('encrypt-clear-btn')?.addEventListener('click', () => {
|
|
3409
|
+
$('encrypt-plaintext').value = '';
|
|
3410
|
+
$('encrypt-recipient-pubkey').value = '';
|
|
3411
|
+
$('encrypt-output-section').style.display = 'none';
|
|
3412
|
+
$('decrypt-payload').value = '';
|
|
3413
|
+
$('decrypt-result').style.display = 'none';
|
|
3414
|
+
$('encrypt-btn').disabled = !state.hdRoot;
|
|
3415
|
+
$('decrypt-btn').disabled = true;
|
|
3416
|
+
});
|
|
3132
3417
|
}
|
|
3133
3418
|
|
|
3134
3419
|
// =============================================================================
|
package/src/template.js
CHANGED
|
@@ -4,13 +4,14 @@ export function getModalHTML() {
|
|
|
4
4
|
<div id="keys-modal" class="modal">
|
|
5
5
|
<div class="modal-glass modal-wide">
|
|
6
6
|
<div class="modal-header"><div class="account-header-info"><div class="account-header-top"><h3>Account</h3><h3 class="account-total-value" id="account-total-value"></h3></div><div class="account-address-row"><select id="account-address-select" class="glass-select compact account-address-select"><option value="xpub">xpub</option></select><code class="account-address-display" id="account-address-display"></code><button class="account-address-copy" id="account-address-copy" title="Copy address"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg></button><span class="account-address-value" id="account-address-value"></span></div></div><button class="modal-close">×</button></div>
|
|
7
|
+
<div class="modal-tabs">
|
|
8
|
+
<button class="modal-tab active" data-modal-tab="vcard-tab-content">Identity</button>
|
|
9
|
+
<button class="modal-tab" data-modal-tab="keys-tab-content">Keys</button>
|
|
10
|
+
<button class="modal-tab" data-modal-tab="trust-tab-content">Trust Map</button>
|
|
11
|
+
<button class="modal-tab" data-modal-tab="bond-tab-content">Security Bond</button>
|
|
12
|
+
<button class="modal-tab" data-modal-tab="encryption-tab-content">Encrypt</button>
|
|
13
|
+
</div>
|
|
7
14
|
<div class="modal-body">
|
|
8
|
-
<div class="modal-tabs">
|
|
9
|
-
<button class="modal-tab active" data-modal-tab="vcard-tab-content">Identity</button>
|
|
10
|
-
<button class="modal-tab" data-modal-tab="keys-tab-content">Keys</button>
|
|
11
|
-
<button class="modal-tab" data-modal-tab="trust-tab-content">Trust Map</button>
|
|
12
|
-
<button class="modal-tab" data-modal-tab="bond-tab-content">Security Bond</button>
|
|
13
|
-
</div>
|
|
14
15
|
<div id="keys-tab-content" class="modal-tab-content">
|
|
15
16
|
<div id="memory-info-box" class="memory-info-box" style="display:none"><div class="memory-info-content"><p><strong>Your keys never leave your device.</strong></p></div><button class="wallet-info-close" id="memory-info-close" title="Close">×</button></div>
|
|
16
17
|
<div class="xpub-section">
|
|
@@ -271,6 +272,94 @@ export function getModalHTML() {
|
|
|
271
272
|
</div>
|
|
272
273
|
</div>
|
|
273
274
|
|
|
275
|
+
<!-- Encryption Tab -->
|
|
276
|
+
<div id="encryption-tab-content" class="modal-tab-content">
|
|
277
|
+
<div class="encrypt-tab-intro">
|
|
278
|
+
<h4 class="section-label">ECIES Encrypt / Decrypt</h4>
|
|
279
|
+
<p>End-to-end encryption using your HD wallet keys. Uses ECDH key agreement + HKDF + AES-256-GCM (ECIES).</p>
|
|
280
|
+
</div>
|
|
281
|
+
|
|
282
|
+
<div class="encrypt-keys-section">
|
|
283
|
+
<div class="encrypt-key-row">
|
|
284
|
+
<div class="encrypt-key-card glass-card">
|
|
285
|
+
<div class="encrypt-key-header">
|
|
286
|
+
<span class="encrypt-role-badge sender">Sender (You)</span>
|
|
287
|
+
</div>
|
|
288
|
+
<div class="encrypt-key-detail">
|
|
289
|
+
<label>Encryption Public Key</label>
|
|
290
|
+
<code id="encrypt-sender-pubkey" class="truncate">--</code>
|
|
291
|
+
</div>
|
|
292
|
+
<div class="encrypt-key-detail">
|
|
293
|
+
<label>Derivation Path</label>
|
|
294
|
+
<code id="encrypt-sender-path">--</code>
|
|
295
|
+
</div>
|
|
296
|
+
</div>
|
|
297
|
+
<div class="encrypt-key-card glass-card">
|
|
298
|
+
<div class="encrypt-key-header">
|
|
299
|
+
<span class="encrypt-role-badge recipient">Recipient</span>
|
|
300
|
+
</div>
|
|
301
|
+
<div class="encrypt-key-detail">
|
|
302
|
+
<label>Recipient Public Key (hex)</label>
|
|
303
|
+
<div class="encrypt-recipient-input-row">
|
|
304
|
+
<input type="text" id="encrypt-recipient-pubkey" class="glass-input compact" placeholder="Paste recipient's secp256k1 public key (hex)">
|
|
305
|
+
<button id="encrypt-use-self" class="glass-btn small" title="Use your own key (for testing)">Self</button>
|
|
306
|
+
</div>
|
|
307
|
+
</div>
|
|
308
|
+
</div>
|
|
309
|
+
</div>
|
|
310
|
+
</div>
|
|
311
|
+
|
|
312
|
+
<div class="encrypt-message-section">
|
|
313
|
+
<div class="encrypt-input-group">
|
|
314
|
+
<label class="section-label">Message</label>
|
|
315
|
+
<textarea id="encrypt-plaintext" class="glass-input glass-textarea" rows="3" placeholder="Enter a message to encrypt..."></textarea>
|
|
316
|
+
</div>
|
|
317
|
+
<div class="encrypt-actions">
|
|
318
|
+
<button id="encrypt-btn" class="glass-btn primary" disabled>Encrypt</button>
|
|
319
|
+
<button id="decrypt-btn" class="glass-btn" disabled>Decrypt</button>
|
|
320
|
+
<button id="encrypt-clear-btn" class="glass-btn small">Clear</button>
|
|
321
|
+
</div>
|
|
322
|
+
</div>
|
|
323
|
+
|
|
324
|
+
<div id="encrypt-output-section" class="encrypt-output-section" style="display:none;">
|
|
325
|
+
<h4 class="section-label">Encrypted Payload</h4>
|
|
326
|
+
<div class="encrypt-output-fields">
|
|
327
|
+
<div class="encrypt-field"><label>Ciphertext</label><code id="encrypt-out-ciphertext" class="encrypt-out-value truncate"></code><button class="copy-btn" data-copy="encrypt-out-ciphertext" title="Copy"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path></svg></button></div>
|
|
328
|
+
<div class="encrypt-field"><label>Auth Tag</label><code id="encrypt-out-tag" class="encrypt-out-value truncate"></code></div>
|
|
329
|
+
<div class="encrypt-field"><label>IV (nonce)</label><code id="encrypt-out-iv" class="encrypt-out-value truncate"></code></div>
|
|
330
|
+
<div class="encrypt-field"><label>HKDF Salt</label><code id="encrypt-out-salt" class="encrypt-out-value truncate"></code></div>
|
|
331
|
+
<div class="encrypt-field"><label>Sender Public Key</label><code id="encrypt-out-sender-pub" class="encrypt-out-value truncate"></code></div>
|
|
332
|
+
</div>
|
|
333
|
+
<div class="encrypt-bundle-group">
|
|
334
|
+
<div class="encrypt-format-toggle">
|
|
335
|
+
<label class="section-label">Payload Bundle</label>
|
|
336
|
+
<div class="encrypt-format-btns">
|
|
337
|
+
<button class="glass-btn small encrypt-fmt-btn active" data-format="json">JSON</button>
|
|
338
|
+
<button class="glass-btn small encrypt-fmt-btn" data-format="flatbuffer">FlatBuffer</button>
|
|
339
|
+
</div>
|
|
340
|
+
</div>
|
|
341
|
+
<div class="encrypt-format-info">
|
|
342
|
+
<span id="encrypt-format-label" class="encrypt-format-label">EME (Encrypted Message Envelope) — SpaceDataStandards.org</span>
|
|
343
|
+
</div>
|
|
344
|
+
<textarea id="encrypt-bundle" class="glass-input glass-textarea" rows="4" readonly></textarea>
|
|
345
|
+
<div class="encrypt-bundle-actions">
|
|
346
|
+
<button class="glass-btn small" id="encrypt-copy-bundle">Copy</button>
|
|
347
|
+
<button class="glass-btn small" id="encrypt-download-bundle">Download</button>
|
|
348
|
+
</div>
|
|
349
|
+
</div>
|
|
350
|
+
</div>
|
|
351
|
+
|
|
352
|
+
<div class="encrypt-decrypt-section">
|
|
353
|
+
<h4 class="section-label">Decrypt a Message</h4>
|
|
354
|
+
<p class="encrypt-decrypt-info">Paste an EME payload (JSON or base64 FlatBuffer) to decrypt with your key.</p>
|
|
355
|
+
<textarea id="decrypt-payload" class="glass-input glass-textarea" rows="4" placeholder='Paste EME JSON or base64 FlatBuffer here...'></textarea>
|
|
356
|
+
<div id="decrypt-result" class="decrypt-result" style="display:none;">
|
|
357
|
+
<label>Decrypted Message</label>
|
|
358
|
+
<div class="decrypt-result-value" id="decrypt-result-value"></div>
|
|
359
|
+
</div>
|
|
360
|
+
</div>
|
|
361
|
+
</div>
|
|
362
|
+
|
|
274
363
|
</div>
|
|
275
364
|
</div>
|
|
276
365
|
</div>
|
package/styles/main.css
CHANGED
|
@@ -616,39 +616,42 @@ body:has(.modal.active) .nav-bar {
|
|
|
616
616
|
display: block;
|
|
617
617
|
}
|
|
618
618
|
|
|
619
|
-
/* Modal Tabs (Account modal - Keys / vCard) */
|
|
619
|
+
/* Modal Tabs (Account modal - Keys / vCard) - Bootstrap-style nav tabs */
|
|
620
620
|
.modal-tabs {
|
|
621
621
|
display: flex;
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
border-radius: var(--radius-sm);
|
|
622
|
+
gap: 0;
|
|
623
|
+
margin: 0;
|
|
624
|
+
padding: 0 24px;
|
|
625
|
+
background: transparent;
|
|
626
|
+
border-bottom: 2px solid var(--white-10, rgba(255,255,255,0.1));
|
|
628
627
|
}
|
|
629
628
|
|
|
630
629
|
.modal-tab {
|
|
631
|
-
|
|
632
|
-
padding: 8px 4px;
|
|
630
|
+
padding: 10px 16px;
|
|
633
631
|
background: transparent;
|
|
634
632
|
border: none;
|
|
635
|
-
border-
|
|
633
|
+
border-bottom: 2px solid transparent;
|
|
634
|
+
margin-bottom: -2px;
|
|
636
635
|
color: var(--white-60);
|
|
637
636
|
font-family: var(--font-sans);
|
|
638
637
|
font-size: 13px;
|
|
639
638
|
font-weight: 500;
|
|
640
639
|
line-height: 1;
|
|
641
|
-
display: flex;
|
|
642
|
-
align-items: center;
|
|
643
|
-
justify-content: center;
|
|
644
640
|
cursor: pointer;
|
|
645
|
-
transition:
|
|
641
|
+
transition: color 0.2s, border-color 0.2s;
|
|
646
642
|
text-align: center;
|
|
643
|
+
white-space: nowrap;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
.modal-tab:hover {
|
|
647
|
+
color: var(--white, #fff);
|
|
648
|
+
border-bottom-color: var(--white-30, rgba(255,255,255,0.3));
|
|
647
649
|
}
|
|
648
650
|
|
|
649
651
|
.modal-tab.active {
|
|
650
|
-
|
|
651
|
-
color: var(--
|
|
652
|
+
color: var(--white, #fff);
|
|
653
|
+
border-bottom-color: var(--accent, #00dc82);
|
|
654
|
+
background: transparent;
|
|
652
655
|
}
|
|
653
656
|
|
|
654
657
|
.modal-tab-content {
|
|
@@ -3991,46 +3994,35 @@ body:has(.modal.active) .nav-bar {
|
|
|
3991
3994
|
min-width: 0;
|
|
3992
3995
|
}
|
|
3993
3996
|
|
|
3994
|
-
.modal-tabs {
|
|
3995
|
-
flex-shrink: 0;
|
|
3996
|
-
position: sticky;
|
|
3997
|
-
top: 0;
|
|
3998
|
-
z-index: 10;
|
|
3999
|
-
background: var(--glass-bg);
|
|
4000
|
-
backdrop-filter: var(--glass-blur);
|
|
4001
|
-
border-bottom: 1px solid var(--glass-border);
|
|
4002
|
-
margin: -24px -24px 0 -24px;
|
|
4003
|
-
padding: 12px 24px;
|
|
4004
|
-
}
|
|
4005
3997
|
|
|
4006
|
-
.modal-tab-content {
|
|
3998
|
+
.modal-wide .modal-tab-content {
|
|
4007
3999
|
flex: 1;
|
|
4008
4000
|
overflow-y: auto;
|
|
4009
4001
|
overflow-x: hidden;
|
|
4010
|
-
|
|
4002
|
+
|
|
4011
4003
|
scrollbar-width: thin;
|
|
4012
4004
|
scrollbar-color: rgba(255, 255, 255, 0.2) transparent;
|
|
4013
4005
|
}
|
|
4014
4006
|
|
|
4015
|
-
.modal-tab-content::-webkit-scrollbar {
|
|
4007
|
+
.modal-wide .modal-tab-content::-webkit-scrollbar {
|
|
4016
4008
|
width: 3px;
|
|
4017
4009
|
}
|
|
4018
4010
|
|
|
4019
|
-
.modal-tab-content::-webkit-scrollbar-track {
|
|
4011
|
+
.modal-wide .modal-tab-content::-webkit-scrollbar-track {
|
|
4020
4012
|
background: transparent;
|
|
4021
4013
|
}
|
|
4022
4014
|
|
|
4023
|
-
.modal-tab-content::-webkit-scrollbar-thumb {
|
|
4015
|
+
.modal-wide .modal-tab-content::-webkit-scrollbar-thumb {
|
|
4024
4016
|
background: rgba(255, 255, 255, 0.2);
|
|
4025
4017
|
border-radius: 3px;
|
|
4026
4018
|
}
|
|
4027
4019
|
|
|
4028
|
-
.modal-tab-content::-webkit-scrollbar-thumb:hover {
|
|
4020
|
+
.modal-wide .modal-tab-content::-webkit-scrollbar-thumb:hover {
|
|
4029
4021
|
background: rgba(255, 255, 255, 0.3);
|
|
4030
4022
|
}
|
|
4031
4023
|
|
|
4032
4024
|
/* Ensure content doesn't get cut off */
|
|
4033
|
-
.modal-tab-content.active {
|
|
4025
|
+
.modal-wide .modal-tab-content.active {
|
|
4034
4026
|
display: block;
|
|
4035
4027
|
}
|
|
4036
4028
|
|
|
@@ -4142,6 +4134,7 @@ body:has(.modal.active) .nav-bar {
|
|
|
4142
4134
|
padding: 80px 24px;
|
|
4143
4135
|
position: relative;
|
|
4144
4136
|
z-index: 1;
|
|
4137
|
+
overflow: hidden;
|
|
4145
4138
|
}
|
|
4146
4139
|
|
|
4147
4140
|
.page-section {
|
|
@@ -4163,6 +4156,7 @@ html {
|
|
|
4163
4156
|
.section-container {
|
|
4164
4157
|
max-width: 1100px;
|
|
4165
4158
|
margin: 0 auto;
|
|
4159
|
+
width: 100%;
|
|
4166
4160
|
}
|
|
4167
4161
|
|
|
4168
4162
|
.section-container.section-sm {
|
|
@@ -4237,6 +4231,8 @@ html {
|
|
|
4237
4231
|
border: 1px solid var(--glass-border);
|
|
4238
4232
|
border-radius: var(--radius-sm);
|
|
4239
4233
|
overflow: hidden;
|
|
4234
|
+
width: 100%;
|
|
4235
|
+
min-width: 0;
|
|
4240
4236
|
}
|
|
4241
4237
|
|
|
4242
4238
|
.code-block-header {
|
|
@@ -4277,6 +4273,8 @@ html {
|
|
|
4277
4273
|
font-size: 0.8125rem;
|
|
4278
4274
|
line-height: 1.7;
|
|
4279
4275
|
color: var(--white-70);
|
|
4276
|
+
width: 100%;
|
|
4277
|
+
min-width: 0;
|
|
4280
4278
|
}
|
|
4281
4279
|
|
|
4282
4280
|
.chains-grid {
|
|
@@ -4288,6 +4286,17 @@ html {
|
|
|
4288
4286
|
.chain-card {
|
|
4289
4287
|
padding: 20px;
|
|
4290
4288
|
text-align: center;
|
|
4289
|
+
display: flex;
|
|
4290
|
+
flex-direction: column;
|
|
4291
|
+
align-items: center;
|
|
4292
|
+
text-decoration: none;
|
|
4293
|
+
color: inherit;
|
|
4294
|
+
transition: transform 0.15s ease, border-color 0.15s ease;
|
|
4295
|
+
}
|
|
4296
|
+
|
|
4297
|
+
a.chain-card:hover {
|
|
4298
|
+
transform: translateY(-2px);
|
|
4299
|
+
border-color: var(--white-20);
|
|
4291
4300
|
}
|
|
4292
4301
|
|
|
4293
4302
|
.chain-card strong {
|
|
@@ -4519,3 +4528,208 @@ html {
|
|
|
4519
4528
|
.example-code.active {
|
|
4520
4529
|
display: block;
|
|
4521
4530
|
}
|
|
4531
|
+
|
|
4532
|
+
@media (max-width: 600px) {
|
|
4533
|
+
.code-block pre {
|
|
4534
|
+
padding: 14px;
|
|
4535
|
+
font-size: 0.7rem;
|
|
4536
|
+
line-height: 1.6;
|
|
4537
|
+
}
|
|
4538
|
+
.example-tab {
|
|
4539
|
+
padding: 8px 8px;
|
|
4540
|
+
font-size: 0.75rem;
|
|
4541
|
+
}
|
|
4542
|
+
.section-sm {
|
|
4543
|
+
padding-left: 8px;
|
|
4544
|
+
padding-right: 8px;
|
|
4545
|
+
}
|
|
4546
|
+
}
|
|
4547
|
+
|
|
4548
|
+
/* =============================================================================
|
|
4549
|
+
Encryption Tab
|
|
4550
|
+
============================================================================= */
|
|
4551
|
+
|
|
4552
|
+
.encrypt-tab-intro {
|
|
4553
|
+
margin-bottom: 20px;
|
|
4554
|
+
}
|
|
4555
|
+
.encrypt-tab-intro p {
|
|
4556
|
+
font-size: 0.85rem;
|
|
4557
|
+
color: var(--white-60);
|
|
4558
|
+
margin: 4px 0 0;
|
|
4559
|
+
}
|
|
4560
|
+
|
|
4561
|
+
.encrypt-key-row {
|
|
4562
|
+
display: grid;
|
|
4563
|
+
grid-template-columns: 1fr 1fr;
|
|
4564
|
+
gap: 12px;
|
|
4565
|
+
margin-bottom: 20px;
|
|
4566
|
+
}
|
|
4567
|
+
@media (max-width: 640px) {
|
|
4568
|
+
.encrypt-key-row {
|
|
4569
|
+
grid-template-columns: 1fr;
|
|
4570
|
+
}
|
|
4571
|
+
}
|
|
4572
|
+
|
|
4573
|
+
.encrypt-key-card {
|
|
4574
|
+
padding: 14px;
|
|
4575
|
+
border-radius: var(--radius-sm);
|
|
4576
|
+
}
|
|
4577
|
+
.encrypt-key-header {
|
|
4578
|
+
margin-bottom: 10px;
|
|
4579
|
+
}
|
|
4580
|
+
.encrypt-role-badge {
|
|
4581
|
+
font-size: 0.7rem;
|
|
4582
|
+
text-transform: uppercase;
|
|
4583
|
+
letter-spacing: 0.08em;
|
|
4584
|
+
font-weight: 600;
|
|
4585
|
+
padding: 3px 8px;
|
|
4586
|
+
border-radius: 6px;
|
|
4587
|
+
}
|
|
4588
|
+
.encrypt-role-badge.sender {
|
|
4589
|
+
background: rgba(59, 130, 246, 0.2);
|
|
4590
|
+
color: #60a5fa;
|
|
4591
|
+
}
|
|
4592
|
+
.encrypt-role-badge.recipient {
|
|
4593
|
+
background: rgba(16, 185, 129, 0.2);
|
|
4594
|
+
color: #34d399;
|
|
4595
|
+
}
|
|
4596
|
+
.encrypt-key-detail {
|
|
4597
|
+
margin-bottom: 8px;
|
|
4598
|
+
}
|
|
4599
|
+
.encrypt-key-detail label {
|
|
4600
|
+
display: block;
|
|
4601
|
+
font-size: 0.7rem;
|
|
4602
|
+
text-transform: uppercase;
|
|
4603
|
+
letter-spacing: 0.06em;
|
|
4604
|
+
color: var(--white-40);
|
|
4605
|
+
margin-bottom: 4px;
|
|
4606
|
+
}
|
|
4607
|
+
.encrypt-key-detail code {
|
|
4608
|
+
font-size: 0.8rem;
|
|
4609
|
+
color: var(--white-70);
|
|
4610
|
+
word-break: break-all;
|
|
4611
|
+
}
|
|
4612
|
+
.encrypt-recipient-input-row {
|
|
4613
|
+
display: flex;
|
|
4614
|
+
gap: 8px;
|
|
4615
|
+
align-items: center;
|
|
4616
|
+
}
|
|
4617
|
+
.encrypt-recipient-input-row input {
|
|
4618
|
+
flex: 1;
|
|
4619
|
+
min-width: 0;
|
|
4620
|
+
}
|
|
4621
|
+
|
|
4622
|
+
.encrypt-message-section {
|
|
4623
|
+
margin-bottom: 20px;
|
|
4624
|
+
}
|
|
4625
|
+
.encrypt-actions {
|
|
4626
|
+
display: flex;
|
|
4627
|
+
gap: 8px;
|
|
4628
|
+
margin-top: 10px;
|
|
4629
|
+
}
|
|
4630
|
+
|
|
4631
|
+
.encrypt-output-section {
|
|
4632
|
+
margin-bottom: 24px;
|
|
4633
|
+
padding: 16px;
|
|
4634
|
+
background: var(--glass-bg);
|
|
4635
|
+
border: 1px solid var(--glass-border);
|
|
4636
|
+
border-radius: var(--radius-sm);
|
|
4637
|
+
}
|
|
4638
|
+
.encrypt-output-fields {
|
|
4639
|
+
display: flex;
|
|
4640
|
+
flex-direction: column;
|
|
4641
|
+
gap: 8px;
|
|
4642
|
+
margin-bottom: 16px;
|
|
4643
|
+
}
|
|
4644
|
+
.encrypt-field {
|
|
4645
|
+
display: flex;
|
|
4646
|
+
align-items: center;
|
|
4647
|
+
gap: 8px;
|
|
4648
|
+
}
|
|
4649
|
+
.encrypt-field label {
|
|
4650
|
+
font-size: 0.7rem;
|
|
4651
|
+
text-transform: uppercase;
|
|
4652
|
+
letter-spacing: 0.06em;
|
|
4653
|
+
color: var(--white-40);
|
|
4654
|
+
white-space: nowrap;
|
|
4655
|
+
min-width: 100px;
|
|
4656
|
+
}
|
|
4657
|
+
.encrypt-out-value {
|
|
4658
|
+
font-size: 0.8rem;
|
|
4659
|
+
color: var(--white-70);
|
|
4660
|
+
overflow: hidden;
|
|
4661
|
+
text-overflow: ellipsis;
|
|
4662
|
+
white-space: nowrap;
|
|
4663
|
+
flex: 1;
|
|
4664
|
+
min-width: 0;
|
|
4665
|
+
}
|
|
4666
|
+
.encrypt-bundle-group {
|
|
4667
|
+
margin-top: 12px;
|
|
4668
|
+
}
|
|
4669
|
+
.encrypt-format-toggle {
|
|
4670
|
+
display: flex;
|
|
4671
|
+
align-items: center;
|
|
4672
|
+
justify-content: space-between;
|
|
4673
|
+
margin-bottom: 6px;
|
|
4674
|
+
}
|
|
4675
|
+
.encrypt-format-btns {
|
|
4676
|
+
display: flex;
|
|
4677
|
+
gap: 4px;
|
|
4678
|
+
}
|
|
4679
|
+
.encrypt-fmt-btn {
|
|
4680
|
+
font-size: 0.72rem !important;
|
|
4681
|
+
padding: 4px 10px !important;
|
|
4682
|
+
opacity: 0.5;
|
|
4683
|
+
transition: opacity 0.2s;
|
|
4684
|
+
}
|
|
4685
|
+
.encrypt-fmt-btn.active {
|
|
4686
|
+
opacity: 1;
|
|
4687
|
+
border-color: var(--accent, #00dc82);
|
|
4688
|
+
color: var(--accent, #00dc82);
|
|
4689
|
+
}
|
|
4690
|
+
.encrypt-format-info {
|
|
4691
|
+
margin-bottom: 8px;
|
|
4692
|
+
}
|
|
4693
|
+
.encrypt-format-label {
|
|
4694
|
+
font-size: 0.72rem;
|
|
4695
|
+
color: var(--white-40);
|
|
4696
|
+
font-style: italic;
|
|
4697
|
+
}
|
|
4698
|
+
.encrypt-bundle-group textarea {
|
|
4699
|
+
font-size: 0.78rem;
|
|
4700
|
+
font-family: 'SF Mono', monospace;
|
|
4701
|
+
}
|
|
4702
|
+
.encrypt-bundle-actions {
|
|
4703
|
+
display: flex;
|
|
4704
|
+
gap: 8px;
|
|
4705
|
+
margin-top: 8px;
|
|
4706
|
+
}
|
|
4707
|
+
|
|
4708
|
+
.encrypt-decrypt-section {
|
|
4709
|
+
margin-bottom: 16px;
|
|
4710
|
+
}
|
|
4711
|
+
.encrypt-decrypt-info {
|
|
4712
|
+
font-size: 0.85rem;
|
|
4713
|
+
color: var(--white-60);
|
|
4714
|
+
margin: 4px 0 10px;
|
|
4715
|
+
}
|
|
4716
|
+
.decrypt-result {
|
|
4717
|
+
margin-top: 12px;
|
|
4718
|
+
padding: 12px;
|
|
4719
|
+
background: rgba(16, 185, 129, 0.1);
|
|
4720
|
+
border: 1px solid rgba(16, 185, 129, 0.25);
|
|
4721
|
+
border-radius: var(--radius-sm);
|
|
4722
|
+
}
|
|
4723
|
+
.decrypt-result label {
|
|
4724
|
+
display: block;
|
|
4725
|
+
font-size: 0.7rem;
|
|
4726
|
+
text-transform: uppercase;
|
|
4727
|
+
letter-spacing: 0.06em;
|
|
4728
|
+
color: #34d399;
|
|
4729
|
+
margin-bottom: 6px;
|
|
4730
|
+
}
|
|
4731
|
+
.decrypt-result-value {
|
|
4732
|
+
font-size: 0.95rem;
|
|
4733
|
+
color: var(--white);
|
|
4734
|
+
word-break: break-word;
|
|
4735
|
+
}
|