mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-25 05:45:18 +00:00
The TOFU signing-key pin lived only in the in-memory peerRegistry. When a previously seen peer was no longer in the registry (app restart, or reconcileConnectivity pruning an offline peer), the announce trust check saw no pinned key, treated the announce as first contact, and persistIdentity overwrote the cached signing key/nickname — so an attacker could replay a victim's noiseKey/peerID with their own signing key and bypass the spoofing protection for returning/offline peers. Fixes: - BLEAnnounceHandler now falls back to the persisted cryptographic identity (via a new persistedSigningPublicKey environment closure) when the registry has no signing key for the peer, so the pin remains effective across registry eviction and app restarts. BLEService wires it to SecureIdentityStateManager.getCryptoIdentitiesByPeerIDPrefix, the same synchronous identity-manager read already used on the packet path by signedSenderDisplayName. - SecureIdentityStateManager.upsertCryptographicIdentity refuses to replace a persisted signing key with a different one (security-logged, nickname update included in the refusal), mirroring the registry policy. First-writer-wins persistence also removes any race where a concurrent announce could poison the stored identity. - CryptographicIdentity entries (incl. the signing-key pin) are now part of the encrypted IdentityCache, so they actually persist across app restarts; previously they were in-memory only. Old caches without the new field still decode (decodeIfPresent), and clearAllIdentityData / panic wipe clears the pins as before. Legitimate re-keying: presenting a different signing key for the same noise key is exactly the attack being blocked, so refusal is correct and permanent until the peer adopts a new noise identity (new peerID) or the user explicitly clears identity data. Repeated rejected announces follow the existing unverified-announce path (log + ignore); no retry loops or crashes. Tests: handler-level persisted-pin mismatch/match/precedence cases, an end-to-end restart+eviction test with real Ed25519 keys and a real SecureIdentityStateManager showing the attacker replay is rejected and the persisted identity is untouched while the victim re-announce is accepted, and identity-manager tests for the pinned-key refusal and the keychain round-trip of the pin. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Test Harness Guide
This test suite uses an in-memory networking harness to make end-to-end and integration tests deterministic, fast, and race-free without touching production code.
In-Memory Bus
- File:
bitchatTests/Mocks/MockBLEService.swift - Registry/Adjacency: Global
registrymapspeerIDto aMockBLEServiceinstance;adjacencyrecords simulated links between peers. - Setup: Call
MockBLEService.resetTestBus()insetUp()to clear state between tests. - Topology: Use
simulateConnectedPeer(_:)andsimulateDisconnectedPeer(_:)to add/remove links.connectFullMesh()helpers in tests build larger topologies. - Handlers: Tests can observe data via
messageDeliveryHandler(decodedBitchatMessage) andpacketDeliveryHandler(rawBitchatPacket). - De‑duplication: A thread-safe
seenMessageIDsprevents duplicate deliveries during flooding/relays.
Broadcast Flooding
- Flag:
MockBLEService.autoFloodEnabled - Intent: When
true, public broadcasts propagate across the entire connected component (ignores TTL for reach) while still de‑duping to prevent loops. - Usage: Enabled in Integration tests (
setUp) to simulate large-network broadcast; disabled in E2E tests to keep routing explicit and verify TTL behavior (seePublicChatE2ETests.testZeroTTLNotRelayed).
Rehandshake Flow (Noise)
- Why: The legacy NACK recovery path was removed; recovery now relies on Noise session rehandshake after decrypt failure or desync.
- Manager:
NoiseSessionManagermanages per-peer sessions. - Pattern: On decrypt failure, proactively clear the local session and re-initiate a handshake. The peer accepts and replaces their session.
- Test:
IntegrationTests.testRehandshakeAfterDecryptionFailure- Corrupts ciphertext to induce a decrypt error.
- Calls
removeSession(for:)on the initiator’s manager beforeinitiateHandshake(with:)to avoidalreadyEstablished. - Verifies encrypt/decrypt succeeds post-rehandshake.
Tips
- Determinism: Add small async delays only where handler installation/topology changes could race the first send.
- Scoping: Keep
autoFloodEnabledtoggled only within Integration tests; always reset intearDown()to avoid cross-test contamination. - Direct vs Relay: Private messages target a specific peer when adjacent; otherwise they are surfaced to neighbors for relay and, if known, also delivered to the target.
Quick Start
- Create nodes and connect them:
let svc = MockBLEService(); svc.myPeerID = "PEER1"svc.simulateConnectedPeer("PEER2")
- Observe messages:
svc.messageDeliveryHandler = { msg in /* asserts */ }
- Enable broadcast flooding for Integration suites only:
MockBLEService.autoFloodEnabled = true