hd-wallet-ui 1.0.0 → 1.1.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hd-wallet-ui",
3
- "version": "1.0.0",
3
+ "version": "1.1.2",
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": "file:../wasm",
37
+ "hd-wallet-wasm": "^1.1.2",
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
 
@@ -34,6 +38,7 @@ import {
34
38
  coinTypeToConfig,
35
39
  buildSigningPath,
36
40
  buildEncryptionPath,
41
+ PKI_STORAGE_KEY,
37
42
  } from './constants.js';
38
43
 
39
44
  import {
@@ -1626,7 +1631,7 @@ async function updateAdversarialSecurity() {
1626
1631
  // Update account header total value
1627
1632
  const accountTotalEl = $('account-total-value');
1628
1633
  if (accountTotalEl) {
1629
- accountTotalEl.textContent = 'Security Level: ' + formatCurrencyValue(totalConverted, currency);
1634
+ accountTotalEl.textContent = 'Bond: ' + formatCurrencyValue(totalConverted, currency);
1630
1635
  }
1631
1636
 
1632
1637
  // Update account address dropdown values
@@ -2772,14 +2777,17 @@ function setupMainAppHandlers() {
2772
2777
  $('hd-coin')?.addEventListener('change', () => {
2773
2778
  updatePathDisplay();
2774
2779
  deriveAndDisplayAddress();
2780
+ updateEncryptionTab();
2775
2781
  });
2776
2782
  $('hd-account')?.addEventListener('input', () => {
2777
2783
  updatePathDisplay();
2778
2784
  deriveAndDisplayAddress();
2785
+ updateEncryptionTab();
2779
2786
  });
2780
2787
  $('hd-index')?.addEventListener('input', () => {
2781
2788
  updatePathDisplay();
2782
2789
  deriveAndDisplayAddress();
2790
+ updateEncryptionTab();
2783
2791
  });
2784
2792
 
2785
2793
  // PKI clear keys
@@ -3129,6 +3137,287 @@ function setupTrustHandlers() {
3129
3137
  // Expose start/stop for login/logout
3130
3138
  state._startTrustScanning = startTrustScanning;
3131
3139
  state._stopTrustScanning = stopTrustScanning;
3140
+
3141
+ // =========================================================================
3142
+ // Encryption Tab Handlers (ECIES: ECDH + HKDF + AES-256-GCM)
3143
+ // =========================================================================
3144
+
3145
+ function updateEncryptionTab() {
3146
+ if (!state.hdRoot || !state.hdWalletModule) return;
3147
+ const coin = $('hd-coin')?.value || '0';
3148
+ const account = $('hd-account')?.value || '0';
3149
+ const index = $('hd-index')?.value || '0';
3150
+ const encPath = buildEncryptionPath(coin, account, index);
3151
+ const encKey = deriveHDKey(encPath);
3152
+ const pubKey = encKey.publicKey();
3153
+ const pubHex = toHexCompact(pubKey);
3154
+
3155
+ const senderPubEl = $('encrypt-sender-pubkey');
3156
+ const senderPathEl = $('encrypt-sender-path');
3157
+ if (senderPubEl) senderPubEl.textContent = pubHex;
3158
+ if (senderPathEl) senderPathEl.textContent = encPath;
3159
+
3160
+ const encryptBtn = $('encrypt-btn');
3161
+ if (encryptBtn) encryptBtn.disabled = false;
3162
+ }
3163
+
3164
+ // Update encryption tab when it becomes active or HD controls change
3165
+ $qa('.modal-tab[data-modal-tab="encrypt-tab-content"]').forEach(tab => {
3166
+ tab.addEventListener('click', () => {
3167
+ if (state.hdRoot) updateEncryptionTab();
3168
+ });
3169
+ });
3170
+
3171
+ // "Self" button - fill recipient with own public key for testing
3172
+ $('encrypt-use-self')?.addEventListener('click', () => {
3173
+ const senderPub = $('encrypt-sender-pubkey')?.textContent;
3174
+ if (senderPub && senderPub !== '--') {
3175
+ $('encrypt-recipient-pubkey').value = senderPub;
3176
+ }
3177
+ });
3178
+
3179
+ // ---- EME state for current encryption result ----
3180
+ let currentEME = null; // EMET instance
3181
+ let currentFormat = 'json';
3182
+
3183
+ function emeToJSON(eme) {
3184
+ return JSON.stringify({
3185
+ ENCRYPTED_BLOB: eme.ENCRYPTED_BLOB,
3186
+ EPHEMERAL_PUBLIC_KEY: eme.EPHEMERAL_PUBLIC_KEY,
3187
+ MAC: eme.MAC,
3188
+ NONCE: eme.NONCE,
3189
+ TAG: eme.TAG,
3190
+ IV: eme.IV,
3191
+ SALT: eme.SALT,
3192
+ PUBLIC_KEY_IDENTIFIER: eme.PUBLIC_KEY_IDENTIFIER,
3193
+ CIPHER_SUITE: eme.CIPHER_SUITE,
3194
+ KDF_PARAMETERS: eme.KDF_PARAMETERS,
3195
+ ENCRYPTION_ALGORITHM_PARAMETERS: eme.ENCRYPTION_ALGORITHM_PARAMETERS,
3196
+ }, null, 2);
3197
+ }
3198
+
3199
+ function emeToFlatBuffer(eme) {
3200
+ const builder = new flatbuffers.Builder(1);
3201
+ const packed = eme.pack(builder);
3202
+ builder.finishSizePrefixed(packed, '$EME');
3203
+ return builder.asUint8Array();
3204
+ }
3205
+
3206
+ function emeToFlatBufferBase64(eme) {
3207
+ return Buffer.from(emeToFlatBuffer(eme)).toString('base64');
3208
+ }
3209
+
3210
+ function updateBundleDisplay() {
3211
+ if (!currentEME) return;
3212
+ const textarea = $('encrypt-bundle');
3213
+ if (!textarea) return;
3214
+ if (currentFormat === 'json') {
3215
+ textarea.value = emeToJSON(currentEME);
3216
+ } else {
3217
+ textarea.value = emeToFlatBufferBase64(currentEME);
3218
+ }
3219
+ }
3220
+
3221
+ // Format toggle buttons
3222
+ $qa('.encrypt-fmt-btn').forEach(btn => {
3223
+ btn.addEventListener('click', () => {
3224
+ $qa('.encrypt-fmt-btn').forEach(b => b.classList.remove('active'));
3225
+ btn.classList.add('active');
3226
+ currentFormat = btn.dataset.format;
3227
+ const label = $('encrypt-format-label');
3228
+ if (label) label.textContent = currentFormat === 'json'
3229
+ ? 'EME (Encrypted Message Envelope) — SpaceDataStandards.org'
3230
+ : 'EME FlatBuffer binary (base64-encoded)';
3231
+ updateBundleDisplay();
3232
+ });
3233
+ });
3234
+
3235
+ // Encrypt button
3236
+ $('encrypt-btn')?.addEventListener('click', () => {
3237
+ const w = state.hdWalletModule;
3238
+ if (!w || !state.hdRoot) return;
3239
+
3240
+ const recipientHex = $('encrypt-recipient-pubkey')?.value?.trim();
3241
+ const plainStr = $('encrypt-plaintext')?.value;
3242
+ if (!recipientHex || !plainStr) {
3243
+ alert('Please enter both a recipient public key and a message.');
3244
+ return;
3245
+ }
3246
+
3247
+ try {
3248
+ const coin = $('hd-coin')?.value || '0';
3249
+ const account = $('hd-account')?.value || '0';
3250
+ const index = $('hd-index')?.value || '0';
3251
+ const encPath = buildEncryptionPath(coin, account, index);
3252
+ const senderKey = deriveHDKey(encPath);
3253
+ const senderPriv = senderKey.privateKey();
3254
+ const senderPub = senderKey.publicKey();
3255
+
3256
+ // Parse recipient public key from hex
3257
+ const recipientPub = new Uint8Array(recipientHex.match(/.{1,2}/g).map(b => parseInt(b, 16)));
3258
+
3259
+ // 1. ECDH shared secret
3260
+ const shared = w.curves.secp256k1.ecdh(senderPriv, recipientPub);
3261
+
3262
+ // 2. HKDF: derive 32-byte AES key from shared secret
3263
+ const salt = w.utils.getRandomBytes(32);
3264
+ const info = new TextEncoder().encode('ecies-secp256k1-aes256gcm');
3265
+ const aesKey = w.utils.hkdf(shared, salt, info, 32);
3266
+
3267
+ // 3. AES-256-GCM encrypt
3268
+ const iv = w.utils.generateIv();
3269
+ const plaintext = new TextEncoder().encode(plainStr);
3270
+ const { ciphertext, tag } = w.utils.aesGcm.encrypt(aesKey, plaintext, iv);
3271
+
3272
+ // Display field-level results
3273
+ $('encrypt-out-ciphertext').textContent = toHexCompact(ciphertext);
3274
+ $('encrypt-out-tag').textContent = toHexCompact(tag);
3275
+ $('encrypt-out-iv').textContent = toHexCompact(iv);
3276
+ $('encrypt-out-salt').textContent = toHexCompact(salt);
3277
+ $('encrypt-out-sender-pub').textContent = toHexCompact(senderPub);
3278
+ // Build EME (Encrypted Message Envelope) standard object
3279
+ currentEME = new EMET(
3280
+ Array.from(ciphertext), // ENCRYPTED_BLOB
3281
+ toHexCompact(senderPub), // EPHEMERAL_PUBLIC_KEY
3282
+ null, // MAC (not used, tag covers it)
3283
+ null, // NONCE (we use IV field instead)
3284
+ toHexCompact(tag), // TAG
3285
+ toHexCompact(iv), // IV
3286
+ toHexCompact(salt), // SALT
3287
+ null, // PUBLIC_KEY_IDENTIFIER
3288
+ 'aes-256-gcm', // CIPHER_SUITE
3289
+ 'hkdf-sha256', // KDF_PARAMETERS
3290
+ 'secp256k1', // ENCRYPTION_ALGORITHM_PARAMETERS
3291
+ );
3292
+
3293
+ updateBundleDisplay();
3294
+
3295
+ // Switch to result step
3296
+ $('encrypt-step-compose').style.display = 'none';
3297
+ $('encrypt-step-result').style.display = 'block';
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
+
3395
+ // Switch to result step
3396
+ $('decrypt-step-input').style.display = 'none';
3397
+ $('decrypt-step-result').style.display = 'block';
3398
+ } catch (err) {
3399
+ console.error('Decryption failed:', err);
3400
+ alert('Decryption failed: ' + err.message);
3401
+ }
3402
+ });
3403
+
3404
+ // Enable decrypt button when payload is pasted
3405
+ $('decrypt-payload')?.addEventListener('input', () => {
3406
+ const btn = $('decrypt-btn');
3407
+ if (btn) btn.disabled = !$('decrypt-payload')?.value?.trim();
3408
+ });
3409
+
3410
+ // Back button: encrypt result -> compose
3411
+ $('encrypt-back-btn')?.addEventListener('click', () => {
3412
+ $('encrypt-step-result').style.display = 'none';
3413
+ $('encrypt-step-compose').style.display = 'block';
3414
+ });
3415
+
3416
+ // Back button: decrypt result -> input
3417
+ $('decrypt-back-btn')?.addEventListener('click', () => {
3418
+ $('decrypt-step-result').style.display = 'none';
3419
+ $('decrypt-step-input').style.display = 'block';
3420
+ });
3132
3421
  }
3133
3422
 
3134
3423
  // =============================================================================
package/src/template.js CHANGED
@@ -4,13 +4,15 @@ 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">&times;</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">Bond</button>
12
+ <button class="modal-tab" data-modal-tab="encrypt-tab-content">Encrypt</button>
13
+ <button class="modal-tab" data-modal-tab="decrypt-tab-content">Decrypt</button>
14
+ </div>
7
15
  <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
16
  <div id="keys-tab-content" class="modal-tab-content">
15
17
  <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">&times;</button></div>
16
18
  <div class="xpub-section">
@@ -271,6 +273,115 @@ export function getModalHTML() {
271
273
  </div>
272
274
  </div>
273
275
 
276
+ <!-- Encrypt Tab -->
277
+ <div id="encrypt-tab-content" class="modal-tab-content">
278
+ <!-- Step 1: Compose -->
279
+ <div id="encrypt-step-compose" class="encrypt-step">
280
+ <div class="encrypt-tab-intro">
281
+ <h4 class="section-label">Encrypt a Message</h4>
282
+ <p>ECDH key agreement + HKDF + AES-256-GCM (ECIES)</p>
283
+ </div>
284
+
285
+ <div class="encrypt-keys-section">
286
+ <div class="encrypt-key-row">
287
+ <div class="encrypt-key-card glass-card">
288
+ <div class="encrypt-key-header">
289
+ <span class="encrypt-role-badge sender">Sender (You)</span>
290
+ </div>
291
+ <div class="encrypt-key-detail">
292
+ <label>Encryption Public Key</label>
293
+ <code id="encrypt-sender-pubkey" class="truncate">--</code>
294
+ </div>
295
+ <div class="encrypt-key-detail">
296
+ <label>Derivation Path</label>
297
+ <code id="encrypt-sender-path">--</code>
298
+ </div>
299
+ </div>
300
+ <div class="encrypt-key-card glass-card">
301
+ <div class="encrypt-key-header">
302
+ <span class="encrypt-role-badge recipient">Recipient</span>
303
+ </div>
304
+ <div class="encrypt-key-detail">
305
+ <label>Recipient Public Key (hex)</label>
306
+ <div class="encrypt-recipient-input-row">
307
+ <input type="text" id="encrypt-recipient-pubkey" class="glass-input compact" placeholder="Paste recipient's secp256k1 public key (hex)">
308
+ <button id="encrypt-use-self" class="glass-btn small" title="Use your own key (for testing)">Self</button>
309
+ </div>
310
+ </div>
311
+ </div>
312
+ </div>
313
+ </div>
314
+
315
+ <div class="encrypt-message-section">
316
+ <div class="encrypt-input-group">
317
+ <label class="section-label">Message</label>
318
+ <textarea id="encrypt-plaintext" class="glass-input glass-textarea" rows="3" placeholder="Enter a message to encrypt..."></textarea>
319
+ </div>
320
+ <div class="encrypt-actions">
321
+ <button id="encrypt-btn" class="glass-btn primary" disabled>Encrypt</button>
322
+ </div>
323
+ </div>
324
+ </div>
325
+
326
+ <!-- Step 2: Result (replaces compose) -->
327
+ <div id="encrypt-step-result" class="encrypt-step" style="display:none;">
328
+ <div class="encrypt-step-header">
329
+ <button id="encrypt-back-btn" class="glass-btn small encrypt-back-btn"><svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2"><path d="M19 12H5M12 19l-7-7 7-7"/></svg> Back</button>
330
+ <h4 class="section-label">Encrypted Payload</h4>
331
+ </div>
332
+ <div class="encrypt-output-fields">
333
+ <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>
334
+ <div class="encrypt-field"><label>Auth Tag</label><code id="encrypt-out-tag" class="encrypt-out-value truncate"></code></div>
335
+ <div class="encrypt-field"><label>IV (nonce)</label><code id="encrypt-out-iv" class="encrypt-out-value truncate"></code></div>
336
+ <div class="encrypt-field"><label>HKDF Salt</label><code id="encrypt-out-salt" class="encrypt-out-value truncate"></code></div>
337
+ <div class="encrypt-field"><label>Sender Public Key</label><code id="encrypt-out-sender-pub" class="encrypt-out-value truncate"></code></div>
338
+ </div>
339
+ <div class="encrypt-bundle-group">
340
+ <div class="encrypt-format-toggle">
341
+ <label class="section-label">Payload Bundle</label>
342
+ <div class="encrypt-format-btns">
343
+ <button class="glass-btn small encrypt-fmt-btn active" data-format="json">JSON</button>
344
+ <button class="glass-btn small encrypt-fmt-btn" data-format="flatbuffer">FlatBuffer</button>
345
+ </div>
346
+ </div>
347
+ <div class="encrypt-format-info">
348
+ <span id="encrypt-format-label" class="encrypt-format-label">EME (Encrypted Message Envelope) — SpaceDataStandards.org</span>
349
+ </div>
350
+ <textarea id="encrypt-bundle" class="glass-input glass-textarea" rows="4" readonly></textarea>
351
+ <div class="encrypt-bundle-actions">
352
+ <button class="glass-btn small" id="encrypt-copy-bundle">Copy</button>
353
+ <button class="glass-btn small" id="encrypt-download-bundle">Download</button>
354
+ </div>
355
+ </div>
356
+ </div>
357
+ </div>
358
+
359
+ <!-- Decrypt Tab -->
360
+ <div id="decrypt-tab-content" class="modal-tab-content">
361
+ <!-- Step 1: Input -->
362
+ <div id="decrypt-step-input" class="encrypt-step">
363
+ <div class="encrypt-tab-intro">
364
+ <h4 class="section-label">Decrypt a Message</h4>
365
+ <p>Paste an EME payload (JSON or base64 FlatBuffer) to decrypt with your key.</p>
366
+ </div>
367
+ <textarea id="decrypt-payload" class="glass-input glass-textarea" rows="6" placeholder='Paste EME JSON or base64 FlatBuffer here...'></textarea>
368
+ <div class="encrypt-actions">
369
+ <button id="decrypt-btn" class="glass-btn primary" disabled>Decrypt</button>
370
+ </div>
371
+ </div>
372
+
373
+ <!-- Step 2: Result (replaces input) -->
374
+ <div id="decrypt-step-result" class="encrypt-step" style="display:none;">
375
+ <div class="encrypt-step-header">
376
+ <button id="decrypt-back-btn" class="glass-btn small encrypt-back-btn"><svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2"><path d="M19 12H5M12 19l-7-7 7-7"/></svg> Back</button>
377
+ <h4 class="section-label">Decrypted Message</h4>
378
+ </div>
379
+ <div class="decrypt-result">
380
+ <div class="decrypt-result-value" id="decrypt-result-value"></div>
381
+ </div>
382
+ </div>
383
+ </div>
384
+
274
385
  </div>
275
386
  </div>
276
387
  </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
- align-items: center;
623
- gap: 4px;
624
- margin-bottom: 20px;
625
- padding: 4px;
626
- background: var(--white-05);
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
- flex: 1;
632
- padding: 8px 4px;
630
+ padding: 10px 16px;
633
631
  background: transparent;
634
632
  border: none;
635
- border-radius: 10px;
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: all 0.2s;
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
- background: var(--glass-hover);
651
- color: var(--white);
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,238 @@ 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-step-header {
4632
+ display: flex;
4633
+ align-items: center;
4634
+ gap: 12px;
4635
+ margin-bottom: 16px;
4636
+ }
4637
+
4638
+ .encrypt-step-header .section-label {
4639
+ margin: 0;
4640
+ }
4641
+
4642
+ .encrypt-back-btn {
4643
+ display: inline-flex;
4644
+ align-items: center;
4645
+ gap: 4px;
4646
+ flex-shrink: 0;
4647
+ }
4648
+
4649
+ .encrypt-output-section {
4650
+ margin-bottom: 24px;
4651
+ padding: 16px;
4652
+ background: var(--glass-bg);
4653
+ border: 1px solid var(--glass-border);
4654
+ border-radius: var(--radius-sm);
4655
+ }
4656
+ .encrypt-output-fields {
4657
+ display: flex;
4658
+ flex-direction: column;
4659
+ gap: 8px;
4660
+ margin-bottom: 16px;
4661
+ }
4662
+ .encrypt-field {
4663
+ display: flex;
4664
+ align-items: center;
4665
+ gap: 8px;
4666
+ }
4667
+ .encrypt-field label {
4668
+ font-size: 0.7rem;
4669
+ text-transform: uppercase;
4670
+ letter-spacing: 0.06em;
4671
+ color: var(--white-40);
4672
+ white-space: nowrap;
4673
+ min-width: 100px;
4674
+ }
4675
+ .encrypt-out-value {
4676
+ font-size: 0.8rem;
4677
+ color: var(--white-70);
4678
+ overflow: hidden;
4679
+ text-overflow: ellipsis;
4680
+ white-space: nowrap;
4681
+ flex: 1;
4682
+ min-width: 0;
4683
+ }
4684
+ .encrypt-field .copy-btn {
4685
+ background: none;
4686
+ border: none;
4687
+ padding: 4px;
4688
+ cursor: pointer;
4689
+ color: var(--white-30);
4690
+ flex-shrink: 0;
4691
+ transition: color 0.2s;
4692
+ }
4693
+ .encrypt-field .copy-btn:hover {
4694
+ color: var(--white);
4695
+ }
4696
+ .encrypt-bundle-group {
4697
+ margin-top: 12px;
4698
+ }
4699
+ .encrypt-format-toggle {
4700
+ display: flex;
4701
+ align-items: center;
4702
+ justify-content: space-between;
4703
+ margin-bottom: 6px;
4704
+ }
4705
+ .encrypt-format-btns {
4706
+ display: flex;
4707
+ gap: 4px;
4708
+ }
4709
+ .encrypt-fmt-btn {
4710
+ font-size: 0.72rem !important;
4711
+ padding: 4px 10px !important;
4712
+ opacity: 0.5;
4713
+ transition: opacity 0.2s;
4714
+ }
4715
+ .encrypt-fmt-btn.active {
4716
+ opacity: 1;
4717
+ border-color: var(--accent, #00dc82);
4718
+ color: var(--accent, #00dc82);
4719
+ }
4720
+ .encrypt-format-info {
4721
+ margin-bottom: 8px;
4722
+ }
4723
+ .encrypt-format-label {
4724
+ font-size: 0.72rem;
4725
+ color: var(--white-40);
4726
+ font-style: italic;
4727
+ }
4728
+ .encrypt-bundle-group textarea {
4729
+ font-size: 0.78rem;
4730
+ font-family: 'SF Mono', monospace;
4731
+ }
4732
+ .encrypt-bundle-actions {
4733
+ display: flex;
4734
+ gap: 8px;
4735
+ margin-top: 8px;
4736
+ }
4737
+
4738
+ .encrypt-decrypt-section {
4739
+ margin-bottom: 16px;
4740
+ }
4741
+ .encrypt-decrypt-info {
4742
+ font-size: 0.85rem;
4743
+ color: var(--white-60);
4744
+ margin: 4px 0 10px;
4745
+ }
4746
+ .decrypt-result {
4747
+ margin-top: 12px;
4748
+ padding: 12px;
4749
+ background: rgba(16, 185, 129, 0.1);
4750
+ border: 1px solid rgba(16, 185, 129, 0.25);
4751
+ border-radius: var(--radius-sm);
4752
+ }
4753
+ .decrypt-result label {
4754
+ display: block;
4755
+ font-size: 0.7rem;
4756
+ text-transform: uppercase;
4757
+ letter-spacing: 0.06em;
4758
+ color: #34d399;
4759
+ margin-bottom: 6px;
4760
+ }
4761
+ .decrypt-result-value {
4762
+ font-size: 0.95rem;
4763
+ color: var(--white);
4764
+ word-break: break-word;
4765
+ }