v0.1.1 - Fixed Teensy 4.1 NIP-04 crash and NIP-44 dispatch bug: reduced secp256k1 ECMULT_CONST_GROUP_SIZE 5->4 to halve ECDH stack usage (~3.5KB->~1.7KB) preventing DTCM stack overflow in secp256k1_ecmult_const during nip04/nip44 ECDH; fixed is_nip44 selector in dispatch.cpp (method[7] was always 'i', digit is at method[9]) so nostr_nip44_* verbs now reach the nip44 implementation instead of silently calling nip04; verified nip04+nip44 round-trip on hardware

This commit is contained in:
Laan Tungir
2026-07-27 07:16:53 -04:00
parent 64fbd5c874
commit 2ba81c4bc8
316 changed files with 98571 additions and 231 deletions
+592 -222
View File
@@ -4,238 +4,577 @@
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>n_signer CYD Web Serial Demo</title>
<script>
/* §12 Dark mode: set class synchronously before paint. */
(function () {
if (localStorage.getItem('theme') === 'dark') {
document.documentElement.classList.add('dark-mode');
}
})();
</script>
<style>
/*
* Two-tier rule block (§13). This is a single-file demo so the "global"
* sheet is inlined here, but the same rules apply: variables everywhere,
* no hardcoded hex in component rules, spacing on the 5/10/15/20/40 ladder.
*/
/* ---- §17 Minimum variable set ---- */
:root {
--bg: #0b0f14;
--panel: #121821;
--panel-2: #182231;
--text: #e6edf3;
--muted: #9fb0c3;
--accent: #58a6ff;
--good: #3fb950;
--bad: #f85149;
--border: #263448;
--font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, "Courier New", Courier, monospace;
--primary-color: #000000;
--secondary-color: #ffffff;
--accent-color: #ff0000;
--muted-color: #dddddd;
--color: var(--primary-color);
--background-color: var(--secondary-color);
--border-color: var(--muted-color);
--border-radius: 5px;
--border-width: 1px;
--border: var(--border-width) solid var(--border-color);
--header-height: 2vh;
--header-min-height: 60px;
--footer-height: 2vh;
--footer-min-height: 20px;
--image-grayscale: 100%;
--image-grayscale-hover: 20%;
}
* { box-sizing: border-box; }
body {
/* §12 Dark mode = swap variables only. */
html.dark-mode, body.dark-mode {
--primary-color: #ffffff;
--secondary-color: #000000;
--muted-color: #777777;
}
/* ---- §3 Typography / global reset ---- */
* {
font-family: var(--font-family);
margin: 0;
font-family: Inter, system-ui, -apple-system, Segoe UI, Roboto, sans-serif;
background: var(--bg);
color: var(--text);
line-height: 1.35;
padding: 0;
box-sizing: border-box;
}
html, body {
color: var(--color);
background-color: var(--background-color);
line-height: 1.4;
}
body {
display: flex;
flex-direction: column;
min-height: 100vh;
}
a { color: var(--accent-color); }
/* ---- §4 Layout shell ---- */
#divHeader {
position: fixed;
top: 0; left: 0; right: 0;
height: var(--header-height);
min-height: var(--header-min-height);
background-color: var(--secondary-color);
border-bottom: 3px solid var(--primary-color);
display: flex;
align-items: center;
padding: 0 20px;
z-index: 2;
}
#divHeader .divHeaderText {
font-size: 200%;
font-weight: bold;
flex: 1;
text-align: center;
}
#divHeader .divHeaderButtons {
width: 40px;
display: flex;
justify-content: flex-start;
}
/* Keep the hamburger clickable above the open sidenav (§10 overlay). */
#btnHamburger { position: relative; z-index: 4; }
#divBody {
position: relative;
top: max(var(--header-height), var(--header-min-height));
display: flex;
flex-direction: row;
flex-wrap: wrap;
justify-content: space-around;
width: 100%;
padding: 20px;
gap: 20px;
}
#divFooter {
position: fixed;
bottom: 0; left: 0; right: 0;
height: var(--footer-height);
min-height: var(--footer-min-height);
border-top: 3px solid var(--primary-color);
background-color: var(--secondary-color);
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
padding: 0 20px;
z-index: 2;
}
.divFooterBox {
font-size: 12px;
color: var(--muted-color);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
#divFooterCenter { text-align: center; flex: 1; }
#divFooterRight { text-align: right; }
/* ---- §10 SideNav ---- */
#divSideNav {
position: fixed;
top: 0; left: 0;
height: 100%;
width: 0;
transition: width 0.5s;
border-right: 3px solid var(--primary-color);
background-color: var(--secondary-color);
display: flex;
flex-direction: column;
overflow: hidden;
z-index: 3;
}
#divSideNav.open { width: clamp(400px, 50vw, 600px); }
#divSideNavBody {
flex: 1;
padding: 20px;
overflow-y: auto;
font-size: 14px;
}
/* Close button pinned to the top-right of the open sidenav. */
#btnCloseSideNav {
position: absolute;
top: 10px;
right: 10px;
width: 32px;
height: 32px;
background: var(--secondary-color);
color: var(--primary-color);
border: var(--border);
border-radius: var(--border-radius);
cursor: pointer;
font-size: 20px;
line-height: 1;
display: flex;
align-items: center;
justify-content: center;
transition: border-color 0.2s;
z-index: 4;
}
#btnCloseSideNav:hover { border-color: var(--accent-color); }
#divSideNavBody h3 {
font-size: 14px;
font-weight: bold;
margin-top: 20px;
margin-bottom: 10px;
}
#divSideNavBody h3:first-child { margin-top: 0; }
#divSideNavBody p {
margin-bottom: 10px;
color: var(--muted-color);
}
#divSideNavBody code {
color: var(--primary-color);
white-space: pre-wrap;
}
#divVersionBar {
flex-shrink: 0;
border-top: 3px solid var(--primary-color);
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px 20px;
}
#versionDisplay { font-size: 14px; }
#divVersionBarButtons { display: flex; gap: 10px; }
/* Hamburger button (§6 icon-only button) */
#btnHamburger {
width: 32px; height: 32px;
background: var(--secondary-color);
color: var(--primary-color);
border: var(--border);
border-radius: var(--border-radius);
cursor: pointer;
font-size: 20px;
line-height: 1;
display: flex;
align-items: center;
justify-content: center;
transition: border-color 0.2s;
}
#btnHamburger:hover { border-color: var(--accent-color); }
/* ---- §6 Buttons ---- */
.btn {
font-family: var(--font-family);
color: var(--primary-color);
background-color: var(--secondary-color);
border: var(--border-width) solid var(--primary-color);
border-radius: var(--border-radius);
padding: 8px 16px;
font-size: 14px;
font-weight: bold;
cursor: pointer;
transition: border-color 0.2s, background-color 0.2s, color 0.2s;
}
.btn:hover { border-color: var(--accent-color); }
.btn:active {
background-color: var(--accent-color);
color: var(--secondary-color);
border-color: var(--primary-color);
}
.btn:disabled { opacity: 0.5; cursor: not-allowed; }
.btn.secondary { border-color: var(--muted-color); }
.btn.danger { border-color: var(--accent-color); color: var(--accent-color); }
.btn.danger:active { background-color: var(--accent-color); color: var(--secondary-color); }
/* ---- §7 Forms ---- */
.inpStyle {
border: var(--border);
border-radius: var(--border-radius);
padding: 10px;
font-size: 100%;
font-family: var(--font-family);
color: var(--primary-color);
background-color: var(--secondary-color);
width: 100%;
}
.inpStyle:focus { outline: none; border-color: var(--accent-color); }
textarea.inpStyle { resize: vertical; min-height: 64px; }
input[type="number"].inpStyle { max-width: 130px; }
label {
font-size: 12px;
color: var(--muted-color);
display: block;
margin: 10px 0 5px;
}
/* ---- §9 Cards ---- */
.divPostItem {
padding: 15px;
border: 1px solid var(--primary-color);
border-radius: 10px;
margin-bottom: 15px;
display: flex;
flex-direction: column;
background-color: var(--secondary-color);
}
.divPostItem h2 {
font-size: 14px;
font-weight: bold;
margin-bottom: 10px;
}
.divPostItem .row {
display: flex;
gap: 10px;
flex-wrap: wrap;
align-items: center;
margin-top: 10px;
}
.divPostItem .note {
font-size: 12px;
color: var(--muted-color);
margin-bottom: 10px;
}
.divPostItem .warn {
font-size: 12px;
color: var(--accent-color);
margin-bottom: 10px;
}
/* Connection card spans full width */
.divPostItem.full { width: 100%; }
.divPostItem.section { flex: 1 1 320px; min-width: 320px; }
/* ---- §11 Feedback / state ---- */
.status {
padding: 5px 10px;
border: var(--border);
border-radius: 999px;
font-size: 12px;
color: var(--muted-color);
}
.status.ok { border-color: var(--primary-color); color: var(--primary-color); }
.status.err { border-color: var(--accent-color); color: var(--accent-color); }
pre {
margin-top: 10px;
padding: 10px;
border: var(--border);
border-radius: var(--border-radius);
background-color: var(--secondary-color);
color: var(--primary-color);
overflow: auto;
max-height: 200px;
font-size: 12px;
white-space: pre-wrap;
word-break: break-all;
}
/* §14 reduced motion */
@media (prefers-reduced-motion: reduce) {
#divSideNav { transition: none; }
.btn, #btnHamburger, img { transition: none; }
}
.wrap { max-width: 980px; margin: 20px auto; padding: 0 14px 24px; }
h1 { margin: 0 0 8px; font-size: 1.45rem; }
p.note { margin: 0 0 14px; color: var(--muted); }
.row { display: flex; gap: 10px; flex-wrap: wrap; align-items: center; }
.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); gap: 12px; margin-top: 12px; }
.card { background: linear-gradient(180deg, var(--panel), var(--panel-2)); border: 1px solid var(--border); border-radius: 12px; padding: 12px; }
.card h2 { font-size: 1rem; margin: 0 0 10px; }
label { font-size: 0.86rem; color: var(--muted); display: block; margin: 6px 0 4px; }
input, textarea, select, button { font: inherit; border-radius: 8px; border: 1px solid var(--border); }
input, textarea, select { width: 100%; background: #0c131d; color: var(--text); padding: 8px 10px; }
textarea { min-height: 64px; resize: vertical; }
input[type="number"] { max-width: 130px; }
button { background: #1f6feb; color: white; padding: 8px 12px; cursor: pointer; border: 0; }
button[disabled] { opacity: 0.55; cursor: not-allowed; }
.secondary { background: #334155; }
.status { padding: 6px 10px; border-radius: 999px; background: #2a3648; color: var(--muted); font-size: 0.85rem; border: 1px solid var(--border); }
.status.ok { color: var(--good); border-color: #2f5a3a; }
.status.err { color: var(--bad); border-color: #6a3131; }
pre { margin: 8px 0 0; background: #0a1018; border: 1px solid #1b2636; color: #d7e2ee; padding: 10px; border-radius: 8px; overflow: auto; max-height: 200px; font-size: 12px; }
.mono { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; }
.warn { color: #d29922; font-size: 0.8rem; }
</style>
</head>
<body>
<div class="wrap">
<h1>n_signer CYD Web Serial Demo</h1>
<p class="note">Connect to the CYD (CH340 serial) via Web Serial, then exercise every algorithm and verb in the n_signer API. Chrome/Edge/Brave/Opera only.</p>
<!-- §16 canonical shell -->
<div id="divHeader">
<div class="divHeaderButtons">
<button id="btnHamburger" title="Menu" aria-label="Open menu"></button>
</div>
<div class="divHeaderText">n_signer CYD Web Serial</div>
<div class="divHeaderButtons"></div>
</div>
<div class="card">
<div id="divBody">
<!-- Connection card (full width) -->
<section class="divPostItem full">
<h2>Connection</h2>
<p class="note">Connect to the CYD (CH340 serial) via Web Serial, then exercise every algorithm and verb in the n_signer API. Chrome/Edge/Brave/Opera only.</p>
<div class="row">
<button id="connectBtn">Connect Web Serial</button>
<button id="disconnectBtn" class="secondary" disabled>Disconnect</button>
<button id="connectBtn" class="btn">Connect Web Serial</button>
<button id="disconnectBtn" class="btn secondary" disabled>Disconnect</button>
<span id="connStatus" class="status">Disconnected</span>
</div>
<pre id="log" class="mono"></pre>
<pre id="log"></pre>
</section>
<!-- get_info (metadata) -->
<section class="divPostItem section">
<h2>get_info (version / capabilities)</h2>
<p class="note">No approval required. Safe to call before the mnemonic is loaded.</p>
<div class="row">
<button id="infoBtn" class="btn" disabled>get_info</button>
</div>
<pre id="infoOut"></pre>
</section>
<!-- Get Public Key (algorithm-based) -->
<section class="divPostItem section">
<h2>get_public_key (algorithm)</h2>
<label for="gpkAlg">Algorithm</label>
<select id="gpkAlg" class="inpStyle">
<option>secp256k1</option><option>ed25519</option><option>x25519</option>
<option>ml-dsa-65</option><option>slh-dsa-128s</option><option>ml-kem-768</option>
</select>
<label for="gpkIdx">Index</label>
<input id="gpkIdx" type="number" value="0" min="0" class="inpStyle" />
<div class="row">
<button id="gpkBtn" class="btn" disabled>get_public_key</button>
</div>
<pre id="gpkOut"></pre>
</section>
<!-- Nostr Get Public Key -->
<section class="divPostItem section">
<h2>nostr_get_public_key</h2>
<label for="ngpkIdx">nostr_index</label>
<input id="ngpkIdx" type="number" value="0" min="0" class="inpStyle" />
<label for="ngpkFmt">format</label>
<select id="ngpkFmt" class="inpStyle"><option>bare</option><option>structured</option></select>
<div class="row">
<button id="ngpkBtn" class="btn" disabled>nostr_get_public_key</button>
</div>
<pre id="ngpkOut"></pre>
</section>
<!-- Sign / Verify -->
<section class="divPostItem section">
<h2>sign / verify</h2>
<label for="signAlg">Algorithm</label>
<select id="signAlg" class="inpStyle">
<option>secp256k1</option><option>ed25519</option><option>ml-dsa-65</option><option>slh-dsa-128s</option>
</select>
<label for="signIdx">Index</label>
<input id="signIdx" type="number" value="0" min="0" class="inpStyle" />
<label for="signScheme">scheme (secp256k1 only)</label>
<select id="signScheme" class="inpStyle"><option>schnorr</option><option>ecdsa</option></select>
<label for="signMsg">message (hex — 32 bytes / 64 hex for schnorr)</label>
<input id="signMsg" value="2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824" class="inpStyle" />
<div class="row">
<button id="signBtn" class="btn" disabled>sign</button>
<button id="verifyBtn" class="btn" disabled>verify</button>
</div>
<pre id="signOut"></pre>
</section>
<!-- KEM encapsulate / decapsulate -->
<section class="divPostItem section">
<h2>encapsulate / decapsulate (ml-kem-768)</h2>
<label for="kemPeer">peer pubkey hex (1184 bytes / 2368 hex) — leave empty to use self pubkey</label>
<textarea id="kemPeer" class="inpStyle" placeholder="auto: uses ml-kem-768 get_public_key"></textarea>
<label for="kemIdx">index (for decapsulate)</label>
<input id="kemIdx" type="number" value="0" min="0" class="inpStyle" />
<label for="kemCt">ciphertext hex (for decapsulate)</label>
<textarea id="kemCt" class="inpStyle" placeholder="filled by encapsulate"></textarea>
<div class="row">
<button id="encapBtn" class="btn" disabled>encapsulate</button>
<button id="decapBtn" class="btn" disabled>decapsulate</button>
</div>
<pre id="kemOut"></pre>
</section>
<!-- derive_shared_secret (x25519) -->
<section class="divPostItem section">
<h2>derive_shared_secret (x25519)</h2>
<label for="x25519Peer">peer pubkey hex (32 bytes / 64 hex)</label>
<input id="x25519Peer" class="inpStyle" placeholder="64 hex chars" />
<label for="x25519Idx">index</label>
<input id="x25519Idx" type="number" value="0" min="0" class="inpStyle" />
<div class="row">
<button id="x25519Btn" class="btn" disabled>derive_shared_secret</button>
</div>
<pre id="x25519Out"></pre>
</section>
<!-- derive (HMAC) -->
<section class="divPostItem section">
<h2>derive (secp256k1 HMAC-SHA256)</h2>
<label for="deriveData">data (UTF-8 string)</label>
<input id="deriveData" value="hello-derive" class="inpStyle" />
<label for="deriveIdx">index (required)</label>
<input id="deriveIdx" type="number" value="0" min="0" class="inpStyle" />
<div class="row">
<button id="deriveBtn" class="btn" disabled>derive</button>
</div>
<pre id="deriveOut"></pre>
</section>
<!-- Nostr Sign Event -->
<section class="divPostItem section">
<h2>nostr_sign_event</h2>
<label for="nseContent">content</label>
<textarea id="nseContent" class="inpStyle">hello from cyd webserial demo</textarea>
<label for="nseIdx">nostr_index</label>
<input id="nseIdx" type="number" value="0" min="0" class="inpStyle" />
<div class="row">
<button id="nseBtn" class="btn" disabled>nostr_sign_event</button>
</div>
<pre id="nseOut"></pre>
</section>
<!-- Nostr Mine Event -->
<section class="divPostItem section">
<h2>nostr_mine_event</h2>
<p class="warn">Slow on ESP32 — uses single-threaded PoW. Keep difficulty low.</p>
<label for="nmeContent">content</label>
<textarea id="nmeContent" class="inpStyle">mined by cyd</textarea>
<label for="nmeIdx">nostr_index</label>
<input id="nmeIdx" type="number" value="0" min="0" class="inpStyle" />
<label for="nmeDiff">difficulty (leading zero bits)</label>
<input id="nmeDiff" type="number" value="4" min="1" max="16" class="inpStyle" />
<label for="nmeTimeout">timeout (sec)</label>
<input id="nmeTimeout" type="number" value="30" min="1" max="60" class="inpStyle" />
<div class="row">
<button id="nmeBtn" class="btn" disabled>nostr_mine_event</button>
</div>
<pre id="nmeOut"></pre>
</section>
<!-- NIP-04 -->
<section class="divPostItem section">
<h2>nostr_nip04_encrypt / decrypt</h2>
<label for="nip04Peer">peer pubkey hex (32-byte x-only)</label>
<input id="nip04Peer" class="inpStyle" placeholder="64 hex chars" />
<label for="nip04Msg">plaintext</label>
<textarea id="nip04Msg" class="inpStyle">hello via nip04</textarea>
<label for="nip04Cipher">ciphertext (for decrypt)</label>
<textarea id="nip04Cipher" class="inpStyle" placeholder="ciphertext?iv=..."></textarea>
<label for="nip04Idx">nostr_index</label>
<input id="nip04Idx" type="number" value="0" min="0" class="inpStyle" />
<div class="row">
<button id="nip04EncBtn" class="btn" disabled>encrypt</button>
<button id="nip04DecBtn" class="btn" disabled>decrypt</button>
</div>
<pre id="nip04Out"></pre>
</section>
<!-- NIP-44 -->
<section class="divPostItem section">
<h2>nostr_nip44_encrypt / decrypt</h2>
<label for="nip44Peer">peer pubkey hex (32-byte x-only)</label>
<input id="nip44Peer" class="inpStyle" placeholder="64 hex chars" />
<label for="nip44Msg">plaintext</label>
<textarea id="nip44Msg" class="inpStyle">hello via nip44</textarea>
<label for="nip44Cipher">ciphertext (for decrypt)</label>
<textarea id="nip44Cipher" class="inpStyle" placeholder="base64 payload"></textarea>
<label for="nip44Idx">nostr_index</label>
<input id="nip44Idx" type="number" value="0" min="0" class="inpStyle" />
<div class="row">
<button id="nip44EncBtn" class="btn" disabled>encrypt</button>
<button id="nip44DecBtn" class="btn" disabled>decrypt</button>
</div>
<pre id="nip44Out"></pre>
</section>
<!-- OTP encrypt / decrypt -->
<section class="divPostItem section">
<h2>encrypt / decrypt (otp)</h2>
<p class="note">OTP pad is derived from the mnemonic on the CYD. Offset advances monotonically.</p>
<label for="otpPlain">plaintext (base64)</label>
<textarea id="otpPlain" class="inpStyle">SGVsbG8sIE9UUCB3b3JsZCE=</textarea>
<label for="otpCipher">ciphertext (for decrypt, base64)</label>
<textarea id="otpCipher" class="inpStyle" placeholder="filled by encrypt"></textarea>
<label for="otpEnc">encoding</label>
<select id="otpEnc" class="inpStyle"><option>ascii</option><option>binary</option></select>
<div class="row">
<button id="otpEncBtn" class="btn" disabled>encrypt</button>
<button id="otpDecBtn" class="btn" disabled>decrypt</button>
</div>
<pre id="otpOut"></pre>
</section>
</div>
<div id="divFooter">
<div id="divFooterLeft" class="divFooterBox">n_signer</div>
<div id="divFooterCenter" class="divFooterBox">ready</div>
<div id="divFooterRight" class="divFooterBox">Web Serial</div>
</div>
<!-- §10 SideNav -->
<div id="divSideNav">
<button id="btnCloseSideNav" title="Close menu" aria-label="Close menu">×</button>
<div id="divSideNavBody">
<h3>n_signer CYD Web Serial Demo</h3>
<p>A browser control panel for the CYD (ESP32-2432S028) signer firmware. Speaks the n_signer JSON-RPC protocol over Web Serial at 115200 baud.</p>
<h3>Getting started</h3>
<p>1. Plug the CYD into a USB port (CH340 enumerates as /dev/ttyUSB*).</p>
<p>2. Click <code>Connect Web Serial</code> and pick the CYD port.</p>
<p>3. <code>get_info</code> fires automatically and reports firmware version + supported verbs.</p>
<p>4. Enter a mnemonic on the CYD touchscreen to enable the signing verbs.</p>
<h3>Auth envelope</h3>
<p>Every request is signed with a demo secp256k1 key (priv = 0x0102…2020). The CYD verifies the kind-27235 auth event and replays-protects by event id.</p>
<h3>Transports</h3>
<p>Frames are 4-byte big-endian length + UTF-8 JSON payload. Same wire format as the host Unix-socket and TCP transports.</p>
</div>
<div class="grid">
<!-- Get Public Key (algorithm-based) -->
<section class="card">
<h2>Get Public Key (algorithm)</h2>
<label for="gpkAlg">Algorithm</label>
<select id="gpkAlg">
<option>secp256k1</option><option>ed25519</option><option>x25519</option>
<option>ml-dsa-65</option><option>slh-dsa-128s</option><option>ml-kem-768</option>
</select>
<label for="gpkIdx">Index</label>
<input id="gpkIdx" type="number" value="0" min="0" />
<div class="row" style="margin-top:10px">
<button id="gpkBtn" disabled>get_public_key</button>
</div>
<pre id="gpkOut" class="mono"></pre>
</section>
<!-- Nostr Get Public Key -->
<section class="card">
<h2>nostr_get_public_key</h2>
<label for="ngpkIdx">nostr_index</label>
<input id="ngpkIdx" type="number" value="0" min="0" />
<label for="ngpkFmt">format</label>
<select id="ngpkFmt"><option>bare</option><option>structured</option></select>
<div class="row" style="margin-top:10px">
<button id="ngpkBtn" disabled>nostr_get_public_key</button>
</div>
<pre id="ngpkOut" class="mono"></pre>
</section>
<!-- Sign / Verify -->
<section class="card">
<h2>sign / verify</h2>
<label for="signAlg">Algorithm</label>
<select id="signAlg">
<option>secp256k1</option><option>ed25519</option><option>ml-dsa-65</option><option>slh-dsa-128s</option>
</select>
<label for="signIdx">Index</label>
<input id="signIdx" type="number" value="0" min="0" />
<label for="signScheme">scheme (secp256k1 only)</label>
<select id="signScheme"><option>schnorr</option><option>ecdsa</option></select>
<label for="signMsg">message (hex)</label>
<input id="signMsg" value="68656c6c6f" />
<div class="row" style="margin-top:10px">
<button id="signBtn" disabled>sign</button>
<button id="verifyBtn" disabled>verify</button>
</div>
<pre id="signOut" class="mono"></pre>
</section>
<!-- KEM encapsulate / decapsulate -->
<section class="card">
<h2>encapsulate / decapsulate (ml-kem-768)</h2>
<label for="kemPeer">peer pubkey hex (1184 bytes / 2368 hex) — leave empty to use self pubkey</label>
<textarea id="kemPeer" placeholder="auto: uses ml-kem-768 get_public_key"></textarea>
<label for="kemIdx">index (for decapsulate)</label>
<input id="kemIdx" type="number" value="0" min="0" />
<label for="kemCt">ciphertext hex (for decapsulate)</label>
<textarea id="kemCt" placeholder="filled by encapsulate"></textarea>
<div class="row" style="margin-top:10px">
<button id="encapBtn" disabled>encapsulate</button>
<button id="decapBtn" disabled>decapsulate</button>
</div>
<pre id="kemOut" class="mono"></pre>
</section>
<!-- derive_shared_secret (x25519) -->
<section class="card">
<h2>derive_shared_secret (x25519)</h2>
<label for="x25519Peer">peer pubkey hex (32 bytes / 64 hex)</label>
<input id="x25519Peer" placeholder="64 hex chars" />
<label for="x25519Idx">index</label>
<input id="x25519Idx" type="number" value="0" min="0" />
<div class="row" style="margin-top:10px">
<button id="x25519Btn" disabled>derive_shared_secret</button>
</div>
<pre id="x25519Out" class="mono"></pre>
</section>
<!-- derive (HMAC) -->
<section class="card">
<h2>derive (secp256k1 HMAC-SHA256)</h2>
<label for="deriveData">data (UTF-8 string)</label>
<input id="deriveData" value="hello-derive" />
<label for="deriveIdx">index (required)</label>
<input id="deriveIdx" type="number" value="0" min="0" />
<div class="row" style="margin-top:10px">
<button id="deriveBtn" disabled>derive</button>
</div>
<pre id="deriveOut" class="mono"></pre>
</section>
<!-- Nostr Sign Event -->
<section class="card">
<h2>nostr_sign_event</h2>
<label for="nseContent">content</label>
<textarea id="nseContent">hello from cyd webserial demo</textarea>
<label for="nseIdx">nostr_index</label>
<input id="nseIdx" type="number" value="0" min="0" />
<div class="row" style="margin-top:10px">
<button id="nseBtn" disabled>nostr_sign_event</button>
</div>
<pre id="nseOut" class="mono"></pre>
</section>
<!-- Nostr Mine Event -->
<section class="card">
<h2>nostr_mine_event</h2>
<p class="warn">Slow on ESP32 — uses single-threaded PoW. Keep difficulty low.</p>
<label for="nmeContent">content</label>
<textarea id="nmeContent">mined by cyd</textarea>
<label for="nmeIdx">nostr_index</label>
<input id="nmeIdx" type="number" value="0" min="0" />
<label for="nmeDiff">difficulty (leading zero bits)</label>
<input id="nmeDiff" type="number" value="4" min="1" max="16" />
<label for="nmeTimeout">timeout (sec)</label>
<input id="nmeTimeout" type="number" value="30" min="1" max="60" />
<div class="row" style="margin-top:10px">
<button id="nmeBtn" disabled>nostr_mine_event</button>
</div>
<pre id="nmeOut" class="mono"></pre>
</section>
<!-- NIP-04 -->
<section class="card">
<h2>nostr_nip04_encrypt / decrypt</h2>
<label for="nip04Peer">peer pubkey hex (32-byte x-only)</label>
<input id="nip04Peer" placeholder="64 hex chars" />
<label for="nip04Msg">plaintext</label>
<textarea id="nip04Msg">hello via nip04</textarea>
<label for="nip04Cipher">ciphertext (for decrypt)</label>
<textarea id="nip04Cipher" placeholder="ciphertext?iv=..."></textarea>
<label for="nip04Idx">nostr_index</label>
<input id="nip04Idx" type="number" value="0" min="0" />
<div class="row" style="margin-top:10px">
<button id="nip04EncBtn" disabled>encrypt</button>
<button id="nip04DecBtn" disabled>decrypt</button>
</div>
<pre id="nip04Out" class="mono"></pre>
</section>
<!-- NIP-44 -->
<section class="card">
<h2>nostr_nip44_encrypt / decrypt</h2>
<label for="nip44Peer">peer pubkey hex (32-byte x-only)</label>
<input id="nip44Peer" placeholder="64 hex chars" />
<label for="nip44Msg">plaintext</label>
<textarea id="nip44Msg">hello via nip44</textarea>
<label for="nip44Cipher">ciphertext (for decrypt)</label>
<textarea id="nip44Cipher" placeholder="base64 payload"></textarea>
<label for="nip44Idx">nostr_index</label>
<input id="nip44Idx" type="number" value="0" min="0" />
<div class="row" style="margin-top:10px">
<button id="nip44EncBtn" disabled>encrypt</button>
<button id="nip44DecBtn" disabled>decrypt</button>
</div>
<pre id="nip44Out" class="mono"></pre>
</section>
<!-- OTP encrypt / decrypt -->
<section class="card">
<h2>encrypt / decrypt (otp)</h2>
<p class="note">OTP pad is derived from the mnemonic on the CYD. Offset advances monotonically.</p>
<label for="otpPlain">plaintext (base64)</label>
<textarea id="otpPlain">SGVsbG8sIE9UUCB3b3JsZCE=</textarea>
<label for="otpCipher">ciphertext (for decrypt, base64)</label>
<textarea id="otpCipher" placeholder="filled by encrypt"></textarea>
<label for="otpEnc">encoding</label>
<select id="otpEnc"><option>ascii</option><option>binary</option></select>
<div class="row" style="margin-top:10px">
<button id="otpEncBtn" disabled>encrypt</button>
<button id="otpDecBtn" disabled>decrypt</button>
</div>
<pre id="otpOut" class="mono"></pre>
</section>
<div id="divVersionBar">
<span id="versionDisplay">demo v1</span>
<div id="divVersionBarButtons">
<button id="themeToggleButton" class="btn secondary" title="Toggle theme">theme</button>
</div>
</div>
</div>
@@ -244,8 +583,12 @@
const logEl = document.getElementById("log");
const connStatusEl = document.getElementById("connStatus");
const footerStatusEl = document.getElementById("divFooterCenter");
const connectBtn = document.getElementById("connectBtn");
const disconnectBtn = document.getElementById("disconnectBtn");
const btnHamburger = document.getElementById("btnHamburger");
const divSideNav = document.getElementById("divSideNav");
const themeToggleButton = document.getElementById("themeToggleButton");
let port = null;
let reader = null;
@@ -261,6 +604,7 @@
function setStatus(text, mode = "") {
connStatusEl.textContent = text;
connStatusEl.className = `status ${mode}`.trim();
footerStatusEl.textContent = text;
}
function hex(bytes) {
return Array.from(bytes).map(b => b.toString(16).padStart(2, "0")).join("");
@@ -323,7 +667,6 @@
while (rxBuffer.length >= 4) {
const len = (rxBuffer[0] << 24) | (rxBuffer[1] << 16) | (rxBuffer[2] << 8) | rxBuffer[3];
if (len <= 0 || len > 16384) {
/* resync: drop one byte */
rxBuffer = rxBuffer.slice(1);
continue;
}
@@ -371,6 +714,29 @@
}
}
/* ---- SideNav + theme (§10, §12) ---- */
function openSideNav() { divSideNav.classList.add("open"); }
function closeSideNav() { divSideNav.classList.remove("open"); }
btnHamburger.addEventListener("click", () => {
if (divSideNav.classList.contains("open")) closeSideNav(); else openSideNav();
});
document.getElementById("btnCloseSideNav").addEventListener("click", closeSideNav);
/* Click on the scrim (the body, outside the sidenav) closes it. */
document.getElementById("divBody").addEventListener("click", () => {
if (divSideNav.classList.contains("open")) closeSideNav();
});
/* ESC closes sidenav (§14). */
document.addEventListener("keydown", (e) => {
if (e.key === "Escape" && divSideNav.classList.contains("open")) {
closeSideNav();
}
});
themeToggleButton.addEventListener("click", () => {
const isDark = document.documentElement.classList.toggle("dark-mode");
localStorage.setItem('theme', isDark ? 'dark' : 'light');
});
/* ---- Connect / Disconnect ---- */
connectBtn.addEventListener("click", async () => {
try {
@@ -387,6 +753,8 @@
});
disconnectBtn.disabled = false;
connectBtn.disabled = true;
/* Auto-fetch version/capabilities so the user sees what firmware is running. */
try { document.getElementById("infoBtn").click(); } catch (e) { log("auto get_info failed:", e.message); }
} catch (e) {
setStatus("Error", "err");
log("connect failed:", e.message);
@@ -411,6 +779,10 @@
/* ---- Verb wiring ---- */
const $ = id => document.getElementById(id);
$("infoBtn").addEventListener("click", () => {
callVerb("get_info", [], $("infoOut"));
});
$("gpkBtn").addEventListener("click", () => {
const alg = $("gpkAlg").value, idx = Number($("gpkIdx").value || 0);
callVerb("get_public_key", [{ algorithm: alg, index: idx }], $("gpkOut"));
@@ -434,7 +806,6 @@
const scheme = $("signScheme").value, msg = $("signMsg").value;
const opts = { algorithm: alg, index: idx };
if (alg === "secp256k1") opts.scheme = scheme;
/* parse the last sign result to get the signature */
const outText = $("signOut").textContent;
const m = outText.match(/"signature"\s*:\s*"([0-9a-f]+)"/);
if (!m) { $("signOut").textContent += "\n✗ no signature found — run sign first"; return; }
@@ -444,7 +815,6 @@
$("encapBtn").addEventListener("click", async () => {
let peer = $("kemPeer").value.trim();
if (!peer) {
/* fetch self ml-kem-768 pubkey first */
const opts = [{ algorithm: "ml-kem-768", index: Number($("kemIdx").value || 0) }];
const auth = await buildAuth("get_public_key", opts);
const resp = await sendRpc({ id: String(Math.floor(Math.random()*1e9)), method: "get_public_key", params: opts, auth });