diff --git a/examples/companion_radio/main.cpp b/examples/companion_radio/main.cpp index ffd1a4b8..a2729d84 100644 --- a/examples/companion_radio/main.cpp +++ b/examples/companion_radio/main.cpp @@ -430,6 +430,22 @@ extern "C" EMSCRIPTEN_KEEPALIVE int sim_test_get_num_contacts() { return the_mesh.getNumContacts(); } +// The live radio channel config -- lets a host page (meshcore-solo-site's +// WebSocket relay bridge) tag this instance's outgoing packets with what +// it's currently tuned to, so a server-side relay can fan bytes out only to +// other instances tuned to the same freq/bw/sf/cr, mirroring real LoRa +// channel isolation. Reads NodePrefs directly (not SimRadio, which never +// stores the values SimRadio::setParams() is called with) since NodePrefs +// is the actual live source of truth -- the same fields the on-device +// Settings > Radio screen edits via UITask::applyRadioParams(). +extern "C" EMSCRIPTEN_KEEPALIVE void sim_radio_get_params(float* out_freq, float* out_bw, int* out_sf, int* out_cr) { + NodePrefs* p = g_sim_ready ? the_mesh.getNodePrefs() : nullptr; + *out_freq = p ? p->freq : 0; + *out_bw = p ? p->bw : 0; + *out_sf = p ? p->sf : 0; + *out_cr = p ? p->cr : 0; +} + // Real hardware ships with NodePrefs::HP_DEFAULT -- a curated 5-page Home // carousel (Clock/Tools/Shutdown/Favourites/Map) -- so a first-time user // isn't handed 13 pages to joystick through; the rest (Recent/Radio/ diff --git a/variants/sim/SimInstance.h b/variants/sim/SimInstance.h index 7f5a0283..14ed6d65 100644 --- a/variants/sim/SimInstance.h +++ b/variants/sim/SimInstance.h @@ -51,6 +51,35 @@ inline uint32_t sim_instance_salt() { return h >>> 0; }); } + +// meshcore-solo-site's WebSocket relay bridge (Phase 3) exposed a real gap +// in sim_instance_salt() alone: it's a pure function of the simInstanceTag +// STRING ('hero'/'B'/'R'), so it's only ever useful for telling apart +// same-tab instances that use different tags -- it's the SAME value every +// time for two genuinely different browser tabs/machines that both boot a +// 'hero' instance, which is exactly the new cross-visitor case. Combined +// with SimRNG::begin()'s other two seed ingredients -- time(NULL) (1-second +// resolution: two real visitors loading the page in the same second collide +// outright) and `(uintptr_t)this` (a WASM linear-memory address, which is +// fully deterministic across independent boots of the same binary doing the +// same allocation sequence -- there's no ASLR inside a wasm sandbox, so this +// contributes zero actual entropy, not "usually" different) -- two distinct +// real visitors landing on the exact same wall-clock second reliably +// produced byte-identical generated Ed25519 identities (confirmed while +// testing the relay bridge: two independent, freshly-IDBFS browser contexts +// launched together produced provably identical advert packets end to end). +// A host page now passes one genuinely random value from the one place that +// actually has real entropy per browser session -- crypto.getRandomValues() +// -- as `simEntropy` on the Module config object (see meshcore-solo-site's +// bootInstance()/bootRepeater()), read back here the same way +// simInstanceTag already is. Defaults to 0 (this function's old, sole +// behavior) if a host page doesn't set it, so nothing else changes. +inline uint32_t sim_instance_entropy() { + return (uint32_t)EM_ASM_INT({ + return (typeof Module !== 'undefined' && Module['simEntropy']) ? (Module['simEntropy'] >>> 0) : 0; + }); +} #else inline uint32_t sim_instance_salt() { return 0; } +inline uint32_t sim_instance_entropy() { return 0; } #endif diff --git a/variants/sim/SimRNG.h b/variants/sim/SimRNG.h index 7133e298..536161a6 100644 --- a/variants/sim/SimRNG.h +++ b/variants/sim/SimRNG.h @@ -10,12 +10,20 @@ // in sim_instance_salt() so that two module instances of the SAME compiled // binary (the browser's two companion_radio instances) can't end up with // the same seed -- see SimInstance.h for why that's a real risk here, not -// a hypothetical one. +// a hypothetical one. sim_instance_entropy() (also SimInstance.h) mixes in +// real crypto.getRandomValues()-sourced entropy from the host page on top +// of that -- salt alone is a pure function of the tag string ('hero'/'B'/ +// 'R'), so it's identical across two genuinely different browser tabs that +// both boot a 'hero' instance; combined with time(NULL)'s 1-second +// resolution and a WASM heap pointer that's fully deterministic (no ASLR +// inside the sandbox) across independent boots, two different real visitors +// landing on the same wall-clock second used to get byte-identical +// generated identities without this. class SimRNG : public mesh::RNG { public: SimRNG() { } void begin() { - unsigned seed = (unsigned)time(NULL) ^ (unsigned)(uintptr_t)this ^ sim_instance_salt(); + unsigned seed = (unsigned)time(NULL) ^ (unsigned)(uintptr_t)this ^ sim_instance_salt() ^ sim_instance_entropy(); srand(seed); } void random(uint8_t* dest, size_t sz) override { diff --git a/variants/sim/SimRadio.h b/variants/sim/SimRadio.h index 1d29d090..c229b18d 100644 --- a/variants/sim/SimRadio.h +++ b/variants/sim/SimRadio.h @@ -116,7 +116,14 @@ public: // time(NULL) second, same `rand()` process state, often the same // `this` address across independent-but-identically-laid-out linear // memories) and end up with correlated "random" behaviour. - return (uint32_t)time(NULL) ^ (uint32_t)(uintptr_t)this ^ (uint32_t)rand() ^ sim_instance_salt(); + // sim_instance_entropy(): same reasoning, but for two genuinely + // different browser tabs/machines both booting a same-tagged instance + // (e.g. two different visitors' 'hero') -- salt alone is a pure + // function of the tag string, identical for both; this mixes in real + // crypto.getRandomValues()-sourced entropy from the host page. See + // SimInstance.h and SimRNG.h for the full story (found while testing + // meshcore-solo-site's cross-visitor relay bridge). + return (uint32_t)time(NULL) ^ (uint32_t)(uintptr_t)this ^ (uint32_t)rand() ^ sim_instance_salt() ^ sim_instance_entropy(); } void getFreqBounds(float& min_mhz, float& max_mhz) const { diff --git a/variants/sim/build_wasm.sh b/variants/sim/build_wasm.sh index d5651158..73f801fc 100755 --- a/variants/sim/build_wasm.sh +++ b/variants/sim/build_wasm.sh @@ -213,6 +213,11 @@ done # included in the build" -- confirmed by hitting exactly that during Phase # 3. Purely additive: nothing Phase 2's web/index.html already does # (sim_enqueue_key() with a plain number, no buffer marshaling) is affected. +# +# HEAPF32,HEAP32 (relay-bridge addition): sim_radio_get_params() writes its +# freq/bw (float) and sf/cr (int) results into a JS-malloc'd scratch buffer +# via out-params -- reading them back needs the typed-array views, same +# reasoning as HEAPU8 above for sim_radio_poll_tx()'s byte buffer. "$EMXX" \ "${OBJS[@]}" \ -lidbfs.js \ @@ -222,7 +227,7 @@ done -sEXPORT_NAME=MeshCoreSim \ -sENVIRONMENT=web \ -sEXIT_RUNTIME=0 \ - -sEXPORTED_RUNTIME_METHODS=FS,ccall,cwrap,HEAPU8 \ + -sEXPORTED_RUNTIME_METHODS=FS,ccall,cwrap,HEAPU8,HEAPF32,HEAP32 \ -sEXPORTED_FUNCTIONS=_main,_malloc,_free \ -o "$OUT_DIR/meshcore_sim.js"