v0.1.8 - Consolidated USB test page, fixed OTP round-trip, reformatted SD card, added pad checksum logging
This commit is contained in:
@@ -1,527 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>n_signer Feather WebUSB Demo</title>
|
||||
<style>
|
||||
:root {
|
||||
--bg: #0b0f14;
|
||||
--panel: #121821;
|
||||
--panel-2: #182231;
|
||||
--text: #e6edf3;
|
||||
--muted: #9fb0c3;
|
||||
--accent: #58a6ff;
|
||||
--good: #3fb950;
|
||||
--bad: #f85149;
|
||||
--border: #263448;
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: Inter, system-ui, -apple-system, Segoe UI, Roboto, sans-serif;
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
line-height: 1.35;
|
||||
}
|
||||
.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, button {
|
||||
font: inherit;
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
input, textarea {
|
||||
width: 100%;
|
||||
background: #0c131d;
|
||||
color: var(--text);
|
||||
padding: 8px 10px;
|
||||
}
|
||||
textarea { min-height: 84px; 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: 220px;
|
||||
font-size: 12px;
|
||||
}
|
||||
.mono { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrap">
|
||||
<h1>n_signer Feather WebUSB Demo</h1>
|
||||
<p class="note">Connect to the board, choose a key index, fetch a pubkey, sign a kind 1 event, and test NIP-04 / NIP-44 encrypt + decrypt RPCs.</p>
|
||||
|
||||
<div class="card">
|
||||
<div class="row">
|
||||
<button id="connectBtn">Connect WebUSB</button>
|
||||
<span id="connStatus" class="status">Disconnected</span>
|
||||
</div>
|
||||
<pre id="log" class="mono"></pre>
|
||||
</div>
|
||||
|
||||
<div class="grid">
|
||||
<section class="card">
|
||||
<h2>Public Key</h2>
|
||||
<label for="keyIndex">Key index (nostr_index)</label>
|
||||
<input id="keyIndex" type="text" inputmode="numeric" pattern="[0-9]*" value="0" />
|
||||
<div class="row" style="margin-top:10px">
|
||||
<button id="pubkeyBtn" disabled>Get Public Key</button>
|
||||
</div>
|
||||
<pre id="pubkeyOut" class="mono"></pre>
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<h2>Sign Kind 1 Event</h2>
|
||||
<label for="kind1Content">Content</label>
|
||||
<textarea id="kind1Content">hello from feather webusb demo</textarea>
|
||||
<label for="kind1Tags">Tags JSON (array)</label>
|
||||
<input id="kind1Tags" value="[]" />
|
||||
<div class="row" style="margin-top:10px">
|
||||
<button id="signKind1Btn" disabled>Create + Sign kind 1</button>
|
||||
</div>
|
||||
<pre id="signOut" class="mono"></pre>
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<h2>NIP-04 Encrypt</h2>
|
||||
<label for="nip04Peer">Peer pubkey (hex, 32-byte x-only)</label>
|
||||
<input id="nip04Peer" placeholder="e.g. 64 hex chars" />
|
||||
<label for="nip04Msg">Plaintext</label>
|
||||
<textarea id="nip04Msg">hello via nip04</textarea>
|
||||
<div class="row" style="margin-top:10px">
|
||||
<button id="nip04EncBtn" disabled>Encrypt (nip04_encrypt)</button>
|
||||
</div>
|
||||
<pre id="nip04Out" class="mono"></pre>
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<h2>NIP-04 Decrypt</h2>
|
||||
<label for="nip04DecPeer">Peer pubkey (hex, 32-byte x-only)</label>
|
||||
<input id="nip04DecPeer" placeholder="e.g. 64 hex chars" />
|
||||
<label for="nip04Cipher">Ciphertext</label>
|
||||
<textarea id="nip04Cipher" placeholder="ciphertext?iv=..."></textarea>
|
||||
<div class="row" style="margin-top:10px">
|
||||
<button id="nip04DecBtn" disabled>Decrypt (nip04_decrypt)</button>
|
||||
</div>
|
||||
<pre id="nip04DecOut" class="mono"></pre>
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<h2>NIP-44 Encrypt</h2>
|
||||
<label for="nip44Peer">Peer pubkey (hex, 32-byte x-only)</label>
|
||||
<input id="nip44Peer" placeholder="e.g. 64 hex chars" />
|
||||
<label for="nip44Msg">Plaintext</label>
|
||||
<textarea id="nip44Msg">hello via nip44</textarea>
|
||||
<div class="row" style="margin-top:10px">
|
||||
<button id="nip44EncBtn" disabled>Encrypt (nip44_encrypt)</button>
|
||||
</div>
|
||||
<pre id="nip44Out" class="mono"></pre>
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<h2>NIP-44 Decrypt</h2>
|
||||
<label for="nip44DecPeer">Peer pubkey (hex, 32-byte x-only)</label>
|
||||
<input id="nip44DecPeer" placeholder="e.g. 64 hex chars" />
|
||||
<label for="nip44Cipher">Ciphertext</label>
|
||||
<textarea id="nip44Cipher" placeholder="base64 payload"></textarea>
|
||||
<div class="row" style="margin-top:10px">
|
||||
<button id="nip44DecBtn" disabled>Decrypt (nip44_decrypt)</button>
|
||||
</div>
|
||||
<pre id="nip44DecOut" class="mono"></pre>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script type="module">
|
||||
import { schnorr } from "https://esm.sh/@noble/curves@1.5.0/secp256k1?bundle";
|
||||
|
||||
const logEl = document.getElementById("log");
|
||||
const connStatusEl = document.getElementById("connStatus");
|
||||
|
||||
const connectBtn = document.getElementById("connectBtn");
|
||||
const pubkeyBtn = document.getElementById("pubkeyBtn");
|
||||
const signKind1Btn = document.getElementById("signKind1Btn");
|
||||
const nip04EncBtn = document.getElementById("nip04EncBtn");
|
||||
const nip04DecBtn = document.getElementById("nip04DecBtn");
|
||||
const nip44EncBtn = document.getElementById("nip44EncBtn");
|
||||
const nip44DecBtn = document.getElementById("nip44DecBtn");
|
||||
|
||||
const keyIndexEl = document.getElementById("keyIndex");
|
||||
const pubkeyOutEl = document.getElementById("pubkeyOut");
|
||||
|
||||
const kind1ContentEl = document.getElementById("kind1Content");
|
||||
const kind1TagsEl = document.getElementById("kind1Tags");
|
||||
const signOutEl = document.getElementById("signOut");
|
||||
|
||||
const nip04PeerEl = document.getElementById("nip04Peer");
|
||||
const nip04MsgEl = document.getElementById("nip04Msg");
|
||||
const nip04OutEl = document.getElementById("nip04Out");
|
||||
const nip04DecPeerEl = document.getElementById("nip04DecPeer");
|
||||
const nip04CipherEl = document.getElementById("nip04Cipher");
|
||||
const nip04DecOutEl = document.getElementById("nip04DecOut");
|
||||
|
||||
const nip44PeerEl = document.getElementById("nip44Peer");
|
||||
const nip44MsgEl = document.getElementById("nip44Msg");
|
||||
const nip44OutEl = document.getElementById("nip44Out");
|
||||
const nip44DecPeerEl = document.getElementById("nip44DecPeer");
|
||||
const nip44CipherEl = document.getElementById("nip44Cipher");
|
||||
const nip44DecOutEl = document.getElementById("nip44DecOut");
|
||||
|
||||
let dev = null;
|
||||
let iface = null;
|
||||
let ownPubkey = "";
|
||||
const EP_OUT = 1;
|
||||
const EP_IN = 1;
|
||||
|
||||
function log(...args) {
|
||||
logEl.textContent += args.join(" ") + "\n";
|
||||
logEl.scrollTop = logEl.scrollHeight;
|
||||
}
|
||||
|
||||
function setStatus(text, mode = "") {
|
||||
connStatusEl.textContent = text;
|
||||
connStatusEl.className = `status ${mode}`.trim();
|
||||
}
|
||||
|
||||
function hex(bytes) {
|
||||
return Array.from(bytes).map(b => b.toString(16).padStart(2, "0")).join("");
|
||||
}
|
||||
|
||||
function utf8(s) {
|
||||
return new TextEncoder().encode(s);
|
||||
}
|
||||
|
||||
function be32(n) {
|
||||
return new Uint8Array([(n >>> 24) & 0xff, (n >>> 16) & 0xff, (n >>> 8) & 0xff, n & 0xff]);
|
||||
}
|
||||
|
||||
async function sha256Hex(dataBytes) {
|
||||
const h = await crypto.subtle.digest("SHA-256", dataBytes);
|
||||
return hex(new Uint8Array(h));
|
||||
}
|
||||
|
||||
function getIndexOptions() {
|
||||
const raw = Number.parseInt(String(keyIndexEl.value ?? "0"), 10);
|
||||
const index = Number.isFinite(raw) && raw >= 0 ? raw : 0;
|
||||
return { nostr_index: index };
|
||||
}
|
||||
|
||||
function pretty(value) {
|
||||
try {
|
||||
return JSON.stringify(value, null, 2);
|
||||
} catch {
|
||||
return String(value);
|
||||
}
|
||||
}
|
||||
|
||||
async function buildAuth(method, params) {
|
||||
// Demo caller key only (not secret in browser demo).
|
||||
const callerPriv = Uint8Array.from({ length: 32 }, (_, i) => i + 1);
|
||||
const callerPubX = hex(schnorr.getPublicKey(callerPriv));
|
||||
|
||||
const createdAt = Math.floor(Date.now() / 1000);
|
||||
const paramsJson = JSON.stringify(params);
|
||||
const bodyHash = await sha256Hex(utf8(paramsJson));
|
||||
const tags = [
|
||||
["nsigner_rpc", "1"],
|
||||
["nsigner_method", method],
|
||||
["nsigner_body_hash", bodyHash],
|
||||
];
|
||||
|
||||
const content = "webusb-demo";
|
||||
const ser = JSON.stringify([0, callerPubX, createdAt, 27235, tags, content]);
|
||||
const id = await sha256Hex(utf8(ser));
|
||||
const sigBytes = await schnorr.sign(id, callerPriv, new Uint8Array(32));
|
||||
const sigHex = typeof sigBytes === "string" ? sigBytes : hex(sigBytes);
|
||||
|
||||
return {
|
||||
id,
|
||||
pubkey: callerPubX,
|
||||
created_at: createdAt,
|
||||
kind: 27235,
|
||||
tags,
|
||||
content,
|
||||
sig: sigHex,
|
||||
};
|
||||
}
|
||||
|
||||
async function sendRpc(reqObj) {
|
||||
const body = utf8(JSON.stringify(reqObj));
|
||||
const frame = new Uint8Array(4 + body.length);
|
||||
frame.set(be32(body.length), 0);
|
||||
frame.set(body, 4);
|
||||
await dev.transferOut(EP_OUT, frame);
|
||||
|
||||
const deadline = Date.now() + 10000;
|
||||
let ring = new Uint8Array(0);
|
||||
while (Date.now() < deadline) {
|
||||
const r = await dev.transferIn(EP_IN, 512);
|
||||
if (!r.data || r.data.byteLength === 0) continue;
|
||||
|
||||
const chunk = new Uint8Array(r.data.buffer, r.data.byteOffset, r.data.byteLength);
|
||||
const next = new Uint8Array(ring.length + chunk.length);
|
||||
next.set(ring, 0);
|
||||
next.set(chunk, ring.length);
|
||||
ring = next;
|
||||
|
||||
while (ring.length >= 4) {
|
||||
const n = (ring[0] << 24) | (ring[1] << 16) | (ring[2] << 8) | ring[3];
|
||||
if (n <= 0 || n > 1_000_000) {
|
||||
ring = ring.slice(1);
|
||||
continue;
|
||||
}
|
||||
if (ring.length < 4 + n) break;
|
||||
|
||||
const payload = ring.slice(4, 4 + n);
|
||||
ring = ring.slice(4 + n);
|
||||
const txt = new TextDecoder().decode(payload);
|
||||
return JSON.parse(txt);
|
||||
}
|
||||
}
|
||||
throw new Error("Timed out waiting for framed response");
|
||||
}
|
||||
|
||||
async function rpcCall(method, params, id = "web-1") {
|
||||
const auth = await buildAuth(method, params);
|
||||
const req = { jsonrpc: "2.0", id, method, params, auth };
|
||||
log("→", method, JSON.stringify(params));
|
||||
const resp = await sendRpc(req);
|
||||
log("←", method, JSON.stringify(resp));
|
||||
return resp;
|
||||
}
|
||||
|
||||
function requirePeerHex(peer) {
|
||||
const v = String(peer || "").trim().toLowerCase();
|
||||
if (!/^[0-9a-f]{64}$/.test(v)) {
|
||||
throw new Error("Peer pubkey must be exactly 64 hex chars (x-only pubkey)");
|
||||
}
|
||||
return v;
|
||||
}
|
||||
|
||||
function requireStringResult(resp, what) {
|
||||
if (resp && typeof resp.result === "string") {
|
||||
return resp.result;
|
||||
}
|
||||
throw new Error(`${what} failed: ${pretty(resp)}`);
|
||||
}
|
||||
|
||||
async function fetchOwnPubkeyAndFillPeers() {
|
||||
const params = [getIndexOptions()];
|
||||
const resp = await rpcCall("get_public_key", params, "web-own-pubkey");
|
||||
if (resp && typeof resp.result === "string") {
|
||||
ownPubkey = resp.result.trim().toLowerCase();
|
||||
pubkeyOutEl.textContent = pretty(resp);
|
||||
if (/^[0-9a-f]{64}$/.test(ownPubkey)) {
|
||||
nip04PeerEl.value = ownPubkey;
|
||||
nip04DecPeerEl.value = ownPubkey;
|
||||
nip44PeerEl.value = ownPubkey;
|
||||
nip44DecPeerEl.value = ownPubkey;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function connect() {
|
||||
dev = await navigator.usb.requestDevice({ filters: [{ vendorId: 0x303a }] });
|
||||
await dev.open();
|
||||
if (dev.configuration === null) {
|
||||
await dev.selectConfiguration(1);
|
||||
}
|
||||
|
||||
const intf = dev.configuration.interfaces.find(i =>
|
||||
i.alternates.some(a => a.interfaceClass === 0xff)
|
||||
);
|
||||
if (!intf) throw new Error("No vendor WebUSB interface found");
|
||||
|
||||
iface = intf.interfaceNumber;
|
||||
await dev.claimInterface(iface);
|
||||
const alt = intf.alternates.find(a => a.interfaceClass === 0xff);
|
||||
await dev.selectAlternateInterface(iface, alt.alternateSetting);
|
||||
|
||||
await dev.controlTransferOut({
|
||||
requestType: "class",
|
||||
recipient: "interface",
|
||||
request: 0x22,
|
||||
value: 1,
|
||||
index: iface,
|
||||
});
|
||||
|
||||
pubkeyBtn.disabled = false;
|
||||
signKind1Btn.disabled = false;
|
||||
nip04EncBtn.disabled = false;
|
||||
nip04DecBtn.disabled = false;
|
||||
nip44EncBtn.disabled = false;
|
||||
nip44DecBtn.disabled = false;
|
||||
setStatus(`Connected (iface ${iface})`, "ok");
|
||||
log("Connected. Interface", String(iface));
|
||||
|
||||
try {
|
||||
await fetchOwnPubkeyAndFillPeers();
|
||||
log("Default peer pubkeys set to selected signer pubkey");
|
||||
} catch (e) {
|
||||
log("Auto pubkey fetch failed:", String(e));
|
||||
}
|
||||
}
|
||||
|
||||
connectBtn.addEventListener("click", async () => {
|
||||
try {
|
||||
await connect();
|
||||
} catch (e) {
|
||||
setStatus("Connect failed", "err");
|
||||
log("Connect failed:", String(e));
|
||||
}
|
||||
});
|
||||
|
||||
pubkeyBtn.addEventListener("click", async () => {
|
||||
try {
|
||||
await fetchOwnPubkeyAndFillPeers();
|
||||
} catch (e) {
|
||||
pubkeyOutEl.textContent = String(e);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
signKind1Btn.addEventListener("click", async () => {
|
||||
try {
|
||||
let tags = [];
|
||||
try {
|
||||
tags = JSON.parse(kind1TagsEl.value || "[]");
|
||||
if (!Array.isArray(tags)) throw new Error("tags must be an array");
|
||||
} catch (e) {
|
||||
throw new Error(`Invalid tags JSON: ${String(e)}`);
|
||||
}
|
||||
|
||||
const unsignedEvent = {
|
||||
kind: 1,
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
tags,
|
||||
content: String(kind1ContentEl.value || ""),
|
||||
};
|
||||
|
||||
const params = [unsignedEvent, getIndexOptions()];
|
||||
const resp = await rpcCall("nostr_sign_event", params, "web-sign-kind1");
|
||||
signOutEl.textContent = pretty(resp?.result ?? resp);
|
||||
} catch (e) {
|
||||
signOutEl.textContent = String(e);
|
||||
}
|
||||
});
|
||||
|
||||
nip04EncBtn.addEventListener("click", async () => {
|
||||
try {
|
||||
const peer = requirePeerHex(nip04PeerEl.value);
|
||||
const msg = String(nip04MsgEl.value || "");
|
||||
const params = [peer, msg, getIndexOptions()];
|
||||
const resp = await rpcCall("nostr_nip04_encrypt", params, "web-nip04-enc");
|
||||
nip04OutEl.textContent = pretty(resp?.result ?? resp);
|
||||
if (resp && typeof resp.result === "string") {
|
||||
nip04DecPeerEl.value = peer;
|
||||
nip04CipherEl.value = resp.result;
|
||||
}
|
||||
} catch (e) {
|
||||
nip04OutEl.textContent = String(e);
|
||||
}
|
||||
});
|
||||
|
||||
nip04DecBtn.addEventListener("click", async () => {
|
||||
try {
|
||||
const peer = requirePeerHex(nip04DecPeerEl.value);
|
||||
const ciphertext = String(nip04CipherEl.value || "");
|
||||
const params = [peer, ciphertext, getIndexOptions()];
|
||||
const resp = await rpcCall("nostr_nip04_decrypt", params, "web-nip04-dec");
|
||||
nip04DecOutEl.textContent = requireStringResult(resp, "NIP-04 decrypt");
|
||||
} catch (e) {
|
||||
nip04DecOutEl.textContent = String(e);
|
||||
}
|
||||
});
|
||||
|
||||
nip44EncBtn.addEventListener("click", async () => {
|
||||
try {
|
||||
const peer = requirePeerHex(nip44PeerEl.value);
|
||||
const msg = String(nip44MsgEl.value || "");
|
||||
const params = [peer, msg, getIndexOptions()];
|
||||
const resp = await rpcCall("nostr_nip44_encrypt", params, "web-nip44-enc");
|
||||
nip44OutEl.textContent = pretty(resp?.result ?? resp);
|
||||
if (resp && typeof resp.result === "string") {
|
||||
nip44DecPeerEl.value = peer;
|
||||
nip44CipherEl.value = resp.result;
|
||||
}
|
||||
} catch (e) {
|
||||
nip44OutEl.textContent = String(e);
|
||||
}
|
||||
});
|
||||
|
||||
nip44DecBtn.addEventListener("click", async () => {
|
||||
try {
|
||||
const peer = requirePeerHex(nip44DecPeerEl.value);
|
||||
const ciphertext = String(nip44CipherEl.value || "");
|
||||
const params = [peer, ciphertext, getIndexOptions()];
|
||||
const resp = await rpcCall("nostr_nip44_decrypt", params, "web-nip44-dec");
|
||||
nip44DecOutEl.textContent = requireStringResult(resp, "NIP-44 decrypt");
|
||||
} catch (e) {
|
||||
nip44DecOutEl.textContent = String(e);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
+5
-5
@@ -46,10 +46,10 @@ CDC path:
|
||||
./.venv/bin/python ./examples/feather_sign_event.py /dev/ttyACM0
|
||||
```
|
||||
|
||||
WebUSB path:
|
||||
WebUSB / Web Serial path (unified):
|
||||
|
||||
- Open [`examples/feather_webusb_demo.html`](../examples/feather_webusb_demo.html)
|
||||
- Connect in Chrome/Edge
|
||||
- Open [`usb-test.html`](../usb-test.html) in Chrome/Edge
|
||||
- Connect via Web Serial (works for Teensy 4.1 USB CDC, CYD CH340, Feather, etc.)
|
||||
- Run `get_public_key`
|
||||
- Confirm pubkey matches CDC result
|
||||
|
||||
@@ -58,9 +58,9 @@ WebUSB path:
|
||||
The CYD has no native USB; its CH340 bridge exposes a serial port. The browser
|
||||
transport is **Web Serial** (`navigator.serial`), Chromium-only. A full test
|
||||
page covering every algorithm and verb lives at
|
||||
[`examples/cyd_webserial_demo.html`](../examples/cyd_webserial_demo.html):
|
||||
[`usb-test.html`](../usb-test.html):
|
||||
|
||||
- Open [`examples/cyd_webserial_demo.html`](../examples/cyd_webserial_demo.html) in Chrome/Edge
|
||||
- Open [`usb-test.html`](../usb-test.html) in Chrome/Edge
|
||||
- Click **Connect Web Serial**, select the CH340 port (`1a86:7523`)
|
||||
- Exercise each card: `get_public_key` (all 6 algorithms), `sign`/`verify`,
|
||||
`encapsulate`/`decapsulate`, `derive_shared_secret`, `derive`,
|
||||
|
||||
@@ -95,9 +95,9 @@ hardware note below.
|
||||
The CYD has no native USB; the CH340 bridge exposes a serial port. The browser
|
||||
transport is **Web Serial** (`navigator.serial`), Chromium-only. A full test
|
||||
page covering every algorithm and verb lives at
|
||||
[`examples/cyd_webserial_demo.html`](../../examples/cyd_webserial_demo.html):
|
||||
[`usb-test.html`](../../usb-test.html):
|
||||
|
||||
1. Open [`examples/cyd_webserial_demo.html`](../../examples/cyd_webserial_demo.html) in Chrome/Edge.
|
||||
1. Open [`usb-test.html`](../../usb-test.html) in Chrome/Edge.
|
||||
2. Click **Connect Web Serial**, select the CH340 port (`1a86:7523`).
|
||||
3. On the CYD touchscreen, enter or generate a mnemonic to reach the "ready" state.
|
||||
4. Exercise each card: `get_public_key` (all 6 algorithms), `sign`/`verify`,
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
// each boot. Mirrors CYD_DEBUG_AUTO_GENERATE in
|
||||
// firmware/cyd_esp32_2432s028/main/main.c. Set to 0 for the normal interactive
|
||||
// boot flow (show the startup menu, wait for the user to tap Generate/Enter).
|
||||
#define DEBUG_AUTO_GENERATE 1
|
||||
#define DEBUG_AUTO_GENERATE 0
|
||||
|
||||
#include <lvgl.h>
|
||||
#include <ST7796_t3.h>
|
||||
@@ -507,7 +507,17 @@ void setup() {
|
||||
} else {
|
||||
Serial.print("Found ");
|
||||
Serial.print(n_pads);
|
||||
Serial.println(" pad(s), showing selection screen...");
|
||||
Serial.println(" pad(s):");
|
||||
for (int i = 0; i < n_pads; i++) {
|
||||
Serial.print(" [");
|
||||
Serial.print(i);
|
||||
Serial.print("] ");
|
||||
Serial.print(pad_chksums[i]);
|
||||
Serial.print(" (");
|
||||
Serial.print((unsigned long)pad_sizes[i]);
|
||||
Serial.println(" bytes)");
|
||||
}
|
||||
Serial.println("Showing selection screen...");
|
||||
const char *chksum_ptrs[4];
|
||||
for (int i = 0; i < n_pads; i++) chksum_ptrs[i] = pad_chksums[i];
|
||||
char selected[65];
|
||||
|
||||
+2
-2
@@ -762,8 +762,8 @@ int socket_name_random(char *out, size_t out_len);
|
||||
/* Version information (auto-updated by build/version tooling) */
|
||||
#define NSIGNER_VERSION_MAJOR 0
|
||||
#define NSIGNER_VERSION_MINOR 1
|
||||
#define NSIGNER_VERSION_PATCH 7
|
||||
#define NSIGNER_VERSION "v0.1.7"
|
||||
#define NSIGNER_VERSION_PATCH 8
|
||||
#define NSIGNER_VERSION "v0.1.8"
|
||||
|
||||
|
||||
/* NSIGNER_HEADERLESS_DECLS_END */
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>n_signer CYD Web Serial Demo</title>
|
||||
<title>n_signer USB Test</title>
|
||||
<script>
|
||||
/* §12 Dark mode: set class synchronously before paint. */
|
||||
(function () {
|
||||
@@ -342,7 +342,7 @@
|
||||
<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="divHeaderText">n_signer USB Test</div>
|
||||
<div class="divHeaderButtons"></div>
|
||||
</div>
|
||||
|
||||
@@ -350,7 +350,7 @@
|
||||
<!-- 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>
|
||||
<p class="note">Connect to the signer (Teensy 4.1 USB CDC, CYD CH340 serial, etc.) 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" class="btn">Connect Web Serial</button>
|
||||
<button id="disconnectBtn" class="btn secondary" disabled>Disconnect</button>
|
||||
@@ -464,7 +464,7 @@
|
||||
<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>
|
||||
<textarea id="nseContent" class="inpStyle">hello from usb test</textarea>
|
||||
<label for="nseIdx">nostr_index</label>
|
||||
<input id="nseIdx" type="number" value="0" min="0" class="inpStyle" />
|
||||
<div class="row">
|
||||
@@ -478,7 +478,7 @@
|
||||
<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>
|
||||
<textarea id="nmeContent" class="inpStyle">mined by usb test</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>
|
||||
@@ -530,9 +530,9 @@
|
||||
<!-- 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>
|
||||
<p class="note">OTP pad is bound from the SD card (Teensy 4.1) or derived from the mnemonic (CYD). Offset advances monotonically.</p>
|
||||
<label for="otpPlain">plaintext</label>
|
||||
<textarea id="otpPlain" class="inpStyle">Secret message.</textarea>
|
||||
<label for="otpCipher">ciphertext (for decrypt, base64)</label>
|
||||
<textarea id="otpCipher" class="inpStyle" placeholder="filled by encrypt"></textarea>
|
||||
<label for="otpEnc">encoding</label>
|
||||
@@ -555,17 +555,17 @@
|
||||
<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>n_signer USB Test</h3>
|
||||
<p>A browser control panel for any n_signer hardware signer (Teensy 4.1, CYD ESP32-2432S028, etc.). 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>1. Plug the signer into a USB port (Teensy 4.1 enumerates as /dev/ttyACM*, CYD CH340 enumerates as /dev/ttyUSB*).</p>
|
||||
<p>2. Click <code>Connect Web Serial</code> and pick the signer's 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>
|
||||
<p>4. Enter a mnemonic on the signer's 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>
|
||||
<p>Every request is signed with a demo secp256k1 key (priv = 0x0102…2020). The signer 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>
|
||||
@@ -632,7 +632,7 @@
|
||||
["nsigner_method", method],
|
||||
["nsigner_body_hash", bodyHash],
|
||||
];
|
||||
const content = "cyd-webserial-demo";
|
||||
const content = "usb-test";
|
||||
const ser = JSON.stringify([0, callerPubX, createdAt, 27235, tags, content]);
|
||||
const id = await sha256Hex(utf8(ser));
|
||||
const sigBytes = await schnorr.sign(id, callerPriv, new Uint8Array(32));
|
||||
@@ -695,7 +695,7 @@
|
||||
setTimeout(() => {
|
||||
if (pendingResolve === resolve) {
|
||||
pendingResolve = null;
|
||||
reject(new Error("timeout (30s) — check the CYD screen for an approval prompt"));
|
||||
reject(new Error("timeout (30s) — check the signer screen for an approval prompt"));
|
||||
}
|
||||
}, 65000);
|
||||
});
|
||||
@@ -747,7 +747,7 @@
|
||||
rxBuffer = new Uint8Array(0);
|
||||
readLoop();
|
||||
setStatus("Connected", "ok");
|
||||
log("Connected to CYD via Web Serial @ 115200 baud");
|
||||
log("Connected to signer via Web Serial @ 115200 baud");
|
||||
document.querySelectorAll("button[id$='Btn']").forEach(b => {
|
||||
if (b !== connectBtn && b !== disconnectBtn) b.disabled = false;
|
||||
});
|
||||
@@ -860,10 +860,21 @@
|
||||
callVerb("nostr_mine_event", [event, { nostr_index: idx, difficulty: diff, timeout_sec: timeout }], $("nmeOut"));
|
||||
});
|
||||
|
||||
const nip04Enc = () => {
|
||||
const nip04Enc = async () => {
|
||||
const peer = $("nip04Peer").value.trim(), msg = $("nip04Msg").value, idx = Number($("nip04Idx").value || 0);
|
||||
if (!peer) { $("nip04Out").textContent = "✗ enter peer pubkey"; return; }
|
||||
callVerb("nostr_nip04_encrypt", [peer, msg, { nostr_index: idx }], $("nip04Out"));
|
||||
const params = [peer, msg, { nostr_index: idx }];
|
||||
$("nip04Out").textContent = "→ nostr_nip04_encrypt " + JSON.stringify(params);
|
||||
try {
|
||||
const auth = await buildAuth("nostr_nip04_encrypt", params);
|
||||
const resp = await sendRpc({ id: String(Math.floor(Math.random()*1e9)), method: "nostr_nip04_encrypt", params, auth });
|
||||
$("nip04Out").textContent += "\n← " + pretty(resp);
|
||||
if (resp && resp.result) {
|
||||
$("nip04Cipher").value = resp.result;
|
||||
}
|
||||
} catch (e) {
|
||||
$("nip04Out").textContent += "\n✗ " + e.message;
|
||||
}
|
||||
};
|
||||
const nip04Dec = () => {
|
||||
const peer = $("nip04Peer").value.trim(), ct = $("nip04Cipher").value, idx = Number($("nip04Idx").value || 0);
|
||||
@@ -873,10 +884,21 @@
|
||||
$("nip04EncBtn").addEventListener("click", nip04Enc);
|
||||
$("nip04DecBtn").addEventListener("click", nip04Dec);
|
||||
|
||||
const nip44Enc = () => {
|
||||
const nip44Enc = async () => {
|
||||
const peer = $("nip44Peer").value.trim(), msg = $("nip44Msg").value, idx = Number($("nip44Idx").value || 0);
|
||||
if (!peer) { $("nip44Out").textContent = "✗ enter peer pubkey"; return; }
|
||||
callVerb("nostr_nip44_encrypt", [peer, msg, { nostr_index: idx }], $("nip44Out"));
|
||||
const params = [peer, msg, { nostr_index: idx }];
|
||||
$("nip44Out").textContent = "→ nostr_nip44_encrypt " + JSON.stringify(params);
|
||||
try {
|
||||
const auth = await buildAuth("nostr_nip44_encrypt", params);
|
||||
const resp = await sendRpc({ id: String(Math.floor(Math.random()*1e9)), method: "nostr_nip44_encrypt", params, auth });
|
||||
$("nip44Out").textContent += "\n← " + pretty(resp);
|
||||
if (resp && resp.result) {
|
||||
$("nip44Cipher").value = resp.result;
|
||||
}
|
||||
} catch (e) {
|
||||
$("nip44Out").textContent += "\n✗ " + e.message;
|
||||
}
|
||||
};
|
||||
const nip44Dec = () => {
|
||||
const peer = $("nip44Peer").value.trim(), ct = $("nip44Cipher").value, idx = Number($("nip44Idx").value || 0);
|
||||
@@ -886,14 +908,55 @@
|
||||
$("nip44EncBtn").addEventListener("click", nip44Enc);
|
||||
$("nip44DecBtn").addEventListener("click", nip44Dec);
|
||||
|
||||
$("otpEncBtn").addEventListener("click", () => {
|
||||
/* OTP encrypt expects base64 plaintext; decrypt returns base64 plaintext.
|
||||
The UI lets the user type plain text, so we encode/decode transparently. */
|
||||
function utf8ToB64(str) {
|
||||
return btoa(unescape(encodeURIComponent(str)));
|
||||
}
|
||||
function b64ToUtf8(b64) {
|
||||
try { return decodeURIComponent(escape(atob(b64))); }
|
||||
catch (e) { return atob(b64); }
|
||||
}
|
||||
$("otpEncBtn").addEventListener("click", async () => {
|
||||
const pt = $("otpPlain").value, enc = $("otpEnc").value;
|
||||
callVerb("encrypt", [pt, { algorithm: "otp", encoding: enc }], $("otpOut"));
|
||||
const ptB64 = utf8ToB64(pt);
|
||||
const params = [ptB64, { algorithm: "otp", encoding: enc }];
|
||||
$("otpOut").textContent = "→ encrypt " + JSON.stringify(params);
|
||||
try {
|
||||
const auth = await buildAuth("encrypt", params);
|
||||
const resp = await sendRpc({ id: String(Math.floor(Math.random()*1e9)), method: "encrypt", params, auth });
|
||||
$("otpOut").textContent += "\n← " + pretty(resp);
|
||||
if (resp && resp.result) {
|
||||
/* OTP result is a JSON string: {"ciphertext": ...} */
|
||||
let r = resp.result;
|
||||
if (typeof r === "string") { try { r = JSON.parse(r); } catch (_) { /* leave as string */ } }
|
||||
const ct = (r && r.ciphertext) ? r.ciphertext : (typeof resp.result === "string" ? resp.result : null);
|
||||
if (ct) { $("otpCipher").value = ct; }
|
||||
}
|
||||
} catch (e) {
|
||||
$("otpOut").textContent += "\n✗ " + e.message;
|
||||
}
|
||||
});
|
||||
$("otpDecBtn").addEventListener("click", () => {
|
||||
$("otpDecBtn").addEventListener("click", async () => {
|
||||
const ct = $("otpCipher").value, enc = $("otpEnc").value;
|
||||
if (!ct) { $("otpOut").textContent = "✗ paste ciphertext first (from encrypt)"; return; }
|
||||
callVerb("decrypt", [ct, { algorithm: "otp", encoding: enc }], $("otpOut"));
|
||||
const params = [ct, { algorithm: "otp", encoding: enc }];
|
||||
$("otpOut").textContent = "→ decrypt " + JSON.stringify(params);
|
||||
try {
|
||||
const auth = await buildAuth("decrypt", params);
|
||||
const resp = await sendRpc({ id: String(Math.floor(Math.random()*1e9)), method: "decrypt", params, auth });
|
||||
$("otpOut").textContent += "\n← " + pretty(resp);
|
||||
if (resp && resp.result) {
|
||||
let r = resp.result;
|
||||
if (typeof r === "string") { try { r = JSON.parse(r); } catch (_) { /* leave as string */ } }
|
||||
const ptB64 = (r && r.plaintext) ? r.plaintext : (typeof resp.result === "string" ? resp.result : null);
|
||||
if (ptB64) {
|
||||
$("otpOut").textContent += "\nplaintext: " + b64ToUtf8(ptB64);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
$("otpOut").textContent += "\n✗ " + e.message;
|
||||
}
|
||||
});
|
||||
|
||||
if (!("serial" in navigator)) {
|
||||
Reference in New Issue
Block a user