mirror of
https://github.com/Kpa-clawbot/meshcore-analyzer.git
synced 2026-09-16 10:22:42 +00:00
Fixes #1864. ## Problem `PAYLOAD_TYPE_ANON_REQ` was effectively treated like `REQUEST`. The two differ on the wire: ``` REQUEST : <dest hash 1B> <source hash 1B> <hmac 2B> <encrypted> ANON_REQ: <dest hash 1B> <source pubkey 32B, full> <hmac 2B> <encrypted> ``` The decoders read the right bytes but surfaced the sender key as `ephemeralPubKey`, which meant: - `store.go`'s node indexer keys on `pubKey`/`destPubKey`/`srcPubKey`, so ANON_REQ packets were **not** indexed — they didn't show up on a node's packet view; and - the packets list "details" rendered a bare `anon → <destHash>`, throwing away the sender identity the packet actually carries. - the detail side-view byte breakdown fell through the REQ catch-all, mislabelling a nonexistent 1-byte "Src Hash" and placing MAC/Encrypted-Data at the wrong offsets (`+2`/`+4` instead of `+33`/`+35`). ## Fix **Backend** (`cmd/ingestor` + `cmd/server` decoders) - Surface the ANON_REQ sender key as `srcPubKey` (json) so it's indexed and resolvable. The frontend keeps a legacy `ephemeralPubKey` reader so packets decoded before this rename still resolve — no DB migration needed. - `TestDecodeAnonReqValid` now asserts the full 32-byte `srcPubKey`. **Frontend** - `hop-resolver.js`: new O(1) `nameForKey(pubkey)` using the existing `pubkeyIdx` (all nodes). - `getDetailPreview`: resolve the source pubkey to a node **name** when known, else show the first 8 hex chars — no more bare `anon`. - Detail side-view: explicit ANON_REQ breakdown — `Dest Hash (1B)` | `Src Public Key (32B)` (node-linked) | `MAC @+33` | `Encrypted Data @+35`. - Detail header `srcLabel` falls back to the resolved ANON_REQ sender. All rendered names are `escapeHtml`-wrapped. ## Testing - `go test ./...` green for both `cmd/ingestor` and `cmd/server` (incl. strengthened `TestDecodeAnonReqValid`). - `node --check` on `packets.js`; brace/paren balance + markers verified on `hop-resolver.js`. - No HTML sink lines added → XSS preflight gate unaffected; every interpolated name is escaped. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: SaarMesh-Bot <300107934+SaarMesh-Bot@users.noreply.github.com> Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
co-authored by
SaarMesh-Bot
Claude
parent
a3454e7508
commit
0d6f59ab2d
@@ -143,7 +143,12 @@ type Payload struct {
|
||||
Text string `json:"text,omitempty"`
|
||||
Sender string `json:"sender,omitempty"`
|
||||
SenderTimestamp uint32 `json:"sender_timestamp,omitempty"`
|
||||
EphemeralPubKey string `json:"ephemeralPubKey,omitempty"`
|
||||
// ANON_REQ carries the sender's FULL 32-byte public key (not a 1-byte
|
||||
// srcHash like REQ) — MeshCore firmware/src/Mesh.cpp. Surfaced as
|
||||
// srcPubKey so store.go's node indexer picks it up and it resolves to a
|
||||
// node name; frontend also reads legacy ephemeralPubKey for pre-rename
|
||||
// packets (#1864).
|
||||
SrcPubKey string `json:"srcPubKey,omitempty"`
|
||||
PathData string `json:"pathData,omitempty"`
|
||||
SNRValues []float64 `json:"snrValues,omitempty"`
|
||||
Tag uint32 `json:"tag,omitempty"`
|
||||
@@ -857,7 +862,7 @@ func decodeAnonReq(buf []byte) Payload {
|
||||
return Payload{
|
||||
Type: "ANON_REQ",
|
||||
DestHash: hex.EncodeToString(buf[0:1]),
|
||||
EphemeralPubKey: hex.EncodeToString(buf[1:33]),
|
||||
SrcPubKey: hex.EncodeToString(buf[1:33]),
|
||||
MAC: hex.EncodeToString(buf[33:35]),
|
||||
EncryptedData: hex.EncodeToString(buf[35:]),
|
||||
}
|
||||
|
||||
@@ -523,6 +523,12 @@ func TestDecodeAnonReqValid(t *testing.T) {
|
||||
if p.MAC != "aabb" {
|
||||
t.Errorf("mac=%s, want aabb", p.MAC)
|
||||
}
|
||||
// #1864: ANON_REQ must capture the FULL 32-byte source pubkey (buf[1:33]),
|
||||
// not a 1-byte srcHash. buf[i]=i for i in 1..32 -> 0102..20.
|
||||
wantPK := "0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20"
|
||||
if p.SrcPubKey != wantPK {
|
||||
t.Errorf("srcPubKey=%s, want %s", p.SrcPubKey, wantPK)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecodePathPayloadShort(t *testing.T) {
|
||||
|
||||
@@ -106,7 +106,12 @@ type Payload struct {
|
||||
Lon *float64 `json:"lon,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
ChannelHash int `json:"channelHash,omitempty"`
|
||||
EphemeralPubKey string `json:"ephemeralPubKey,omitempty"`
|
||||
// ANON_REQ carries the sender's FULL 32-byte public key (not a 1-byte
|
||||
// srcHash like REQ) — MeshCore firmware/src/Mesh.cpp. Surfaced as
|
||||
// srcPubKey so store.go's node indexer picks it up and it resolves to a
|
||||
// node name; frontend also reads legacy ephemeralPubKey for pre-rename
|
||||
// packets (#1864).
|
||||
SrcPubKey string `json:"srcPubKey,omitempty"`
|
||||
PathData string `json:"pathData,omitempty"`
|
||||
Tag uint32 `json:"tag,omitempty"`
|
||||
AuthCode uint32 `json:"authCode,omitempty"`
|
||||
@@ -468,7 +473,7 @@ func decodeAnonReq(buf []byte) Payload {
|
||||
return Payload{
|
||||
Type: "ANON_REQ",
|
||||
DestHash: hex.EncodeToString(buf[0:1]),
|
||||
EphemeralPubKey: hex.EncodeToString(buf[1:33]),
|
||||
SrcPubKey: hex.EncodeToString(buf[1:33]),
|
||||
MAC: hex.EncodeToString(buf[33:35]),
|
||||
EncryptedData: hex.EncodeToString(buf[35:]),
|
||||
}
|
||||
|
||||
+10
-1
@@ -338,6 +338,15 @@ window.HopResolver = (function() {
|
||||
return affinityMap[pubkeyA][pubkeyB] || 0;
|
||||
}
|
||||
|
||||
// #1864: O(1) node-name lookup by FULL pubkey (64-char hex). Returns the
|
||||
// node name when a node with this exact public key is known, else null.
|
||||
// Used to resolve ANON_REQ source pubkeys to a display name.
|
||||
function nameForKey(pubkey) {
|
||||
if (!pubkey) return null;
|
||||
const n = pubkeyIdx[String(pubkey).toLowerCase()];
|
||||
return n && n.name ? n.name : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve hops using server-provided resolved_path (full pubkeys).
|
||||
* Returns the same format as resolve() — { [hop]: { name, pubkey, ... } }.
|
||||
@@ -363,5 +372,5 @@ window.HopResolver = (function() {
|
||||
return result;
|
||||
}
|
||||
|
||||
return { init: init, resolve: resolve, resolveFromServer: resolveFromServer, ready: ready, haversineKm: haversineKm, setAffinity: setAffinity, getAffinity: getAffinity };
|
||||
return { init: init, resolve: resolve, resolveFromServer: resolveFromServer, ready: ready, haversineKm: haversineKm, setAffinity: setAffinity, getAffinity: getAffinity, nameForKey: nameForKey };
|
||||
})();
|
||||
|
||||
+27
-3
@@ -2929,8 +2929,16 @@
|
||||
if (decoded.type === 'PATH') return `<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor-sprite.svg#ph-shuffle"/></svg> ${decoded.srcHash?.slice(0,8) || '?'} → ${decoded.destHash?.slice(0,8) || '?'}`;
|
||||
// Requests/responses (encrypted)
|
||||
if (decoded.type === 'REQ' || decoded.type === 'RESPONSE') return `<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor-sprite.svg#ph-lock"/></svg> ${decoded.srcHash?.slice(0,8) || '?'} → ${decoded.destHash?.slice(0,8) || '?'}`;
|
||||
// Anonymous requests
|
||||
if (decoded.type === 'ANON_REQ') return `<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor-sprite.svg#ph-lock"/></svg> anon → ${decoded.destHash?.slice(0,8) || '?'}`;
|
||||
// Anonymous requests (#1864). ANON_REQ carries the sender's FULL 32-byte
|
||||
// pubkey (not a 1-byte srcHash) — resolve it to a node name if known,
|
||||
// else show the first 8 hex chars. Legacy ephemeralPubKey fallback covers
|
||||
// packets decoded before the backend field was renamed to srcPubKey.
|
||||
if (decoded.type === 'ANON_REQ') {
|
||||
const anonKey = decoded.srcPubKey || decoded.ephemeralPubKey || '';
|
||||
const anonName = (anonKey && window.HopResolver && HopResolver.nameForKey) ? HopResolver.nameForKey(anonKey) : null;
|
||||
const anonSrc = anonName ? escapeHtml(anonName) : (anonKey ? escapeHtml(anonKey.slice(0, 8)) : 'anon');
|
||||
return `<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor-sprite.svg#ph-lock"/></svg> ${anonSrc} → ${decoded.destHash?.slice(0,8) || '?'}`;
|
||||
}
|
||||
// CONTROL packets (#1802) — DISCOVER_REQ / DISCOVER_RESP body fields,
|
||||
// decoded by cmd/ingestor/decoder.go decodeControl(). Wire format:
|
||||
// firmware/src/Mesh.cpp:69
|
||||
@@ -3286,7 +3294,10 @@
|
||||
// src→dst). Replaces the prior byte-count title that buried packet
|
||||
// identity behind a byte counter (#1458 P0-A).
|
||||
const semanticSummary = getDetailPreview(decoded);
|
||||
const srcLabel = decoded.sender || decoded.name || (decoded.srcHash ? decoded.srcHash.slice(0,8) : null) || (decoded.pubKey ? decoded.pubKey.slice(0,8) + '…' : null);
|
||||
// #1864: ANON_REQ has no srcHash — its sender is the full srcPubKey.
|
||||
const _anonKey = decoded.srcPubKey || decoded.ephemeralPubKey || '';
|
||||
const _anonName = (_anonKey && window.HopResolver && HopResolver.nameForKey) ? HopResolver.nameForKey(_anonKey) : null;
|
||||
const srcLabel = decoded.sender || decoded.name || (decoded.srcHash ? decoded.srcHash.slice(0,8) : null) || _anonName || (_anonKey ? _anonKey.slice(0,8) + '…' : null) || (decoded.pubKey ? decoded.pubKey.slice(0,8) + '…' : null);
|
||||
const dstLabel = decoded.recipient || (decoded.destHash ? decoded.destHash.slice(0,8) : null);
|
||||
const srcDstHtml = (srcLabel || dstLabel)
|
||||
? `<div class="detail-srcdst">${escapeHtml(srcLabel || '?')} <span class="arrow">→</span> ${escapeHtml(dstLabel || (decoded.channel ? '#' + decoded.channel : '?'))}</div>`
|
||||
@@ -3604,6 +3615,19 @@
|
||||
if (decoded.pathData) {
|
||||
rows += fieldRow(off + 9, 'Route Hops', decoded.pathData.toUpperCase(), pathHops.length + ' hop(s)');
|
||||
}
|
||||
} else if (decoded.type === 'ANON_REQ') {
|
||||
// #1864: ANON_REQ layout differs from REQ — the source is a FULL 32-byte
|
||||
// pubkey, not a 1-byte srcHash, so MAC/data sit at off+33/off+35 (not
|
||||
// off+2/off+4). Decode explicitly and resolve the key to a node link.
|
||||
const anonKey = decoded.srcPubKey || decoded.ephemeralPubKey || '';
|
||||
const anonName = (anonKey && window.HopResolver && HopResolver.nameForKey) ? HopResolver.nameForKey(anonKey) : null;
|
||||
rows += fieldRow(off, 'Dest Hash (1B)', decoded.destHash || '', '');
|
||||
const anonKeyCell = anonKey
|
||||
? `<a href="#/nodes/${encodeURIComponent(anonKey)}" class="hop-link ${anonName ? 'hop-named' : ''}" data-hop-link="true">${anonName ? escapeHtml(anonName) : truncate(anonKey, 24)}</a>`
|
||||
: '—';
|
||||
rows += fieldRow(off + 1, 'Src Public Key (32B)', anonKeyCell, anonName ? '' : 'sender pubkey (unresolved)');
|
||||
rows += fieldRow(off + 33, 'MAC (2B)', decoded.mac || '', '');
|
||||
rows += fieldRow(off + 35, 'Encrypted Data', truncate(decoded.encryptedData || '', 30), '');
|
||||
} else if (decoded.destHash !== undefined) {
|
||||
rows += fieldRow(off, 'Dest Hash (1B)', decoded.destHash || '', '');
|
||||
rows += fieldRow(off + 1, 'Src Hash (1B)', decoded.srcHash || '', '');
|
||||
|
||||
Reference in New Issue
Block a user