Skip to content

fix(meshcore): hashtag channel secret not derived in non-secure HTTP contexts (#3606)#3608

Merged
Yeraze merged 1 commit into
mainfrom
claude/great-dijkstra-o9i5jv
Jun 22, 2026
Merged

fix(meshcore): hashtag channel secret not derived in non-secure HTTP contexts (#3606)#3608
Yeraze merged 1 commit into
mainfrom
claude/great-dijkstra-o9i5jv

Conversation

@Yeraze

@Yeraze Yeraze commented Jun 21, 2026

Copy link
Copy Markdown
Owner

Summary

Fixes #3606 — MeshCore #channel secrets were always random instead of being derived from the channel name.

Root cause: crypto.subtle is only available in secure contexts (HTTPS or localhost). When MeshMonitor is accessed over plain HTTP via an IP address — the typical Docker-on-a-separate-host deployment — crypto.subtle is undefined. The call to crypto.subtle.digest(...) inside deriveHashtagSecretHex threw a TypeError that was caught and silently discarded, leaving the randomly-seeded secret in place. crypto.getRandomValues (used to generate the seed) is available in non-secure contexts, so the seed was always set, but was never replaced by the deterministic derived value.

Fix: Add a pure-JS SHA-256 implementation (FIPS 180-4) to meshcoreHelpers.ts as a fallback that activates automatically when crypto.subtle is unavailable. deriveHashtagSecretHex is unchanged from the caller's perspective and now works correctly in all deployment contexts.

Changes

  • src/utils/meshcoreHelpers.ts: Added sha256PureJS(data: Uint8Array): Uint8Array (FIPS 180-4, ~60 LOC). Updated deriveHashtagSecretHex to detect crypto.subtle availability and use the pure-JS fallback when it's absent.
  • src/utils/meshcoreHelpers.test.ts: Three new regression tests:
    1. sha256PureJS matches the authoritative #test vector (9cd8fcf22a47333b591d96a2b848b73f)
    2. sha256PureJS matches the authoritative #general vector (4c49f3f24629f5ee4ad5b3965db47985)
    3. deriveHashtagSecretHex produces the correct result when crypto.subtle is overridden to undefined (simulates HTTP-over-IP context)

Test plan

  • All existing meshcoreHelpers tests pass (7 tests)
  • All existing MeshCoreChannelsConfigSection tests pass (10 tests)
  • New fallback tests pass (3 tests)
  • No new failures introduced in the full test suite (pre-existing failures are proto-submodule env issues, identical before/after)

🤖 Generated with Claude Code

https://claude.ai/code/session_01BFCHwsMfG4XUctxKt2oT2k


Generated by Claude Code

@github-actions

github-actions Bot commented Jun 21, 2026

Copy link
Copy Markdown

Claude encountered an error —— View job


I'll analyze this and get back to you.

…back when crypto.subtle is unavailable (fixes #3606)

crypto.subtle is only available in secure contexts (HTTPS or localhost).
In the common Docker-on-a-separate-host case where users access MeshMonitor
over plain HTTP via an IP address, crypto.subtle is undefined. The
deriveHashtagSecretHex call in the useEffect threw a silent TypeError which
was swallowed by the catch block, leaving the random seed in place. Every
"Add channel" click produced a new random key and typing a #hashtag name
never updated the secret to the deterministic value.

Add a pure-JS SHA-256 implementation (FIPS 180-4) as a fallback used
automatically when crypto.subtle is unavailable. deriveHashtagSecretHex
now works in all deployment contexts without any API changes.

Three regression tests added: known-vector checks for the pure-JS path
(#test and #general) and a test that simulates the non-secure context by
overriding crypto.subtle to undefined.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BFCHwsMfG4XUctxKt2oT2k
@Yeraze Yeraze force-pushed the claude/great-dijkstra-o9i5jv branch from dc38112 to 171bdd2 Compare June 22, 2026 15:50
@github-actions

github-actions Bot commented Jun 22, 2026

Copy link
Copy Markdown

Claude finished @Yeraze's task —— View job


PR Review: fix(meshcore): hashtag channel secret not derived in non-secure HTTP contexts

  • Read changed files
  • Analyze code quality and best practices
  • Check for bugs and security concerns
  • Evaluate test coverage
  • Post review feedback

Summary

This is a well-targeted fix for a real deployment issue. The root cause analysis is correct — crypto.subtle is unavailable in non-secure contexts (plain HTTP over IP), and the pure-JS SHA-256 fallback is a reasonable approach to handle this transparently.


Code Quality

sha256PureJS implementation (meshcoreHelpers.ts:76–136)

The FIPS 180-4 SHA-256 implementation looks correct. A few observations:

  1. Correctness of ch function (line 115): The ch computation is (e & f) ^ (~e & g). In JavaScript, ~e performs bitwise NOT on a signed 32-bit integer, which is correct for SHA-256 where all words are 32-bit. This is fine.

  2. Potential integer overflow in bitLen (line 95): data.length * 8 could overflow JavaScript's safe integer range for inputs larger than ~2^50 bytes. For the channel name use case (short strings), this is a non-issue, but it's worth noting if the function is ever reused more broadly.

  3. Padding length calculation (line 96): Math.ceil((data.length + 9) / 64) * 64 correctly accounts for the 1 byte for 0x80 and 8 bytes for the length field. Correct.

  4. High 32 bits of bitLen (line 101): Math.floor(bitLen / 0x100000000) >>> 0 handles inputs up to 4GB correctly in practice. For short strings, bitLen will be well below 0x100000000, so this will correctly be 0.

  5. Style: The implementation is dense but functional. The compression loop (lines 113–122) packs 4 variable updates per line, which saves vertical space at some cost to readability. This is a common tradeoff for inline crypto implementations.

deriveHashtagSecretHex (meshcoreHelpers.ts:167–180)

The fallback detection is clean:

if (typeof crypto !== 'undefined' && crypto.subtle) {

This correctly handles both crypto being undefined (non-browser environments) and crypto.subtle being undefined (non-secure HTTP contexts). Good defensive check.

Minor issue — typeof crypto guard: In modern browser environments crypto is always a global, but the typeof check is correct defensive coding for Node.js / test environments.


Potential Issue: Writable crypto.subtle Assumption in Tests

meshcoreHelpers.test.ts:89:

Object.defineProperty(crypto, 'subtle', { value: undefined, configurable: true });

This works in the test environment (jsdom/happy-dom) where crypto.subtle is configurable: true, but it relies on an implementation detail that may differ across test runners or future environments. The try/finally restore block is correct and prevents test pollution. This is acceptable for a regression test.


Security

No concerns. The SHA-256 implementation is standard FIPS 180-4 and is only used as a derivation function for a channel key — not for authentication, password hashing, or any context where the pure-JS timing properties matter. The derived key matches what the official MeshCore app produces, so interoperability is preserved.


Test Coverage

The three new tests are well-chosen:

Test Purpose
sha256PureJS matches #test vector Verifies the pure-JS impl against a known-good value
sha256PureJS matches #general vector Second authoritative vector, cross-checked against base64 from component tests
deriveHashtagSecretHex fallback path End-to-end regression for the original bug, simulates the non-secure context

The existing tests (case sensitivity, format, normalization) continue to exercise the crypto.subtle path. Good balance.

One gap: There's no test verifying that sha256PureJS produces the full 32-byte output correctly (the existing tests only check result.slice(0, 16)). This isn't a blocking concern for this use case since SHA-256 truncation to the first 16 bytes is well-defined, but a full-digest comparison would make the unit test more robust.


Verdict

Approve. The fix is correct, targeted, and well-tested. The pure-JS SHA-256 fallback is a standard approach for this class of problem. No blocking issues found.

@Yeraze

Yeraze commented Jun 22, 2026

Copy link
Copy Markdown
Owner Author

Reviewed and verified independently before merge: the two test vectors are authoritative (SHA256("#test")[:16]=9cd8fcf2…b848b73f, SHA256("#general")[:16]=4c49f3f2…5db47985), the pure-JS routine is a correct FIPS 180-4 SHA-256 (round constants, init values, padding incl. 64-bit length, schedule, compression all standard), and the fallback only activates when crypto.subtle is absent so secure-context behavior is unchanged. On the review's full-digest nit: the production path only consumes the first 16 bytes, so the existing slice tests cover the used output; I also confirmed the full 32-byte digest matches (…f457b1bb1a3ea2453a885f9e5787765b1). Merging.

@Yeraze Yeraze merged commit c403751 into main Jun 22, 2026
20 checks passed
@Yeraze Yeraze deleted the claude/great-dijkstra-o9i5jv branch June 22, 2026 17:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG] MeshCore #channel secret not deterministically derived — random key shown instead

2 participants