mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-24 22:45:19 +00:00
main
43
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
820c933958 |
Harden PTT audio and the 1.7.1 release (#1423)
Centralize PTT and voice audio-session ownership, harden courier/bridge/outbox delivery and recovery, correct location and delivery-state races, add privacy/release metadata, and ship reproducible universal Arti slices with Release CI coverage. Validated by the full iOS suite, repeated audio/fragment/performance regressions, BitFoundation tests, strict lint and dead-code analysis, universal iOS Release builds, and iOS/macOS archives. |
||
|
|
b7c6f42b3a |
Close forged-directness link-rebind DoS on DM routing (#1421)
* Gate connected-link trust on a secure session; deny forged direct announces the connected shortcut
Residual gap after the rotation-heal containment (#1401): "verified
direct" announces prove the signature but not directness — TTL is
unsigned — so a malicious connected peer can replay a victim's fresh
announce with its TTL restored. When the victim has no live link, the
replayer's link rebinds to the victim's ID and reads as "connected",
and MessageRouter's connected fast-path then trusts it outright: every
DM stalls on a Noise handshake the replayer can never complete and is
silently lost while showing "sent".
Router-level trust gate (no wire change):
- Transport gains canDeliverSecurely(to:) — BLE answers with an
established Noise session; Nostr keeps its prompt-delivery predicate;
the protocol default forwards to canDeliverPromptly for transports
without a forgeable link layer.
- MessageRouter.sendPrivate only trusts a connected link outright when
it can deliver securely; otherwise it still sends (kicking the
handshake on a genuine link) but retains a copy and hands a sealed
copy to couriers, like the reachable path. flushOutbox gets the same
gate so a flush over an insecure link resends instead of dropping the
retained copy.
Presence hardening (defense in depth): a "direct" announce arriving on
a link already bound to a different peer no longer shortcuts the
claimed peer into "connected" — only real link state does. Genuine
first-contact and direct announces are unaffected; only the ambiguous
heal path loses the forgeable shortcut.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* Harden the insecure-link outbox bound; document the second-replay presence gap and the courier metadata tradeoff
Review follow-ups on the canDeliverSecurely gate:
- flushOutbox no longer counts connected-but-insecure flushes toward the
maxSendAttempts drop: the message was actually transmitted over a live
link, so a peer whose Noise handshake stalls across reconnect flapping
must not burn through the cap and lose the store-and-forward copy the
gate exists to preserve. Retention stays bounded by the 24h outbox TTL
and the per-peer FIFO cap; acks still clear it. Attempt-counting stays
for reachable-only (heuristic) sends. Regression test: >8 connected-
insecure flushes keep the retained copy, drop callback never fires.
- Document the known second-replay presence gap: linkBoundToOtherPeer
reads the binding before rebindLinkAfterVerifiedDirectAnnounce steals
the link, so once a first replay has rebound a link to an absent
victim's ID, a second replay marks the victim connected. Presence
display only — DMs stay on the retain+courier path via the router
gate. Not closed at the announce layer because the post-rebind state
is indistinguishable from a legitimate rotation/reconnect heal (a
supported, field-verified flow). Covered by a two-announce test.
- Note the accepted courier-spray metadata tradeoff at the connected-
insecure send: nearby verified peers receive a sealed copy (they learn
a DM exists, never its content), cleared on ack.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Promote a healed rotation to connected; normalize the secure-delivery probe; retain Codable properties in Periphery
Codex review follow-ups on
|
||
|
|
dd6b624cae |
Quality pass on the 1.7.0 batch: fix confirmed bugs, bump to 1.7.1 (#1418)
* Quality pass on the 1.7.0 batch: fix confirmed bugs, bump to 1.7.1 Post-merge review of PRs #1400–#1417 (push-to-talk, mesh bridging, DM store-and-forward, empty-mesh liveliness, geo-notes). Fixes the confirmed, well-scoped findings; deeper architectural/security items are tracked separately. - PTT hot-mic leak: releasing the mic during VoiceCaptureSession.start()'s 150ms retry pause left the mic live and streaming for up to 120s, because cancel() no-op'd once `completed` was set. Bail after the sleep if the hold was released, and make cancel() always tear down a late-started capture. - Bridge courier depositDrop reported success and burned the dedup slot before the drop was actually published (evicted/compose-fail = lying 📦 "carried" with no retry). Only consume publishedDropKeys on durable accept; add BoundedIDSet.remove() to release evicted/failed slots (uses the dead dedupKey). - Blocked senders resurfaced via archived "heard here earlier" echoes, the one path that bypassed the live block filter — filter at seed time. - A late optimistic .sent clobbered the router's .carried state; extend ConversationStore.shouldSkipStatusUpdate to a full precedence guard (sending < sent < carried < delivered < read). - Read receipts were permanently burned when the router dropped them (marked sent then dropped). sendReadReceipt/routeReadReceipt now return Bool; only record as sent on a successful route, else retry on the next read scan. - MessageRouter.cleanupExpiredMessages() had no production caller, so DMs to a peer that never reconnects sat on .sending until relaunch — run it in the 120s bridge sweep. - Sightings tally now rolls over at midnight while idle; wave notification action localized across all 29 locales; bridged anon#tag uses suffix(4) like everything else; makeThrowawayIdentity delegates to NostrIdentity.generate(); .swiftlint.yml excludes .claude worktrees. - Add regression tests: carried→sent no-downgrade, carried→delivered upgrade, evicted pending drop stays retryable. - Bump MARKETING_VERSION to 1.7.1. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Fix CI: adjust delivery-status benchmark and drop now-dead addSystemMessage The stricter no-downgrade guard (delivered/carried never regress to sent) broke two things the earlier commit didn't catch locally (perf tests are skipped in the default run, and Periphery runs only in CI): - PerformanceBaselineTests delivery benchmarks alternated sent <-> delivered assuming both directions apply; the delivered -> sent half is now correctly skipped, so the pass measured 0 updates. Alternate two delivered timestamps instead — every update is real, no downgrade. - Routing the geoDM "not in a location channel" error into the thread removed the only caller of ChatPrivateConversationContext.addSystemMessage, leaving it (and its mock) dead per Periphery. Drop the protocol requirement, the mock impl, and the now-vacuous systemMessages.isEmpty assertions (the invariant is compile-time enforced: the context can no longer emit a public system line). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Address review findings: read-receipt dedup, carried-vs-sending, block-time echo purge - Read receipts: claim the receipt in sentReadReceipts synchronously before spawning the routing task (chat open runs two read scans in one MainActor stretch, so the async insert let every unread message route twice), and release the claim when the route fails so the retry-on-failed-route behavior is preserved. - Delivery status: extend the no-downgrade guard so the `.sending` stamp a pre-handshake resend emits can no longer clobber carried/delivered/read (the 📦 indicator survived `.sent` but not `.sending`). - Archived echoes: blocking a peer now purges their carried public messages from the gossip archive at block time (UnifiedPeerService and /block), while the fingerprint-to-peerID mapping is still known — the seed-time filter can't resolve offline non-favorite strangers and stays only as defense-in-depth. New Transport hook (default no-op) + GossipSyncManager.removePublicMessages with immediate persist. - Bridge courier: an envelope that can't encode within the drop size caps fails identically on every attempt; consume the dedup slot so the 120s retry sweep stops re-running Noise sealing on it. - MeshSightingsTracker: cache the day-key DateFormatter instead of building one per call. Tests: double-markAsRead dedup + failed-route retry, carried→sending no-downgrade matrix, block-time purge (manager + service wiring), oversize-drop slot consumption. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Also skip the sent→sending downgrade in the delivery-status guard Codex review follow-up: sendPrivateMessage without an established Noise session emits `.sending` asynchronously, so it can land after the message already reached `.sent` and visibly walk "Sent" back to "Sending...". Treat `.sending` as weaker than `.sent` too — the status was already truthful. `.failed` → `.sending` stays allowed so a retry after a real failure remains visible. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Retain Codable properties in Periphery scan (same fix as #1421) The noiseKey assign-only false positive fired persistently on this branch (twice, including a rerun) despite the baselined USR. Byte-identical to the fix on fix/announce-replay-link-steal so the branches merge cleanly in either order. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: jack <jackjackbits@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
d499c3e415 |
Isolate tests from the developer's real login keychain (#1413)
* Isolate tests from the developer's real login keychain Every test run prompted for the login keychain password (repeatedly, since the xctest runner's code signature changes each build, so "Always Allow" can never stick) and silently deleted the developer's real Nostr identity via panic-mode tests. Two holes let tests reach the real keychain: - NostrIdentityBridge() defaulted to the real KeychainManager. Tests inject mocks, but app-side constructions with no injection point (LocationNotesManager's static bridge, GeohashPresenceService, BoardManager, AppRuntime) read the real chat.bitchat.nostr item when exercised under test. - clearAllAssociations() used raw SecItem* calls that bypassed the injected keychain entirely, so panicClearAllData tests wiped the real Nostr identity items on every run. Fix: centralize FavoritesPersistenceService's test-guarded in-memory default as KeychainManager.makeDefault() and use it for all default keychain parameters, and add deleteAll(service:) to KeychainManagerProtocol so clearAllAssociations() goes through the injected keychain like every other operation. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Share one in-memory test keychain per process (Codex P2) A fresh store per makeDefault() call diverges from production, where separate default-constructed bridges share chat.bitchat.nostr — BoardManager's publish and NIP-09 delete paths would derive different geohash identities under test. PreviewKeychainManager gains a lock since the shared instance is reached from arbitrary threads. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: jack <jackjackbits@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
ef848857b7 |
Remove dead code found by full Periphery audit; add scan config + advisory CI (#1410)
Periphery 3.7.4 audit of both schemes (macOS + iOS, intersected so platform-specific code is never touched), with test targets indexed and the share extension built. 277 dead declarations removed or demoted: dead forwarding wrappers (ChatViewModel+Nostr/+PrivateChat), removed- feature remnants (autocomplete command suggestions, back-swipe tuning, MediaSendError, GeohashParticipantTracker), unused Tor dormancy bindings, assign-only properties, unused parameters (renamed to _), and redundant public accessibility. 13 orphaned localization keys deleted across all 29 locales (old pre-#1392 location-notes UI, app_info warnings). Two real tests were flagged as unused because they never ran: Swift Testing methods missing @Test (NostrProtocolTests. testAckRoundTripNIP44V2_Delivered, NotificationStreamAssemblerTests. testAssemblesCompressedLargeFrame). Re-armed both; they pass. Deliberately kept, now recorded in .periphery.baseline.json: iOS-only code invisible to the CI macOS scan, C FFI signatures, keep-alive NWPathMonitor reference, InboundEventKey.eventID (dedup semantics), wifiBulk capability bit (reserved for Wi-Fi bulk work, used by BitFoundation package tests), and the String secureClear cluster (exercised by package tests). New: .periphery.yml config and an advisory Dead Code CI job (mirrors the SwiftLint precedent from #1361) that fails on findings not in the committed baseline. Verified: full macOS app suite, BitFoundation (119) and BitLogger (13) package tests green; periphery scan --strict exits clean. Co-authored-by: jack <jackjackbits@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
f9032cf2b9 |
Add mesh diagnostics: /ping, /trace, and topology map (#1377)
* Add mesh diagnostics: /ping, /trace, and topology map - New protocol types ping=0x26 / pong=0x27 (9-byte payload: 8-byte nonce + origin TTL) with per-peer inbound rate limiting (5 per 10s) - /ping @name reports RTT and hop count, 10s timeout - /trace @name prints the estimated path from gossiped directNeighbors - Topology map sheet (circular Canvas layout) reachable from App Info - Ping/pong ride the deterministic directed-relay path like DMs - Tests: payload round-trip, hop-count math, command output, edge normalization, layout Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Fix CI media-wipe race, per-link ping rate limiting, and /ping output routing Three fixes for PR #1377 review: 1. CI flake (sendImage_privateChatProcessesAndTransfersImage): the panicClearAllData / clearCurrentPublicTimeline detached utility-priority tasks delete the real ~/Library/Application Support/files tree, which the test process shares. The wipe fires at a nondeterministic time and raced the sendImage test's JPEG in files/images/outgoing (write then re-read), so prepareImagePacket threw and the test timed out. Both wipes are now skipped under tests (existing TestEnvironment.isRunningTests pattern); this also stops test runs from deleting the developer's real media. 2. Codex P1: ping packets are unsigned, so keying the pong rate limiter on packet.senderID let one connected peer rotate forged sender IDs to bypass the 5-per-10s budget. The limiter now keys on the ingress link (the directly connected peer that delivered the packet); the pong still goes to the claimed sender. Regression test proves rotating senders over one link exhaust one budget (fails 10 vs 5 pongs on the old code). 3. Codex P2: /ping output arrived up to 10s later and was routed from selectedPrivateChatPeer at callback time, misrouting the result after a chat switch. The origin conversation is now captured when the command is issued (CommandOutputDestination) and deferred output is routed there: a DM result lands in the origin chat's history even if deselected, and a mesh-timeline result pins to #mesh instead of the active channel. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * App Info: move NETWORK section under HOW TO USE and uppercase NETWORK/SYMBOLS headers Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Conform DiagnosticsMockContext to sendPublicMessage CommandContextProvider gained sendPublicMessage (Cashu /pay, #1376) after this branch forked, so the diagnostics test mock no longer conformed once main was merged in. Add the no-op stub. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: jack <jackjackbits@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
2360140760 |
Transitive verification: vouch for verified peers over Noise (#1380)
* Add capability bits to announce TLV Announces now carry an optional capabilities TLV (0x05): a little-endian bitfield with named bits for upcoming features (prekeys, wifiBulk, gateway, groups, board, vouch, meshDiagnostics). Old clients skip the unknown TLV; peers without it decode as nil so features can distinguish "legacy peer" from "advertises nothing". PeerCapabilities lives in BitFoundation with a minimal-length encoding that preserves unknown bits for forward compatibility. Peer capabilities are stored in the BLE peer registry on verified announce and exposed via BLEService.peerCapabilities(_:). The local advertisement set is empty until each feature ships its bit. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Transitive verification: vouch for verified peers over Noise When a Noise session establishes with a peer I verified and that peer advertises the .vouch capability, send signed attestations (up to 16, most recently verified first, at most once per peer per 24h) for the OTHER fingerprints I verified. Receivers accept vouches only from senders they verified themselves, verify the Ed25519 signature against the sender's announce-bound signing key, and surface the result as a new derived trust tier: vouched (unfilled seal) between casual and trusted. Protocol: - NoisePayloadType.vouch = 0x12 carries a batch of TLV attestations: voucheeFingerprint (32B), voucheeSigningKey (32B), timestamp (uint64 ms BE), Ed25519 signature over "bitchat-vouch-v1" | fingerprint | signingKey | timestamp. The voucher is implicit in the authenticated session. - PeerCapabilities.localSupported now advertises .vouch. Storage (SecureIdentityStateManager / IdentityCache): - vouches keyed by vouchee, capped at 8 vouchers each; validity is recomputed on read (voucher still verified-by-me, < 30 days old), so unverifying a voucher retires their vouches without cascade deletes. - New IdentityCache fields are Optional so pre-existing encrypted caches decode cleanly; TrustLevel.vouched is inserted mid-ladder but raw values are strings, so persisted values are unaffected (and vouched itself is never persisted). - Panic wipe clears vouch state with the rest of the identity cache. UI: unfilled checkmark.seal badge in the mesh peer list (filled seal stays exclusive to verified) and a "vouched for by N people you verified" section with voucher names in FingerprintView; VoiceOver labels and xcstrings entries included. Tests: attestation encode/decode + signature (forged/tampered/expired), accept-policy gates, batch cap, trust-level derivation incl. voucher invalidation, persistence compat, and coordinator exchange/accept policies. Full macOS suite: 1088 tests passing. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Fix CI deadlock in vouch tests and live-refresh the fingerprint sheet on vouch acceptance Two fixes for PR #1380 review findings: 1. CI "Run Swift Tests (app)" hang (exit 137): the new SecureIdentityStateManagerVouchTests suite was nonisolated, so Swift Testing ran its tests in parallel on the Swift Concurrency cooperative pool. Each test enqueues a queue.async(.barrier) write (setVerified) and immediately blocks in queue.sync / queue.sync(.barrier) (recordVouch / effectiveTrustLevel). On CI's few-core runners every cooperative-pool thread ended up parked behind a pending barrier that never got a dispatch worker, deadlocking the whole test process until the watchdog SIGKILLed it. The suite is now @MainActor, matching the production isolation of the vouch API (ChatVouchCoordinator is @MainActor) and keeping blocking syncs off the cooperative pool. 2. Codex P2: an open fingerprint sheet did not refresh its vouched badge when a vouch batch was accepted - VerificationModel.bind() never observed the trust-change signal. It now subscribes to the "peerStatusUpdated" notification that ChatVouchCoordinator.notifyPeerTrustChanged() posts (same source PeerListModel uses) and forwards it to objectWillChange. Added a regression test that pins VerificationModel's own subscription (verified to fail without the fix). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Skip media-wipe detached tasks under tests (shared-filesystem race) panicClearAllData and clearCurrentPublicTimeline delete the real ~/Library/Application Support/files tree in detached utility-priority tasks. The SPM test process shares that tree and ChatViewModelTests invoke both methods, so under parallel scheduling the wipe lands at a nondeterministic time — deleting media a concurrently running test just wrote (and the developer's real app data with it). Guard both with the existing TestEnvironment.isRunningTests pattern, mirroring the same fix on feat/mesh-diagnostics (#1377). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Port vouch capability-race fix to feat/vouching (ports b8adcbe9) Ports the on-device-confirmed fix from the integration test branch (commit b8adcbe9) onto feat/vouching so PR #1380 is actually correct. On-device testing confirmed the transitive vouch propagated once the send was triggered on verify / announce arrival rather than auth alone. Vouch attestations only ever sent from peerAuthenticated, gated on the peer's .vouch capability. That capability arrives via the peer's announce, processed independently of the Noise handshake, so at auth time the set was usually empty -> gate failed -> vouch silently skipped and never retried. - Refactor the send path into a reusable attemptVouch(to:fingerprint:now:). - Trigger on peer-list updates (peersUpdated): fired after every verified announce, so the batch goes out once the .vouch bit actually arrives. - Trigger on local verification (vouchToConnectedVerifiedPeers): verifying a peer runs a vouch pass over connected verified peers, covering the verify-while-connected case and propagating the new identity onward. - Relax the capability gate: treat an empty/unknown set as eligible (the Noise 0x12 payload is ignored by non-supporting peers); only skip when a non-empty set explicitly lacks .vouch. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: jack <jackjackbits@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
7341696280 |
Expand store-and-forward: open couriers, spray-and-wait, persistent outbox, 6h public history (#1372)
Store-and-forward previously delivered to an out-of-range peer only if a
mutual favorite happened to be connected at send time and later met the
recipient directly, and everything except courier envelopes died with the
app process. This closes those gaps end to end:
- Persist the MessageRouter outbox to disk, sealed with a ChaChaPoly key
held only in the Keychain (no plaintext at rest); queued private
messages now survive an app kill and flush on next launch.
- Deposit retry: queued messages are re-deposited whenever a new eligible
courier connects, tracked per message so the same courier is never
double-burned, until 3 distinct couriers carry it or it expires.
- Tiered open couriering: signature-verified strangers can now carry mail
(2 envelopes/depositor into a 20-slot pool) alongside mutual favorites
(5 each); overflow evicts verified-tier mail before favorites'.
- Spray-and-wait: envelopes carry a copy budget (4, capped 8, new TLV,
wire-compatible with old clients); couriers split half their remaining
budget with each newly encountered courier so mail diffuses through a
moving crowd.
- Remote handover: a verified relayed announce now floods a copy toward
the multi-hop recipient (directed-relay treatment, 10-min per-envelope
cooldown) while the carried original stays put for a direct encounter.
- Public history: gossip-sync window for whole public messages widened
from 15 min to 6 h, matched on the receive-acceptance side, and the
message store persists to disk so devices bridge partitions and
restarts ("town crier").
- Privacy-safe local delivery counters (bare tallies, log-only) so the
store-and-forward stack is measurable on-device.
- Panic wipe now also clears the sealed outbox, gossip archive, and
counters.
- Rewrite WHITEPAPER.md to describe the app as implemented (Noise XX/X,
actual flood control, courier system, gossip sync, Nostr path); the old
document described a bloom filter, three fragment types, and a
MessageRetryService that don't exist.
1037 macOS tests pass (17 new); iOS builds.
Co-authored-by: jack <jackjackbits@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
8a867a17a1 |
Remove noise service exposure; single-owner selection state
Transport callers no longer reach the raw NoiseEncryptionService: getNoiseService() is deleted in favor of narrow purpose-named Transport methods (session public key, identity fingerprint, static/signing keys, sign/verify, callback installation). VerificationService now reaches crypto through the transport, so it can no longer pin a stale service across a panic reset. myPeerID/myNickname become private(set); the existing setNickname mutator is the sole nickname path. ConversationStore is now the sole owner of private-chat selection: PrivateChatManager.selectedPeer is a published read-only mirror, and startChat/endChat mutate through the store intent. The bridge method and its five call sites are deleted, removing a latent bug where a stale manager selection pushed back into the store could resurrect a just-removed conversation. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
ab0da61533 |
[codex] Refactor BLE transport event handling (#1266)
* Refactor BLE transport event handling * Make image output paths unique * Keep queued Nostr read receipts alive * Allow self-authored RSR ingress replies --------- Co-authored-by: jack <jackjackbits@users.noreply.github.com> |
||
|
|
c60eff2c11 | Move additional files/tests to BitFoundation (#1102) | ||
|
|
4cfcefcda6 |
BitFoundation module to centralize shared components (#1089)
* Run local packages’ tests as well on CI * BitFoundation module to centralize shared components |
||
|
|
7e86d2061f |
Expand coverage for transport, chat, and media flows (#1056)
* Expand coverage for transport, chat, and media flows * Stabilize transport and media coverage tests --------- Co-authored-by: jack <jackjackbits@users.noreply.github.com> |
||
|
|
c043cf6354 |
Fix BLEService test local echo timing (#1054)
Co-authored-by: jack <jackjackbits@users.noreply.github.com> |
||
|
|
806c420313 |
Add comprehensive test coverage for ChatViewModel and BLEService (#962)
* Add comprehensive test coverage for ChatViewModel and BLEService This commit adds 14 new tests to improve the safety net for future refactoring of ChatViewModel and BLEService: New test files: - ChatViewModelTorTests: Tor lifecycle notification handlers (8 tests) - ChatViewModelDeliveryStatusTests: Delivery status state machine (6 tests) - ChatViewModelRefactoringTests: Command routing and message handling (4 tests) - BLEServiceCoreTests: Packet deduplication and stale broadcast filtering (2 tests) - PublicMessagePipelineTests: Message ordering and deduplication (4 tests) - MessageRouterTests: Transport selection and outbox behavior (4 tests) - PrivateChatManagerTests: Chat selection and read receipts (2 tests) - UnifiedPeerServiceTests: Fingerprint resolution and blocking (2 tests) - RelayControllerTests: TTL, handshake, and fragment relay logic (4 tests) Modified files: - MockTransport: Added peer snapshot publishing on connect/disconnect - TestHelpers: Added waitUntil polling helper for async tests - MockIdentityManager: Extended for new test scenarios Total: 454 tests across 64 suites (was 440) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Fix flaky test by using waitUntil instead of fixed sleep Replace fixed 200ms sleep with waitUntil helper for more reliable async assertion in routing tests. This prevents timing issues on CI. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> --------- Co-authored-by: jack <jackjackbits@users.noreply.github.com> Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com> |
||
|
|
31be6b83a7 |
fix(security): improve keychain error handling [BCH-01-009]
Add proper error classification to distinguish expected states (item not found) from critical failures (access denied, storage full) and recoverable errors (device locked, auth failed). Changes: - Add KeychainReadResult and KeychainSaveResult enums with error types - Add getIdentityKeyWithResult/saveIdentityKeyWithResult protocol methods - Implement retry logic with exponential backoff for transient errors - Add consistent SecureLogger logging for all keychain operations - Update NoiseEncryptionService identity loading to handle errors gracefully - Update MockKeychain and TrackingMockKeychain with error simulation - Add comprehensive KeychainErrorHandlingTests (18 new tests) Security: Applications can now properly detect and respond to critical keychain failures, improving resilience during device lock states and providing actionable diagnostics for access issues. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> |
||
|
|
84fd92ef4b |
fix: clear DH shared secrets after Noise handshake operations
Added secureClear() calls for all 6 DH operations in NoiseProtocol.swift to properly clear sensitive shared secrets from memory after use: - writeMessage(): .es initiator/responder, .se initiator/responder - performDHOperation(): .ee and .ss operations This fixes a forward secrecy vulnerability where shared secrets could persist in memory after handshake completion. Also adds: - TrackingMockKeychain to count secureClear calls in tests - 3 new tests verifying secureClear is called during handshake 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> |
||
|
|
a221b22691 |
refactor: consolidate KeychainHelper into KeychainManager (#797)
Merge KeychainHelper functionality into KeychainManager to provide a single, unified API for all keychain operations. - Remove KeychainHelper.swift and KeychainHelperProtocol - Add generic save/load/delete methods to KeychainManagerProtocol - Update NostrIdentityBridge to use KeychainManagerProtocol - Update FavoritesPersistenceService to use KeychainManagerProtocol - Update PreviewKeychainManager with new methods - Update MockKeychain and add MockKeychainHelper typealias for backwards compatibility 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> |
||
|
|
5d2745eae6 | Add tests for blocking and migration logic | ||
|
|
3e680f40bf |
Add ChatViewModel testability and unit tests
- Add testable ChatViewModel initializer accepting Transport dependency - Create MockTransport implementing full Transport protocol for testing - Add 21 unit tests covering: - Initialization (delegate, services, nickname) - Message sending (basic, empty, mentions, commands) - Message receiving (delegate calls, public messages) - Peer connections (connect, disconnect, isPeerConnected) - Deduplication service integration - Private chat routing - Bluetooth state handling - Panic clear functionality - Guard NotificationService methods against test environment crashes - Make deduplicationService internal for test access All 303 tests pass. |
||
|
|
b839ce5f6c |
PeerID 31/n: Remove String interop to be explicit (#840)
* PeerID 28/n: `ChatViewModel.getShortIDForNoiseKey` * PeerID 29/n: `BLEService` + remove dupe funcs from #823 * `handleFileTransfer` to use PeerID * `sendMessage` and `sendPrivateMessage` * PeerID 30/n: Update some leftovers * `lowercased()` inside PeerID for normalization * PeerID 31/n: Remove String interop to be explicit * MockBLEService: Remove direct target delivery This causes a delivery even if the sender and receiver are not connected --------- Co-authored-by: jack <jackjackbits@users.noreply.github.com> |
||
|
|
40e54a5120 |
Add voice notes and images over BLE mesh (audio + image only) (#823)
* Add BLE file transfer support and media UX * Gracefully disable mac attachment pickers in sandbox * Tighten spacing above media message bubbles * Reduce vertical padding between chat rows * Restore iOS file importer for attachments * Copy imported files before sending to preserve access * Allow file transfers from connected but unverified peers * Raise BLE notification buffer cap for large file transfers * Revert "Raise BLE notification buffer cap for large file transfers" This reverts commit b624523af843475db84e4a846db8dcbe824ae408. * Add guard to drop oversized BLE notification assemblies * Let BLE assembler accept large frames up to hard cap * Add detailed logging for BLE fragment assembly * Log incomplete BLE frames for debugging * Stop dropping partial BLE frames while assembling notifications * Fix compressed BLE file transfers * Enable mac attachment importers * Allow mac microphone access * Permit mac media library access * Describe microphone usage * Harden attachment transfer bookkeeping * Display recording milliseconds * Restore mac photo picker access * Use Photos picker on mac * Allow long-press reblur on images * Reblur images via swipe * Lowercase image preview buttons * Keep processed images for outgoing messages * Use save panel for mac image export * Fix image attachment detection * Allow user-selected write access * Align mac image JPEG encoding * Revert unsupported JPEG option * Strip metadata in mac image encoding * Normalize mac JPEG color space * Target image byte size across platforms * Fix CFMutableData handling * Preserve packet version when signing * Use unique transfer identifiers * Stub file transfer methods in mock * Stub file transfer methods in mock * Hide absolute paths in media messages * Resolve image/voice path handling * Fix cleanupLocalFile lookup * Restore BLE broadcasts when notify buffer is saturated * Guard peer map reads on BLE message path * Drop attachment ceilings to 1 MiB and bump release version * Reset BLE assembler on stalled fragment trains * Fix binary protocol test fixtures * Fix critical issues from PR #681 review Critical fixes: - BinaryProtocol: Return nil for unknown versions (prevents buffer underflows) - Add BinaryProtocol.Offsets struct to centralize magic numbers - Replace magic offset calculations with named constants Security/Privacy: - FileAttachmentView: Use url.lastPathComponent instead of url.path (prevents exposing full system paths) Documentation: - Fix compression algorithm documentation (zlib, not LZ4) All tests passing. * Fix UI freeze when receiving voice notes Problem: AVAudioPlayer initialization in VoiceNotePlaybackController.init() was running synchronously on main thread during view creation, blocking UI for 50-200ms per voice note. Solution: - Remove eager preparePlayer() call from init - Load duration asynchronously on background queue - Player is only prepared when playback is actually requested via ensurePlayerReady() This prevents UI freezes when voice notes appear in the chat. * Fix memory leaks and post-playback freeze Fixes: 1. Post-playback freeze: audioPlayerDidFinishPlaying now dispatches to main thread before updating @Published properties (Swift concurrency violation) 2. Unbounded waveform cache: Implement LRU eviction with 20-entry limit - Track last access time for each cached waveform - Evict oldest entry when cache is full - Prevents unlimited memory growth as voice notes accumulate 3. Audio buffer memory leaks: Wrap computeWaveform in autoreleasepool - AVAudioPCMBuffer allocations are autoreleased - Pool ensures buffers are freed promptly 4. Image processing memory: Add autoreleasepool around compression loops - Each jpegData() call creates temporary objects - Inner pool per iteration prevents memory spikes during quality search Memory should now remain stable during extended use. * Eliminate disk I/O from SwiftUI view rendering path Critical performance fix for UI freezes when receiving media: Problem: mediaAttachment(for:) was called during every SwiftUI render, performing synchronous disk I/O on main thread: - FileManager.fileExists() called 2-6x per message (checking subdirs) - applicationFilesDirectory() creating directories on every call - With multiple media messages, this meant 20-100+ disk ops per render Solution: 1. Remove fileExists checks - construct URLs directly - Files are validated during playback/display (fail gracefully if missing) - Sender determines subdirectory (outgoing vs incoming) 2. Cache applicationFilesDirectory() result - Static cache prevents repeated FileManager.url() calls - Directory created only once 3. Remove redundant playback.replaceURL() in VoiceNoteView.onAppear - Controller already initialized with correct URL This eliminates ALL disk I/O from the view rendering hot path. * Cache Nostr identity derivation to prevent crypto during view rendering Critical performance fix: Problem: formatMessageHeader() called deriveIdentity(forGeohash:) during every SwiftUI render for every media message. Each call performed: - Keychain I/O (getOrCreateDeviceSeed) - HMAC-SHA256 computation - Up to 10 secp256k1 key validations (elliptic curve crypto) With multiple media messages, this resulted in 100s of milliseconds of blocking crypto on main thread per render cycle. Solution: Add thread-safe cache for derived identities - Check cache before expensive crypto operations - NSLock protects concurrent access - Identity is deterministic per geohash, so caching is safe This eliminates crypto from the hot rendering path. * Cache geohash identity in ChatViewModel to prevent crypto during rendering Additional optimization for location channels (voice notes are mesh-only, but this helps with text message rendering in geohash channels): - Add cachedGeohashIdentity to avoid deriveIdentity calls during rendering - Check cache before falling back to crypto derivation - Reduces main thread crypto work in location channels * Make voice note loading completely lazy with deferred initialization Aggressive performance optimization to prevent UI freezes: Problem: Even with async loading, creating 10+ VoiceNotePlaybackController instances simultaneously (when scrolling past multiple voice notes) spawned 20+ concurrent background tasks, potentially starving main thread. Solution - Ultra-lazy loading: 1. VoiceNotePlaybackController.init() now does ZERO work - No duration loading - No player creation - Instant initialization 2. Duration loaded on-demand via public loadDuration() method - Called from VoiceNoteView.onAppear after 150ms delay - Reduced priority: .utility instead of .userInitiated - Guard prevents duplicate loading 3. Waveform loading also deferred 150ms - Gives UI time to settle after message appears - Prevents task storms when multiple voice notes appear This spreads the work over time instead of all at once. * Ensure /clear and panic triple-tap delete media files Fix: /clear command and panicClearAllData() now properly delete media files 1. /clear (triple-tap on chat): - Deletes outgoing media (voice notes, images, files) - Conservative: only our sent media, preserves received media - Runs in background to avoid UI freeze 2. panicClearAllData() (triple-tap on bitchat/ header): - Deletes ALL media files (incoming + outgoing) - Removes entire files directory and recreates structure - Ensures complete data wipe for emergency scenarios Both operations run async on .utility queue to prevent blocking UI. * Fix infinite render loop and apply all security fixes CRITICAL BUG FIX - Infinite Render Loop: Root Cause: Duplicate view identity in ContentView.swift:368 ForEach(messageItems) { item in // Already uses item.id via Identifiable messageRow(...) .id(item.id) // ❌ REDUNDANT modifier caused identity re-evaluation loop } When @Published properties updated, SwiftUI re-evaluated .id() → appeared as 'new' identity → triggered re-render → infinite loop. Caused UI freezes, keyboard failures, and 100% CPU usage. Fix: Remove redundant .id() modifier - ForEach already has stable identity. PERFORMANCE FIXES: 1. Waveform Cache Deadlock (Waveform.swift) - Removed nested queue.async(barrier) on cache hits - Was causing task saturation and potential deadlocks 2. Async Send Pattern (ContentView.swift) - Clear input immediately, defer actual send to next runloop - Prevents blocking current event handler 3. Proper Swift Concurrency (VoiceNoteView.swift) - Switch from .onAppear + DispatchQueue to .task - Cleaner async/await pattern for loading 4. Remove Redundant objectWillChange (ChatViewModel.swift) - @Published already triggers updates automatically - Explicit send() was causing double update cycles SECURITY FIXES (C1-C5, H1-H2): C1. Path Traversal Protection (BLEService.swift) - Unicode normalization, null byte removal - Replace ALL path separators, reject dotfiles - Validate paths don't escape directory C2. Integer Overflow (BitchatFilePacket.swift) - Use UInt64 for TLV parsing, safe Int conversion C3. MIME Validation (BLEService.swift) - Whitelist: JPEG, PNG, GIF, WebP, M4A, MP3, WAV, OGG, PDF - Magic byte validation for all types - Lenient on M4A (platform variations) C4. Compression Bomb (BinaryProtocol.swift) - Ratio validation <= 50,000:1 - Defense-in-depth with 1MB size cap C5. TOCTOU Race (ChatViewModel.swift) - Direct removeItem without fileExists check H1. File Size Validation (ChatViewModel, ImageUtils) - Check attributes BEFORE Data(contentsOf:) - Prevents memory exhaustion H2. Metadata Stripping (ImageUtils.swift) - Remove ALL metadata keys from JPEG encoding - Only compression quality set - Protects GPS/EXIF/device info privacy RESULT: ✅ No render loops ✅ Works with Xcode debugger ✅ Voice notes display properly ✅ All security vulnerabilities fixed ✅ 164 tests passing Production ready. * Complete all translations to 100% and fix auto-extraction - Mark non-localizable strings with Text(verbatim:) to prevent extraction - Update UI strings to lowercase per style guide (open, save, close, recording) - Add complete translations for all 29 languages (194/194 strings at 100%) - Remove empty/duplicate entries (@, bitchat/, Open, Recording %@) - Add proper localization comments for all user-facing strings * macOS: Focus message input on launch instead of nickname field * Remove debug print statements from sendMessage * Optimize voice note codec to 16 kHz / 20 kbps for smaller file sizes - Reduce sample rate from 44.1 kHz to 16 kHz (telephony standard) - Lower bitrate from 32 kbps to 20 kbps - Results in ~37% file size reduction (~150 KB/min vs 240 KB/min) - Increases max voice note length from 4.4 to 7 minutes over 1 MiB BLE limit - Maintains excellent voice quality using native AAC-LC codec * Fix critical security issues in fragment reassembly and file cleanup Fragment Reassembly Race Condition (CRITICAL): - Wrap all incomingFragments/fragmentMetadata access in collectionsQueue.sync - Prevents concurrent modification crashes from multi-threaded access - Minimizes lock contention by doing heavy work (reassembly/decode) outside locks - Add upper bound check: reject fragments with total > 10,000 (DoS prevention) - Add cumulative size validation before storing fragments (memory DoS prevention) File Cleanup Path Traversal (CRITICAL): - Use NSString.lastPathComponent to extract filename safely - Prevents directory traversal attacks via malicious filenames - Add path prefix validation before file deletion - Now checks both incoming and outgoing directories (fixes disk leak) Additional Protections: - Fragment assemblies now limited by both count (128) and cumulative bytes (1MB) - Explicit checks for "." and ".." filenames in cleanup - Defense-in-depth: multiple validation layers * Fix post-rebase compilation errors - Remove duplicate NostrIdentityBridge and Bech32 from NostrIdentity.swift (now in separate files) - Add caching to NostrIdentityBridge.deriveIdentity() for performance - Remove duplicate NotificationStreamAssembler from BLEService.swift - Remove duplicate function declarations in BLEService.swift - Remove duplicate DeliveryStatusView and PaymentChipView from ContentView.swift - Fix PeerID type conversions throughout (use .id for String, PeerID(str:) for wrapping) - Update ContentView body to use main's simple VStack structure - Fix NostrIdentityBridge instance method calls - Remove privateChatView (replaced with sheet-based UI in main) Build and tests passing (137/139 tests pass). * Fix remaining compilation issues after rebase - Fix PhotosUI import order (must be after platform imports) - Fix Data.WritingOptions.atomic reference - Add identity derivation caching to NostrIdentityBridge - Fix all remaining PeerID type conversions in ChatViewModel - Fix ContentView body structure to use main's VStack layout - Fix PaymentChipView API usage (now uses PaymentType enum) Build and tests now passing. * Add proper availability checks for PhotosPickerItem PhotosPickerItem requires iOS 16+ / macOS 13+ but canImport(PhotosUI) succeeds on older macOS versions. Add compiler version check to ensure PhotosPicker code only compiles when actually available. This fixes CI build failures on older macOS environments. * Limit PhotosPicker to iOS only to fix CI PhotosPickerItem has SDK availability issues on macOS in CI. Change PhotosPicker from canImport(PhotosUI) to os(iOS) only. macOS users can still import images via file importer (.fileImporter). This is actually cleaner as macOS file picker is more familiar to users. Fixes CI build failures. * Convert new tests to Swift Testing * Fix compilation issue * Add the missing `fileTransfer` case * Explicitly list all Enum cases to get compile-time errors * Revive lost `NotificationStreamAssembler` changes * Allow file fragments to account for protocol overhead * Simplify PR: Focus on audio+image, fix EXIF stripping, remove file transfers - Fix critical EXIF privacy issue in iOS image processing - Both iOS and macOS now use CGImageDestination for metadata stripping - Shared encodeJPEG function ensures no GPS, camera, or metadata leaks - Remove file transfer functionality to simplify PR scope - Deleted FileAttachmentView - Removed sendFileAttachment from ChatViewModel - Removed file picker UI from ContentView - Simplified attachment dialog to image only (iOS) or voice only - Keep focused media features: - Voice recording and playback - Image sending with progressive reveal - Binary protocol for media transfer * Improve camera UX: Direct camera access with camera icon - Change paperclip icon to camera icon for clearer affordance - Open camera directly on tap (no confirmation dialog) - Add CameraPickerView wrapper for UIImagePickerController - Remove PhotosPicker in favor of direct camera access - Images still processed through ImageUtils with EXIF stripping - Accessibility: Added 'Take photo' label * Full-screen camera with photo library option - Change to fullScreenCover for immersive camera experience - Add action sheet with 'Take Photo' and 'Choose from Library' options - Renamed ImagePickerView to support both camera and library sources - Both options open full-screen for better UX - Updated accessibility label to 'Add photo' (more accurate) * Fix camera white bars with overFullScreen presentation - Changed modalPresentationStyle from .fullScreen to .overFullScreen - This should eliminate white bars at top/bottom on notched devices - Explicitly set showsCameraControls and cameraOverlayView for camera mode * Gesture-based photo access: Tap for library, long-press for camera UX improvements: - Tap camera icon → Photo library (common use case) - Long press camera icon (0.3s) → Direct camera (quick photos) - Removed action sheet entirely for cleaner flow - Power users can long-press for instant camera access This is more discoverable and eliminates an extra step in the UI. * Simplify camera presentation to reduce frame errors - Changed back to standard .fullScreen presentation - Removed overFullScreen which was causing frame dimension errors - Let iOS handle safe areas automatically (white bars are intentional) - Reduces gesture gate timeout warnings Note: White bars on notched devices are iOS default behavior for UIImagePickerController. This respects safe areas for status bar and home indicator. True edge-to-edge would require custom AVFoundation camera implementation. * Force dark mode on camera/picker for black safe area bars - Set overrideUserInterfaceStyle = .dark on UIImagePickerController - Changes white bars to black (much better looking) - Camera controls and photo library also appear in dark mode - Consistent dark appearance regardless of system settings * Optimize sheet presentation for camera UI - Force .large detent for maximum height - Hide drag indicator for cleaner look - Use ignoresSafeArea to give camera full space - Should show complete flash button and controls * Fix P1: Add DoS protections to PeerID fragment handler Critical security fix addressing Codex review feedback: The PeerID overload of handleFragment (which is actually called by CoreBluetooth) was missing key safety checks that existed in the String overload: 1. Added total <= 10000 check to prevent unbounded fragment counts 2. Added cumulative size check against FileTransferLimits before storing each fragment 3. Prevents memory exhaustion DoS attacks via malicious fragment streams This ensures the actually-used code path has proper bounds checking. * Fix decompression size limit to support max-sized file transfers Root cause: BinaryProtocol.decode() was rejecting decompressed payloads larger than maxPayloadBytes (1 MB), but TLV-encoded file transfers are slightly larger due to metadata overhead. Fixes: - Changed decompression limit from maxPayloadBytes to maxFramedFileBytes - This accounts for TLV overhead (~50 bytes) + binary protocol headers - Now allows ~1.12 MB decompressed payloads (1 MB + overhead budget) The failing test was: - Creating 1 MB file content - TLV encoding adds ~50 bytes (1,048,627 total) - Compression reduces to ~1,084 bytes (highly repetitive data) - During decode, decompression was rejecting the 1,048,627 byte output - Now correctly allows it since 1,048,627 < 1,179,760 (maxFramedFileBytes) All 154 tests now pass including 'Max-sized file transfer survives reassembly' * Fix critical thread-safety crash in PeerID fragment handler CRITICAL: The PeerID version of _handleFragment was accessing incomingFragments dictionary without collectionsQueue synchronization, causing crashes when multiple BLE threads processed fragments concurrently. Crash stack trace pointed to line 3431 (dictionary subscript) with: 'doesNotRecognizeSelector' - classic concurrent mutation crash. Fix: - Wrapped ALL incomingFragments/fragmentMetadata access in collectionsQueue.sync(flags: .barrier) - Matches the thread-safe pattern used in String version - Separate cleanup into its own barrier block after reassembly - Prevents concurrent dictionary mutations from multiple BLE threads This is the same pattern as the String version (line 1128) which didn't crash. * Remove Localizable.xcstrings formatting noise The Localizable.xcstrings file had massive formatting-only changes (spacing: 'key' vs 'key :') that added 50K+ lines to the PR diff. This was just Xcode reformatting with no actual string changes. Reverted to main's version to keep PR focused on actual code changes. * Add macOS photo picker support - Added MacImagePickerView with NSOpenPanel for macOS - macOS shows photo.circle.fill icon (no camera hardware) - Opens native file picker for images (.png, .jpeg, .heic) - Images processed through ImageUtils with EXIF stripping - Simple sheet with Select/Cancel buttons Cross-platform photo sharing now works: - iOS: Tap for library, long-press for camera - macOS: Tap for file picker * Add Localizable strings for camera and voice features Xcode auto-generated localization strings for new UI elements: - Camera/photo picker labels - Voice recording UI strings - Media attachment descriptions These are legitimate new strings needed for the audio+image feature, not just formatting changes. --------- Co-authored-by: jack <jackjackbits@users.noreply.github.com> Co-authored-by: islam <2553451+qalandarov@users.noreply.github.com> |
||
|
|
3d914dcf46 |
Convert the remaining tests to Swift Testing (#781)
* SwiftTesting: NoiseProtocolTests + BinaryProtocolPaddingTests * SwiftTesting: `NotificationStreamAssemblerTests` * SwiftTesting: `NostrProtocolTests` * SwiftTesting: `BinaryProtocolTests` * SwiftTesting: `PeerIDTests` * SwiftTesting: `BLEServiceTests` * SwiftTesting: `CommandProcessorTests` * SwiftTesting: `GCSFilterTests` * SwiftTesting: `GeohashBookmarksStoreTests` * Remove `peerID` test constants * Remove PeerID + String interop from tests * Refactor IntegrationTests to extract state management * Refactor global state management of MockBLEService * NoiseProtocolSwiftTests: `actor` -> `struct` * Remove measurement tests w/ no benchmark * `NoiseProtocolSwiftTests` -> `NoiseProtocolTests` * SwiftTesting: `LocationChannelsTests` * SwiftTesting: `GossipSyncManagerTests` * SwiftTesting: `LocationNotesManagerTests` * Global `sleep` function for tests * SwiftTesting: `IntegrationTests` --------- Co-authored-by: jack <jackjackbits@users.noreply.github.com> |
||
|
|
ad4103bacc |
Refactor Nostr ID Bridge & Keychain Helper (#796)
* Extract each type to a separate file * Nostr ID Bridge: Convert static func/vars to instance * `KeychainHelper` behind a protocol to easily mock * Update tests with ID Bridge and MockKeychainHelper --------- Co-authored-by: jack <212554440+jackjackbits@users.noreply.github.com> |
||
|
|
d371131ad5 |
Remove MockBluetoothMeshService (#777)
|
||
|
|
551a843691 |
PeerID 18/n: BitchatDelegate + Tests (#769)
|
||
|
|
eb13eec4c5 |
Swift Testing (#748)
* Swift Testing: `PrivateChatE2ETests` + minor refactor * Swift Testing: `PublicChatE2ETests` * Swift Testing: `FragmentationTests` * Fix MockBLEService init to accept PeerID and remove _testRegister call * Add peerID property to MockBLEService and fix ttlDecrement test * Remove duplicate peerID property and fix type comparisons --------- Co-authored-by: jack <jackjackbits@users.noreply.github.com> |
||
|
|
5f44c56a90 | PeerID 15/n: Bitchat Message & Packet accept in init (#754) | ||
|
|
03c357f048 |
PeerID 11/n: Noise types use PeerID + create separate files (#750)
* Noise types use PeerID * Fix tests * Extract `NoiseSessionManager` into a separate file * Extract `NoiseSessionState` into a separate file * Remove `failed` state from `NoiseSessionState` * Extract `NoiseSessionError` into a separate file |
||
|
|
bdc31d3be3 | Don’t auto-register mock BLE services on creation (#746) | ||
|
|
e4ec2ef3fe | PeerID 5/n: Ephemeral and Secure Identities (#742) | ||
|
|
ea8d51a36b |
Refactor: BitchatMessage (#610)
* Extract BitchatMessage into a separate file * Convert `fromBinaryPayload` to `convenience init?` * Extract message dedup into an extension * Remove dead `formatMessageContent` * Minor refactor of timestamp and username formatting * Remove dead `getSenderColor` |
||
|
|
920dc31795 |
Refactor: Testable Keychain and Identity Manager (#584)
* Make static functions instance functions to be testable * Injectable KeychainManager + Mock + updated tests * Remove `pendingActions` from identity manager (dead code) * Remove `getHandshakeState` from identity manager (dead code) * Remove `getAllSocialIdentities` from identity manager (dead code) * Remove `getCryptographicIdentity` from identity manager (dead code) * Remove `resolveIdentity` from identity manager (dead code) * Identity Manager: minor clean up * Put Identity Manager behind a protocol * Remove Keychain and Identity Manager singletons * Tests: include MockKeychain/MockIdentityManager in project; init identityManager in CommandProcessorTests --------- Co-authored-by: jack <jackjackbits@users.noreply.github.com> |
||
|
|
e72fe50ffa |
Perf: Add final to classes that are not inherited (#574)
|
||
|
|
a30b73dd99 |
Feature/fragmentation fixes (#453)
* Fix fragmentation + BLE long-write + padding - Accumulate CBATTRequest long writes by offset and decode once per central - Decode original packet after fragment reassembly (preserve flags/compression) - Switch MessagePadding to strict PKCS#7 and validate before unpadding - Make BinaryProtocol.decode robust: try raw first, then unpad fallback - Unpad frames before BLE notify; fragment when exceeding centrals' max update length - Skip notify path when max update length < 21 bytes (protocol minimum) Verified large PMs and announces route without decode errors and peers show reliably. * Tests: fix weak delegate lifetime, legacy constants, and unused vars; fragment unpadded frames - BLEServiceTests: hold strong reference to MockBitchatDelegate - IntegrationTests: fix inline comment braces; replace removed types with test-safe values; use numeric 0x06 for legacy handshake resp checks - BinaryProtocolTests: remove unused minResult variable - BLEService: fragment the unpadded frame so fragments are efficient * tests: add Noise rehandshake recovery test; document in-memory test bus and autoFlood; stabilize large-network broadcast * tests: align suite with current behavior (compression, padding, routing, nonce); deflake and stabilize --------- Co-authored-by: jack <jackjackbits@users.noreply.github.com> |
||
|
|
7836daa6d3 |
Adds O(1) method for peer nickname retrieval (#450)
* Adds O(1) method for peerNickname retrieval * Uses peerNickname method where possible |
||
|
|
6fbf7eee25 |
Refactor/ble nostr boundaries (#449)
* Refactor: move Nostr embedding and TLVs out of BLE; add NostrEmbeddedBitChat, Packets, PeerIDUtils; centralize MessageDeduplicator; update ChatViewModel to use new helpers; remove AI_CONTEXT.md and CLAUDE.md * Rename class to BLEService with compatibility alias; move mention parsing out of BLE; emit low-level BLE events to delegate; unify hex helpers; accept 64-hex in isPeerConnected; add PeerIDResolver * Project: rename file to BLEService.swift and update Xcode project; keep typealias SimplifiedBluetoothService = BLEService for compatibility * Remove SimplifiedBluetoothService alias; update app code to use BLEService explicitly * Tests: rename MockSimplifiedBluetoothService to MockBLEService; update typealiases and Xcode project * Docs: update comments to refer to BLEService (tests, protocol, noise service) * Tests: rename SimplifiedBluetoothServiceTests to BLEServiceTests; update project references and class names * Introduce Transport protocol; BLEService conforms; document delegate-only event pattern in BLEService; keep publishers internal for UnifiedPeerService * Adopt Transport end-to-end: add TransportPeerSnapshot + publishers; BLEService maps to Transport snapshots; UnifiedPeerService consumes Transport; ChatViewModel holds Transport * Fix Transport integration: replace getPeerFingerprint with getFingerprint(for:); update PrivateChatManager and CommandProcessor to use Transport; add BLEService.getFingerprint(for:); update PeerManager to use Transport * Refactor transport and BLE/Nostr layers; unify UI events; fix MainActor isolation - Rename SimplifiedBluetoothService to BLEService and slim responsibilities - Introduce Transport protocol and peerEventsDelegate for UI updates - Add NostrTransport and MessageRouter to route PM/read/favorite via BLE or Nostr - Centralize TLVs, PeerID utils, and MessageDeduplicator outside BLE - Update UnifiedPeerService and ChatViewModel to use Transport and delegate events - Fix MainActor isolation: route delegate calls via Task on MainActor; update notifyUI helper - Adjust related files and tests accordingly * BLEService: remove internal publishers; switch to delegate-only events - Drop legacy messages/peers/fullPeers publishers - Provide lightweight peerSnapshotSubject only to satisfy Transport - Rework publishFullPeerData to build snapshots from internal state and notify delegate + subject - Remove all peersPublisher.send call sites - Keep UnifiedPeerService on delegate updates exclusively * Remove inlined Nostr send helpers from ChatViewModel; route via MessageRouter - Replace direct Nostr sends (PM, ACKs, favorites) with MessageRouter - Add router method for delivery ACKs and implement NostrTransport.sendDeliveryAck - Simplify ChatViewModel favorite notification path to use router - Keep Nostr receive handling intact; reduce duplication * Fix ReadReceipt initializer usage in ChatViewModel (readerID + readerNickname) * Fix unused variable warning: replace shadowed 'nostrPubkey' bind with boolean check in ChatViewModel * Fix queued PM format: use TLV for pending messages after Noise handshake - Pending messages (including first-time favorite notifications) now use the same TLV encoding as normal sends - Ensures ChatViewModel can decode on first send, even if handshake completes after queuing --------- Co-authored-by: jack <jackjackbits@users.noreply.github.com> |
||
|
|
845ffc601b |
Refactor/robustness (#446)
* Refactor BitChat for improved robustness and performance Major refactoring to simplify architecture and fix critical issues: Architecture Improvements: - Replace complex BluetoothMeshService with simplified SimplifiedBluetoothService - Consolidate message routing and peer management into unified services - Remove redundant caching layers and optimize performance Bug Fixes: - Fix critical BLE peer mapping corruption in mesh networks - Fix encrypted message routing failures in multi-peer scenarios - Fix app freezes and Main Thread Checker warnings - Fix BLE message delivery in dual-role connections - Fix favorite toggle UI not updating instantly - Fix Nostr offline messaging with 24-hour message filtering Features: - Add command processor for chat commands - Add autocomplete service for mentions and commands - Improve private chat management with dedicated service - Add unified peer service for consistent state management Performance: - Optimize BLE reconnection speed - Reduce excessive logging throughout codebase - Improve message deduplication efficiency - Optimize UI updates and state management * Improvements refactor robust (#441) * remove unused code * remove more * TLV for announcement * restore * restore * restore? * messages tlv too (#442) * Fix Nostr notification and read receipt issues, add TLV encoding, cleanup unused code ## Notification & Read Receipt Fixes - Fixed toolbar notification icon appearing incorrectly on app restart for already-read messages - Fixed read receipts being incorrectly deleted on startup when privateChats was empty - Fixed messages not being marked as read when opening chat for first time - Fixed senderPeerID not being updated during message consolidation - Added startup phase logic to block old messages (>30s) while allowing recent ones - Fixed unread status checking across all storage locations (ephemeral, stable Noise keys, temporary Nostr IDs) ## TLV Encoding Implementation - Implemented Type-Length-Value encoding for private message payloads - Added PrivateMessagePacket struct with TLV encode/decode methods - Enhanced message structure for better extensibility and robustness ## Code Cleanup - Removed unused PeerStateManager class and related dependencies - Removed dead protocol types (DeliveryAck, ProtocolAck/Nack, NoiseIdentityAnnouncement) - Cleaned up BitchatDelegate by removing unused methods - Removed excessive debug logging throughout ChatViewModel - Added test-only ProtocolNack helper for integration tests ## Technical Details - Messages stored under three ID types: ephemeral peer IDs, stable Noise key hexes, temporary Nostr IDs - Fixed cleanupOldReadReceipts() to skip when privateChats is empty or during startup - Updated message consolidation to properly update senderPeerID - Restored NIP-17 timestamp randomization (±15 minutes) for privacy --------- Co-authored-by: jack <jackjackbits@users.noreply.github.com> Co-authored-by: callebtc <93376500+callebtc@users.noreply.github.com> |
||
|
|
f4b8168ef9 |
Remove dead code and simplify codebase
- Remove unused BinaryEncodable protocol and BinaryMessageType enum - Delete MockNoiseSession.swift (never used in tests) - Remove all relay detection code (hardcoded to false) - Removed isRelayConnected property from BitchatPeer - Removed relayConnected case from ConnectionState enum - Cleaned up relay-related UI indicators in ContentView - Removed relay status checks from ChatViewModel - Simplified peer connection logic by removing relay layer Total: 169 lines removed across 5 files |
||
|
|
a97d5c2d5e |
Implement Nostr NIP-17 for offline messaging and performance optimizations (#358)
* Implement Nostr NIP-17 integration for offline mutual favorite messaging - Add Nostr relay connectivity and NIP-17 gift-wrapped private messages - Implement dual transport system: Bluetooth mesh + Nostr relays - Add favorites persistence with mutual detection and Nostr key exchange - Support offline messaging for mutual favorites via Nostr relays - Handle peer identity rotation with automatic favorite key updates - Fix UI to show all favorites (online and offline) in peer list - Add proper message routing based on peer availability - Update peer list icons: 📶 for mesh, 🌐 for Nostr, 🌙 for one-sided - Fix toolbar display for offline peers in private chat view - Add network entitlements for macOS and iOS - Implement automatic noise key updates when peers reconnect * Implement Nostr NIP-17 for private messaging between mutual favorites - Add support for NIP-17 gift-wrapped private messages with double encryption - Enable private messaging via Nostr when mutual favorites are offline - Fix peer reconnection issues: users now stay in private chat when peer reconnects - Fix read receipt delivery: send pending receipts when peer comes back online - Add message ID tracking through Nostr transport for proper delivery acknowledgments - Update peer noise key mapping when peers reconnect with different IDs - Check for Nostr messages when app becomes active - Implement 7-day message retrieval window for better reliability * Fix private message UI refresh and adjust PEOPLE header spacing - Fix UI not updating when receiving private messages on mesh - Add immediate batch processing for messages in active chat - Force UI update when viewing current chat peer - Ensure real-time message display without navigation - Reduce PEOPLE header spacing from 16 to 12 points for tighter UI * Fix build errors and unused value warnings in Nostr favorites integration * Implement read receipts via Nostr - Added sendReadReceipt method to MessageRouter to send receipts via mesh or Nostr - Added handleReadReceipt to process incoming read receipts from Nostr - Made ReadReceipt.readerID mutable to allow updates - Added missing notification names and error cases - Uncommented and enabled read receipt handling in ChatViewModel - Read receipts now work seamlessly via both mesh and Nostr transports * Fix ReadReceipt initialization - use correct constructor * Implement persistent message deduplication for Nostr - Added ProcessedMessagesService to track messages across app restarts - Store processed message IDs and last timestamp in UserDefaults - Skip already processed messages when receiving from Nostr - Adjust subscription filter to use smart timestamp (last processed or 24h) - Prevents duplicate messages when reconnecting to Nostr relays * Fix peer list UI not updating to Nostr mode on disconnect - Remove peer from peerNicknames when connection state changes to disconnected - Ensures UI properly reflects peer disconnection state - Peer list now correctly shows Nostr mode (🌐) when peer walks out of range * Update peer count to include Nostr peers and improve UI indicators - Peer count now shows total peers including those available via Nostr - Count appears purple when only Nostr peers are connected - Private message header shows purple globe icon for Nostr transport - Consistent visual language for Nostr connectivity across the app * Improve RSSI real-time updates and fix UI flashing - Reduce RSSI update timer from 10s to 5s and per-peripheral from 5s to 3s - Add RSSI change detection with 2 dBm threshold for responsive updates - Always update previous RSSI values to fix gradual change detection bug - Trigger RSSI read on peer authentication for immediate status - Fix UI flashing 'nobody around' by removing array clearing on updates - Add proper cleanup of RSSI tracking on disconnect and peer rotation * Fix favorite nickname updates and peer list filtering - Add updateNickname method to FavoritesPersistenceService to update nicknames while preserving favorite status - Update announce handler to check for existing favorites and update their nicknames - Remove dead BluetoothMeshService+PublicAPI.swift file - Move sendFavoriteNotification to main BluetoothMeshService - Fix peer list to only show connected peers and user's favorites (not peers who favorite the user) - Remove UI logic for showing peers who favorite us but we don't favorite back * Remove dead code and fix ghost connections Phase 1 - Remove abandoned peer ID rotation code: - Remove previousPeerID property and rotationGracePeriod constant - Remove grace period logic from isPeerIDOurs() - Remove previousPeerID handling from announce packets - Pass nil for previousPeerID in identity announcements Phase 2 - Fix ghost connections from relayed packets: - CRITICAL FIX: Only add peers to activePeers if they have a peripheral connection - Check for peripheral connection before marking peer as active - Prevents ghost connections when announce packets are relayed - Log warning when rejecting relayed announce without peripheral Phase 3 - Begin consolidating redundant peer tracking: - Create new PeerSession class to unify peer data in one place - Add helper methods for PeerSession management - Integrate PeerSession into announce packet handling - Update authentication state changes to use PeerSession - Update peripheral mapping and RSSI to sync with PeerSession - Update disconnect and leave handling to update PeerSession - Add consolidated getter methods for peer info This fixes the issue where peers appeared connected without actually having a Bluetooth connection, and begins the migration to a cleaner single-source-of-truth peer tracking system. * Fix multiple connect messages on peer restart - Move hasPeripheralConnection check outside sync block to fix scope issue - Add debug logging to track connect message conditions - Ensure connect messages only show on first connection or reconnection with peripheral * Optimize RSSI updates for better battery life - Add app state tracking to BluetoothMeshService - Only update RSSI when app is in foreground and peer list is visible - Add setPeerListVisible method to control RSSI updates - Remove individual periodic RSSI updates in favor of centralized timer - Update ContentView to notify mesh service of peer list visibility changes - Improve battery efficiency by avoiding unnecessary RSSI reads * Initialize peer list visibility state on view appear - Ensure RSSI timer state is properly initialized when view loads - Call setPeerListVisible with initial showSidebar value * Fix duplicate peers and multiple disconnect messages - Fixed duplicate peer entries when relay-connected by adding relay-connected peers to connectedNicknames set - Added deduplication logic for disconnect messages with 2-second window to prevent multiple disconnect notifications for same peer - Added cleanup for old disconnect notification tracking to prevent memory growth * Fix peer count indicator color logic - Show green for any mesh peer (direct Bluetooth or relay connected) - Show purple only for Nostr-only peers (no mesh connections) - Show red only when no peers are reachable at all - Fixed to use meshPeerCount instead of viewModel.isConnected which only checked direct connections * Fix relay connection issues and peripheral mapping cleanup - Fixed relay-connected peers being marked as directly connected when receiving identity announce - Added proper cleanup of temp peripheral mappings when discovering real peer ID - Fixed disconnect notification deduplication cleanup - Improved debug logging to show actual connection state (direct/relay/nostr/offline) - Added debug logging for relay connection detection - Fixed compiler warning about unused variable * Fix Unknown peer disconnect notifications and disable faulty relay detection - Add check to prevent disconnect notifications for Unknown peers that never announced - Disable relay connection detection until proper relay tracking is implemented - In a 2-peer network, peers should never show as relay-connected * Fix RSSI updates and peer visibility after reconnection - Add updatePeers() call in didUpdatePeerList to refresh RSSI values in UI - Track version hello times to better detect direct connections - Allow peers to be marked active if recent version hello received - Fix thread safety for version hello tracking - Clean up old version hello times to prevent memory leaks * Remove RSSI tracking completely and replace with radio icon for mesh connections * Fix build errors after RSSI removal - Add missing peripheralID declaration in didDiscover delegate method - Remove obsolete setPeerListVisible calls from ContentView * Center private message header elements using ZStack layout - Replace HStack with ZStack for perfect centering - Globe/nick/lock cluster now always centered regardless of button sizes - Back and favorite buttons positioned in overlay HStack * Fix private chat view showing Unknown when peer reconnects with new ID - Update FavoritesPersistenceService to notify with both old and new keys - Handle peer ID changes in ChatViewModel to migrate private chat data - Update selectedPrivateChatPeer when favorite's noise key changes - Maintain chat history and unread status across peer ID changes * Fix read receipts after peer reconnection and replace nos.lol relay - Updated sendReadReceipt to resolve current peer ID when peers reconnect with new IDs - Enhanced MessageRouter to check favorites for current noise keys - Replaced nos.lol relay with relay.snort.social to avoid PoW requirements * Fix message routing to use Nostr when peers are disconnected Changed message routing logic to check actual peer connection status using isPeerConnected() instead of just checking if peer exists in nickname list. This ensures that offline mutual favorites correctly route messages through Nostr instead of attempting Bluetooth handshakes. Also added safety check to prevent starting private chat with ourselves. * Add debug logging for Nostr timestamp randomization Added logging to track the random offset being applied to Nostr event timestamps to debug why messages appear 8-9 minutes in the future. * Fix Nostr timestamp issue by reducing randomization range Temporarily reduced the timestamp randomization from +/-15 minutes to +/-1 minute to address messages appearing 8-9 minutes in the future. Added detailed UTC/local time logging to help debug the issue. The random offset should have been evenly distributed but was consistently showing positive offsets. This change mitigates the issue while we investigate the root cause. * Fix message routing for offline favorites and reduce Nostr timestamp randomization - Fix transport selection to properly detect disconnected peers using isPeerConnected() - Change from checking peer nicknames to checking actual connection status - Reduce Nostr timestamp randomization from ±15 minutes to ±1 minute - Add detailed timestamp logging for debugging * Improve PM header UI and encryption status display - Show transport icons (radio/link/globe) in PM header matching peer list - Always show lock icon if noise session ever established (no handshake icon) - Change verified icon from shield to checkmark seal - Use consistent green color (textColor) for PM header and encryption icons * Update AI_CONTEXT.md with comprehensive Nostr implementation details - Add Nostr and MessageRouter to architecture diagram - Document NIP-17 gift wrap implementation - Explain favorites integration and mutual requirement - Detail message routing logic and transport selection - Add security considerations and debugging tips - Update common tasks with Nostr-specific guidance * Fix data consistency issues in favorites, chat migration, and bloom filter - Fix favorites deduplication to use public key instead of nickname Prevents losing favorites when multiple peers use same nickname - Fix private chat migration to use fingerprints instead of nicknames Prevents merging unrelated conversations that share nicknames Fallback to nickname matching only for legacy data without fingerprints - Fix bloom filter reset to preserve messages from last 10 minutes Prevents duplicate message processing after bloom filter resets Keeps processedMessages for 10 minutes while bloom filter resets every 5 * Add mutual favorites internet messaging to app info * Remove excessive debug/info logging for production readiness - Removed ~140 debug/info level logs across core services - Preserved critical logs: errors, warnings, security events, state changes - Kept logs for: peer join/leave, favorite status, mutual relationships - Cleaned up verbose logging in: Bluetooth mesh, Nostr, message routing - Improved performance by reducing log I/O overhead 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> * Implement performance optimizations and fix build warnings - Add UI update debouncing (50ms) to prevent excessive SwiftUI refreshes - Implement memory bounds for processedMessages with LRU eviction - Add encryption queue cleanup for disconnected peers - Optimize peer lookups from O(n) to O(1) with indexed dictionary - Fix multiple compiler warnings (unused variables, missing break statements) - Optimize peer counting with single-pass reduce operation - Fix ViewBuilder control flow issue in ContentView - Fix Dictionary initialization type mismatches with Array wrapper * Add TTL-based cleanup for Noise handshake sessions - Add session TTL (5 minutes) and max session limit (50) to NoiseHandshakeCoordinator - Clean up old established sessions to prevent unbounded memory growth - Move handshake cleanup timer out of DEBUG conditional for production use - Run cleanup every 60 seconds in production (vs 30s in debug) - Clean up crypto state immediately on peer disconnect - Prevents memory leaks from accumulating Noise sessions * Pre-compute and store fingerprints in PeerSession for O(1) lookups - Store fingerprint in PeerSession when peer authenticates - Update getPeerFingerprint() and getFingerprint() to check PeerSession first - Replace all noiseService.getPeerFingerprint() calls with optimized version - Eliminates repeated SHA256 calculations during message processing - Improves performance for favorite checks and encryption status updates * Implement exponential backoff for Nostr relay connections - Add reconnection tracking fields to Relay struct (attempts, timing) - Replace fixed 5-second delay with exponential backoff (1s → 2s → 4s... max 5min) - Stop reconnection attempts after 10 failures to prevent infinite retries - Reset attempt counter on successful connection - Add utility methods: retryConnection(), getRelayStatuses(), resetAllConnections() - DNS failures still bypass retry logic as before - Improves battery life and reduces server load from constant reconnection attempts --------- Co-authored-by: jack <jackjackbits@users.noreply.github.com> Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
492f90edd5 |
Optimize BLE mesh network for robustness and range (#314)
- Fixed relay probability calculation for 2-node networks (0% relay needed) - Added protocol ACKs to prevent unnecessary retransmissions - Implemented MessageState for enhanced duplicate detection - Added exponential backoff for collision avoidance - Fixed duplicate sends for bidirectional connections - Resolved packet ID generation issues using immutable fields only - Implemented smart rate limiting with progressive throttling - Removed unnecessary debug logging and fixed build warnings - Optimized message routing to prevent flooding in small networks Co-authored-by: jack <jackjackbits@users.noreply.github.com> |
||
|
|
847d333366 |
Fix all compilation errors and warnings in test suite
- Fixed mock service property overrides to match base class properties - Added missing CryptoKit imports where needed - Fixed immutable property assignments by creating new instances - Replaced XCTAssertThrows with XCTAssertThrowsError - Fixed DeliveryAck serialization method names (serialize -> encode) - Fixed unused variable warnings - Ensured all BitchatPacket modifications create new instances - Fixed BitchatMessage property mutations by creating new instances All test targets now build successfully for both iOS and macOS platforms. |
||
|
|
96136ec364 |
Add comprehensive test suite for bitchat
- Created test utilities and helpers for common test operations - Implemented Binary Protocol tests covering encoding/decoding, compression, and padding - Added Noise Protocol tests for handshake, encryption, and session management - Created Public Chat E2E tests for broadcasting, routing, TTL, and mesh topologies - Implemented Private Chat E2E tests for direct messaging, delivery ACKs, and retry logic - Added Integration tests for multi-peer scenarios, network resilience, and mixed traffic patterns - Created mock implementations for BluetoothMeshService and NoiseSession Test coverage includes: - Protocol layer (binary encoding, message serialization) - Security layer (Noise handshake, encryption/decryption) - Application layer (public/private messaging, delivery tracking) - Network scenarios (mesh topology, partitions, churn) - Performance and stress testing |