Improve bridge startup reliability and token validation#341
Improve bridge startup reliability and token validation#341
Conversation
PR #341 — 5-Model Consensus ReviewTitle: Improve bridge startup reliability and token validation 🔴 CRITICAL — DevTunnel loopback removal breaks all mobile connections (2/5 models)
The PR removes the loopback bypass and simultaneously sets a random ephemeral token before Before this PR, the flow was: remote client → DevTunnel → localhost → loopback bypass → accepted. After: remote client → DevTunnel (strips header) → localhost → no loopback bypass, no token → rejected. Fix: The random guard token covers the window before the real token is known. Once the real token is set, DevTunnel connections carry it. But the loopback bypass was load-bearing for DevTunnel-proxied connections whose token header gets stripped. Either restore the loopback bypass for DevTunnel connections specifically, or confirm the header is preserved by the DevTunnel infrastructure for this deployment. 🔴 CRITICAL — Silent secret loss during migration (4/5 models)
if (string.IsNullOrEmpty(storedRemote) && !string.IsNullOrEmpty(legacyRemote))
{ SetSecureStorageSync(RemoteTokenKey, legacyRemote); storedRemote = legacyRemote; }
// ...
_secretsDirty = false;
if (hadLegacySecrets)
Save(); // scrubs JSON — [JsonIgnore] means RemoteToken won't be serialized
Fix: Only call 🔴 CRITICAL — Deadlock on iOS main thread (4/5 models)
private static string? GetSecureStorageSync(string key)
{
try { return Task.Run(() => SecureStorage.Default.GetAsync(key)).GetAwaiter().GetResult(); }
catch { return null; }
}
Fix: Make 🟡 MODERATE —
|
PR #341 Re-Review (Round 2) — New commit
|
| # | Finding | Prior Severity | Status |
|---|---|---|---|
| 1 | GetSecureStorageSync deadlock on iOS |
🔴 CRITICAL | |
| 2 | Silent secret loss during migration | 🔴 CRITICAL | 🟡 PARTIALLY FIXED — per-field check added, but partial migration bug remains |
| 3 | DevTunnel loopback removal + mobile rejected | 🔴 CRITICAL | ✅ FIXED — X-Bridge-Authorization header survives DevTunnel proxying |
| 4 | TokenEquals length oracle |
🟡 MODERATE | ✅ FIXED — SHA256 + CryptographicOperations.FixedTimeEquals |
| 5 | Plugin hash scope mismatch | 🟡 MODERATE | ✅ FIXED — both paths use ComputeDirectoryHash(Path.GetFullPath(...)) |
| 6 | Test cleanup not in finally |
🟢 MINOR | ✅ FIXED — both tests use try/finally |
Remaining Issues
🟡 MODERATE — GetSecureStorageSync sync-over-async pattern (Finding #1)
The code still uses:
try { return Task.Run(() => SecureStorage.Default.GetAsync(key)).GetAwaiter().GetResult(); }Task.Run mitigates the classic SynchronizationContext deadlock, and iOS Keychain APIs are generally thread-safe (no main-thread dispatch). So this works in practice on current MAUI/iOS. Downgrading from CRITICAL to MODERATE because:
- The pattern is architecturally brittle (would break if SecureStorage internals ever change)
.GetAwaiter().GetResult()still blocks a thread- An async
Load()overload would be cleaner long-term
Not a ship-blocker, but worth a follow-up.
🟡 MODERATE — Partial migration can still lose secrets (Finding #2, partially fixed)
The per-field SetSecureStorageSync check is good — if a single field fails to migrate, it stays as null in memory. However, the migratedAny flag is shared across all fields:
if (SetSecureStorageSync(RemoteTokenKey, legacyRemote))
{ storedRemote = legacyRemote; migratedAny = true; }
// ...
if (migratedAny)
Save(); // Serializes with [JsonIgnore] → scrubs ALL legacy tokens from JSONScenario: RemoteToken migrates ✅, but LanToken fails ❌ (Keychain full / entitlement issue):
migratedAny = true→Save()calledSave()serializes with[JsonIgnore]on all three token properties- JSON file no longer contains
"LanToken": "..." - LanToken is not in SecureStorage (SetAsync failed) and not in JSON → lost
Suggested fix: Only scrub from JSON after verifying all attempted migrations succeeded, or track per-field migration state:
bool allMigrationsSucceeded = true;
// ... set to false if any SetSecureStorageSync fails
if (migratedAny && allMigrationsSucceeded)
Save();Verdict: ⚠️ Request changes
4 of 6 original findings fixed — excellent progress. The two remaining issues are:
- Sync-over-async pattern (MODERATE, follow-up OK)
- Partial migration secret loss (MODERATE, should fix before merge — low probability but real data loss)
Fix #2 and this is ✅ ready to merge.
PR #341 — Round 3 Re-Review (5-Model Consensus)Models: claude-opus-4.6 ×2, claude-sonnet-4.6, gemini-3-pro-preview, gpt-5.3-codex Previous Findings Status
🟡 MODERATE — Finding #2: Silent secret loss on partial migration (5/5 models — STILL PRESENT)
The Scenario (unchanged from Round 2):
Fix: Use per-field migration flags; only call bool allSucceeded = true;
if (!string.IsNullOrEmpty(legacyLan) && !SetSecureStorageSync(...))
allSucceeded = false;
// ...
if (migratedAny && allSucceeded)
Save();🟡 MODERATE — Finding #1:
|
| Issue | Severity | Ship-blocker? |
|---|---|---|
migratedAny partial migration data loss |
🟡 MODERATE | Yes — real data loss on any Keychain error |
SaveMobileSecretsIfDirty ignores write failures |
🟡 MODERATE | Yes — same data-loss class |
| DevTunnel failure leaves bridge locked | 🟡 MODERATE | Yes — silent breakage with no recovery path |
GetSecureStorageSync sync-over-async |
🟡 MODERATE | No — acceptable follow-up |
ComputeDirectoryHash framing |
🟢 MINOR | No |
MigrateAndLoadMobileSecrets/SaveMobileSecretsIfDirty and the DevTunnel failure lockout are all straightforward fixes.
Round 3 — 5-model review: claude-opus-4.6 ×2, claude-sonnet-4.6, gemini-3-pro-preview, gpt-5.3-codex. Consensus filter: issues flagged by 2+ models only.
- WsBridgeServer: remove unconditional loopback trust when a token is configured; loopback-only mode (no token set) still allows unauthenticated local connections as before - WsBridgeServer: replace string.Equals token comparison with constant-time comparison using CryptographicOperations.FixedTimeEquals - DevTunnelService: set a random ephemeral token on the bridge before Start() so the listening socket is never open without auth while the tunnel handshake is in progress; replaced by the real access token once issued - PluginLoader: extend integrity verification to cover all DLL files in the plugin directory rather than only the entry-point assembly; filenames are included in the hash so renames are detected; existing plugin approvals will require re-confirmation - ConnectionSettings: on iOS/Android/MacCatalyst, store RemoteToken, LanToken, and ServerPassword in the platform SecureStorage (Keychain on Apple) rather than plain JSON; existing values are migrated and scrubbed from settings.json on first load - Update WsBridgeIntegrationTests to reflect new auth behaviour Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…n safety - WsBridgeServer: fix TokenEquals length oracle by hashing both sides to SHA-256 before FixedTimeEquals, so comparison time is always constant regardless of input length - WsBridgeClient + WsBridgeServer: add X-Bridge-Authorization header alongside X-Tunnel-Authorization; DevTunnel strips X-Tunnel-Authorization before proxying to localhost but passes custom headers through, so mobile clients now reach the server's token validator even when connecting via DevTunnel - ConnectionSettings: SetSecureStorageSync now returns bool; migration only scrubs legacy JSON field after confirming SecureStorage write succeeded, preventing silent token loss if Keychain write fails - PluginLoader: canonicalize plugin directory path via Path.GetFullPath before hashing to avoid hash mismatches when discovery and load paths differ under symlinks or relative path segments - WsBridgeIntegrationTests: wrap AccessToken teardown in finally blocks so subsequent tests are not affected if an assertion throws Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- ConnectionSettings: per-field SecureStorage migration tracking; JSON is only scrubbed when every attempted field was written successfully so a partial Keychain failure cannot silently lose any token - ConnectionSettings: SaveMobileSecretsIfDirty checks SetSecureStorageSync return values and keeps _secretsDirty=true on partial write failure so the next Save() will retry - DevTunnelService: if IssueAccessTokenAsync returns null, clear the random placeholder token so the bridge reverts to local-only mode rather than remaining permanently locked with an opaque token no client can obtain - PluginLoader: add null-byte framing between filename and content bytes in ComputeDirectoryHash to make the hash scheme provably collision-free - WsBridgeServerAuthTests: update HTTP probe test to reflect new behaviour (loopback no longer bypasses auth when token is configured; correct token via X-Bridge-Authorization is accepted from any origin) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
068b070 to
b25cd66
Compare
PureWeen
left a comment
There was a problem hiding this comment.
PR #341 Round 4 Review (Multi-Model AI Consensus)
Round 3 Findings Status
| # | Finding | Status |
|---|---|---|
| 1 | Partial migration data loss (MigrateAndLoadMobileSecrets) | ✅ FIXED -- per-field tracking + atomic scrub-on-all-success |
| 2 | SaveMobileSecrets ignores failures | ✅ FIXED -- retains _secretsDirty=true on failure for retry |
| 3 | DevTunnel permanent lockout | ✅ FIXED -- clears AccessToken=null on failure, local-only fallback |
New Findings
🟡 MODERATE -- Crash-path migration residual (ConnectionSettings.cs MigrateAndLoadMobileSecrets)
allSucceeded uses migratedRemote (write-this-launch flag) instead of !string.IsNullOrEmpty(storedRemote) (value-in-SecureStorage). If app crashes between SecureStorage.SetAsync and Save(), next launch sees storedRemote populated, skips migration, migratedRemote=false, allSucceeded=false → plaintext stays in JSON permanently. One-line fix: check stored values not migration flags.
🟡 MODERATE -- Stale test helper (DevTunnelServiceTests.cs:~893)
InvokeValidateClientToken helper still contains old if (isLoopback) return true logic that was removed from production code. Tests pass but validate stale behavior, not the new token-based validation.
🟢 MINOR -- Plugin hash invalidation (PluginLoader.cs)
ComputeDirectoryHash changes hash scheme, invalidating all existing plugin approvals on upgrade with no migration path. Consider a HashVersion field.
🟢 MINOR -- Missing test coverage (PluginLoader.cs)
ComputeDirectoryHash has no unit tests. Existing ComputeHash tests exercise dead code path.
Verdict: ⚠️ Request Changes (minor)
Two moderate findings need one-line fixes each. Overall the security hardening is excellent.
5-model AI consensus review (Round 4)
Changes
WsBridgeServer
DevTunnelService
PluginLoader
ConnectionSettings
Tests