mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-26 15:25:20 +00:00
16324c819f745381afab794bda0ec61d9119f87b
8
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
020de96519 |
Mesh bridging: stitch nearby mesh islands over the internet, courier drops, unified settings (#1412)
* Mesh bridging: stitch nearby mesh islands over Nostr, courier drops, settings surface Three features plus a UI consolidation, all opt-in behind a new Bridge toggle: Channel bridge: while bridging, outgoing public mesh messages are also signed (with a derived, unlinkable per-cell Nostr identity) as kind-20000 events tagged #r with the local geohash-6 cell and published to the cell's deterministic geo relays; mesh-only peers deposit via new toBridge/fromBridge carrier directions through a bridge gateway (bridge + gateway toggles). Remote islands' events render into the mesh timeline marked with a network glyph. Events carry the original mesh message ID so the store's insert-by-ID absorbs radio/bridge duplicates in either order. Loop prevention mirrors GatewayService (three BoundedIDSet caches + skip-if-seen-locally + budgets). Nothing crosses a bridge unless its author signed it for the bridge; a per-message "nearby only" composer toggle keeps a message radio-only. Courier over the bridge: sealed courier envelopes park on default relays as kind-1401 drops tagged #x with their day-rotating recipient tag (NIP-40 expiry), signed by per-drop throwaway keys. Recipients subscribe for their own candidate tags; bridge gateways watch verified local peers' tags and hand matching drops over as directed courier packets. DM delivery to known peers stops requiring a physical courier encounter; the Noise-X seal never opens in transit. Presence: kind-20001 heartbeats on the rendezvous feed a "people across the bridge" count in the header (approximate: local participants subtracted by radio-copy attribution). Settings/Info: AppInfoView is now a segmented Settings/Info sheet. Settings hosts appearance, voice (fixes the duplicated Voice section), a Connectivity section (bridge + gateway + Tor toggles, the latter two moved out of the location sheet), and a confirmed panic-wipe button. New announce TLV 0x06 advertises the gateway's rendezvous cell; PeerCapabilities gains .bridge. i18n: 23 new keys across all 29 locales; coverage tests green. Tests: 50 new app tests + 3 BitFoundation tests; full suite 1445 green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Settings polish: one toggle style, sticky Info-first tab, location access in Settings - The live-voice toggle now uses the same settings card + IRC pill as the connectivity toggles (settingToggle, renamed from connectivityToggle). - Segmented control orders Info first; the selected pane persists across opens (AppStorage), so first-ever open lands on Info and afterwards the sheet reopens where it was left. - "remove location access" moved from the channels sheet into the Settings Connectivity section (same key, still deep-links to system settings). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Location UX + copy: state-aware access control, honest empty state, clearer bridge/gateway text - Settings' location control now covers all three permission states: grant (real prompt, only possible while never-asked), open-system-settings when denied, remove-access when granted. The channels sheet keeps its own grant path for people who start there. - The channels list no longer spins forever without permission; it shows "grant location access to find nearby channels" instead (new key, 29 locales). - Bridge and gateway subtitles rewritten for clarity; the gateway subtitle moved to a new key since it now carries bridge traffic, and the old geohash-only key is deleted. The word "user" is banned from copy in every locale ("this person is blocked"). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Field-test fixes: dedupe relay-connectivity triggers, throttle presence, log bridge decisions First on-device run confirmed publishes accepted and the subscription delivering, but exposed trigger spam: NostrRelayManager's isConnected re-emits per relay recompute, so presence published 5x/second and the courier-drop subscription rebuilt 6x in 300ms. removeDuplicates() on the sinks + a 30s presence throttle (same-second heartbeats are byte-identical events anyway). Also: injection/skip/downlink now log under 🌉 so field verification is observable — the first test looked silent precisely because dedup correctly suppressed same-island bridged copies. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Fix restart self-echo: recognize own rendezvous events by derived pubkey Second field run proved bidirectional bridging live (~1-2s Mac<->iPhone via Tor) but caught a bug: relay backfill after an app relaunch re-delivered the device's own pre-restart events, and with the in-memory published-ID cache wiped they rendered as bridged copies of your own messages. The rendezvous identity is deterministically derived per cell, so self-recognition by pubkey needs no cache and survives restarts; own events are also marked never-downlink. Regression test simulates the fresh-launch state. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * People sheet: mesh, bridge, and groups in one list The header's bridged count had no matching faces anywhere — the people sheet only knew mesh peers. BridgeService now publishes named participants (nickname from message tags, geohash-style #last4 disambiguation, presence keeps a known name alive) and the mesh people sheet gains an "across the bridge" section between mesh peers and groups. Display-only rows in v1 (bridged identities have no DM route yet). Two new catalog keys x29 locales; also normalizes one out-of-sort-order entry inherited from a hand-edited key on main. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Header: one people icon, one count, one sheet Fold the bridged-people count into the main person.2.fill count instead of a second network-glyph counter; the merged people sheet (mesh / across the bridge / groups) is the breakdown. VoiceOver still announces how many of the total are across the bridge. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * People sheet symmetry: #mesh section header, no active count Every section now gets the same glyph+label header shape (shared PeopleSectionHeader): #mesh over the peer list, across-the-bridge over bridged people. The "N active" line is gone (mesh); location channels keep their geohash subtitle. Dead subtitle/count helpers removed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * One switch: the bridge toggle drives all internet sharing Field feedback: two toggles (bridge + gateway) with an invisible dependency was a trap — bridged messages silently never reached mesh-only neighbors unless a second lever was found and flipped. Collapsed to a single switch that does the right thing for your situation: - Bridge ON + internet: your messages cross, you see the bridge, AND your device serves its island — accepts toBridge deposits, carries remote messages onto the radio, watches courier drops for verified local peers, advertises the cell, and runs the geohash-channel gateway. - Bridge ON, no internet: you ride whoever nearby is serving. - Bridge OFF: nothing of yours crosses; radio reception of bridged traffic stays passive and free. With every online bridger serving, downlink gets a 0.2-1.5s jittered holdoff + send-time suppression recheck so co-located gateways don't burn duplicate airtime (two-gateway test included). The internet-gateway card is gone from Settings (GatewayService now follows the bridge switch, with launch-time migration); its orphaned catalog keys deleted and the bridge subtitle broadened across all 29 locales. Also: MeshPeerList's empty state ("nobody around...") aligned to the section row rhythm. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Bridge pumps its own location; fix centered people sheet; stop drop-subscription churn Field session 3 found the bridge silently cell-less: it read availableChannels passively, which only flow while some other feature (channels sheet, location notes, geo sampling) happens to pump location — turn those off and the bridge never gets a rendezvous. BridgeService now requests a one-shot fix whenever it's enabled without a cell (and piggybacks one on the presence timer so moving devices migrate cells). Also from the session: the people sheet's scroll content hugged its widest child and got centered on iPhone when the list was empty — pinned to full width, leading. And the courier-drop subscription rebuilt every ~60s on verified announces despite an unchanged tag set — now resubscribes only when the tags actually change. (Also merges origin/main: keychain test isolation #1413.) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Fix launch race: bridge reacts to the permission callback, retries cell-less Field session 4: the launch-time location request ran before the CoreLocation authorization callback delivered, so refreshChannels() silently no-opped (it requires .authorized) and nothing ever retried — the bridge stayed cell-less all session. Three layers now close it: - a $permissionState sink re-enters refreshRendezvous the moment authorization resolves (the fast path), - the maintenance timer arms even without a cell and retries the full rendezvous refresh (the backstop; it previously required a cell, which made it useless for exactly this failure), - flipping the bridge switch while never-asked triggers the location prompt — that's the user-initiated moment for it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * People sheet: one header style for every section; bridged people visible while own bridge is off GroupChatList's header now uses the shared PeopleSectionHeader (glyph + label, same size/padding as #mesh and across-the-bridge; keeps its key and header trait). Bridge section and the header count are no longer gated on this device's own toggle: bridged people arrive over passive radio from a serving neighbor, and whoever is visible in the timeline belongs in the sheet and the count. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * People sheet: equalize the first section's gap MeshPeerList's first row kept a legacy 10pt top bump from when nothing sat above it, and the outer VStack's 6pt inter-child spacing applied between the #mesh header and the list but not inside the other sections. Both gone: sections own their rhythm (header 12/4, rows 4), spacing 0 outside. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * People sheet: rows drop their leading glyphs — the section header carries the type Mesh rows lose the per-state transport icon (connected/relayed/nostr/ offline), bridge rows the network glyph, group rows the person.3 icon. Trailing state badges (star, lock, verified, unread, blocked, crown) stay, and the row accessibility description still announces connection state, so VoiceOver loses nothing. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Remove dead code left behind by the Settings|Info consolidation Periphery (--strict) flagged five leftovers on this branch: - LocationChannelsModel.setGatewayEnabled: the standalone internet-gateway toggle is gone (the bridge switch drives internet sharing), so nothing calls it; the gatewayEnabled published property stays for the header dot. - AppInfoView Strings.Location title/enable/openSettings: the old Location section's header and permission buttons no longer exist. Their orphaned Localizable.xcstrings entries go with them (the gateway-toggle keys were already pruned). - BridgePeopleList's appTheme environment value was never read. The sixth CI finding (PrekeyBundleStore.StoredBundle.noiseKey assign-only) is a Periphery flake: the property is read in loadFromDisk, the finding didn't reproduce locally or on the next CI run of unchanged code. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * People sheet: mesh rows get their transport glyph back The mesh section is the one heterogeneous list — the leading icon encodes HOW a peer is reachable (radio / relayed / nostr-only / offline), which the header can't say. Bridge and group rows stay glyph-free. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Fix courier drop skipped by stale BLE reachability; periodic deposit sweep Field test: a DM sent seconds after the recipient's radio vanished still saw them as "reachable" (60s verified retention), so canDeliverPromptly held, every deposit was skipped, and the message sat spooled with no retry path. MessageRouter now sweeps its outbox every 2 minutes and publishes bridge drops for messages whose recipient no transport can promptly reach; the drop layer's message-ID dedup makes the sweep idempotent. Regression test reproduces the exact field sequence. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Show "carried" when a message ships as a bridge drop Field feedback: dropped DMs delivered but the sender saw nothing — the drop path never fired onMessageCarried, and the recipient's delivery ack has no radio route back until the peers next share a transport. depositDrop now reports whether a fresh drop was sealed and the router marks the message carried (📦) on both the send path and the sweep; the ack still upgrades it to delivered whenever a route exists. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Fix locale word order: offline tag after name in DM header; localized "ago" The private-chat header rendered the localized offline word in the availability-glyph slot, before the name — "sin conexión bob". Offline now shows the same dimmed person glyph the mesh list uses, with the word as a small trailing tag after the name and lock, so it reads correctly in every locale. Full sweep of views found one more composition bug: notice timestamps glued English "ago" onto a localized duration; now the whole phrase comes from RelativeDateTimeFormatter (same as the "fades" label). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * DM header offline state: icon only The availability slot now reads uniformly as a glyph (radio / relayed / globe / dimmed person), matching the mesh list; the text tag is gone. VoiceOver still announces "offline" via the glyph's accessibility label. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Offline glyph: slashed antenna instead of dimmed person Offline is now the visual negation of connected (same antenna glyph, slashed) in both the DM header and mesh list rows; a generic person icon didn't say "unreachable". 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> |
||
|
|
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> |
||
|
|
ca63893197 |
Fix security audit findings: 3 critical, 7 high
A broad audit surfaced ten critical/high issues across the crypto, transport, identity, and panic-wipe layers. This fixes all ten. Critical: - Nostr DMs were unauthenticated. The NIP-17 seal was signed with a throwaway ephemeral key and the receiver never verified it, so anyone who knows a recipient's npub could forge messages (and delivery/read receipts) into an existing trusted conversation. The seal is now signed with the sender's real identity key, and the receiver verifies the seal signature and that seal.pubkey == rumor.pubkey. NOTE: this is a breaking wire-protocol change (see PR). - Public BLE messages trusted registry membership instead of the packet signature. Since senderID is attacker-controlled, any verified peer could be impersonated in public chat. A valid signature from the claimed sender is now required before any registry identity is used. - Unverified announces still persisted the announced identity, letting a replayed noisePublicKey overwrite a victim's stored signing key and nickname. persistIdentity is now gated on verification. High: - Noise decrypt trapped on a 16-19 byte ciphertext (negative prefix length after nonce extraction) — a remote crash. Now validated. - Identity-cache debounce save used Timer.scheduledTimer on a GCD queue with no run loop, so it never fired; block/verify/favorite changes only persisted on explicit forceSave. Replaced with a DispatchSourceTimer on the queue; forceSave is now serialized. - Identity-cache key load couldn't tell "missing" from a transient keychain failure and would regenerate (deleting) the key, orphaning the cache. Now uses getIdentityKeyWithResult and falls back to a session-only ephemeral key without clobbering the persisted key/cache. - BLE receive-dedup key lacked a payload digest, so post-handshake flushes (queued msgs + delivery/read acks in the same ms) were dropped as duplicates. Digest added, matching the ingress registry. - Maintenance timer was created only in init and never recreated after a panic stop/start, silently degrading the mesh until app restart. Now recreated in startServices. - Panic wipe left persisted location state (selected channel, teleport set, bookmarks) and cached per-geohash Nostr private keys behind. Both are now cleared. - Panic spawned an orphan NostrRelayManager instead of reusing .shared, splitting relay state from every other component. Now reuses .shared. Tests updated to assert the fixed behavior (announce no longer persists unverified identities; public messages require a signature; receive dedup ID includes the payload digest). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
c60eff2c11 | Move additional files/tests to BitFoundation (#1102) | ||
|
|
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> |
||
|
|
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> |
||
|
|
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> |