fix(meshcore): derive hashtag channel secret at save time (#3607)#3609
Conversation
A user who typed a "#hashtag" channel name and clicked Save before the
asynchronous live-derive useEffect committed could persist the random
placeholder secret seeded by startAdd()/regenerate. The result: the saved
PSK was non-deterministic (different on every attempt) and never matched
the MeshCore app's well-known key.
handleSave now re-derives the deterministic SHA-256("#name")[0:16] key at
save time whenever the channel name is a hashtag channel, ignoring whatever
(possibly stale, random) value sits in the secret field. Non-hashtag
channels keep their explicit/random PSK unchanged.
Derivation confirmed against the MeshCore reference (SHA-256 of the UTF-8
name INCLUDING the leading "#"); e.g. #test = 9cd8fcf22a47333b591d96a2b848b73f
and #bot = eb50a1bcb3e4e5d7bf69a57c9dada211.
Adds a regression test that types "#bot" and saves in the same flush
(no wait on the live-derive effect) and asserts the PUT carries the
deterministic key, not the random placeholder.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011JEaCGwY9Wz8jeV4e22GW4
|
Claude encountered an error —— View job I'll analyze this and get back to you. |
|
Claude finished @Yeraze's task —— View job PR Review: fix(meshcore): derive hashtag channel secret at save time
SummaryThis is a clean, well-targeted fix for a real race condition. The root cause analysis in the PR description is accurate, and the fix is straightforward and correct. A few observations: Code Quality
The fix is correct and well-placed. Re-deriving at save time is the right defensive approach rather than relying on state that may be stale from an async One minor style note: the error toast message used on derivation failure (line 234) is the same // line 234 — derivation failure
showToast(t('meshcore.channels.invalid_secret', 'Secret must be exactly 32 hex characters (16 bytes)'), 'error');
// line 240 — invalid hex
showToast(t('meshcore.channels.invalid_secret', 'Secret must be exactly 32 hex characters (16 bytes)'), 'error');The derivation failure isn't meaningfully about "invalid hex" — it's a
Potential Bugs / Issues
No feedback that the displayed secret was overridden at save time If the live-derive effect hasn't fired (the race scenario), the user sees a random hex placeholder in the field, clicks Save, and the correct deterministic secret is stored silently. The field and toast (
Worth tracking as a potential UX improvement for a future pass. PerformanceNo concerns. SecurityThe derivation correctly hashes the full channel name including the leading Test CoverageThe new regression test (
Two minor observations:
Overall test coverage for the changed behavior is solid. VerdictApprove. The fix is correct, minimal, and well-tested. The PR description is thorough and the root cause analysis is accurate. The nits above are non-blocking. |
Summary
Closes #3607.
MeshCore
#(hashtag) channels created in MeshMonitor were being saved with a random secret that differed on every attempt and never matched the MeshCore app's well-known key. Reporters on v4.11.3 saw two different secrets for the same#botchannel (e16510550f7c984bea37d0005b28c4a1,1e8fdf3de67a9d01cfcbda1a1961724a) — both random, neither equal to the deterministicSHA-256("#bot")[0:16].Root cause
Both symptoms (non-determinism and mismatch) had a single cause: a race condition, not a wrong algorithm.
MeshCoreChannelsConfigSectionalready had a correct deterministic helper (deriveHashtagSecretHex→SHA-256("#name")[0:16]) and a live-deriveuseEffectthat fills the secret field when the name starts with#. But:startAdd()/Regenerateseed the field withcrypto.getRandomValues(random placeholder).awaitscrypto.subtle.digest).handleSave()read the secret straight from theeditSecretHexstate.A user who typed
#botand clicked Save before the async derivation effect committed persisted the stale random placeholder. Because it's random, it differs every attempt and never matches the app.Fix
handleSave()now re-derives the deterministic key at save time whenever the (trimmed) name is a hashtag channel, ignoring whatever value sits in the field:Non-hashtag channels are untouched — they keep their explicit/random PSK (a legitimate, different case).
Derivation algorithm (verified)
secret = SHA-256(<name including the leading "#">)[0:16], UTF-8, case-sensitive, displayed as lowercase hex.Confirmed against:
#testkey is9cd8fcf22a47333b591d96a2b848b73f=SHA-256("#test")[0:16], hashing with the#), andsrc/utils/meshcoreHelpers.ts,meshcoreHelpers.test.ts), which already encode this scheme.Worked examples:
#test→9cd8fcf22a47333b591d96a2b848b73f,#bot→eb50a1bcb3e4e5d7bf69a57c9dada211.Tests
Added a regression test that types
#botand clicks Save in the same flush — i.e. without waiting for the live-derive effect — and asserts the PUT body carries the deterministic key (eb50a1bcb3e4e5d7bf69a57c9dada211, base6461ChvLPk5de/aaV8na2iEQ==) and not the random placeholder. Two existing hashtag PUT assertions were made robust to the extra async derive tick viawaitFor.tsc -p tsconfig.server.json --noEmit: only pre-existingTelemetryChart.tsxerrors (unrelated, untouched).Files changed
src/components/MeshCore/MeshCoreChannelsConfigSection.tsxsrc/components/MeshCore/MeshCoreChannelsConfigSection.test.tsx🤖 Generated with Claude Code