Compare commits

..
18 Commits
Author SHA1 Message Date
jack 3afacca7f6 fix(iOS App): resolve merge artifacts in scenePhase handler\n- Remove duplicate didEnterBackground state\n- Fix switch/if braces and logic for foreground restart gating 2025-09-11 18:36:15 +02:00
ddadccc554 Update bitchat/BitchatApp.swift
Co-authored-by: asmo <asmogo@protonmail.com>
2025-09-11 18:31:55 +02:00
74589539c3 Update bitchat/BitchatApp.swift
Co-authored-by: asmo <asmogo@protonmail.com>
2025-09-11 18:31:28 +02:00
860c06c1b1 Update BitchatApp.swift
Co-authored-by: asmo <asmogo@protonmail.com>
2025-09-11 16:42:21 +02:00
789bbf6862 Update BitchatApp.swift
Co-authored-by: asmo <asmogo@protonmail.com>
2025-09-11 16:42:13 +02:00
10be302854 Update BitchatApp.swift
Co-authored-by: asmo <asmogo@protonmail.com>
2025-09-11 16:42:04 +02:00
jack 4894a0b04e geo: disable background sampling + notifications\n- Gate sampling to foreground only (beginGeohashSampling, watchers)\n- Suppress geohash activity notifications unless app is active\n- Stop sampling explicitly on background scene phase 2025-09-11 13:35:36 +02:00
jack 36b5fdabc4 UI: scope Tor restart messages to geohash channels; skip initial foreground restart on cold launch to avoid confusing system message in #mesh 2025-09-11 13:29:17 +02:00
jack 5020190164 Always queue subscriptions and flush on connection; avoid immediate sends
- Prevents early send/cancel churn at launch and during reconnects
- If relays are already connected, flush immediately; otherwise pending until connected
2025-09-11 12:33:03 +02:00
jack b0f6568ff5 On launch, queue Nostr subscriptions without initiating connects; let centralized connect handle it
- In subscribe(), if no connections exist, just list relays and queue subs
- Avoids early send/cancel churn before connect() runs post-Tor-ready
2025-09-11 12:29:19 +02:00
jack f7b78452d0 Make Nostr connect single-sourced; defer DM subscription until connected
- Remove duplicate connect call from ChatViewModel; let scene-based flow connect
- Setup DM subscription once on first connection via  sink
- Reduce early subscription send/cancel noise after Tor restarts
2025-09-11 12:24:36 +02:00
jack ca10724e64 Gate Nostr init/subscription logs until Tor is ready
- ChatViewModel: await Tor readiness before initializing Nostr and logging
- Only log GeoDM subscription when Tor is ready to avoid early noise
2025-09-11 12:21:49 +02:00
jack 1a353b216d Defer Nostr setup logs until Tor is ready; fix subscribe coalescing and reconnect generation
- Move "Connecting to Nostr relays" log after awaitReady()
- Log "Queuing subscription" when Tor not ready; only coalesce when handler exists
- Clear coalescer on unsubscribe
- Cancel stale reconnect timers using connectionGeneration
- Remove app-level TorDidBecomeReady reset to avoid duplicate reconnects
- Debounce path-change restarts
2025-09-11 12:18:49 +02:00
jack 96992c0a59 Embed Tor via tor_api; deterministic restart + Nostr gating; add Tor notifications
- Run Tor via tor_api in a dedicated thread with OwningControllerFD
- Cleanly stop Tor on background; restart on .active (single instance)
- Avoid fallback to tor_main/dlopen; add is-running check to prevent duplicates
- Fix argv lifetime in C glue to avoid strcmp crash on start
- Gate Nostr connect/subscribe/send until Tor is fully ready
- Rebuild URLSession + reset relays after Tor readiness (scene-based)
- Remove TorDidBecomeReady double-reset and appDidBecomeActive resubscribe
- Add TorWillRestart/TorDidBecomeReady notifications and chat system messages
- Debounce path-change restarts; ACTIVE poke first; coalesce subs; cancel stale reconnect timers
- Project: add CTorHost.c and TorNotifications.swift to targets; fix libz.tbd path
2025-09-11 12:14:55 +02:00
jack 08d1fa7653 Relays: treat repeated -1011 handshake failures as permanent; skip reconnects
- Classify NSURLErrorBadServerResponse as permanent and stop retrying
- Filter permanently-failed relays from subscribe/connect attempts
- Avoid reconnect scheduling for permanently failed relays
2025-09-10 14:21:32 +02:00
jack 8dad90685d UI: show Tor system messages only in geohash channels (not mesh)
- Gate "starting tor..." and readiness/timeout messages to geohash view
- Add helper addGeohashOnlySystemMessage() to avoid posting to mesh timeline
- Persist system messages in geohash backing store via addPublicSystemMessage()
2025-09-10 13:57:53 +02:00
jack 7f829dcbaa iOS: deterministic Tor recovery + 100% gating; BLE-first; session rebuild
- Restart/wake Tor on foreground via ControlPort (ACTIVE/SHUTDOWN),
  avoid restarts during bootstrap; add NWPathMonitor to trigger checks
- Use NWConnection control polling for GETINFO; remove blocking CFStream
  readers to avoid QoS inversions; compute readiness from SOCKS + 100%
- Rebuild TorURLSession on resume; reset Nostr connections to rebind
- Gate all internet after full bootstrap; keep BLE mesh startup fast
- Fix Swift 6 capture issues; hop UI updates to @MainActor
- Remove Tor progress spam; persist initial "starting tor..." system message
2025-09-10 13:45:03 +02:00
jack 37d655d065 feat(tor): Tor-by-default scaffold and integration
- Add TorManager with static/dlopen start, torrc generation, SOCKS probe
- Add TorURLSession; route Nostr/Web fetches via SOCKS proxy
- Add chat system messages for Tor status; show progress (macOS) and ready
- Disable ControlPort bootstrap monitor on iOS; keep it on macOS
- Make Tor waits non-blocking; avoid main-actor stalls on startup
- Queue & flush Nostr subscriptions on relay connect; skip duplicates
- Always rewrite torrc at launch to fix iOS container path mismatches
- Link libz; add project wiring for tor-nolzma.xcframework
- Minor fixes: SOCKS probe resumeOnce guard, entitlement for network.server (macOS)
2025-09-07 13:52:07 +02:00
52 changed files with 781 additions and 714 deletions
-40
View File
@@ -1,40 +0,0 @@
name: Fetch GeoRelays Data
on:
schedule:
- cron: '0 6 * * 0'
workflow_dispatch:
permissions:
contents: write
jobs:
update-relay-data:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
token: ${{ secrets.GITHUB_TOKEN }}
- name: Fetch GeoRelays
run: |
wget https://raw.githubusercontent.com/permissionlesstech/georelays/refs/heads/main/nostr_relays.csv
mv nostr_relays.csv ./relays/online_relays_gps.csv
- name: Check for changes
id: git-check
run: |
git diff --exit-code || echo "changes=true" >> $GITHUB_OUTPUT
- name: Commit and push changes
if: steps.git-check.outputs.changes == 'true'
run: |
git config --local user.email "action@github.com"
git config --local user.name "GitHub Action"
git add relays/online_relays_gps.csv
git commit -m "Automated update of relay data - $(date -u)"
git push
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+3 -3
View File
@@ -37,7 +37,7 @@ This three-message pattern provides:
#### NoiseEncryptionService #### NoiseEncryptionService
The main service managing all Noise operations: The main service managing all Noise operations:
```swift ```swift
final class NoiseEncryptionService { class NoiseEncryptionService {
private let staticIdentityKey: Curve25519.KeyAgreement.PrivateKey private let staticIdentityKey: Curve25519.KeyAgreement.PrivateKey
private let sessionManager: NoiseSessionManager private let sessionManager: NoiseSessionManager
private let channelEncryption = NoiseChannelEncryption() private let channelEncryption = NoiseChannelEncryption()
@@ -47,7 +47,7 @@ final class NoiseEncryptionService {
#### NoiseSession #### NoiseSession
Individual session state for each peer: Individual session state for each peer:
```swift ```swift
final class NoiseSession { class NoiseSession {
private var handshakeState: NoiseHandshakeState? private var handshakeState: NoiseHandshakeState?
private var sendCipher: NoiseCipherState? private var sendCipher: NoiseCipherState?
private var receiveCipher: NoiseCipherState? private var receiveCipher: NoiseCipherState?
@@ -58,7 +58,7 @@ final class NoiseSession {
#### NoiseSessionManager #### NoiseSessionManager
Thread-safe session management: Thread-safe session management:
```swift ```swift
final class NoiseSessionManager { class NoiseSessionManager {
private var sessions: [String: NoiseSession] = [:] private var sessions: [String: NoiseSession] = [:]
private let sessionsQueue = DispatchQueue(label: "noise.sessions", attributes: .concurrent) private let sessionsQueue = DispatchQueue(label: "noise.sessions", attributes: .concurrent)
} }
+7 -19
View File
@@ -39,8 +39,10 @@
0481A3592E6D929E00FC845E /* tor-nolzma.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0481A3572E6D929E00FC845E /* tor-nolzma.xcframework */; }; 0481A3592E6D929E00FC845E /* tor-nolzma.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0481A3572E6D929E00FC845E /* tor-nolzma.xcframework */; };
0481A35B2E6D9BEF00FC845E /* libz.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 0481A35A2E6D9BEF00FC845E /* libz.tbd */; }; 0481A35B2E6D9BEF00FC845E /* libz.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 0481A35A2E6D9BEF00FC845E /* libz.tbd */; };
0481A35D2E6DA18600FC845E /* libz.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 0481A35C2E6DA18600FC845E /* libz.tbd */; }; 0481A35D2E6DA18600FC845E /* libz.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 0481A35C2E6DA18600FC845E /* libz.tbd */; };
0481A3902E734CAE00FC845E /* CommandProcessorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0481A38F2E734CAE00FC845E /* CommandProcessorTests.swift */; }; 0C0EFA112E6EAAAA00ABCDEF /* CTorHost.c in Sources */ = {isa = PBXBuildFile; fileRef = 0C0EFA102E6EAAAA00ABCDEF /* CTorHost.c */; };
0481A3912E734CAE00FC845E /* CommandProcessorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0481A38F2E734CAE00FC845E /* CommandProcessorTests.swift */; }; 0C0EFA122E6EAAAA00ABCDF0 /* CTorHost.c in Sources */ = {isa = PBXBuildFile; fileRef = 0C0EFA102E6EAAAA00ABCDEF /* CTorHost.c */; };
0C0EFA162E6EAABB00ABCDF4 /* TorNotifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C0EFA132E6EAABB00ABCDF1 /* TorNotifications.swift */; };
0C0EFA172E6EAABB00ABCDF5 /* TorNotifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C0EFA132E6EAABB00ABCDF1 /* TorNotifications.swift */; };
048A4BE72E5CCCC300162C4A /* TransportConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 048A4BE62E5CCCC300162C4A /* TransportConfig.swift */; }; 048A4BE72E5CCCC300162C4A /* TransportConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 048A4BE62E5CCCC300162C4A /* TransportConfig.swift */; };
048A4BE82E5CCCC300162C4A /* TransportConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 048A4BE62E5CCCC300162C4A /* TransportConfig.swift */; }; 048A4BE82E5CCCC300162C4A /* TransportConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 048A4BE62E5CCCC300162C4A /* TransportConfig.swift */; };
048A4BE92E5CCCC300162C4B /* TransportConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 048A4BE62E5CCCC300162C4A /* TransportConfig.swift */; }; 048A4BE92E5CCCC300162C4B /* TransportConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 048A4BE62E5CCCC300162C4A /* TransportConfig.swift */; };
@@ -74,10 +76,6 @@
049BD3B52E51F319001A566B /* MessageRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 049BD3B02E51F319001A566B /* MessageRouter.swift */; }; 049BD3B52E51F319001A566B /* MessageRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 049BD3B02E51F319001A566B /* MessageRouter.swift */; };
0AE840940F21AFC07C226636 /* PrivateChatE2ETests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A262EDDC04B7D7B5E31F321 /* PrivateChatE2ETests.swift */; }; 0AE840940F21AFC07C226636 /* PrivateChatE2ETests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A262EDDC04B7D7B5E31F321 /* PrivateChatE2ETests.swift */; };
0B6F25559A21F8C69C8357C6 /* BinaryProtocolTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B3CC6FA298729906109F61B /* BinaryProtocolTests.swift */; }; 0B6F25559A21F8C69C8357C6 /* BinaryProtocolTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B3CC6FA298729906109F61B /* BinaryProtocolTests.swift */; };
0C0EFA112E6EAAAA00ABCDEF /* CTorHost.c in Sources */ = {isa = PBXBuildFile; fileRef = 0C0EFA102E6EAAAA00ABCDEF /* CTorHost.c */; };
0C0EFA122E6EAAAA00ABCDF0 /* CTorHost.c in Sources */ = {isa = PBXBuildFile; fileRef = 0C0EFA102E6EAAAA00ABCDEF /* CTorHost.c */; };
0C0EFA162E6EAABB00ABCDF4 /* TorNotifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C0EFA132E6EAABB00ABCDF1 /* TorNotifications.swift */; };
0C0EFA172E6EAABB00ABCDF5 /* TorNotifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C0EFA132E6EAABB00ABCDF1 /* TorNotifications.swift */; };
10E68BB889356219189E38EC /* BitchatApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = EF625BB3AD919322C01A46B2 /* BitchatApp.swift */; }; 10E68BB889356219189E38EC /* BitchatApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = EF625BB3AD919322C01A46B2 /* BitchatApp.swift */; };
1234567890ABCDEFFEDCBA13 /* PeerDisplayNameResolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1234567890ABCDEFFEDCBA02 /* PeerDisplayNameResolver.swift */; }; 1234567890ABCDEFFEDCBA13 /* PeerDisplayNameResolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1234567890ABCDEFFEDCBA02 /* PeerDisplayNameResolver.swift */; };
1234567890ABCDEFFEDCBA14 /* PeerDisplayNameResolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1234567890ABCDEFFEDCBA02 /* PeerDisplayNameResolver.swift */; }; 1234567890ABCDEFFEDCBA14 /* PeerDisplayNameResolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1234567890ABCDEFFEDCBA02 /* PeerDisplayNameResolver.swift */; };
@@ -168,8 +166,6 @@
EE8C3ECADAB3083A2687D50B /* NostrProtocolTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C272F137CE00FC5A96E0CC06 /* NostrProtocolTests.swift */; }; EE8C3ECADAB3083A2687D50B /* NostrProtocolTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C272F137CE00FC5A96E0CC06 /* NostrProtocolTests.swift */; };
EF49C600C1E464710DD6CA29 /* InputValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 90CB7A5CD1D1A521CD31F380 /* InputValidator.swift */; }; EF49C600C1E464710DD6CA29 /* InputValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 90CB7A5CD1D1A521CD31F380 /* InputValidator.swift */; };
F06732B1719EE13C5D09CE77 /* NostrProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2E5A9FF4AEA8A923317ED26A /* NostrProtocol.swift */; }; F06732B1719EE13C5D09CE77 /* NostrProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2E5A9FF4AEA8A923317ED26A /* NostrProtocol.swift */; };
F0A1B2C3D4E5F60718293A4B /* OSLog+Categories.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0A1B2C3D4E5F60718293A4A /* OSLog+Categories.swift */; };
F0A1B2C3D4E5F60718293A4C /* OSLog+Categories.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0A1B2C3D4E5F60718293A4A /* OSLog+Categories.swift */; };
F455F011B3B648ADA233F998 /* BinaryProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2136C3E22D02D4A8DBE7EAB /* BinaryProtocol.swift */; }; F455F011B3B648ADA233F998 /* BinaryProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2136C3E22D02D4A8DBE7EAB /* BinaryProtocol.swift */; };
FB8819B4C84FAFEF5C36B216 /* KeychainManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 136696FC4436A02D98CE6A77 /* KeychainManager.swift */; }; FB8819B4C84FAFEF5C36B216 /* KeychainManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 136696FC4436A02D98CE6A77 /* KeychainManager.swift */; };
FBC409E105493C491531B59A /* NostrProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2E5A9FF4AEA8A923317ED26A /* NostrProtocol.swift */; }; FBC409E105493C491531B59A /* NostrProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2E5A9FF4AEA8A923317ED26A /* NostrProtocol.swift */; };
@@ -228,11 +224,12 @@
047502B82E560F690083520F /* RelayController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayController.swift; sourceTree = "<group>"; }; 047502B82E560F690083520F /* RelayController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayController.swift; sourceTree = "<group>"; };
0481A3432E6D869F00FC845E /* TorManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TorManager.swift; sourceTree = "<group>"; }; 0481A3432E6D869F00FC845E /* TorManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TorManager.swift; sourceTree = "<group>"; };
0481A3442E6D869F00FC845E /* TorURLSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TorURLSession.swift; sourceTree = "<group>"; }; 0481A3442E6D869F00FC845E /* TorURLSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TorURLSession.swift; sourceTree = "<group>"; };
0C0EFA102E6EAAAA00ABCDEF /* CTorHost.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; path = CTorHost.c; sourceTree = "<group>"; };
0C0EFA132E6EAABB00ABCDF1 /* TorNotifications.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TorNotifications.swift; sourceTree = "<group>"; };
0481A3532E6D877600FC845E /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = "<group>"; }; 0481A3532E6D877600FC845E /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = "<group>"; };
0481A3572E6D929E00FC845E /* tor-nolzma.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; path = "tor-nolzma.xcframework"; sourceTree = "<group>"; }; 0481A3572E6D929E00FC845E /* tor-nolzma.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; path = "tor-nolzma.xcframework"; sourceTree = "<group>"; };
0481A35A2E6D9BEF00FC845E /* libz.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libz.tbd; path = usr/lib/libz.tbd; sourceTree = SDKROOT; }; 0481A35A2E6D9BEF00FC845E /* libz.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libz.tbd; path = usr/lib/libz.tbd; sourceTree = SDKROOT; };
0481A35C2E6DA18600FC845E /* libz.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libz.tbd; path = usr/lib/libz.tbd; sourceTree = SDKROOT; }; 0481A35C2E6DA18600FC845E /* libz.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libz.tbd; path = usr/lib/libz.tbd; sourceTree = SDKROOT; };
0481A38F2E734CAE00FC845E /* CommandProcessorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommandProcessorTests.swift; sourceTree = "<group>"; };
048A4BE62E5CCCC300162C4A /* TransportConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransportConfig.swift; sourceTree = "<group>"; }; 048A4BE62E5CCCC300162C4A /* TransportConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransportConfig.swift; sourceTree = "<group>"; };
048A4C272E5FCD6600162C4A /* GeohashBookmarksStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeohashBookmarksStore.swift; sourceTree = "<group>"; }; 048A4C272E5FCD6600162C4A /* GeohashBookmarksStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeohashBookmarksStore.swift; sourceTree = "<group>"; };
048A4C2A2E5FCE0300162C4A /* GeohashBookmarksStoreTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeohashBookmarksStoreTests.swift; sourceTree = "<group>"; }; 048A4C2A2E5FCE0300162C4A /* GeohashBookmarksStoreTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeohashBookmarksStoreTests.swift; sourceTree = "<group>"; };
@@ -250,8 +247,6 @@
049BD3B12E51F319001A566B /* NostrTransport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NostrTransport.swift; sourceTree = "<group>"; }; 049BD3B12E51F319001A566B /* NostrTransport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NostrTransport.swift; sourceTree = "<group>"; };
05BA20BC0F123F1507C5C247 /* IdentityModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IdentityModels.swift; sourceTree = "<group>"; }; 05BA20BC0F123F1507C5C247 /* IdentityModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IdentityModels.swift; sourceTree = "<group>"; };
0B3CC6FA298729906109F61B /* BinaryProtocolTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BinaryProtocolTests.swift; sourceTree = "<group>"; }; 0B3CC6FA298729906109F61B /* BinaryProtocolTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BinaryProtocolTests.swift; sourceTree = "<group>"; };
0C0EFA102E6EAAAA00ABCDEF /* CTorHost.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; path = CTorHost.c; sourceTree = "<group>"; };
0C0EFA132E6EAABB00ABCDF1 /* TorNotifications.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TorNotifications.swift; sourceTree = "<group>"; };
11186E29A064E8D210880E1B /* BitchatPeer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BitchatPeer.swift; sourceTree = "<group>"; }; 11186E29A064E8D210880E1B /* BitchatPeer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BitchatPeer.swift; sourceTree = "<group>"; };
1234567890ABCDEFFEDCBA02 /* PeerDisplayNameResolver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PeerDisplayNameResolver.swift; sourceTree = "<group>"; }; 1234567890ABCDEFFEDCBA02 /* PeerDisplayNameResolver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PeerDisplayNameResolver.swift; sourceTree = "<group>"; };
136696FC4436A02D98CE6A77 /* KeychainManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainManager.swift; sourceTree = "<group>"; }; 136696FC4436A02D98CE6A77 /* KeychainManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainManager.swift; sourceTree = "<group>"; };
@@ -303,7 +298,6 @@
EA706D8E5097785414646A8E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; }; EA706D8E5097785414646A8E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; };
EE7EFB209C86BBD956B749EC /* SecureLogger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureLogger.swift; sourceTree = "<group>"; }; EE7EFB209C86BBD956B749EC /* SecureLogger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureLogger.swift; sourceTree = "<group>"; };
EF625BB3AD919322C01A46B2 /* BitchatApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BitchatApp.swift; sourceTree = "<group>"; }; EF625BB3AD919322C01A46B2 /* BitchatApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BitchatApp.swift; sourceTree = "<group>"; };
F0A1B2C3D4E5F60718293A4A /* OSLog+Categories.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OSLog+Categories.swift"; sourceTree = "<group>"; };
FC75901A0F0073B5BB8356E7 /* TestConstants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestConstants.swift; sourceTree = "<group>"; }; FC75901A0F0073B5BB8356E7 /* TestConstants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestConstants.swift; sourceTree = "<group>"; };
FDC18D910D6FF2E8B1B6C885 /* SecureIdentityStateManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureIdentityStateManager.swift; sourceTree = "<group>"; }; FDC18D910D6FF2E8B1B6C885 /* SecureIdentityStateManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureIdentityStateManager.swift; sourceTree = "<group>"; };
FE7CCF2BD78A3F3DAE6DA145 /* MockBLEService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockBLEService.swift; sourceTree = "<group>"; }; FE7CCF2BD78A3F3DAE6DA145 /* MockBLEService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockBLEService.swift; sourceTree = "<group>"; };
@@ -477,7 +471,6 @@
32F149C43D1915831B60FE09 /* CompressionUtil.swift */, 32F149C43D1915831B60FE09 /* CompressionUtil.swift */,
90CB7A5CD1D1A521CD31F380 /* InputValidator.swift */, 90CB7A5CD1D1A521CD31F380 /* InputValidator.swift */,
EE7EFB209C86BBD956B749EC /* SecureLogger.swift */, EE7EFB209C86BBD956B749EC /* SecureLogger.swift */,
F0A1B2C3D4E5F60718293A4A /* OSLog+Categories.swift */,
); );
path = Utils; path = Utils;
sourceTree = "<group>"; sourceTree = "<group>";
@@ -545,9 +538,8 @@
C3D98EB3E1B455E321F519F4 /* bitchatTests */ = { C3D98EB3E1B455E321F519F4 /* bitchatTests */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
D69A18D27F9A565FD6041E12 /* Info.plist */,
0481A38F2E734CAE00FC845E /* CommandProcessorTests.swift */,
048A4C2A2E5FCE0300162C4A /* GeohashBookmarksStoreTests.swift */, 048A4C2A2E5FCE0300162C4A /* GeohashBookmarksStoreTests.swift */,
D69A18D27F9A565FD6041E12 /* Info.plist */,
047502912E547ACC0083520F /* LocationChannelsTests.swift */, 047502912E547ACC0083520F /* LocationChannelsTests.swift */,
C272F137CE00FC5A96E0CC06 /* NostrProtocolTests.swift */, C272F137CE00FC5A96E0CC06 /* NostrProtocolTests.swift */,
980B109CBA72BC996455C62B /* BLEServiceTests.swift */, 980B109CBA72BC996455C62B /* BLEServiceTests.swift */,
@@ -810,7 +802,6 @@
isa = PBXSourcesBuildPhase; isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
F0A1B2C3D4E5F60718293A4C /* OSLog+Categories.swift in Sources */,
0C0EFA122E6EAAAA00ABCDF0 /* CTorHost.c in Sources */, 0C0EFA122E6EAAAA00ABCDF0 /* CTorHost.c in Sources */,
0C0EFA172E6EAABB00ABCDF5 /* TorNotifications.swift in Sources */, 0C0EFA172E6EAABB00ABCDF5 /* TorNotifications.swift in Sources */,
048A4BE72E5CCCC300162C4A /* TransportConfig.swift in Sources */, 048A4BE72E5CCCC300162C4A /* TransportConfig.swift in Sources */,
@@ -875,7 +866,6 @@
isa = PBXSourcesBuildPhase; isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
F0A1B2C3D4E5F60718293A4B /* OSLog+Categories.swift in Sources */,
0C0EFA112E6EAAAA00ABCDEF /* CTorHost.c in Sources */, 0C0EFA112E6EAAAA00ABCDEF /* CTorHost.c in Sources */,
0C0EFA162E6EAABB00ABCDF4 /* TorNotifications.swift in Sources */, 0C0EFA162E6EAABB00ABCDF4 /* TorNotifications.swift in Sources */,
048A4BE82E5CCCC300162C4A /* TransportConfig.swift in Sources */, 048A4BE82E5CCCC300162C4A /* TransportConfig.swift in Sources */,
@@ -944,7 +934,6 @@
047502802E53A0FC0083520F /* FragmentationTests.swift in Sources */, 047502802E53A0FC0083520F /* FragmentationTests.swift in Sources */,
8F282E9CCA5AE1ECC001D2E4 /* IntegrationTests.swift in Sources */, 8F282E9CCA5AE1ECC001D2E4 /* IntegrationTests.swift in Sources */,
047502B12E55E8450083520F /* InputValidatorTests.swift in Sources */, 047502B12E55E8450083520F /* InputValidatorTests.swift in Sources */,
0481A3912E734CAE00FC845E /* CommandProcessorTests.swift in Sources */,
D727EA273CB214FC32612469 /* MockBluetoothMeshService.swift in Sources */, D727EA273CB214FC32612469 /* MockBluetoothMeshService.swift in Sources */,
047502932E547ACC0083520F /* LocationChannelsTests.swift in Sources */, 047502932E547ACC0083520F /* LocationChannelsTests.swift in Sources */,
048A4C2B2E5FCE0300162C4A /* GeohashBookmarksStoreTests.swift in Sources */, 048A4C2B2E5FCE0300162C4A /* GeohashBookmarksStoreTests.swift in Sources */,
@@ -968,7 +957,6 @@
047502812E53A0FC0083520F /* FragmentationTests.swift in Sources */, 047502812E53A0FC0083520F /* FragmentationTests.swift in Sources */,
686441ABC2AF83EE98E6ECF2 /* IntegrationTests.swift in Sources */, 686441ABC2AF83EE98E6ECF2 /* IntegrationTests.swift in Sources */,
047502B02E55E8450083520F /* InputValidatorTests.swift in Sources */, 047502B02E55E8450083520F /* InputValidatorTests.swift in Sources */,
0481A3902E734CAE00FC845E /* CommandProcessorTests.swift in Sources */,
8851F08D88C5B1DE7B9F55C6 /* MockBluetoothMeshService.swift in Sources */, 8851F08D88C5B1DE7B9F55C6 /* MockBluetoothMeshService.swift in Sources */,
047502922E547ACC0083520F /* LocationChannelsTests.swift in Sources */, 047502922E547ACC0083520F /* LocationChannelsTests.swift in Sources */,
048A4C2C2E5FCE0300162C4A /* GeohashBookmarksStoreTests.swift in Sources */, 048A4C2C2E5FCE0300162C4A /* GeohashBookmarksStoreTests.swift in Sources */,
@@ -1,9 +1,111 @@
{ {
"images" : [ "images" : [
{
"filename" : "icon_20x20@2x.png",
"idiom" : "iphone",
"scale" : "2x",
"size" : "20x20"
},
{
"filename" : "icon_20x20@3x.png",
"idiom" : "iphone",
"scale" : "3x",
"size" : "20x20"
},
{
"filename" : "icon_29x29@2x.png",
"idiom" : "iphone",
"scale" : "2x",
"size" : "29x29"
},
{
"filename" : "icon_29x29@3x.png",
"idiom" : "iphone",
"scale" : "3x",
"size" : "29x29"
},
{
"filename" : "icon_40x40@2x.png",
"idiom" : "iphone",
"scale" : "2x",
"size" : "40x40"
},
{
"filename" : "icon_40x40@3x.png",
"idiom" : "iphone",
"scale" : "3x",
"size" : "40x40"
},
{
"filename" : "icon_60x60@2x.png",
"idiom" : "iphone",
"scale" : "2x",
"size" : "60x60"
},
{
"filename" : "icon_60x60@3x.png",
"idiom" : "iphone",
"scale" : "3x",
"size" : "60x60"
},
{
"filename" : "icon_20x20.png",
"idiom" : "ipad",
"scale" : "1x",
"size" : "20x20"
},
{
"filename" : "icon_20x20@2x.png",
"idiom" : "ipad",
"scale" : "2x",
"size" : "20x20"
},
{
"filename" : "icon_29x29.png",
"idiom" : "ipad",
"scale" : "1x",
"size" : "29x29"
},
{
"filename" : "icon_29x29@2x.png",
"idiom" : "ipad",
"scale" : "2x",
"size" : "29x29"
},
{
"filename" : "icon_40x40.png",
"idiom" : "ipad",
"scale" : "1x",
"size" : "40x40"
},
{
"filename" : "icon_40x40@2x.png",
"idiom" : "ipad",
"scale" : "2x",
"size" : "40x40"
},
{
"filename" : "icon_76x76.png",
"idiom" : "ipad",
"scale" : "1x",
"size" : "76x76"
},
{
"filename" : "icon_76x76@2x.png",
"idiom" : "ipad",
"scale" : "2x",
"size" : "76x76"
},
{
"filename" : "icon_83.5x83.5@2x.png",
"idiom" : "ipad",
"scale" : "2x",
"size" : "83.5x83.5"
},
{ {
"filename" : "icon_1024x1024.png", "filename" : "icon_1024x1024.png",
"idiom" : "universal", "idiom" : "ios-marketing",
"platform" : "ios", "scale" : "1x",
"size" : "1024x1024" "size" : "1024x1024"
}, },
{ {
Binary file not shown.

After

Width:  |  Height:  |  Size: 378 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 497 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 570 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 401 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 564 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 668 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 497 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 641 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 765 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 765 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 628 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 930 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 976 B

+3 -3
View File
@@ -163,7 +163,7 @@ struct BitchatApp: App {
} }
#if os(iOS) #if os(iOS)
final class AppDelegate: NSObject, UIApplicationDelegate { class AppDelegate: NSObject, UIApplicationDelegate {
weak var chatViewModel: ChatViewModel? weak var chatViewModel: ChatViewModel?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
@@ -175,7 +175,7 @@ final class AppDelegate: NSObject, UIApplicationDelegate {
#if os(macOS) #if os(macOS)
import AppKit import AppKit
final class MacAppDelegate: NSObject, NSApplicationDelegate { class MacAppDelegate: NSObject, NSApplicationDelegate {
weak var chatViewModel: ChatViewModel? weak var chatViewModel: ChatViewModel?
func applicationWillTerminate(_ notification: Notification) { func applicationWillTerminate(_ notification: Notification) {
@@ -188,7 +188,7 @@ final class MacAppDelegate: NSObject, NSApplicationDelegate {
} }
#endif #endif
final class NotificationDelegate: NSObject, UNUserNotificationCenterDelegate { class NotificationDelegate: NSObject, UNUserNotificationCenterDelegate {
static let shared = NotificationDelegate() static let shared = NotificationDelegate()
weak var chatViewModel: ChatViewModel? weak var chatViewModel: ChatViewModel?
@@ -96,7 +96,7 @@ import CryptoKit
/// Singleton manager for secure identity state persistence and retrieval. /// Singleton manager for secure identity state persistence and retrieval.
/// Provides thread-safe access to identity mappings with encryption at rest. /// Provides thread-safe access to identity mappings with encryption at rest.
/// All identity data is stored encrypted in the device Keychain for security. /// All identity data is stored encrypted in the device Keychain for security.
final class SecureIdentityStateManager { class SecureIdentityStateManager {
static let shared = SecureIdentityStateManager() static let shared = SecureIdentityStateManager()
private let keychain = KeychainManager.shared private let keychain = KeychainManager.shared
@@ -129,7 +129,7 @@ final class SecureIdentityStateManager {
// Try to load from keychain // Try to load from keychain
if let keyData = keychain.getIdentityKey(forKey: encryptionKeyName) { if let keyData = keychain.getIdentityKey(forKey: encryptionKeyName) {
loadedKey = SymmetricKey(data: keyData) loadedKey = SymmetricKey(data: keyData)
SecureLogger.logKeyOperation(.load, keyType: "identity cache encryption key", success: true) SecureLogger.logKeyOperation("load", keyType: "identity cache encryption key", success: true)
} }
// Generate new key if needed // Generate new key if needed
else { else {
@@ -137,7 +137,7 @@ final class SecureIdentityStateManager {
let keyData = loadedKey.withUnsafeBytes { Data($0) } let keyData = loadedKey.withUnsafeBytes { Data($0) }
// Save to keychain // Save to keychain
let saved = keychain.saveIdentityKey(keyData, forKey: encryptionKeyName) let saved = keychain.saveIdentityKey(keyData, forKey: encryptionKeyName)
SecureLogger.logKeyOperation(.generate, keyType: "identity cache encryption key", success: saved) SecureLogger.logKeyOperation("generate", keyType: "identity cache encryption key", success: saved)
} }
self.encryptionKey = loadedKey self.encryptionKey = loadedKey
@@ -160,7 +160,7 @@ final class SecureIdentityStateManager {
cache = try JSONDecoder().decode(IdentityCache.self, from: decryptedData) cache = try JSONDecoder().decode(IdentityCache.self, from: decryptedData)
} catch { } catch {
// Log error but continue with empty cache // Log error but continue with empty cache
SecureLogger.error(error, context: "Failed to load identity cache", category: .security) SecureLogger.logError(error, context: "Failed to load identity cache", category: SecureLogger.security)
} }
} }
@@ -191,10 +191,10 @@ final class SecureIdentityStateManager {
let sealedBox = try AES.GCM.seal(data, using: encryptionKey) let sealedBox = try AES.GCM.seal(data, using: encryptionKey)
let saved = keychain.saveIdentityKey(sealedBox.combined!, forKey: cacheKey) let saved = keychain.saveIdentityKey(sealedBox.combined!, forKey: cacheKey)
if saved { if saved {
SecureLogger.debug("Identity cache saved to keychain", category: .security) SecureLogger.log("Identity cache saved to keychain", category: SecureLogger.security, level: .debug)
} }
} catch { } catch {
SecureLogger.error(error, context: "Failed to save identity cache", category: .security) SecureLogger.logError(error, context: "Failed to save identity cache", category: SecureLogger.security)
} }
} }
@@ -395,7 +395,7 @@ final class SecureIdentityStateManager {
} }
func setBlocked(_ fingerprint: String, isBlocked: Bool) { func setBlocked(_ fingerprint: String, isBlocked: Bool) {
SecureLogger.info("User \(isBlocked ? "blocked" : "unblocked"): \(fingerprint)", category: .security) SecureLogger.log("User \(isBlocked ? "blocked" : "unblocked"): \(fingerprint)", category: SecureLogger.security, level: .info)
queue.async(flags: .barrier) { queue.async(flags: .barrier) {
if var identity = self.cache.socialIdentities[fingerprint] { if var identity = self.cache.socialIdentities[fingerprint] {
@@ -519,7 +519,7 @@ final class SecureIdentityStateManager {
// MARK: - Cleanup // MARK: - Cleanup
func clearAllIdentityData() { func clearAllIdentityData() {
SecureLogger.warning("Clearing all identity data", category: .security) SecureLogger.log("Clearing all identity data", category: SecureLogger.security, level: .warning)
queue.async(flags: .barrier) { queue.async(flags: .barrier) {
self.cache = IdentityCache() self.cache = IdentityCache()
@@ -529,7 +529,7 @@ final class SecureIdentityStateManager {
// Delete from keychain // Delete from keychain
let deleted = self.keychain.deleteIdentityKey(forKey: self.cacheKey) let deleted = self.keychain.deleteIdentityKey(forKey: self.cacheKey)
SecureLogger.logKeyOperation(.delete, keyType: "identity cache", success: deleted) SecureLogger.logKeyOperation("delete", keyType: "identity cache", success: deleted)
} }
} }
@@ -543,7 +543,7 @@ final class SecureIdentityStateManager {
// MARK: - Verification // MARK: - Verification
func setVerified(fingerprint: String, verified: Bool) { func setVerified(fingerprint: String, verified: Bool) {
SecureLogger.info("Fingerprint \(verified ? "verified" : "unverified"): \(fingerprint)", category: .security) SecureLogger.log("Fingerprint \(verified ? "verified" : "unverified"): \(fingerprint)", category: SecureLogger.security, level: .info)
queue.async(flags: .barrier) { queue.async(flags: .barrier) {
if verified { if verified {
+2 -2
View File
@@ -35,10 +35,10 @@
<string>bitchat uses Bluetooth to create a secure mesh network for chatting with nearby users.</string> <string>bitchat uses Bluetooth to create a secure mesh network for chatting with nearby users.</string>
<key>NSBluetoothPeripheralUsageDescription</key> <key>NSBluetoothPeripheralUsageDescription</key>
<string>bitchat uses Bluetooth to discover and connect with other bitchat users nearby.</string> <string>bitchat uses Bluetooth to discover and connect with other bitchat users nearby.</string>
<key>NSCameraUsageDescription</key>
<string>bitchat uses the camera to scan QR codes to verify peers.</string>
<key>NSLocationWhenInUseUsageDescription</key> <key>NSLocationWhenInUseUsageDescription</key>
<string>bitchat uses your approximate location to compute local geohash channels for optional public chats. Exact GPS is never shared.</string> <string>bitchat uses your approximate location to compute local geohash channels for optional public chats. Exact GPS is never shared.</string>
<key>NSCameraUsageDescription</key>
<string>bitchat uses the camera to scan QR codes to verify peers.</string>
<key>UIBackgroundModes</key> <key>UIBackgroundModes</key>
<array> <array>
<string>bluetooth-central</string> <string>bluetooth-central</string>
+30 -18
View File
@@ -9,7 +9,7 @@
import Foundation import Foundation
/// Coordinates Noise handshakes to prevent race conditions and ensure reliable encryption establishment /// Coordinates Noise handshakes to prevent race conditions and ensure reliable encryption establishment
final class NoiseHandshakeCoordinator { class NoiseHandshakeCoordinator {
// MARK: - Handshake State // MARK: - Handshake State
@@ -68,7 +68,8 @@ final class NoiseHandshakeCoordinator {
switch state { switch state {
case .initiating(_, let lastAttempt): case .initiating(_, let lastAttempt):
if Date().timeIntervalSince(lastAttempt) > handshakeTimeout { if Date().timeIntervalSince(lastAttempt) > handshakeTimeout {
SecureLogger.warning("Forcing new handshake with \(remotePeerID) - previous stuck in initiating", category: .handshake) SecureLogger.log("Forcing new handshake with \(remotePeerID) - previous stuck in initiating",
category: SecureLogger.handshake, level: .warning)
return true return true
} }
default: default:
@@ -76,7 +77,8 @@ final class NoiseHandshakeCoordinator {
} }
} }
SecureLogger.debug("Already in active handshake with \(remotePeerID), state: \(state)", category: .handshake) SecureLogger.log("Already in active handshake with \(remotePeerID), state: \(state)",
category: SecureLogger.handshake, level: .debug)
return false return false
} }
@@ -105,7 +107,8 @@ final class NoiseHandshakeCoordinator {
handshakeQueue.async(flags: .barrier) { handshakeQueue.async(flags: .barrier) {
let attempt = self.getCurrentAttempt(for: peerID) + 1 let attempt = self.getCurrentAttempt(for: peerID) + 1
self.handshakeStates[peerID] = .initiating(attempt: attempt, lastAttempt: Date()) self.handshakeStates[peerID] = .initiating(attempt: attempt, lastAttempt: Date())
SecureLogger.info("Recording handshake initiation with \(peerID), attempt \(attempt)", category: .handshake) SecureLogger.log("Recording handshake initiation with \(peerID), attempt \(attempt)",
category: SecureLogger.handshake, level: .info)
} }
} }
@@ -113,7 +116,8 @@ final class NoiseHandshakeCoordinator {
func recordHandshakeResponse(peerID: String) { func recordHandshakeResponse(peerID: String) {
handshakeQueue.async(flags: .barrier) { handshakeQueue.async(flags: .barrier) {
self.handshakeStates[peerID] = .responding(since: Date()) self.handshakeStates[peerID] = .responding(since: Date())
SecureLogger.info("Recording handshake response to \(peerID)", category: .handshake) SecureLogger.log("Recording handshake response to \(peerID)",
category: SecureLogger.handshake, level: .info)
} }
} }
@@ -121,7 +125,8 @@ final class NoiseHandshakeCoordinator {
func recordHandshakeSuccess(peerID: String) { func recordHandshakeSuccess(peerID: String) {
handshakeQueue.async(flags: .barrier) { handshakeQueue.async(flags: .barrier) {
self.handshakeStates[peerID] = .established(since: Date()) self.handshakeStates[peerID] = .established(since: Date())
SecureLogger.info("Handshake successfully established with \(peerID)", category: .handshake) SecureLogger.log("Handshake successfully established with \(peerID)",
category: SecureLogger.handshake, level: .info)
} }
} }
@@ -131,7 +136,8 @@ final class NoiseHandshakeCoordinator {
let attempts = self.getCurrentAttempt(for: peerID) let attempts = self.getCurrentAttempt(for: peerID)
let canRetry = attempts < self.maxHandshakeAttempts let canRetry = attempts < self.maxHandshakeAttempts
self.handshakeStates[peerID] = .failed(reason: reason, canRetry: canRetry, lastAttempt: Date()) self.handshakeStates[peerID] = .failed(reason: reason, canRetry: canRetry, lastAttempt: Date())
SecureLogger.warning("Handshake failed with \(peerID): \(reason), canRetry: \(canRetry)", category: .handshake) SecureLogger.log("Handshake failed with \(peerID): \(reason), canRetry: \(canRetry)",
category: SecureLogger.handshake, level: .warning)
} }
} }
@@ -140,7 +146,8 @@ final class NoiseHandshakeCoordinator {
return handshakeQueue.sync { return handshakeQueue.sync {
// If we're already established, reject new handshakes // If we're already established, reject new handshakes
if case .established = handshakeStates[remotePeerID] { if case .established = handshakeStates[remotePeerID] {
SecureLogger.debug("Rejecting handshake from \(remotePeerID) - already established", category: .handshake) SecureLogger.log("Rejecting handshake from \(remotePeerID) - already established",
category: SecureLogger.handshake, level: .debug)
return false return false
} }
@@ -150,7 +157,8 @@ final class NoiseHandshakeCoordinator {
if role == .initiator { if role == .initiator {
if case .initiating = handshakeStates[remotePeerID] { if case .initiating = handshakeStates[remotePeerID] {
// They shouldn't be initiating, but accept it to recover from race condition // They shouldn't be initiating, but accept it to recover from race condition
SecureLogger.warning("Accepting handshake from \(remotePeerID) despite being initiator (race condition recovery)", category: .handshake) SecureLogger.log("Accepting handshake from \(remotePeerID) despite being initiator (race condition recovery)",
category: SecureLogger.handshake, level: .warning)
return true return true
} }
} }
@@ -207,7 +215,8 @@ final class NoiseHandshakeCoordinator {
func resetHandshakeState(for peerID: String) { func resetHandshakeState(for peerID: String) {
handshakeQueue.async(flags: .barrier) { handshakeQueue.async(flags: .barrier) {
self.handshakeStates.removeValue(forKey: peerID) self.handshakeStates.removeValue(forKey: peerID)
SecureLogger.debug("Reset handshake state for \(peerID)", category: .handshake) SecureLogger.log("Reset handshake state for \(peerID)",
category: SecureLogger.handshake, level: .debug)
} }
} }
@@ -247,7 +256,8 @@ final class NoiseHandshakeCoordinator {
if isStale { if isStale {
stalePeerIDs.append(peerID) stalePeerIDs.append(peerID)
SecureLogger.warning("Found stale handshake state for \(peerID): \(state)", category: .handshake) SecureLogger.log("Found stale handshake state for \(peerID): \(state)",
category: SecureLogger.handshake, level: .warning)
} }
} }
@@ -260,7 +270,8 @@ final class NoiseHandshakeCoordinator {
for i in 0..<sessionsToRemove { for i in 0..<sessionsToRemove {
let peerID = sortedSessions[i].peerID let peerID = sortedSessions[i].peerID
stalePeerIDs.append(peerID) stalePeerIDs.append(peerID)
SecureLogger.info("Removing old established session for \(peerID) to maintain session limit", category: .handshake) SecureLogger.log("Removing old established session for \(peerID) to maintain session limit",
category: SecureLogger.handshake, level: .info)
} }
} }
@@ -270,7 +281,8 @@ final class NoiseHandshakeCoordinator {
} }
if !stalePeerIDs.isEmpty { if !stalePeerIDs.isEmpty {
SecureLogger.info("Cleaned up \(stalePeerIDs.count) stale handshake states", category: .handshake) SecureLogger.log("Cleaned up \(stalePeerIDs.count) stale handshake states",
category: SecureLogger.handshake, level: .info)
} }
return stalePeerIDs return stalePeerIDs
@@ -321,7 +333,7 @@ final class NoiseHandshakeCoordinator {
/// Log current handshake states for debugging /// Log current handshake states for debugging
func logHandshakeStates() { func logHandshakeStates() {
handshakeQueue.sync { handshakeQueue.sync {
SecureLogger.debug("=== Handshake States ===", category: .handshake) SecureLogger.log("=== Handshake States ===", category: SecureLogger.handshake, level: .debug)
for (peerID, state) in handshakeStates { for (peerID, state) in handshakeStates {
let stateDesc: String let stateDesc: String
switch state { switch state {
@@ -340,18 +352,18 @@ final class NoiseHandshakeCoordinator {
case .failed(let reason, let canRetry, let lastAttempt): case .failed(let reason, let canRetry, let lastAttempt):
stateDesc = "failed: \(reason) (canRetry: \(canRetry), last: \(lastAttempt))" stateDesc = "failed: \(reason) (canRetry: \(canRetry), last: \(lastAttempt))"
} }
SecureLogger.debug(" \(peerID): \(stateDesc)", category: .handshake) SecureLogger.log(" \(peerID): \(stateDesc)", category: SecureLogger.handshake, level: .debug)
} }
SecureLogger.debug("========================", category: .handshake) SecureLogger.log("========================", category: SecureLogger.handshake, level: .debug)
} }
} }
/// Clear all handshake states - used during panic mode /// Clear all handshake states - used during panic mode
func clearAllHandshakeStates() { func clearAllHandshakeStates() {
handshakeQueue.async(flags: .barrier) { handshakeQueue.async(flags: .barrier) {
SecureLogger.warning("Clearing all handshake states for panic mode", category: .handshake) SecureLogger.log("Clearing all handshake states for panic mode", category: SecureLogger.handshake, level: .warning)
self.handshakeStates.removeAll() self.handshakeStates.removeAll()
self.processedHandshakeMessages.removeAll() self.processedHandshakeMessages.removeAll()
} }
} }
} }
+14 -13
View File
@@ -79,6 +79,7 @@
import Foundation import Foundation
import CryptoKit import CryptoKit
import os.log
// Core Noise Protocol implementation // Core Noise Protocol implementation
// Based on the Noise Protocol Framework specification // Based on the Noise Protocol Framework specification
@@ -126,7 +127,7 @@ struct NoiseProtocolName {
/// Handles ChaCha20-Poly1305 AEAD encryption with automatic nonce management /// Handles ChaCha20-Poly1305 AEAD encryption with automatic nonce management
/// and replay protection using a sliding window algorithm. /// and replay protection using a sliding window algorithm.
/// - Warning: Nonce reuse would be catastrophic for security /// - Warning: Nonce reuse would be catastrophic for security
final class NoiseCipherState { class NoiseCipherState {
// Constants for replay protection // Constants for replay protection
private static let NONCE_SIZE_BYTES = 4 private static let NONCE_SIZE_BYTES = 4
private static let REPLAY_WINDOW_SIZE = 1024 private static let REPLAY_WINDOW_SIZE = 1024
@@ -284,7 +285,7 @@ final class NoiseCipherState {
// Log high nonce values that might indicate issues // Log high nonce values that might indicate issues
if currentNonce > Self.HIGH_NONCE_WARNING_THRESHOLD { if currentNonce > Self.HIGH_NONCE_WARNING_THRESHOLD {
SecureLogger.warning("High nonce value detected: \(currentNonce) - consider rekeying", category: .encryption) SecureLogger.log("High nonce value detected: \(currentNonce) - consider rekeying", category: SecureLogger.encryption, level: .warning)
} }
return combinedPayload return combinedPayload
@@ -306,13 +307,13 @@ final class NoiseCipherState {
if useExtractedNonce { if useExtractedNonce {
// Extract nonce and ciphertext from combined payload // Extract nonce and ciphertext from combined payload
guard let (extractedNonce, actualCiphertext) = try extractNonceFromCiphertextPayload(ciphertext) else { guard let (extractedNonce, actualCiphertext) = try extractNonceFromCiphertextPayload(ciphertext) else {
SecureLogger.debug("Decrypt failed: Could not extract nonce from payload") SecureLogger.log("Decrypt failed: Could not extract nonce from payload")
throw NoiseError.invalidCiphertext throw NoiseError.invalidCiphertext
} }
// Validate nonce with sliding window replay protection // Validate nonce with sliding window replay protection
guard isValidNonce(extractedNonce) else { guard isValidNonce(extractedNonce) else {
SecureLogger.debug("Replay attack detected: nonce \(extractedNonce) rejected") SecureLogger.log("Replay attack detected: nonce \(extractedNonce) rejected")
throw NoiseError.replayDetected throw NoiseError.replayDetected
} }
@@ -341,7 +342,7 @@ final class NoiseCipherState {
// Log high nonce values that might indicate issues // Log high nonce values that might indicate issues
if decryptionNonce > Self.HIGH_NONCE_WARNING_THRESHOLD { if decryptionNonce > Self.HIGH_NONCE_WARNING_THRESHOLD {
SecureLogger.warning("High nonce value detected: \(decryptionNonce) - consider rekeying", category: .encryption) SecureLogger.log("High nonce value detected: \(decryptionNonce) - consider rekeying", category: SecureLogger.encryption, level: .warning)
} }
do { do {
@@ -354,9 +355,9 @@ final class NoiseCipherState {
nonce += 1 nonce += 1
return plaintext return plaintext
} catch { } catch {
SecureLogger.debug("Decrypt failed: \(error) for nonce \(decryptionNonce)") SecureLogger.log("Decrypt failed: \(error) for nonce \(decryptionNonce)")
// Log authentication failures with nonce info // Log authentication failures with nonce info
SecureLogger.error("Decryption failed at nonce \(decryptionNonce)", category: .encryption) SecureLogger.log("Decryption failed at nonce \(decryptionNonce)", category: SecureLogger.encryption, level: .error)
throw error throw error
} }
} }
@@ -383,7 +384,7 @@ final class NoiseCipherState {
/// Responsible for key derivation, protocol name hashing, and maintaining /// Responsible for key derivation, protocol name hashing, and maintaining
/// the chaining key that provides key separation between handshake messages. /// the chaining key that provides key separation between handshake messages.
/// - Note: This class implements the SymmetricState object from the Noise spec /// - Note: This class implements the SymmetricState object from the Noise spec
final class NoiseSymmetricState { class NoiseSymmetricState {
private var cipherState: NoiseCipherState private var cipherState: NoiseCipherState
private var chainingKey: Data private var chainingKey: Data
private var hash: Data private var hash: Data
@@ -487,7 +488,7 @@ final class NoiseSymmetricState {
/// This is the main interface for establishing encrypted sessions between peers. /// This is the main interface for establishing encrypted sessions between peers.
/// Manages the handshake state machine, message patterns, and key derivation. /// Manages the handshake state machine, message patterns, and key derivation.
/// - Important: Each handshake instance should only be used once /// - Important: Each handshake instance should only be used once
final class NoiseHandshakeState { class NoiseHandshakeState {
private let role: NoiseRole private let role: NoiseRole
private let pattern: NoisePattern private let pattern: NoisePattern
private var symmetricState: NoiseSymmetricState private var symmetricState: NoiseSymmetricState
@@ -660,7 +661,7 @@ final class NoiseHandshakeState {
do { do {
remoteEphemeralPublic = try NoiseHandshakeState.validatePublicKey(ephemeralData) remoteEphemeralPublic = try NoiseHandshakeState.validatePublicKey(ephemeralData)
} catch { } catch {
SecureLogger.warning("Invalid ephemeral public key received", category: .security) SecureLogger.log("Invalid ephemeral public key received", category: SecureLogger.security, level: .warning)
throw NoiseError.invalidMessage throw NoiseError.invalidMessage
} }
symmetricState.mixHash(ephemeralData) symmetricState.mixHash(ephemeralData)
@@ -677,7 +678,7 @@ final class NoiseHandshakeState {
let decrypted = try symmetricState.decryptAndHash(staticData) let decrypted = try symmetricState.decryptAndHash(staticData)
remoteStaticPublic = try NoiseHandshakeState.validatePublicKey(decrypted) remoteStaticPublic = try NoiseHandshakeState.validatePublicKey(decrypted)
} catch { } catch {
SecureLogger.error(.authenticationFailed(peerID: "Unknown - handshake")) SecureLogger.logSecurityEvent(.authenticationFailed(peerID: "Unknown - handshake"), level: .error)
throw NoiseError.authenticationFailure throw NoiseError.authenticationFailure
} }
@@ -876,7 +877,7 @@ extension NoiseHandshakeState {
// Check against known bad points // Check against known bad points
if lowOrderPoints.contains(keyData) { if lowOrderPoints.contains(keyData) {
SecureLogger.warning("Low-order point detected", category: .security) SecureLogger.log("Low-order point detected", category: SecureLogger.security, level: .warning)
throw NoiseError.invalidPublicKey throw NoiseError.invalidPublicKey
} }
@@ -886,7 +887,7 @@ extension NoiseHandshakeState {
return publicKey return publicKey
} catch { } catch {
// If CryptoKit rejects it, it's invalid // If CryptoKit rejects it, it's invalid
SecureLogger.warning("CryptoKit validation failed", category: .security) SecureLogger.log("CryptoKit validation failed", category: SecureLogger.security, level: .warning)
throw NoiseError.invalidPublicKey throw NoiseError.invalidPublicKey
} }
} }
@@ -61,7 +61,7 @@ struct NoiseSecurityValidator {
// MARK: - Enhanced Noise Session with Security // MARK: - Enhanced Noise Session with Security
final class SecureNoiseSession: NoiseSession { class SecureNoiseSession: NoiseSession {
private(set) var messageCount: UInt64 = 0 private(set) var messageCount: UInt64 = 0
private let sessionStartTime = Date() private let sessionStartTime = Date()
private(set) var lastActivityTime = Date() private(set) var lastActivityTime = Date()
@@ -135,7 +135,7 @@ final class SecureNoiseSession: NoiseSession {
// MARK: - Rate Limiter // MARK: - Rate Limiter
final class NoiseRateLimiter { class NoiseRateLimiter {
private var handshakeTimestamps: [String: [Date]] = [:] // peerID -> timestamps private var handshakeTimestamps: [String: [Date]] = [:] // peerID -> timestamps
private var messageTimestamps: [String: [Date]] = [:] // peerID -> timestamps private var messageTimestamps: [String: [Date]] = [:] // peerID -> timestamps
@@ -153,7 +153,7 @@ final class NoiseRateLimiter {
// Check global rate limit first // Check global rate limit first
globalHandshakeTimestamps = globalHandshakeTimestamps.filter { $0 > oneMinuteAgo } globalHandshakeTimestamps = globalHandshakeTimestamps.filter { $0 > oneMinuteAgo }
if globalHandshakeTimestamps.count >= NoiseSecurityConstants.maxGlobalHandshakesPerMinute { if globalHandshakeTimestamps.count >= NoiseSecurityConstants.maxGlobalHandshakesPerMinute {
SecureLogger.warning("Global handshake rate limit exceeded: \(globalHandshakeTimestamps.count)/\(NoiseSecurityConstants.maxGlobalHandshakesPerMinute) per minute", category: .security) SecureLogger.log("Global handshake rate limit exceeded: \(globalHandshakeTimestamps.count)/\(NoiseSecurityConstants.maxGlobalHandshakesPerMinute) per minute", category: SecureLogger.security, level: .warning)
return false return false
} }
@@ -162,7 +162,7 @@ final class NoiseRateLimiter {
timestamps = timestamps.filter { $0 > oneMinuteAgo } timestamps = timestamps.filter { $0 > oneMinuteAgo }
if timestamps.count >= NoiseSecurityConstants.maxHandshakesPerMinute { if timestamps.count >= NoiseSecurityConstants.maxHandshakesPerMinute {
SecureLogger.warning("Per-peer handshake rate limit exceeded for \(peerID): \(timestamps.count)/\(NoiseSecurityConstants.maxHandshakesPerMinute) per minute", category: .security) SecureLogger.log("Per-peer handshake rate limit exceeded for \(peerID): \(timestamps.count)/\(NoiseSecurityConstants.maxHandshakesPerMinute) per minute", category: SecureLogger.security, level: .warning)
return false return false
} }
@@ -182,7 +182,7 @@ final class NoiseRateLimiter {
// Check global rate limit first // Check global rate limit first
globalMessageTimestamps = globalMessageTimestamps.filter { $0 > oneSecondAgo } globalMessageTimestamps = globalMessageTimestamps.filter { $0 > oneSecondAgo }
if globalMessageTimestamps.count >= NoiseSecurityConstants.maxGlobalMessagesPerSecond { if globalMessageTimestamps.count >= NoiseSecurityConstants.maxGlobalMessagesPerSecond {
SecureLogger.warning("Global message rate limit exceeded: \(globalMessageTimestamps.count)/\(NoiseSecurityConstants.maxGlobalMessagesPerSecond) per second", category: .security) SecureLogger.log("Global message rate limit exceeded: \(globalMessageTimestamps.count)/\(NoiseSecurityConstants.maxGlobalMessagesPerSecond) per second", category: SecureLogger.security, level: .warning)
return false return false
} }
@@ -191,7 +191,7 @@ final class NoiseRateLimiter {
timestamps = timestamps.filter { $0 > oneSecondAgo } timestamps = timestamps.filter { $0 > oneSecondAgo }
if timestamps.count >= NoiseSecurityConstants.maxMessagesPerSecond { if timestamps.count >= NoiseSecurityConstants.maxMessagesPerSecond {
SecureLogger.warning("Per-peer message rate limit exceeded for \(peerID): \(timestamps.count)/\(NoiseSecurityConstants.maxMessagesPerSecond) per second", category: .security) SecureLogger.log("Per-peer message rate limit exceeded for \(peerID): \(timestamps.count)/\(NoiseSecurityConstants.maxMessagesPerSecond) per second", category: SecureLogger.security, level: .warning)
return false return false
} }
+16 -14
View File
@@ -8,6 +8,7 @@
import Foundation import Foundation
import CryptoKit import CryptoKit
import os.log
// MARK: - Noise Session State // MARK: - Noise Session State
@@ -91,7 +92,7 @@ class NoiseSession {
func processHandshakeMessage(_ message: Data) throws -> Data? { func processHandshakeMessage(_ message: Data) throws -> Data? {
return try sessionQueue.sync(flags: .barrier) { return try sessionQueue.sync(flags: .barrier) {
SecureLogger.debug("NoiseSession[\(peerID)]: Processing handshake message, current state: \(state), role: \(role)") SecureLogger.log("NoiseSession[\(peerID)]: Processing handshake message, current state: \(state), role: \(role)", category: SecureLogger.noise, level: .debug)
// Initialize handshake state if needed (for responders) // Initialize handshake state if needed (for responders)
if state == .uninitialized && role == .responder { if state == .uninitialized && role == .responder {
@@ -102,7 +103,7 @@ class NoiseSession {
remoteStaticKey: nil remoteStaticKey: nil
) )
state = .handshaking state = .handshaking
SecureLogger.debug("NoiseSession[\(peerID)]: Initialized handshake state for responder") SecureLogger.log("NoiseSession[\(peerID)]: Initialized handshake state for responder", category: SecureLogger.noise, level: .debug)
} }
guard case .handshaking = state, let handshake = handshakeState else { guard case .handshaking = state, let handshake = handshakeState else {
@@ -111,7 +112,7 @@ class NoiseSession {
// Process incoming message // Process incoming message
_ = try handshake.readMessage(message) _ = try handshake.readMessage(message)
SecureLogger.debug("NoiseSession[\(peerID)]: Read handshake message, checking if complete") SecureLogger.log("NoiseSession[\(peerID)]: Read handshake message, checking if complete", category: SecureLogger.noise, level: .debug)
// Check if handshake is complete // Check if handshake is complete
if handshake.isHandshakeComplete() { if handshake.isHandshakeComplete() {
@@ -129,15 +130,15 @@ class NoiseSession {
state = .established state = .established
handshakeState = nil // Clear handshake state handshakeState = nil // Clear handshake state
SecureLogger.debug("NoiseSession[\(peerID)]: Handshake complete (no response needed), transitioning to established") SecureLogger.log("NoiseSession[\(peerID)]: Handshake complete (no response needed), transitioning to established", category: SecureLogger.noise, level: .debug)
SecureLogger.info(.handshakeCompleted(peerID: peerID)) SecureLogger.logSecurityEvent(.handshakeCompleted(peerID: peerID))
return nil return nil
} else { } else {
// Generate response // Generate response
let response = try handshake.writeMessage() let response = try handshake.writeMessage()
sentHandshakeMessages.append(response) sentHandshakeMessages.append(response)
SecureLogger.debug("NoiseSession[\(peerID)]: Generated handshake response of size \(response.count)") SecureLogger.log("NoiseSession[\(peerID)]: Generated handshake response of size \(response.count)", category: SecureLogger.noise, level: .debug)
// Check if handshake is complete after writing // Check if handshake is complete after writing
if handshake.isHandshakeComplete() { if handshake.isHandshakeComplete() {
@@ -155,8 +156,8 @@ class NoiseSession {
state = .established state = .established
handshakeState = nil // Clear handshake state handshakeState = nil // Clear handshake state
SecureLogger.debug("NoiseSession[\(peerID)]: Handshake complete after writing response, transitioning to established") SecureLogger.log("NoiseSession[\(peerID)]: Handshake complete after writing response, transitioning to established", category: SecureLogger.noise, level: .debug)
SecureLogger.info(.handshakeCompleted(peerID: peerID)) SecureLogger.logSecurityEvent(.handshakeCompleted(peerID: peerID))
} }
return response return response
@@ -241,7 +242,7 @@ class NoiseSession {
handshakeHash = nil handshakeHash = nil
if wasEstablished { if wasEstablished {
SecureLogger.info(.sessionExpired(peerID: peerID)) SecureLogger.logSecurityEvent(.sessionExpired(peerID: peerID))
} }
} }
} }
@@ -249,7 +250,7 @@ class NoiseSession {
// MARK: - Session Manager // MARK: - Session Manager
final class NoiseSessionManager { class NoiseSessionManager {
private var sessions: [String: NoiseSession] = [:] private var sessions: [String: NoiseSession] = [:]
private let localStaticKey: Curve25519.KeyAgreement.PrivateKey private let localStaticKey: Curve25519.KeyAgreement.PrivateKey
private let managerQueue = DispatchQueue(label: "chat.bitchat.noise.manager", attributes: .concurrent) private let managerQueue = DispatchQueue(label: "chat.bitchat.noise.manager", attributes: .concurrent)
@@ -286,7 +287,7 @@ final class NoiseSessionManager {
managerQueue.sync(flags: .barrier) { managerQueue.sync(flags: .barrier) {
if let session = sessions[peerID] { if let session = sessions[peerID] {
if session.isEstablished() { if session.isEstablished() {
SecureLogger.info(.sessionExpired(peerID: peerID)) SecureLogger.logSecurityEvent(.sessionExpired(peerID: peerID))
} }
// Clear sensitive data before removing // Clear sensitive data before removing
session.reset() session.reset()
@@ -330,7 +331,7 @@ final class NoiseSessionManager {
} catch { } catch {
// Clean up failed session // Clean up failed session
_ = sessions.removeValue(forKey: peerID) _ = sessions.removeValue(forKey: peerID)
SecureLogger.error(.handshakeFailed(peerID: peerID, error: error.localizedDescription)) SecureLogger.logSecurityEvent(.handshakeFailed(peerID: peerID, error: error.localizedDescription), level: .error)
throw error throw error
} }
} }
@@ -347,7 +348,8 @@ final class NoiseSessionManager {
// for a good reason (e.g., decryption failure, restart, etc.) // for a good reason (e.g., decryption failure, restart, etc.)
// We should accept the new handshake to re-establish encryption // We should accept the new handshake to re-establish encryption
if existing.isEstablished() { if existing.isEstablished() {
SecureLogger.info("Accepting handshake from \(peerID) despite existing session - peer likely cleared their session", category: .session) SecureLogger.log("Accepting handshake from \(peerID) despite existing session - peer likely cleared their session",
category: SecureLogger.session, level: .info)
_ = sessions.removeValue(forKey: peerID) _ = sessions.removeValue(forKey: peerID)
shouldCreateNew = true shouldCreateNew = true
} else { } else {
@@ -402,7 +404,7 @@ final class NoiseSessionManager {
self?.onSessionFailed?(peerID, error) self?.onSessionFailed?(peerID, error)
} }
SecureLogger.error(.handshakeFailed(peerID: peerID, error: error.localizedDescription)) SecureLogger.logSecurityEvent(.handshakeFailed(peerID: peerID, error: error.localizedDescription), level: .error)
throw error throw error
} }
} }
+5 -5
View File
@@ -54,7 +54,7 @@ final class GeoRelayDirectory {
Task.detached { Task.detached {
let ready = await TorManager.shared.awaitReady() let ready = await TorManager.shared.awaitReady()
if !ready { if !ready {
SecureLogger.warning("GeoRelayDirectory: Tor not ready; skipping remote fetch (fail-closed)", category: .session) SecureLogger.log("GeoRelayDirectory: Tor not ready; skipping remote fetch (fail-closed)", category: SecureLogger.session, level: .warning)
return return
} }
let task = TorURLSession.shared.session.dataTask(with: req) { [weak self] data, _, error in let task = TorURLSession.shared.session.dataTask(with: req) { [weak self] data, _, error in
@@ -66,12 +66,12 @@ final class GeoRelayDirectory {
self.entries = parsed self.entries = parsed
self.persistCache(text) self.persistCache(text)
UserDefaults.standard.set(Date(), forKey: self.lastFetchKey) UserDefaults.standard.set(Date(), forKey: self.lastFetchKey)
SecureLogger.info("GeoRelayDirectory: refreshed \(parsed.count) relays from remote", category: .session) SecureLogger.log("GeoRelayDirectory: refreshed \(parsed.count) relays from remote", category: SecureLogger.session, level: .info)
} }
return return
} }
} }
SecureLogger.warning("GeoRelayDirectory: remote fetch failed; keeping local entries", category: .session) SecureLogger.log("GeoRelayDirectory: remote fetch failed; keeping local entries", category: SecureLogger.session, level: .warning)
} }
task.resume() task.resume()
} }
@@ -82,7 +82,7 @@ final class GeoRelayDirectory {
do { do {
try text.data(using: .utf8)?.write(to: url, options: .atomic) try text.data(using: .utf8)?.write(to: url, options: .atomic)
} catch { } catch {
SecureLogger.warning("GeoRelayDirectory: failed to write cache: \(error)", category: .session) SecureLogger.log("GeoRelayDirectory: failed to write cache: \(error)", category: SecureLogger.session, level: .warning)
} }
} }
@@ -113,7 +113,7 @@ final class GeoRelayDirectory {
let text = String(data: data, encoding: .utf8) { let text = String(data: data, encoding: .utf8) {
return Self.parseCSV(text) return Self.parseCSV(text)
} }
SecureLogger.warning("GeoRelayDirectory: no local CSV found; entries empty", category: .session) SecureLogger.log("GeoRelayDirectory: no local CSV found; entries empty", category: SecureLogger.session, level: .warning)
return [] return []
} }
+5 -3
View File
@@ -78,7 +78,8 @@ struct NostrProtocol {
) )
// Successfully unwrapped gift wrap // Successfully unwrapped gift wrap
} catch { } catch {
SecureLogger.error("❌ Failed to unwrap gift wrap: \(error)", category: .session) SecureLogger.log("❌ Failed to unwrap gift wrap: \(error)",
category: SecureLogger.session, level: .error)
throw error throw error
} }
@@ -91,7 +92,8 @@ struct NostrProtocol {
) )
// Successfully opened seal // Successfully opened seal
} catch { } catch {
SecureLogger.error("❌ Failed to open seal: \(error)", category: .session) SecureLogger.log("❌ Failed to open seal: \(error)",
category: SecureLogger.session, level: .error)
throw error throw error
} }
@@ -107,7 +109,7 @@ struct NostrProtocol {
teleported: Bool = false teleported: Bool = false
) throws -> NostrEvent { ) throws -> NostrEvent {
var tags = [["g", geohash]] var tags = [["g", geohash]]
if let nickname = nickname?.trimmingCharacters(in: .whitespacesAndNewlines), !nickname.isEmpty { if let nickname = nickname, !nickname.isEmpty {
tags.append(["n", nickname]) tags.append(["n", nickname])
} }
if teleported { if teleported {
+87 -83
View File
@@ -4,7 +4,7 @@ import Combine
/// Manages WebSocket connections to Nostr relays /// Manages WebSocket connections to Nostr relays
@MainActor @MainActor
final class NostrRelayManager: ObservableObject { class NostrRelayManager: ObservableObject {
static let shared = NostrRelayManager() static let shared = NostrRelayManager()
// Track gift-wraps (kind 1059) we initiated so we can log OK acks at info // Track gift-wraps (kind 1059) we initiated so we can log OK acks at info
private(set) static var pendingGiftWrapIDs = Set<String>() private(set) static var pendingGiftWrapIDs = Set<String>()
@@ -82,10 +82,10 @@ final class NostrRelayManager: ObservableObject {
let ready = await TorManager.shared.awaitReady() let ready = await TorManager.shared.awaitReady()
await MainActor.run { await MainActor.run {
if !ready { if !ready {
SecureLogger.error("❌ Tor not ready; aborting relay connections (fail-closed)", category: .session) SecureLogger.log("❌ Tor not ready; aborting relay connections (fail-closed)", category: SecureLogger.session, level: .error)
return return
} }
SecureLogger.debug("🌐 Connecting to \(self.relays.count) Nostr relays (via Tor)", category: .session) SecureLogger.log("🌐 Connecting to \(self.relays.count) Nostr relays (via Tor)", category: SecureLogger.session, level: .debug)
for relay in self.relays { for relay in self.relays {
self.connectToRelay(relay.url) self.connectToRelay(relay.url)
} }
@@ -231,11 +231,12 @@ final class NostrRelayManager: ObservableObject {
do { do {
let message = try encoder.encode(req) let message = try encoder.encode(req)
guard let messageString = String(data: message, encoding: .utf8) else { guard let messageString = String(data: message, encoding: .utf8) else {
SecureLogger.error("❌ Failed to encode subscription request", category: .session) SecureLogger.log("❌ Failed to encode subscription request", category: SecureLogger.session, level: .error)
return return
} }
// SecureLogger.debug("📋 Subscription filter JSON: \(messageString.prefix(200))...", category: .session) // SecureLogger.log("📋 Subscription filter JSON: \(messageString.prefix(200))...",
// category: SecureLogger.session, level: .debug)
// Target specific relays if provided; else default. Filter permanently failed relays. // Target specific relays if provided; else default. Filter permanently failed relays.
let baseUrls = relayUrls ?? Self.defaultRelays let baseUrls = relayUrls ?? Self.defaultRelays
@@ -250,9 +251,8 @@ final class NostrRelayManager: ObservableObject {
map[id] = messageString map[id] = messageString
self.pendingSubscriptions[url] = map self.pendingSubscriptions[url] = map
} }
SecureLogger.debug("📋 Queued subscription id=\(id) for \(urls.count) relay(s)", category: .session) SecureLogger.log("📋 Queued subscription id=\(id) for \(urls.count) relay(s)",
// Ensure we actually have sockets opening to these relays so queued REQs can flush category: SecureLogger.session, level: .debug)
ensureConnections(to: urls)
// If some targets are already connected, flush immediately for them // If some targets are already connected, flush immediately for them
for url in urls { for url in urls {
if let r = relays.first(where: { $0.url == url }), r.isConnected { if let r = relays.first(where: { $0.url == url }), r.isConnected {
@@ -260,7 +260,8 @@ final class NostrRelayManager: ObservableObject {
} }
} }
} catch { } catch {
SecureLogger.error("❌ Failed to encode subscription request: \(error)", category: .session) SecureLogger.log("❌ Failed to encode subscription request: \(error)",
category: SecureLogger.session, level: .error)
} }
} }
@@ -292,7 +293,7 @@ final class NostrRelayManager: ObservableObject {
private func connectToRelay(_ urlString: String) { private func connectToRelay(_ urlString: String) {
guard let url = URL(string: urlString) else { guard let url = URL(string: urlString) else {
SecureLogger.warning("Invalid relay URL: \(urlString)", category: .session) SecureLogger.log("Invalid relay URL: \(urlString)", category: SecureLogger.session, level: .warning)
return return
} }
@@ -318,7 +319,7 @@ final class NostrRelayManager: ObservableObject {
let ready = await TorManager.shared.awaitReady() let ready = await TorManager.shared.awaitReady()
await MainActor.run { await MainActor.run {
if ready { self.connectToRelay(urlString) } if ready { self.connectToRelay(urlString) }
else { SecureLogger.error("❌ Tor not ready; skipping connection to \(urlString)", category: .session) } else { SecureLogger.log("❌ Tor not ready; skipping connection to \(urlString)", category: SecureLogger.session, level: .error) }
} }
} }
return return
@@ -337,12 +338,14 @@ final class NostrRelayManager: ObservableObject {
task.sendPing { [weak self] error in task.sendPing { [weak self] error in
DispatchQueue.main.async { DispatchQueue.main.async {
if error == nil { if error == nil {
SecureLogger.debug("✅ Connected to Nostr relay: \(urlString)", category: .session) SecureLogger.log("✅ Connected to Nostr relay: \(urlString)",
category: SecureLogger.session, level: .debug)
self?.updateRelayStatus(urlString, isConnected: true) self?.updateRelayStatus(urlString, isConnected: true)
// Flush any pending subscriptions for this relay // Flush any pending subscriptions for this relay
self?.flushPendingSubscriptions(for: urlString) self?.flushPendingSubscriptions(for: urlString)
} else { } else {
SecureLogger.error("❌ Failed to connect to Nostr relay \(urlString): \(error?.localizedDescription ?? "Unknown error")", category: .session) SecureLogger.log("❌ Failed to connect to Nostr relay \(urlString): \(error?.localizedDescription ?? "Unknown error")",
category: SecureLogger.session, level: .error)
self?.updateRelayStatus(urlString, isConnected: false, error: error) self?.updateRelayStatus(urlString, isConnected: false, error: error)
// Trigger disconnection handler for proper backoff // Trigger disconnection handler for proper backoff
self?.handleDisconnection(relayUrl: urlString, error: error ?? NSError(domain: "NostrRelay", code: -1, userInfo: nil)) self?.handleDisconnection(relayUrl: urlString, error: error ?? NSError(domain: "NostrRelay", code: -1, userInfo: nil))
@@ -359,7 +362,8 @@ final class NostrRelayManager: ObservableObject {
if self.subscriptions[relayUrl]?.contains(id) == true { continue } if self.subscriptions[relayUrl]?.contains(id) == true { continue }
connection.send(.string(messageString)) { error in connection.send(.string(messageString)) { error in
if let error = error { if let error = error {
SecureLogger.error("❌ Failed to send pending subscription to \(relayUrl): \(error)", category: .session) SecureLogger.log("❌ Failed to send pending subscription to \(relayUrl): \(error)",
category: SecureLogger.session, level: .error)
} else { } else {
Task { @MainActor in Task { @MainActor in
var subs = self.subscriptions[relayUrl] ?? Set<String>() var subs = self.subscriptions[relayUrl] ?? Set<String>()
@@ -378,12 +382,26 @@ final class NostrRelayManager: ObservableObject {
switch result { switch result {
case .success(let message): case .success(let message):
// Parse off-main to reduce UI jank, then hop back for state updates switch message {
Task.detached(priority: .utility) { case .string(let text):
guard let parsed = ParsedInbound(message) else { return } // Parse off-main to reduce UI jank, then hop back for state updates
await MainActor.run { Task.detached(priority: .utility) {
NostrRelayManager.shared.handleParsedMessage(parsed, from: relayUrl) guard let parsed = parseInboundMessage(text) else { return }
await MainActor.run {
NostrRelayManager.shared.handleParsedMessage(parsed, from: relayUrl)
}
} }
case .data(let data):
if let text = String(data: data, encoding: .utf8) {
Task.detached(priority: .utility) {
guard let parsed = parseInboundMessage(text) else { return }
await MainActor.run {
NostrRelayManager.shared.handleParsedMessage(parsed, from: relayUrl)
}
}
}
@unknown default:
break
} }
// Continue receiving // Continue receiving
@@ -408,7 +426,8 @@ final class NostrRelayManager: ObservableObject {
switch parsed { switch parsed {
case .event(let subId, let event): case .event(let subId, let event):
if event.kind != 1059 { if event.kind != 1059 {
SecureLogger.debug("📥 Event kind=\(event.kind) id=\(event.id.prefix(16))… relay=\(relayUrl)", category: .session) SecureLogger.log("📥 Event kind=\(event.kind) id=\(event.id.prefix(16))… relay=\(relayUrl)",
category: SecureLogger.session, level: .debug)
} }
if let index = self.relays.firstIndex(where: { $0.url == relayUrl }) { if let index = self.relays.firstIndex(where: { $0.url == relayUrl }) {
self.relays[index].messagesReceived += 1 self.relays[index].messagesReceived += 1
@@ -416,7 +435,8 @@ final class NostrRelayManager: ObservableObject {
if let handler = self.messageHandlers[subId] { if let handler = self.messageHandlers[subId] {
handler(event) handler(event)
} else { } else {
SecureLogger.warning("⚠️ No handler for subscription \(subId)", category: .session) SecureLogger.log("⚠️ No handler for subscription \(subId)",
category: SecureLogger.session, level: .warning)
} }
case .eose: case .eose:
// No-op for now // No-op for now
@@ -424,14 +444,12 @@ final class NostrRelayManager: ObservableObject {
case .ok(let eventId, let success, let reason): case .ok(let eventId, let success, let reason):
if success { if success {
_ = Self.pendingGiftWrapIDs.remove(eventId) _ = Self.pendingGiftWrapIDs.remove(eventId)
SecureLogger.debug("✅ Accepted id=\(eventId.prefix(16))… relay=\(relayUrl)", category: .session) SecureLogger.log("✅ Accepted id=\(eventId.prefix(16))… relay=\(relayUrl)",
category: SecureLogger.session, level: .debug)
} else { } else {
let isGiftWrap = Self.pendingGiftWrapIDs.remove(eventId) != nil let isGiftWrap = Self.pendingGiftWrapIDs.remove(eventId) != nil
if isGiftWrap { SecureLogger.log("📮 Rejected id=\(eventId.prefix(16))… reason=\(reason)",
SecureLogger.warning("📮 Rejected id=\(eventId.prefix(16))… reason=\(reason)", category: .session) category: SecureLogger.session, level: isGiftWrap ? .warning : .error)
} else {
SecureLogger.error("📮 Rejected id=\(eventId.prefix(16))… reason=\(reason)", category: .session)
}
} }
case .notice: case .notice:
break break
@@ -445,14 +463,17 @@ final class NostrRelayManager: ObservableObject {
let data = try encoder.encode(req) let data = try encoder.encode(req)
let message = String(data: data, encoding: .utf8) ?? "" let message = String(data: data, encoding: .utf8) ?? ""
SecureLogger.debug("📤 Send kind=\(event.kind) id=\(event.id.prefix(16))… relay=\(relayUrl)", category: .session) SecureLogger.log("📤 Send kind=\(event.kind) id=\(event.id.prefix(16))… relay=\(relayUrl)",
category: SecureLogger.session, level: .debug)
connection.send(.string(message)) { [weak self] error in connection.send(.string(message)) { [weak self] error in
DispatchQueue.main.async { DispatchQueue.main.async {
if let error = error { if let error = error {
SecureLogger.error("❌ Failed to send event to \(relayUrl): \(error)", category: .session) SecureLogger.log("❌ Failed to send event to \(relayUrl): \(error)",
category: SecureLogger.session, level: .error)
} else { } else {
// SecureLogger.debug(" Event sent to relay: \(relayUrl)", category: .session) // SecureLogger.log(" Event sent to relay: \(relayUrl)",
// category: SecureLogger.session, level: .debug)
// Update relay stats // Update relay stats
if let index = self?.relays.firstIndex(where: { $0.url == relayUrl }) { if let index = self?.relays.firstIndex(where: { $0.url == relayUrl }) {
self?.relays[index].messagesSent += 1 self?.relays[index].messagesSent += 1
@@ -461,7 +482,7 @@ final class NostrRelayManager: ObservableObject {
} }
} }
} catch { } catch {
SecureLogger.error("Failed to encode event: \(error)", category: .session) SecureLogger.log("Failed to encode event: \(error)", category: SecureLogger.session, level: .error)
} }
} }
@@ -500,7 +521,7 @@ final class NostrRelayManager: ObservableObject {
errorDescription.contains("dns") || errorDescription.contains("dns") ||
(ns.domain == NSURLErrorDomain && ns.code == NSURLErrorBadServerResponse) { (ns.domain == NSURLErrorDomain && ns.code == NSURLErrorBadServerResponse) {
if relays.first(where: { $0.url == relayUrl })?.lastError == nil { if relays.first(where: { $0.url == relayUrl })?.lastError == nil {
SecureLogger.warning("Nostr relay permanent failure for \(relayUrl) - not retrying (code=\(ns.code))", category: .session) SecureLogger.log("Nostr relay permanent failure for \(relayUrl) - not retrying (code=\(ns.code))", category: SecureLogger.session, level: .warning)
} }
if let index = relays.firstIndex(where: { $0.url == relayUrl }) { if let index = relays.firstIndex(where: { $0.url == relayUrl }) {
relays[index].lastError = error relays[index].lastError = error
@@ -518,7 +539,8 @@ final class NostrRelayManager: ObservableObject {
// Stop attempting after max attempts // Stop attempting after max attempts
if relays[index].reconnectAttempts >= maxReconnectAttempts { if relays[index].reconnectAttempts >= maxReconnectAttempts {
SecureLogger.warning("Max reconnection attempts (\(maxReconnectAttempts)) reached for \(relayUrl)", category: .session) SecureLogger.log("Max reconnection attempts (\(maxReconnectAttempts)) reached for \(relayUrl)",
category: SecureLogger.session, level: .warning)
return return
} }
@@ -612,60 +634,42 @@ private enum ParsedInbound {
case ok(eventId: String, success: Bool, reason: String) case ok(eventId: String, success: Bool, reason: String)
case eose(subscriptionId: String) case eose(subscriptionId: String)
case notice(String) case notice(String)
init?(_ message: URLSessionWebSocketTask.Message) {
guard let data = message.data,
let array = try? JSONSerialization.jsonObject(with: data) as? [Any],
array.count >= 2,
let type = array[0] as? String else {
return nil
}
switch type {
case "EVENT":
if array.count >= 3,
let subId = array[1] as? String,
let eventDict = array[2] as? [String: Any],
let event = try? NostrEvent(from: eventDict) {
self = .event(subId: subId, event: event)
return
}
return nil
case "EOSE":
if let subId = array[1] as? String {
self = .eose(subscriptionId: subId)
return
}
return nil
case "OK":
if array.count >= 3,
let eventId = array[1] as? String,
let success = array[2] as? Bool {
let reason = array.count >= 4 ? (array[3] as? String ?? "no reason given") : "no reason given"
self = .ok(eventId: eventId, success: success, reason: reason)
return
}
return nil
case "NOTICE":
if array.count >= 2, let msg = array[1] as? String {
self = .notice(msg)
return
}
return nil
default:
return nil
}
}
} }
private extension URLSessionWebSocketTask.Message { // Off-main JSON parse to avoid UI jank; pure function, not actor-isolated
var data: Data? { private func parseInboundMessage(_ message: String) -> ParsedInbound? {
switch self { guard let data = message.data(using: .utf8) else { return nil }
case .string(let text): text.data(using: .utf8) do {
case .data(let data): data if let array = try JSONSerialization.jsonObject(with: data) as? [Any],
@unknown default: nil array.count >= 2,
let type = array[0] as? String {
switch type {
case "EVENT":
if array.count >= 3,
let subId = array[1] as? String,
let eventDict = array[2] as? [String: Any] {
let event = try NostrEvent(from: eventDict)
return .event(subId: subId, event: event)
}
case "EOSE":
if let subId = array[1] as? String { return .eose(subscriptionId: subId) }
case "OK":
if array.count >= 3,
let eventId = array[1] as? String,
let success = array[2] as? Bool {
let reason = array.count >= 4 ? (array[3] as? String ?? "no reason given") : "no reason given"
return .ok(eventId: eventId, success: success, reason: reason)
}
case "NOTICE":
if array.count >= 2, let msg = array[1] as? String { return .notice(msg) }
default:
return nil
}
} }
} catch {
// Ignore
} }
return nil
} }
// MARK: - Nostr Protocol Types // MARK: - Nostr Protocol Types
+1 -1
View File
@@ -397,7 +397,7 @@ enum DeliveryStatus: Codable, Equatable {
/// Handles both broadcast messages and private encrypted messages, /// Handles both broadcast messages and private encrypted messages,
/// with support for mentions, replies, and delivery tracking. /// with support for mentions, replies, and delivery tracking.
/// - Note: This is the primary data model for chat messages /// - Note: This is the primary data model for chat messages
final class BitchatMessage: Codable { class BitchatMessage: Codable {
let id: String let id: String
let sender: String let sender: String
let content: String let content: String
+1 -1
View File
@@ -9,7 +9,7 @@
import Foundation import Foundation
/// Manages autocomplete functionality for chat /// Manages autocomplete functionality for chat
final class AutocompleteService { class AutocompleteService {
private let mentionRegex = try? NSRegularExpression(pattern: "@([\\p{L}0-9_]*)$", options: []) private let mentionRegex = try? NSRegularExpression(pattern: "@([\\p{L}0-9_]*)$", options: [])
private let commandRegex = try? NSRegularExpression(pattern: "^/([a-z]*)$", options: []) private let commandRegex = try? NSRegularExpression(pattern: "^/([a-z]*)$", options: [])
+121 -94
View File
@@ -230,7 +230,8 @@ final class BLEService: NSObject {
let newSize = data.count let newSize = data.count
// If single chunk exceeds cap, drop it immediately // If single chunk exceeds cap, drop it immediately
if newSize > capBytes { if newSize > capBytes {
SecureLogger.warning("⚠️ Dropping oversized write chunk (\(newSize)B) for peripheral \(uuid)", category: .session) SecureLogger.log("⚠️ Dropping oversized write chunk (\(newSize)B) for peripheral \(uuid)",
category: SecureLogger.session, level: .warning)
} else { } else {
// Append and trim from the front to respect cap // Append and trim from the front to respect cap
var total = queue.reduce(0) { $0 + $1.count } var total = queue.reduce(0) { $0 + $1.count }
@@ -243,7 +244,8 @@ final class BLEService: NSObject {
removedBytes += removed.count removedBytes += removed.count
total -= removed.count total -= removed.count
} }
SecureLogger.warning("📉 Trimmed pending write buffer for \(uuid) by \(removedBytes)B to \(total)B", category: .session) SecureLogger.log("📉 Trimmed pending write buffer for \(uuid) by \(removedBytes)B to \(total)B",
category: SecureLogger.session, level: .warning)
} }
self.pendingPeripheralWrites[uuid] = queue.isEmpty ? nil : queue self.pendingPeripheralWrites[uuid] = queue.isEmpty ? nil : queue
} }
@@ -339,7 +341,8 @@ final class BLEService: NSObject {
// Set up Noise session establishment callback // Set up Noise session establishment callback
// This ensures we send pending messages only when session is truly established // This ensures we send pending messages only when session is truly established
noiseService.onPeerAuthenticated = { [weak self] peerID, fingerprint in noiseService.onPeerAuthenticated = { [weak self] peerID, fingerprint in
SecureLogger.debug("🔐 Noise session authenticated with \(peerID), fingerprint: \(fingerprint.prefix(16))...") SecureLogger.log("🔐 Noise session authenticated with \(peerID), fingerprint: \(fingerprint.prefix(16))...",
category: SecureLogger.noise, level: .debug)
// Send any messages that were queued during handshake // Send any messages that were queued during handshake
self?.messageQueue.async { [weak self] in self?.messageQueue.async { [weak self] in
self?.sendPendingMessagesAfterHandshake(for: peerID) self?.sendPendingMessagesAfterHandshake(for: peerID)
@@ -584,7 +587,8 @@ final class BLEService: NSObject {
} }
func sendFavoriteNotification(to peerID: String, isFavorite: Bool) { func sendFavoriteNotification(to peerID: String, isFavorite: Bool) {
SecureLogger.debug("🔔 sendFavoriteNotification called - peerID: \(peerID), isFavorite: \(isFavorite)", category: .session) SecureLogger.log("🔔 sendFavoriteNotification called - peerID: \(peerID), isFavorite: \(isFavorite)",
category: SecureLogger.session, level: .debug)
// Include Nostr public key in the notification // Include Nostr public key in the notification
var content = isFavorite ? "[FAVORITED]" : "[UNFAVORITED]" var content = isFavorite ? "[FAVORITED]" : "[UNFAVORITED]"
@@ -592,10 +596,12 @@ final class BLEService: NSObject {
// Add our Nostr public key if available // Add our Nostr public key if available
if let myNostrIdentity = try? NostrIdentityBridge.getCurrentNostrIdentity() { if let myNostrIdentity = try? NostrIdentityBridge.getCurrentNostrIdentity() {
content += ":" + myNostrIdentity.npub content += ":" + myNostrIdentity.npub
SecureLogger.debug("📝 Sending favorite notification with Nostr npub: \(myNostrIdentity.npub)", category: .session) SecureLogger.log("📝 Sending favorite notification with Nostr npub: \(myNostrIdentity.npub)",
category: SecureLogger.session, level: .debug)
} }
SecureLogger.debug("📤 Sending favorite notification to \(peerID): \(content)", category: .session) SecureLogger.log("📤 Sending favorite notification to \(peerID): \(content)",
category: SecureLogger.session, level: .debug)
sendPrivateMessage(content, to: peerID, messageID: UUID().uuidString) sendPrivateMessage(content, to: peerID, messageID: UUID().uuidString)
} }
@@ -605,7 +611,8 @@ final class BLEService: NSObject {
payload.append(contentsOf: receipt.originalMessageID.utf8) payload.append(contentsOf: receipt.originalMessageID.utf8)
if noiseService.hasEstablishedSession(with: peerID) { if noiseService.hasEstablishedSession(with: peerID) {
SecureLogger.debug("📤 Sending READ receipt for message \(receipt.originalMessageID) to \(peerID)", category: .session) SecureLogger.log("📤 Sending READ receipt for message \(receipt.originalMessageID) to \(peerID)",
category: SecureLogger.session, level: .debug)
do { do {
let encrypted = try noiseService.encrypt(payload, for: peerID) let encrypted = try noiseService.encrypt(payload, for: peerID)
let packet = BitchatPacket( let packet = BitchatPacket(
@@ -623,7 +630,7 @@ final class BLEService: NSObject {
messageQueue.async { [weak self] in self?.broadcastPacket(packet) } messageQueue.async { [weak self] in self?.broadcastPacket(packet) }
} }
} catch { } catch {
SecureLogger.error("Failed to send read receipt: \(error)") SecureLogger.log("Failed to send read receipt: \(error)", category: SecureLogger.noise, level: .error)
} }
} else { } else {
// Queue for after handshake and initiate if needed // Queue for after handshake and initiate if needed
@@ -632,7 +639,8 @@ final class BLEService: NSObject {
self.pendingNoisePayloadsAfterHandshake[peerID, default: []].append(payload) self.pendingNoisePayloadsAfterHandshake[peerID, default: []].append(payload)
} }
if !noiseService.hasSession(with: peerID) { initiateNoiseHandshake(with: peerID) } if !noiseService.hasSession(with: peerID) { initiateNoiseHandshake(with: peerID) }
SecureLogger.debug("🕒 Queued READ receipt for \(peerID) until handshake completes", category: .session) SecureLogger.log("🕒 Queued READ receipt for \(peerID) until handshake completes",
category: SecureLogger.session, level: .debug)
} }
} }
@@ -674,7 +682,7 @@ final class BLEService: NSObject {
messageQueue.async { [weak self] in self?.broadcastPacket(packet) } messageQueue.async { [weak self] in self?.broadcastPacket(packet) }
} }
} catch { } catch {
SecureLogger.error("Failed to send verification payload: \(error)") SecureLogger.log("Failed to send verification payload: \(error)", category: SecureLogger.noise, level: .error)
} }
} }
@@ -740,7 +748,7 @@ final class BLEService: NSObject {
guard let self = self else { return } guard let self = self else { return }
guard content.count <= self.maxMessageLength else { guard content.count <= self.maxMessageLength else {
SecureLogger.error("Message too long: \(content.count) chars", category: .session) SecureLogger.log("Message too long: \(content.count) chars", category: SecureLogger.session, level: .error)
return return
} }
@@ -763,7 +771,7 @@ final class BLEService: NSObject {
ttl: self.messageTTL ttl: self.messageTTL
) )
guard let signedPacket = self.noiseService.signPacket(basePacket) else { guard let signedPacket = self.noiseService.signPacket(basePacket) else {
SecureLogger.error("❌ Failed to sign public message", category: .security) SecureLogger.log("❌ Failed to sign public message", category: SecureLogger.security, level: .error)
return return
} }
// Pre-mark our own broadcast as processed to avoid handling relayed self copy // Pre-mark our own broadcast as processed to avoid handling relayed self copy
@@ -781,7 +789,7 @@ final class BLEService: NSObject {
// MARK: - Private Message Handling // MARK: - Private Message Handling
private func sendPrivateMessage(_ content: String, to recipientID: String, messageID: String) { private func sendPrivateMessage(_ content: String, to recipientID: String, messageID: String) {
SecureLogger.debug("📨 Sending PM to \(recipientID): \(content.prefix(30))...", category: .session) SecureLogger.log("📨 Sending PM to \(recipientID): \(content.prefix(30))...", category: SecureLogger.session, level: .debug)
// Check if we have an established Noise session // Check if we have an established Noise session
if noiseService.hasEstablishedSession(with: recipientID) { if noiseService.hasEstablishedSession(with: recipientID) {
@@ -790,7 +798,7 @@ final class BLEService: NSObject {
// Create TLV-encoded private message // Create TLV-encoded private message
let privateMessage = PrivateMessagePacket(messageID: messageID, content: content) let privateMessage = PrivateMessagePacket(messageID: messageID, content: content)
guard let tlvData = privateMessage.encode() else { guard let tlvData = privateMessage.encode() else {
SecureLogger.error("Failed to encode private message with TLV") SecureLogger.log("Failed to encode private message with TLV", category: SecureLogger.noise, level: .error)
return return
} }
@@ -839,11 +847,11 @@ final class BLEService: NSObject {
self?.delegate?.didUpdateMessageDeliveryStatus(messageID, status: .sent) self?.delegate?.didUpdateMessageDeliveryStatus(messageID, status: .sent)
} }
} catch { } catch {
SecureLogger.error("Failed to encrypt message: \(error)") SecureLogger.log("Failed to encrypt message: \(error)", category: SecureLogger.noise, level: .error)
} }
} else { } else {
// Queue message for sending after handshake completes // Queue message for sending after handshake completes
SecureLogger.debug("🤝 No session with \(recipientID), initiating handshake and queueing message", category: .session) SecureLogger.log("🤝 No session with \(recipientID), initiating handshake and queueing message", category: SecureLogger.session, level: .debug)
// Queue the message (especially important for favorite notifications) // Queue the message (especially important for favorite notifications)
collectionsQueue.sync(flags: .barrier) { collectionsQueue.sync(flags: .barrier) {
@@ -888,7 +896,7 @@ final class BLEService: NSObject {
} }
} }
} catch { } catch {
SecureLogger.error("Failed to initiate handshake: \(error)") SecureLogger.log("Failed to initiate handshake: \(error)", category: SecureLogger.noise, level: .error)
} }
} }
@@ -902,7 +910,8 @@ final class BLEService: NSObject {
guard let messages = pendingMessages, !messages.isEmpty else { return } guard let messages = pendingMessages, !messages.isEmpty else { return }
SecureLogger.debug("📤 Sending \(messages.count) pending messages after handshake to \(peerID)", category: .session) SecureLogger.log("📤 Sending \(messages.count) pending messages after handshake to \(peerID)",
category: SecureLogger.session, level: .debug)
// Send each pending message directly (we know session is established) // Send each pending message directly (we know session is established)
for (content, messageID) in messages { for (content, messageID) in messages {
@@ -910,7 +919,7 @@ final class BLEService: NSObject {
// Use the same TLV format as normal sends to keep receiver decoding consistent // Use the same TLV format as normal sends to keep receiver decoding consistent
let privateMessage = PrivateMessagePacket(messageID: messageID, content: content) let privateMessage = PrivateMessagePacket(messageID: messageID, content: content)
guard let tlvData = privateMessage.encode() else { guard let tlvData = privateMessage.encode() else {
SecureLogger.error("Failed to encode pending private message TLV") SecureLogger.log("Failed to encode pending private message TLV", category: SecureLogger.noise, level: .error)
continue continue
} }
@@ -937,9 +946,11 @@ final class BLEService: NSObject {
self?.delegate?.didUpdateMessageDeliveryStatus(messageID, status: .sent) self?.delegate?.didUpdateMessageDeliveryStatus(messageID, status: .sent)
} }
SecureLogger.debug("✅ Sent pending message \(messageID) to \(peerID) after handshake", category: .session) SecureLogger.log("✅ Sent pending message \(messageID) to \(peerID) after handshake",
category: SecureLogger.session, level: .debug)
} catch { } catch {
SecureLogger.error("Failed to send pending message after handshake: \(error)") SecureLogger.log("Failed to send pending message after handshake: \(error)",
category: SecureLogger.noise, level: .error)
// Notify delegate of failure // Notify delegate of failure
notifyUI { [weak self] in notifyUI { [weak self] in
@@ -955,7 +966,7 @@ final class BLEService: NSObject {
// Encode once using a small per-type padding policy, then delegate by type // Encode once using a small per-type padding policy, then delegate by type
let padForBLE = padPolicy(for: packet.type) let padForBLE = padPolicy(for: packet.type)
guard let data = packet.toBinaryData(padding: padForBLE) else { guard let data = packet.toBinaryData(padding: padForBLE) else {
SecureLogger.error("❌ Failed to convert packet to binary data", category: .session) SecureLogger.log("❌ Failed to convert packet to binary data", category: SecureLogger.session, level: .error)
return return
} }
if packet.type == MessageType.noiseEncrypted.rawValue { if packet.type == MessageType.noiseEncrypted.rawValue {
@@ -1026,7 +1037,7 @@ final class BLEService: NSObject {
guard let self = self else { return } guard let self = self else { return }
if self.pendingNotifications.count < TransportConfig.blePendingNotificationsCapCount { if self.pendingNotifications.count < TransportConfig.blePendingNotificationsCapCount {
self.pendingNotifications.append((data: data, centrals: [central])) self.pendingNotifications.append((data: data, centrals: [central]))
SecureLogger.debug("📋 Queued encrypted packet for retry (notification queue full)", category: .session) SecureLogger.log("📋 Queued encrypted packet for retry (notification queue full)", category: SecureLogger.session, level: .debug)
} }
} }
} }
@@ -1139,7 +1150,7 @@ final class BLEService: NSObject {
if byMsg[msgID] == nil { if byMsg[msgID] == nil {
byMsg[msgID] = (packet: packet, enqueuedAt: Date()) byMsg[msgID] = (packet: packet, enqueuedAt: Date())
self.pendingDirectedRelays[recipientPeerID] = byMsg self.pendingDirectedRelays[recipientPeerID] = byMsg
SecureLogger.debug("🧳 Spooling directed packet for \(recipientPeerID) mid=\(msgID.prefix(8))", category: .session) SecureLogger.log("🧳 Spooling directed packet for \(recipientPeerID) mid=\(msgID.prefix(8))", category: SecureLogger.session, level: .debug)
} }
} }
} }
@@ -1311,7 +1322,7 @@ final class BLEService: NSObject {
if let originalPacket = BinaryProtocol.decode(reassembled) { if let originalPacket = BinaryProtocol.decode(reassembled) {
handleReceivedPacket(originalPacket, from: peerID) handleReceivedPacket(originalPacket, from: peerID)
} else { } else {
SecureLogger.error("❌ Failed to decode reassembled packet (type=\(originalType), total=\(total))", category: .session) SecureLogger.log("❌ Failed to decode reassembled packet (type=\(originalType), total=\(total))", category: SecureLogger.session, level: .error)
} }
// Cleanup // Cleanup
@@ -1331,7 +1342,8 @@ final class BLEService: NSObject {
// Only log non-announce packets to reduce noise // Only log non-announce packets to reduce noise
if packet.type != MessageType.announce.rawValue { if packet.type != MessageType.announce.rawValue {
// Log packet details for debugging // Log packet details for debugging
SecureLogger.debug("📦 Handling packet type \(packet.type) from \(senderID), messageID: \(messageID)", category: .session) SecureLogger.log("📦 Handling packet type \(packet.type) from \(senderID), messageID: \(messageID)",
category: SecureLogger.session, level: .debug)
} }
// Efficient deduplication // Efficient deduplication
@@ -1340,7 +1352,8 @@ final class BLEService: NSObject {
// Announce packets (type 1) are sent every 10 seconds for peer discovery // Announce packets (type 1) are sent every 10 seconds for peer discovery
// It's normal to see these as duplicates - don't log them to reduce noise // It's normal to see these as duplicates - don't log them to reduce noise
if packet.type != MessageType.announce.rawValue { if packet.type != MessageType.announce.rawValue {
SecureLogger.debug("⚠️ Duplicate packet ignored: \(messageID)", category: .session) SecureLogger.log("⚠️ Duplicate packet ignored: \(messageID)",
category: SecureLogger.session, level: .debug)
} }
// In sparse graphs (<=2 neighbors), keep the pending relay to ensure bridging. // In sparse graphs (<=2 neighbors), keep the pending relay to ensure bridging.
// In denser graphs, cancel the pending relay to reduce redundant floods. // In denser graphs, cancel the pending relay to reduce redundant floods.
@@ -1393,7 +1406,7 @@ final class BLEService: NSObject {
handleLeave(packet, from: senderID) handleLeave(packet, from: senderID)
default: default:
SecureLogger.warning("⚠️ Unknown message type: \(packet.type)", category: .session) SecureLogger.log("⚠️ Unknown message type: \(packet.type)", category: SecureLogger.session, level: .warning)
break break
} }
@@ -1433,7 +1446,7 @@ final class BLEService: NSObject {
private func handleAnnounce(_ packet: BitchatPacket, from peerID: String) { private func handleAnnounce(_ packet: BitchatPacket, from peerID: String) {
guard let announcement = AnnouncementPacket.decode(from: packet.payload) else { guard let announcement = AnnouncementPacket.decode(from: packet.payload) else {
SecureLogger.error("❌ Failed to decode announce packet from \(peerID)", category: .session) SecureLogger.log("❌ Failed to decode announce packet from \(peerID)", category: SecureLogger.session, level: .error)
return return
} }
@@ -1441,7 +1454,7 @@ final class BLEService: NSObject {
// This helps detect relayed or spoofed announces. Only warn in release; assert in debug. // This helps detect relayed or spoofed announces. Only warn in release; assert in debug.
let derivedFromKey = PeerIDUtils.derivePeerID(fromPublicKey: announcement.noisePublicKey) let derivedFromKey = PeerIDUtils.derivePeerID(fromPublicKey: announcement.noisePublicKey)
if derivedFromKey != peerID { if derivedFromKey != peerID {
SecureLogger.warning("⚠️ Announce sender mismatch: derived \(derivedFromKey.prefix(8))… vs packet \(peerID.prefix(8))", category: .security) SecureLogger.log("⚠️ Announce sender mismatch: derived \(derivedFromKey.prefix(8))… vs packet \(peerID.prefix(8))", category: SecureLogger.security, level: .warning)
} }
@@ -1458,11 +1471,11 @@ final class BLEService: NSObject {
if packet.signature != nil { if packet.signature != nil {
verifiedAnnounce = noiseService.verifyPacketSignature(packet, publicKey: announcement.signingPublicKey) verifiedAnnounce = noiseService.verifyPacketSignature(packet, publicKey: announcement.signingPublicKey)
if !verifiedAnnounce { if !verifiedAnnounce {
SecureLogger.warning("⚠️ Signature verification for announce failed \(peerID.prefix(8))", category: .security) SecureLogger.log("⚠️ Signature verification for announce failed \(peerID.prefix(8))", category: SecureLogger.security, level: .warning)
} }
} }
if let existingKey = existingPeerForVerify?.noisePublicKey, existingKey != announcement.noisePublicKey { if let existingKey = existingPeerForVerify?.noisePublicKey, existingKey != announcement.noisePublicKey {
SecureLogger.warning("⚠️ Announce key mismatch for \(peerID.prefix(8))… — keeping unverified", category: .security) SecureLogger.log("⚠️ Announce key mismatch for \(peerID.prefix(8))… — keeping unverified", category: SecureLogger.security, level: .warning)
verifiedAnnounce = false verifiedAnnounce = false
} }
@@ -1495,7 +1508,7 @@ final class BLEService: NSObject {
// Require verified announce; ignore otherwise (no backward compatibility) // Require verified announce; ignore otherwise (no backward compatibility)
if !verified { if !verified {
SecureLogger.warning("❌ Ignoring unverified announce from \(peerID.prefix(8))", category: .security) SecureLogger.log("❌ Ignoring unverified announce from \(peerID.prefix(8))", category: SecureLogger.security, level: .warning)
return return
} }
@@ -1528,17 +1541,17 @@ final class BLEService: NSObject {
if isDirectAnnounce || hasPeripheralConnection || hasCentralSubscription { if isDirectAnnounce || hasPeripheralConnection || hasCentralSubscription {
let now = Date() let now = Date()
if existingPeer == nil { if existingPeer == nil {
SecureLogger.debug("🆕 New peer: \(announcement.nickname)", category: .session) SecureLogger.log("🆕 New peer: \(announcement.nickname)", category: SecureLogger.session, level: .debug)
} else if wasDisconnected { } else if wasDisconnected {
// Debounce 'reconnected' logs within short window // Debounce 'reconnected' logs within short window
if let last = lastReconnectLogAt[peerID], now.timeIntervalSince(last) < TransportConfig.bleReconnectLogDebounceSeconds { if let last = lastReconnectLogAt[peerID], now.timeIntervalSince(last) < TransportConfig.bleReconnectLogDebounceSeconds {
// Skip duplicate log // Skip duplicate log
} else { } else {
SecureLogger.debug("🔄 Peer \(announcement.nickname) reconnected", category: .session) SecureLogger.log("🔄 Peer \(announcement.nickname) reconnected", category: SecureLogger.session, level: .debug)
lastReconnectLogAt[peerID] = now lastReconnectLogAt[peerID] = now
} }
} else if existingPeer?.nickname != announcement.nickname { } else if existingPeer?.nickname != announcement.nickname {
SecureLogger.debug("🔄 Peer \(peerID) changed nickname: \(existingPeer?.nickname ?? "Unknown") -> \(announcement.nickname)", category: .session) SecureLogger.log("🔄 Peer \(peerID) changed nickname: \(existingPeer?.nickname ?? "Unknown") -> \(announcement.nickname)", category: SecureLogger.session, level: .debug)
} }
} }
} }
@@ -1648,12 +1661,12 @@ final class BLEService: NSObject {
} }
guard accepted else { guard accepted else {
SecureLogger.warning("🚫 Dropping public message from unverified or unknown peer \(peerID.prefix(8))", category: .security) SecureLogger.log("🚫 Dropping public message from unverified or unknown peer \(peerID.prefix(8))", category: SecureLogger.security, level: .warning)
return return
} }
guard let content = String(data: packet.payload, encoding: .utf8) else { guard let content = String(data: packet.payload, encoding: .utf8) else {
SecureLogger.error("❌ Failed to decode message payload as UTF-8", category: .session) SecureLogger.log("❌ Failed to decode message payload as UTF-8", category: SecureLogger.session, level: .error)
return return
} }
// Determine if we have a direct link to the sender // Determine if we have a direct link to the sender
@@ -1665,7 +1678,7 @@ final class BLEService: NSObject {
} }
let pathTag = hasDirectLink ? "direct" : "mesh" let pathTag = hasDirectLink ? "direct" : "mesh"
SecureLogger.debug("💬 [\(senderNickname)] TTL:\(packet.ttl) (\(pathTag)): \(String(content.prefix(50)))\(content.count > 50 ? "..." : "")", category: .session) SecureLogger.log("💬 [\(senderNickname)] TTL:\(packet.ttl) (\(pathTag)): \(String(content.prefix(50)))\(content.count > 50 ? "..." : "")", category: SecureLogger.session, level: .debug)
let ts = Date(timeIntervalSince1970: Double(packet.timestamp) / 1000) let ts = Date(timeIntervalSince1970: Double(packet.timestamp) / 1000)
notifyUI { [weak self] in notifyUI { [weak self] in
@@ -1697,7 +1710,7 @@ final class BLEService: NSObject {
// Session establishment will trigger onPeerAuthenticated callback // Session establishment will trigger onPeerAuthenticated callback
// which will send any pending messages at the right time // which will send any pending messages at the right time
} catch { } catch {
SecureLogger.error("Failed to process handshake: \(error)") SecureLogger.log("Failed to process handshake: \(error)", category: SecureLogger.noise, level: .error)
// Try initiating a new handshake // Try initiating a new handshake
if !noiseService.hasSession(with: peerID) { if !noiseService.hasSession(with: peerID) {
initiateNoiseHandshake(with: peerID) initiateNoiseHandshake(with: peerID)
@@ -1707,16 +1720,17 @@ final class BLEService: NSObject {
} }
private func handleNoiseEncrypted(_ packet: BitchatPacket, from peerID: String) { private func handleNoiseEncrypted(_ packet: BitchatPacket, from peerID: String) {
SecureLogger.debug("🔐 handleNoiseEncrypted called for packet from \(peerID)") SecureLogger.log("🔐 handleNoiseEncrypted called for packet from \(peerID)",
category: SecureLogger.noise, level: .debug)
guard let recipientID = packet.recipientID else { guard let recipientID = packet.recipientID else {
SecureLogger.warning("⚠️ Encrypted message has no recipient ID", category: .session) SecureLogger.log("⚠️ Encrypted message has no recipient ID", category: SecureLogger.session, level: .warning)
return return
} }
let recipientHex = recipientID.hexEncodedString() let recipientHex = recipientID.hexEncodedString()
if recipientHex != myPeerID { if recipientHex != myPeerID {
SecureLogger.debug("🔐 Encrypted message not for me (for \(recipientHex), I am \(myPeerID))", category: .session) SecureLogger.log("🔐 Encrypted message not for me (for \(recipientHex), I am \(myPeerID))", category: SecureLogger.session, level: .debug)
return return
} }
@@ -1758,17 +1772,19 @@ final class BLEService: NSObject {
self?.delegate?.didReceiveNoisePayload(from: peerID, type: .verifyResponse, payload: Data(payloadData), timestamp: ts) self?.delegate?.didReceiveNoisePayload(from: peerID, type: .verifyResponse, payload: Data(payloadData), timestamp: ts)
} }
default: default:
SecureLogger.warning("⚠️ Unknown noise payload type: \(payloadType)") SecureLogger.log("⚠️ Unknown noise payload type: \(payloadType)", category: SecureLogger.noise, level: .warning)
} }
} catch NoiseEncryptionError.sessionNotEstablished { } catch NoiseEncryptionError.sessionNotEstablished {
// We received an encrypted message before establishing a session with this peer. // We received an encrypted message before establishing a session with this peer.
// Trigger a handshake so future messages can be decrypted. // Trigger a handshake so future messages can be decrypted.
SecureLogger.debug("🔑 Encrypted message from \(peerID) without session; initiating handshake") SecureLogger.log("🔑 Encrypted message from \(peerID) without session; initiating handshake",
category: SecureLogger.noise, level: .debug)
if !noiseService.hasSession(with: peerID) { if !noiseService.hasSession(with: peerID) {
initiateNoiseHandshake(with: peerID) initiateNoiseHandshake(with: peerID)
} }
} catch { } catch {
SecureLogger.error("❌ Failed to decrypt message from \(peerID): \(error)") SecureLogger.log("❌ Failed to decrypt message from \(peerID): \(error)",
category: SecureLogger.noise, level: .error)
} }
} }
@@ -1792,7 +1808,7 @@ final class BLEService: NSObject {
// MARK: - Helper Functions // MARK: - Helper Functions
private func sendLeave() { private func sendLeave() {
SecureLogger.debug("👋 Sending leave announcement", category: .session) SecureLogger.log("👋 Sending leave announcement", category: SecureLogger.session, level: .debug)
let packet = BitchatPacket( let packet = BitchatPacket(
type: MessageType.leave.rawValue, type: MessageType.leave.rawValue,
ttl: messageTTL, ttl: messageTTL,
@@ -1829,7 +1845,7 @@ final class BLEService: NSObject {
) )
guard let payload = announcement.encode() else { guard let payload = announcement.encode() else {
SecureLogger.error("❌ Failed to encode announce packet", category: .session) SecureLogger.log("❌ Failed to encode announce packet", category: SecureLogger.session, level: .error)
return return
} }
@@ -1846,7 +1862,7 @@ final class BLEService: NSObject {
// Sign the packet using the noise private key // Sign the packet using the noise private key
guard let signedPacket = noiseService.signPacket(packet) else { guard let signedPacket = noiseService.signPacket(packet) else {
SecureLogger.error("❌ Failed to sign announce packet", category: .security) SecureLogger.log("❌ Failed to sign announce packet", category: SecureLogger.security, level: .error)
return return
} }
@@ -1879,7 +1895,7 @@ final class BLEService: NSObject {
) )
broadcastPacket(packet) broadcastPacket(packet)
} catch { } catch {
SecureLogger.error("Failed to send delivery ACK: \(error)") SecureLogger.log("Failed to send delivery ACK: \(error)", category: SecureLogger.noise, level: .error)
} }
} else { } else {
// Queue for after handshake and initiate if needed // Queue for after handshake and initiate if needed
@@ -1888,7 +1904,8 @@ final class BLEService: NSObject {
self.pendingNoisePayloadsAfterHandshake[peerID, default: []].append(payload) self.pendingNoisePayloadsAfterHandshake[peerID, default: []].append(payload)
} }
if !noiseService.hasSession(with: peerID) { initiateNoiseHandshake(with: peerID) } if !noiseService.hasSession(with: peerID) { initiateNoiseHandshake(with: peerID) }
SecureLogger.debug("🕒 Queued DELIVERED ack for \(peerID) until handshake completes", category: .session) SecureLogger.log("🕒 Queued DELIVERED ack for \(peerID) until handshake completes",
category: SecureLogger.session, level: .debug)
} }
} }
@@ -1899,7 +1916,8 @@ final class BLEService: NSObject {
return list return list
} }
guard !payloads.isEmpty else { return } guard !payloads.isEmpty else { return }
SecureLogger.debug("📤 Sending \(payloads.count) pending noise payloads to \(peerID) after handshake", category: .session) SecureLogger.log("📤 Sending \(payloads.count) pending noise payloads to \(peerID) after handshake",
category: SecureLogger.session, level: .debug)
for payload in payloads { for payload in payloads {
do { do {
let encrypted = try noiseService.encrypt(payload, for: peerID) let encrypted = try noiseService.encrypt(payload, for: peerID)
@@ -1914,7 +1932,8 @@ final class BLEService: NSObject {
) )
broadcastPacket(packet) broadcastPacket(packet)
} catch { } catch {
SecureLogger.error("❌ Failed to send pending noise payload to \(peerID): \(error)") SecureLogger.log("❌ Failed to send pending noise payload to \(peerID): \(error)",
category: SecureLogger.noise, level: .error)
} }
} }
} }
@@ -2084,7 +2103,8 @@ final class BLEService: NSObject {
// Cleanup: remove peers that are not connected and past reachability retention // Cleanup: remove peers that are not connected and past reachability retention
if !peer.isConnected { if !peer.isConnected {
if age > retention { if age > retention {
SecureLogger.debug("🗑️ Removing stale peer after reachability window: \(peerID) (\(peer.nickname))", category: .session) SecureLogger.log("🗑️ Removing stale peer after reachability window: \(peerID) (\(peer.nickname))",
category: SecureLogger.session, level: .debug)
peers.removeValue(forKey: peerID) peers.removeValue(forKey: peerID)
removedOfflineCount += 1 removedOfflineCount += 1
} }
@@ -2375,7 +2395,8 @@ extension BLEService: CBCentralManagerDelegate {
peripheral.delegate = self peripheral.delegate = self
// Connect to the peripheral with options for faster connection // Connect to the peripheral with options for faster connection
SecureLogger.debug("📱 Connect: \(advertisedName) [RSSI:\(rssiValue)]", category: .session) SecureLogger.log("📱 Connect: \(advertisedName) [RSSI:\(rssiValue)]",
category: SecureLogger.session, level: .debug)
// Use connection options for faster reconnection // Use connection options for faster reconnection
let options: [String: Any] = [ let options: [String: Any] = [
@@ -2394,7 +2415,8 @@ extension BLEService: CBCentralManagerDelegate {
state.isConnecting && !state.isConnected else { return } state.isConnecting && !state.isConnected else { return }
// Connection timed out - cancel it // Connection timed out - cancel it
SecureLogger.debug("⏱️ Timeout: \(advertisedName)", category: .session) SecureLogger.log("⏱️ Timeout: \(advertisedName)",
category: SecureLogger.session, level: .debug)
central.cancelPeripheralConnection(peripheral) central.cancelPeripheralConnection(peripheral)
self.peripherals[peripheralID] = nil self.peripherals[peripheralID] = nil
self.recentConnectTimeouts[peripheralID] = Date() self.recentConnectTimeouts[peripheralID] = Date()
@@ -2427,7 +2449,7 @@ func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeriph
failureCounts[peripheralID] = 0 failureCounts[peripheralID] = 0
recentConnectTimeouts.removeValue(forKey: peripheralID) recentConnectTimeouts.removeValue(forKey: peripheralID)
SecureLogger.debug("✅ Connected: \(peripheral.name ?? "Unknown") [\(peripheralID)]", category: .session) SecureLogger.log("✅ Connected: \(peripheral.name ?? "Unknown") [\(peripheralID)]", category: SecureLogger.session, level: .debug)
// Discover services // Discover services
peripheral.discoverServices([BLEService.serviceUUID]) peripheral.discoverServices([BLEService.serviceUUID])
@@ -2439,7 +2461,8 @@ func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeriph
// Find the peer ID if we have it // Find the peer ID if we have it
let peerID = peripherals[peripheralID]?.peerID let peerID = peripherals[peripheralID]?.peerID
SecureLogger.debug("📱 Disconnect: \(peerID ?? peripheralID)\(error != nil ? " (\(error!.localizedDescription))" : "")", category: .session) SecureLogger.log("📱 Disconnect: \(peerID ?? peripheralID)\(error != nil ? " (\(error!.localizedDescription))" : "")",
category: SecureLogger.session, level: .debug)
// If disconnect carried an error (often timeout), apply short backoff to avoid thrash // If disconnect carried an error (often timeout), apply short backoff to avoid thrash
if error != nil { if error != nil {
@@ -2494,7 +2517,7 @@ func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeriph
// Clean up the references // Clean up the references
peripherals.removeValue(forKey: peripheralID) peripherals.removeValue(forKey: peripheralID)
SecureLogger.error("❌ Failed to connect to peripheral: \(peripheral.name ?? "Unknown") [\(peripheralID)] - Error: \(error?.localizedDescription ?? "Unknown")", category: .session) SecureLogger.log("❌ Failed to connect to peripheral: \(peripheral.name ?? "Unknown") [\(peripheralID)] - Error: \(error?.localizedDescription ?? "Unknown")", category: SecureLogger.session, level: .error)
failureCounts[peripheralID, default: 0] += 1 failureCounts[peripheralID, default: 0] += 1
// Try next candidate // Try next candidate
bleQueue.async { [weak self] in self?.tryConnectFromQueue() } bleQueue.async { [weak self] in self?.tryConnectFromQueue() }
@@ -2567,7 +2590,7 @@ extension BLEService {
] ]
central.connect(peripheral, options: options) central.connect(peripheral, options: options)
lastGlobalConnectAttempt = Date() lastGlobalConnectAttempt = Date()
SecureLogger.debug("⏩ Queue connect: \(candidate.name) [RSSI:\(candidate.rssi)]", category: .session) SecureLogger.log("⏩ Queue connect: \(candidate.name) [RSSI:\(candidate.rssi)]", category: SecureLogger.session, level: .debug)
} }
} }
@@ -2612,7 +2635,7 @@ extension BLEService {
extension BLEService: CBPeripheralDelegate { extension BLEService: CBPeripheralDelegate {
func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) { func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) {
if let error = error { if let error = error {
SecureLogger.error("❌ Error discovering services for \(peripheral.name ?? "Unknown"): \(error.localizedDescription)", category: .session) SecureLogger.log("❌ Error discovering services for \(peripheral.name ?? "Unknown"): \(error.localizedDescription)", category: SecureLogger.session, level: .error)
// Retry service discovery after a delay // Retry service discovery after a delay
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
guard peripheral.state == .connected else { return } guard peripheral.state == .connected else { return }
@@ -2622,7 +2645,7 @@ extension BLEService: CBPeripheralDelegate {
} }
guard let services = peripheral.services else { guard let services = peripheral.services else {
SecureLogger.warning("⚠️ No services discovered for \(peripheral.name ?? "Unknown")", category: .session) SecureLogger.log("⚠️ No services discovered for \(peripheral.name ?? "Unknown")", category: SecureLogger.session, level: .warning)
return return
} }
@@ -2638,12 +2661,12 @@ extension BLEService: CBPeripheralDelegate {
func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) { func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) {
if let error = error { if let error = error {
SecureLogger.error("❌ Error discovering characteristics for \(peripheral.name ?? "Unknown"): \(error.localizedDescription)", category: .session) SecureLogger.log("❌ Error discovering characteristics for \(peripheral.name ?? "Unknown"): \(error.localizedDescription)", category: SecureLogger.session, level: .error)
return return
} }
guard let characteristic = service.characteristics?.first(where: { $0.uuid == BLEService.characteristicUUID }) else { guard let characteristic = service.characteristics?.first(where: { $0.uuid == BLEService.characteristicUUID }) else {
SecureLogger.warning("⚠️ No matching characteristic found for \(peripheral.name ?? "Unknown")", category: .session) SecureLogger.log("⚠️ No matching characteristic found for \(peripheral.name ?? "Unknown")", category: SecureLogger.session, level: .warning)
return return
} }
@@ -2660,7 +2683,7 @@ extension BLEService: CBPeripheralDelegate {
// Verify characteristic supports reliable writes // Verify characteristic supports reliable writes
if !characteristic.properties.contains(.write) { if !characteristic.properties.contains(.write) {
SecureLogger.warning("⚠️ Characteristic doesn't support reliable writes (withResponse)!", category: .session) SecureLogger.log("⚠️ Characteristic doesn't support reliable writes (withResponse)!", category: SecureLogger.session, level: .warning)
} }
// Store characteristic in our consolidated structure // Store characteristic in our consolidated structure
@@ -2673,7 +2696,7 @@ extension BLEService: CBPeripheralDelegate {
// Subscribe for notifications // Subscribe for notifications
if characteristic.properties.contains(.notify) { if characteristic.properties.contains(.notify) {
peripheral.setNotifyValue(true, for: characteristic) peripheral.setNotifyValue(true, for: characteristic)
SecureLogger.debug("🔔 Subscribed to notifications from \(peripheral.name ?? "Unknown")", category: .session) SecureLogger.log("🔔 Subscribed to notifications from \(peripheral.name ?? "Unknown")", category: SecureLogger.session, level: .debug)
// Send announce after subscription is confirmed (force send for new connection) // Send announce after subscription is confirmed (force send for new connection)
messageQueue.asyncAfter(deadline: .now() + TransportConfig.blePostSubscribeAnnounceDelaySeconds) { [weak self] in messageQueue.asyncAfter(deadline: .now() + TransportConfig.blePostSubscribeAnnounceDelaySeconds) { [weak self] in
@@ -2684,18 +2707,18 @@ extension BLEService: CBPeripheralDelegate {
self?.rebroadcastRecentAnnounces() self?.rebroadcastRecentAnnounces()
} }
} else { } else {
SecureLogger.warning("⚠️ Characteristic does not support notifications", category: .session) SecureLogger.log("⚠️ Characteristic does not support notifications", category: SecureLogger.session, level: .warning)
} }
} }
func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) { func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) {
if let error = error { if let error = error {
SecureLogger.error("❌ Error receiving notification: \(error.localizedDescription)", category: .session) SecureLogger.log("❌ Error receiving notification: \(error.localizedDescription)", category: SecureLogger.session, level: .error)
return return
} }
guard let data = characteristic.value else { guard let data = characteristic.value else {
SecureLogger.warning("⚠️ No data in notification", category: .session) SecureLogger.log("⚠️ No data in notification", category: SecureLogger.session, level: .warning)
return return
} }
@@ -2705,7 +2728,8 @@ extension BLEService: CBPeripheralDelegate {
guard let packet = BinaryProtocol.decode(data) else { guard let packet = BinaryProtocol.decode(data) else {
// Avoid dumping entire payload; log size and short prefix for diagnostics // Avoid dumping entire payload; log size and short prefix for diagnostics
let prefix = data.prefix(16).map { String(format: "%02x", $0) }.joined(separator: " ") let prefix = data.prefix(16).map { String(format: "%02x", $0) }.joined(separator: " ")
SecureLogger.error("❌ Failed to decode notification packet (len=\(data.count), prefix=\(prefix))", category: .session) SecureLogger.log("❌ Failed to decode notification packet (len=\(data.count), prefix=\(prefix))",
category: SecureLogger.session, level: .error)
return return
} }
@@ -2713,7 +2737,7 @@ extension BLEService: CBPeripheralDelegate {
let senderID = packet.senderID.hexEncodedString() let senderID = packet.senderID.hexEncodedString()
// Only log non-announce packets // Only log non-announce packets
if packet.type != MessageType.announce.rawValue { if packet.type != MessageType.announce.rawValue {
SecureLogger.debug("📦 Decoded notification packet type: \(packet.type) from sender: \(senderID)", category: .session) SecureLogger.log("📦 Decoded notification packet type: \(packet.type) from sender: \(senderID)", category: SecureLogger.session, level: .debug)
} }
let peripheralUUID = peripheral.identifier.uuidString let peripheralUUID = peripheral.identifier.uuidString
@@ -2751,10 +2775,10 @@ extension BLEService: CBPeripheralDelegate {
func peripheral(_ peripheral: CBPeripheral, didWriteValueFor characteristic: CBCharacteristic, error: Error?) { func peripheral(_ peripheral: CBPeripheral, didWriteValueFor characteristic: CBCharacteristic, error: Error?) {
if let error = error { if let error = error {
SecureLogger.error("❌ Write failed to \(peripheral.name ?? peripheral.identifier.uuidString): \(error.localizedDescription)", category: .session) SecureLogger.log("❌ Write failed to \(peripheral.name ?? peripheral.identifier.uuidString): \(error.localizedDescription)", category: SecureLogger.session, level: .error)
// Don't retry - just log the error // Don't retry - just log the error
} else { } else {
SecureLogger.debug("✅ Write confirmed to \(peripheral.name ?? peripheral.identifier.uuidString)", category: .session) SecureLogger.log("✅ Write confirmed to \(peripheral.name ?? peripheral.identifier.uuidString)", category: SecureLogger.session, level: .debug)
} }
} }
@@ -2764,14 +2788,14 @@ extension BLEService: CBPeripheralDelegate {
} }
func peripheral(_ peripheral: CBPeripheral, didModifyServices invalidatedServices: [CBService]) { func peripheral(_ peripheral: CBPeripheral, didModifyServices invalidatedServices: [CBService]) {
SecureLogger.warning("⚠️ Services modified for \(peripheral.name ?? peripheral.identifier.uuidString)", category: .session) SecureLogger.log("⚠️ Services modified for \(peripheral.name ?? peripheral.identifier.uuidString)", category: SecureLogger.session, level: .warning)
// Check if our service was invalidated (peer app quit) // Check if our service was invalidated (peer app quit)
let hasOurService = peripheral.services?.contains { $0.uuid == BLEService.serviceUUID } ?? false let hasOurService = peripheral.services?.contains { $0.uuid == BLEService.serviceUUID } ?? false
if !hasOurService { if !hasOurService {
// Service is gone - disconnect // Service is gone - disconnect
SecureLogger.warning("❌ BitChat service removed - disconnecting from \(peripheral.name ?? peripheral.identifier.uuidString)", category: .session) SecureLogger.log("❌ BitChat service removed - disconnecting from \(peripheral.name ?? peripheral.identifier.uuidString)", category: SecureLogger.session, level: .warning)
centralManager?.cancelPeripheralConnection(peripheral) centralManager?.cancelPeripheralConnection(peripheral)
} else { } else {
// Try to rediscover // Try to rediscover
@@ -2781,9 +2805,9 @@ extension BLEService: CBPeripheralDelegate {
func peripheral(_ peripheral: CBPeripheral, didUpdateNotificationStateFor characteristic: CBCharacteristic, error: Error?) { func peripheral(_ peripheral: CBPeripheral, didUpdateNotificationStateFor characteristic: CBCharacteristic, error: Error?) {
if let error = error { if let error = error {
SecureLogger.error("❌ Error updating notification state: \(error.localizedDescription)", category: .session) SecureLogger.log("❌ Error updating notification state: \(error.localizedDescription)", category: SecureLogger.session, level: .error)
} else { } else {
SecureLogger.debug("🔔 Notification state updated for \(peripheral.name ?? peripheral.identifier.uuidString): \(characteristic.isNotifying ? "ON" : "OFF")", category: .session) SecureLogger.log("🔔 Notification state updated for \(peripheral.name ?? peripheral.identifier.uuidString): \(characteristic.isNotifying ? "ON" : "OFF")", category: SecureLogger.session, level: .debug)
// If notifications are now on, send an announce to ensure this peer knows about us // If notifications are now on, send an announce to ensure this peer knows about us
if characteristic.isNotifying { if characteristic.isNotifying {
@@ -2798,7 +2822,7 @@ extension BLEService: CBPeripheralDelegate {
extension BLEService: CBPeripheralManagerDelegate { extension BLEService: CBPeripheralManagerDelegate {
func peripheralManagerDidUpdateState(_ peripheral: CBPeripheralManager) { func peripheralManagerDidUpdateState(_ peripheral: CBPeripheralManager) {
SecureLogger.debug("📡 Peripheral manager state: \(peripheral.state.rawValue)", category: .session) SecureLogger.log("📡 Peripheral manager state: \(peripheral.state.rawValue)", category: SecureLogger.session, level: .debug)
if peripheral.state == .poweredOn { if peripheral.state == .poweredOn {
// Remove all services first to ensure clean state // Remove all services first to ensure clean state
@@ -2817,28 +2841,28 @@ extension BLEService: CBPeripheralManagerDelegate {
service.characteristics = [characteristic!] service.characteristics = [characteristic!]
// Add service (advertising will start in didAdd delegate) // Add service (advertising will start in didAdd delegate)
SecureLogger.debug("🔧 Adding BLE service...", category: .session) SecureLogger.log("🔧 Adding BLE service...", category: SecureLogger.session, level: .debug)
peripheral.add(service) peripheral.add(service)
} }
} }
func peripheralManager(_ peripheral: CBPeripheralManager, didAdd service: CBService, error: Error?) { func peripheralManager(_ peripheral: CBPeripheralManager, didAdd service: CBService, error: Error?) {
if let error = error { if let error = error {
SecureLogger.error("❌ Failed to add service: \(error.localizedDescription)", category: .session) SecureLogger.log("❌ Failed to add service: \(error.localizedDescription)", category: SecureLogger.session, level: .error)
return return
} }
SecureLogger.debug("✅ Service added successfully, starting advertising", category: .session) SecureLogger.log("✅ Service added successfully, starting advertising", category: SecureLogger.session, level: .debug)
// Start advertising after service is confirmed added // Start advertising after service is confirmed added
let adData = buildAdvertisementData() let adData = buildAdvertisementData()
peripheral.startAdvertising(adData) peripheral.startAdvertising(adData)
SecureLogger.debug("📡 Started advertising (LocalName: \((adData[CBAdvertisementDataLocalNameKey] as? String) != nil ? "on" : "off"), ID: \(myPeerID.prefix(8))…)", category: .session) SecureLogger.log("📡 Started advertising (LocalName: \((adData[CBAdvertisementDataLocalNameKey] as? String) != nil ? "on" : "off"), ID: \(myPeerID.prefix(8))…)", category: SecureLogger.session, level: .debug)
} }
func peripheralManager(_ peripheral: CBPeripheralManager, central: CBCentral, didSubscribeTo characteristic: CBCharacteristic) { func peripheralManager(_ peripheral: CBPeripheralManager, central: CBCentral, didSubscribeTo characteristic: CBCharacteristic) {
SecureLogger.debug("📥 Central subscribed: \(central.identifier.uuidString)", category: .session) SecureLogger.log("📥 Central subscribed: \(central.identifier.uuidString)", category: SecureLogger.session, level: .debug)
subscribedCentrals.append(central) subscribedCentrals.append(central)
// Send announce to the newly subscribed central after a small delay to avoid overwhelming // Send announce to the newly subscribed central after a small delay to avoid overwhelming
messageQueue.asyncAfter(deadline: .now() + TransportConfig.blePostAnnounceDelaySeconds) { [weak self] in messageQueue.asyncAfter(deadline: .now() + TransportConfig.blePostAnnounceDelaySeconds) { [weak self] in
@@ -2851,12 +2875,12 @@ extension BLEService: CBPeripheralManagerDelegate {
} }
func peripheralManager(_ peripheral: CBPeripheralManager, central: CBCentral, didUnsubscribeFrom characteristic: CBCharacteristic) { func peripheralManager(_ peripheral: CBPeripheralManager, central: CBCentral, didUnsubscribeFrom characteristic: CBCharacteristic) {
SecureLogger.debug("📤 Central unsubscribed: \(central.identifier.uuidString)", category: .session) SecureLogger.log("📤 Central unsubscribed: \(central.identifier.uuidString)", category: SecureLogger.session, level: .debug)
subscribedCentrals.removeAll { $0.identifier == central.identifier } subscribedCentrals.removeAll { $0.identifier == central.identifier }
// Ensure we're still advertising for other devices to find us // Ensure we're still advertising for other devices to find us
if peripheral.isAdvertising == false { if peripheral.isAdvertising == false {
SecureLogger.debug("📡 Restarting advertising after central unsubscribed", category: .session) SecureLogger.log("📡 Restarting advertising after central unsubscribed", category: SecureLogger.session, level: .debug)
peripheral.startAdvertising(buildAdvertisementData()) peripheral.startAdvertising(buildAdvertisementData())
} }
@@ -2890,7 +2914,7 @@ extension BLEService: CBPeripheralManagerDelegate {
} }
func peripheralManagerIsReady(toUpdateSubscribers peripheral: CBPeripheralManager) { func peripheralManagerIsReady(toUpdateSubscribers peripheral: CBPeripheralManager) {
SecureLogger.debug("📤 Peripheral manager ready to send more notifications", category: .session) SecureLogger.log("📤 Peripheral manager ready to send more notifications", category: SecureLogger.session, level: .debug)
// Retry pending notifications now that queue has space // Retry pending notifications now that queue has space
collectionsQueue.async(flags: .barrier) { [weak self] in collectionsQueue.async(flags: .barrier) { [weak self] in
@@ -2909,10 +2933,12 @@ extension BLEService: CBPeripheralManagerDelegate {
if !success { if !success {
// Still full, re-queue // Still full, re-queue
self.pendingNotifications.append((data: data, centrals: centrals)) self.pendingNotifications.append((data: data, centrals: centrals))
SecureLogger.debug("⚠️ Notification queue still full, re-queuing", category: .session) SecureLogger.log("⚠️ Notification queue still full, re-queuing",
category: SecureLogger.session, level: .debug)
break // Stop trying, wait for next ready callback break // Stop trying, wait for next ready callback
} else { } else {
SecureLogger.debug("✅ Sent pending notification from retry queue", category: .session) SecureLogger.log("✅ Sent pending notification from retry queue",
category: SecureLogger.session, level: .debug)
} }
} else { } else {
// Broadcast to all // Broadcast to all
@@ -2926,7 +2952,8 @@ extension BLEService: CBPeripheralManagerDelegate {
} }
if !self.pendingNotifications.isEmpty { if !self.pendingNotifications.isEmpty {
SecureLogger.debug("📋 Still have \(self.pendingNotifications.count) pending notifications", category: .session) SecureLogger.log("📋 Still have \(self.pendingNotifications.count) pending notifications",
category: SecureLogger.session, level: .debug)
} }
} }
} }
@@ -2934,7 +2961,7 @@ extension BLEService: CBPeripheralManagerDelegate {
func peripheralManager(_ peripheral: CBPeripheralManager, didReceiveWrite requests: [CBATTRequest]) { func peripheralManager(_ peripheral: CBPeripheralManager, didReceiveWrite requests: [CBATTRequest]) {
// Suppress logs for single write requests to reduce noise // Suppress logs for single write requests to reduce noise
if requests.count > 1 { if requests.count > 1 {
SecureLogger.debug("📥 Received \(requests.count) write requests from central", category: .session) SecureLogger.log("📥 Received \(requests.count) write requests from central", category: SecureLogger.session, level: .debug)
} }
// IMPORTANT: Respond immediately to prevent timeouts! // IMPORTANT: Respond immediately to prevent timeouts!
@@ -2973,7 +3000,7 @@ extension BLEService: CBPeripheralManagerDelegate {
if combined.count >= 2 { if combined.count >= 2 {
let peekType = combined[1] let peekType = combined[1]
if peekType != MessageType.announce.rawValue { if peekType != MessageType.announce.rawValue {
SecureLogger.debug("📥 Accumulated write from central \(centralUUID): size=\(combined.count) (+\(appendedBytes)) bytes (type=\(peekType)), offsets=\(offsets)", category: .session) SecureLogger.log("📥 Accumulated write from central \(centralUUID): size=\(combined.count) (+\(appendedBytes)) bytes (type=\(peekType)), offsets=\(offsets)", category: SecureLogger.session, level: .debug)
} }
} }
@@ -2983,7 +3010,7 @@ extension BLEService: CBPeripheralManagerDelegate {
pendingWriteBuffers.removeValue(forKey: centralUUID) pendingWriteBuffers.removeValue(forKey: centralUUID)
let senderID = packet.senderID.hexEncodedString() let senderID = packet.senderID.hexEncodedString()
if packet.type != MessageType.announce.rawValue { if packet.type != MessageType.announce.rawValue {
SecureLogger.debug("📦 Decoded (combined) packet type: \(packet.type) from sender: \(senderID)", category: .session) SecureLogger.log("📦 Decoded (combined) packet type: \(packet.type) from sender: \(senderID)", category: SecureLogger.session, level: .debug)
} }
if !subscribedCentrals.contains(sorted[0].central) { if !subscribedCentrals.contains(sorted[0].central) {
subscribedCentrals.append(sorted[0].central) subscribedCentrals.append(sorted[0].central)
@@ -3008,12 +3035,12 @@ extension BLEService: CBPeripheralManagerDelegate {
// If buffer grows suspiciously large, reset to avoid memory leak // If buffer grows suspiciously large, reset to avoid memory leak
if combined.count > TransportConfig.blePendingWriteBufferCapBytes { // cap for safety if combined.count > TransportConfig.blePendingWriteBufferCapBytes { // cap for safety
pendingWriteBuffers.removeValue(forKey: centralUUID) pendingWriteBuffers.removeValue(forKey: centralUUID)
SecureLogger.warning("⚠️ Dropping oversized pending write buffer (\(combined.count) bytes) for central \(centralUUID)", category: .session) SecureLogger.log("⚠️ Dropping oversized pending write buffer (\(combined.count) bytes) for central \(centralUUID)", category: SecureLogger.session, level: .warning)
} }
// If this was a single short write and still failed, log the raw chunk for debugging // If this was a single short write and still failed, log the raw chunk for debugging
if !hasMultiple, let only = sorted.first, let raw = only.value { if !hasMultiple, let only = sorted.first, let raw = only.value {
let prefix = raw.prefix(16).map { String(format: "%02x", $0) }.joined(separator: " ") let prefix = raw.prefix(16).map { String(format: "%02x", $0) }.joined(separator: " ")
SecureLogger.error("❌ Failed to decode packet from central (len=\(raw.count), prefix=\(prefix))", category: .session) SecureLogger.log("❌ Failed to decode packet from central (len=\(raw.count), prefix=\(prefix))", category: SecureLogger.session, level: .error)
} }
} }
} }
+6 -6
View File
@@ -17,7 +17,7 @@ enum CommandResult {
/// Processes chat commands in a focused, efficient way /// Processes chat commands in a focused, efficient way
@MainActor @MainActor
final class CommandProcessor { class CommandProcessor {
weak var chatViewModel: ChatViewModel? weak var chatViewModel: ChatViewModel?
weak var meshService: Transport? weak var meshService: Transport?
@@ -50,9 +50,9 @@ final class CommandProcessor {
case "/clear": case "/clear":
return handleClear() return handleClear()
case "/hug": case "/hug":
return handleEmote(args, command: "hug", action: "hugs", emoji: "🫂") return handleEmote(args, action: "hugs", emoji: "🫂")
case "/slap": case "/slap":
return handleEmote(args, command: "slap", action: "slaps", emoji: "🐟", suffix: " around a bit with a large trout") return handleEmote(args, action: "slaps", emoji: "🐟", suffix: " around a bit with a large trout")
case "/block": case "/block":
return handleBlock(args) return handleBlock(args)
case "/unblock": case "/unblock":
@@ -129,17 +129,17 @@ final class CommandProcessor {
return .handled return .handled
} }
private func handleEmote(_ args: String, command: String, action: String, emoji: String, suffix: String = "") -> CommandResult { private func handleEmote(_ args: String, action: String, emoji: String, suffix: String = "") -> CommandResult {
let targetName = args.trimmingCharacters(in: .whitespaces) let targetName = args.trimmingCharacters(in: .whitespaces)
guard !targetName.isEmpty else { guard !targetName.isEmpty else {
return .error(message: "usage: /\(command) <nickname>") return .error(message: "usage: /\(action) <nickname>")
} }
let nickname = targetName.hasPrefix("@") ? String(targetName.dropFirst()) : targetName let nickname = targetName.hasPrefix("@") ? String(targetName.dropFirst()) : targetName
guard let targetPeerID = chatViewModel?.getPeerIDForNickname(nickname), guard let targetPeerID = chatViewModel?.getPeerIDForNickname(nickname),
let myNickname = chatViewModel?.nickname else { let myNickname = chatViewModel?.nickname else {
return .error(message: "cannot \(command) \(nickname): not found") return .error(message: "cannot \(action) \(nickname): not found")
} }
let emoteContent = "* \(emoji) \(myNickname) \(action) \(nickname)\(suffix) *" let emoteContent = "* \(emoji) \(myNickname) \(action) \(nickname)\(suffix) *"
@@ -3,7 +3,7 @@ import Combine
/// Manages persistent favorite relationships between peers /// Manages persistent favorite relationships between peers
@MainActor @MainActor
final class FavoritesPersistenceService: ObservableObject { class FavoritesPersistenceService: ObservableObject {
struct FavoriteRelationship: Codable { struct FavoriteRelationship: Codable {
let peerNoisePublicKey: Data let peerNoisePublicKey: Data
@@ -47,7 +47,8 @@ final class FavoritesPersistenceService: ObservableObject {
peerNostrPublicKey: String? = nil, peerNostrPublicKey: String? = nil,
peerNickname: String peerNickname: String
) { ) {
SecureLogger.info("⭐️ Adding favorite: \(peerNickname) (\(peerNoisePublicKey.hexEncodedString()))", category: .session) SecureLogger.log("⭐️ Adding favorite: \(peerNickname) (\(peerNoisePublicKey.hexEncodedString()))",
category: SecureLogger.session, level: .info)
let existing = favorites[peerNoisePublicKey] let existing = favorites[peerNoisePublicKey]
@@ -63,7 +64,8 @@ final class FavoritesPersistenceService: ObservableObject {
// Log if this creates a mutual favorite // Log if this creates a mutual favorite
if relationship.isMutual { if relationship.isMutual {
SecureLogger.info("💕 Mutual favorite relationship established with \(peerNickname)!", category: .session) SecureLogger.log("💕 Mutual favorite relationship established with \(peerNickname)!",
category: SecureLogger.session, level: .info)
} }
favorites[peerNoisePublicKey] = relationship favorites[peerNoisePublicKey] = relationship
@@ -81,7 +83,8 @@ final class FavoritesPersistenceService: ObservableObject {
func removeFavorite(peerNoisePublicKey: Data) { func removeFavorite(peerNoisePublicKey: Data) {
guard let existing = favorites[peerNoisePublicKey] else { return } guard let existing = favorites[peerNoisePublicKey] else { return }
SecureLogger.info("⭐️ Removing favorite: \(existing.peerNickname) (\(peerNoisePublicKey.hexEncodedString()))", category: .session) SecureLogger.log("⭐️ Removing favorite: \(existing.peerNickname) (\(peerNoisePublicKey.hexEncodedString()))",
category: SecureLogger.session, level: .info)
// If they still favorite us, keep the record but mark us as not favoriting // If they still favorite us, keep the record but mark us as not favoriting
if existing.theyFavoritedUs { if existing.theyFavoritedUs {
@@ -122,7 +125,8 @@ final class FavoritesPersistenceService: ObservableObject {
let existing = favorites[peerNoisePublicKey] let existing = favorites[peerNoisePublicKey]
let displayName = peerNickname ?? existing?.peerNickname ?? "Unknown" let displayName = peerNickname ?? existing?.peerNickname ?? "Unknown"
SecureLogger.info("📨 Received favorite notification: \(displayName) \(favorited ? "favorited" : "unfavorited") us", category: .session) SecureLogger.log("📨 Received favorite notification: \(displayName) \(favorited ? "favorited" : "unfavorited") us",
category: SecureLogger.session, level: .info)
let relationship = FavoriteRelationship( let relationship = FavoriteRelationship(
peerNoisePublicKey: peerNoisePublicKey, peerNoisePublicKey: peerNoisePublicKey,
@@ -143,7 +147,8 @@ final class FavoritesPersistenceService: ObservableObject {
// Check if this creates a mutual favorite // Check if this creates a mutual favorite
if relationship.isMutual { if relationship.isMutual {
SecureLogger.info("💕 Mutual favorite relationship established with \(displayName)!", category: .session) SecureLogger.log("💕 Mutual favorite relationship established with \(displayName)!",
category: SecureLogger.session, level: .info)
} }
} }
@@ -235,13 +240,15 @@ final class FavoritesPersistenceService: ObservableObject {
/// Update noise public key when peer reconnects with new ID /// Update noise public key when peer reconnects with new ID
func updateNoisePublicKey(from oldKey: Data, to newKey: Data, peerNickname: String) { func updateNoisePublicKey(from oldKey: Data, to newKey: Data, peerNickname: String) {
guard let existing = favorites[oldKey] else { guard let existing = favorites[oldKey] else {
SecureLogger.warning("⚠️ Cannot update noise key - no favorite found for \(oldKey.hexEncodedString())", category: .session) SecureLogger.log("⚠️ Cannot update noise key - no favorite found for \(oldKey.hexEncodedString())",
category: SecureLogger.session, level: .warning)
return return
} }
// Check if we already have a favorite with the new key // Check if we already have a favorite with the new key
if favorites[newKey] != nil { if favorites[newKey] != nil {
SecureLogger.warning("⚠️ Favorite already exists with new key \(newKey.hexEncodedString()), removing old entry", category: .session) SecureLogger.log("⚠️ Favorite already exists with new key \(newKey.hexEncodedString()), removing old entry",
category: SecureLogger.session, level: .warning)
favorites.removeValue(forKey: oldKey) favorites.removeValue(forKey: oldKey)
saveFavorites() saveFavorites()
return return
@@ -295,7 +302,7 @@ final class FavoritesPersistenceService: ObservableObject {
/// Clear all favorites - used for panic mode /// Clear all favorites - used for panic mode
func clearAllFavorites() { func clearAllFavorites() {
SecureLogger.warning("🧹 Clearing all favorites (panic mode)", category: .session) SecureLogger.log("🧹 Clearing all favorites (panic mode)", category: SecureLogger.session, level: .warning)
favorites.removeAll() favorites.removeAll()
saveFavorites() saveFavorites()
@@ -329,7 +336,7 @@ final class FavoritesPersistenceService: ObservableObject {
// Successfully saved favorites // Successfully saved favorites
} catch { } catch {
SecureLogger.error("Failed to save favorites: \(error)", category: .session) SecureLogger.log("Failed to save favorites: \(error)", category: SecureLogger.session, level: .error)
} }
} }
@@ -347,12 +354,14 @@ final class FavoritesPersistenceService: ObservableObject {
let decoder = JSONDecoder() let decoder = JSONDecoder()
let relationships = try decoder.decode([FavoriteRelationship].self, from: data) let relationships = try decoder.decode([FavoriteRelationship].self, from: data)
SecureLogger.info("✅ Loaded \(relationships.count) favorite relationships", category: .session) SecureLogger.log("✅ Loaded \(relationships.count) favorite relationships",
category: SecureLogger.session, level: .info)
// Log Nostr public key info // Log Nostr public key info
for relationship in relationships { for relationship in relationships {
if relationship.peerNostrPublicKey == nil { if relationship.peerNostrPublicKey == nil {
SecureLogger.warning("⚠️ No Nostr public key stored for '\(relationship.peerNickname)'", category: .session) SecureLogger.log("⚠️ No Nostr public key stored for '\(relationship.peerNickname)'",
category: SecureLogger.session, level: .warning)
} }
} }
@@ -363,7 +372,8 @@ final class FavoritesPersistenceService: ObservableObject {
for relationship in relationships { for relationship in relationships {
// Check for duplicates by public key (the actual unique identifier) // Check for duplicates by public key (the actual unique identifier)
if let existing = seenPublicKeys[relationship.peerNoisePublicKey] { if let existing = seenPublicKeys[relationship.peerNoisePublicKey] {
SecureLogger.warning("⚠️ Duplicate favorite found for public key \(relationship.peerNoisePublicKey.hexEncodedString()) - nicknames: '\(existing.peerNickname)' vs '\(relationship.peerNickname)'", category: .session) SecureLogger.log("⚠️ Duplicate favorite found for public key \(relationship.peerNoisePublicKey.hexEncodedString()) - nicknames: '\(existing.peerNickname)' vs '\(relationship.peerNickname)'",
category: SecureLogger.session, level: .warning)
// Keep the most recent or most complete relationship // Keep the most recent or most complete relationship
if relationship.lastUpdated > existing.lastUpdated || if relationship.lastUpdated > existing.lastUpdated ||
@@ -404,7 +414,7 @@ final class FavoritesPersistenceService: ObservableObject {
// Log loaded relationships // Log loaded relationships
// Loaded relationships successfully // Loaded relationships successfully
} catch { } catch {
SecureLogger.error("Failed to load favorites: \(error)", category: .session) SecureLogger.log("Failed to load favorites: \(error)", category: SecureLogger.session, level: .error)
} }
} }
} }
+10 -9
View File
@@ -8,8 +8,9 @@
import Foundation import Foundation
import Security import Security
import os.log
final class KeychainManager { class KeychainManager {
static let shared = KeychainManager() static let shared = KeychainManager()
// Use consistent service name for all keychain items // Use consistent service name for all keychain items
@@ -52,7 +53,7 @@ final class KeychainManager {
func saveIdentityKey(_ keyData: Data, forKey key: String) -> Bool { func saveIdentityKey(_ keyData: Data, forKey key: String) -> Bool {
let fullKey = "identity_\(key)" let fullKey = "identity_\(key)"
let result = saveData(keyData, forKey: fullKey) let result = saveData(keyData, forKey: fullKey)
SecureLogger.logKeyOperation(.save, keyType: key, success: result) SecureLogger.logKeyOperation("save", keyType: key, success: result)
return result return result
} }
@@ -63,7 +64,7 @@ final class KeychainManager {
func deleteIdentityKey(forKey key: String) -> Bool { func deleteIdentityKey(forKey key: String) -> Bool {
let result = delete(forKey: "identity_\(key)") let result = delete(forKey: "identity_\(key)")
SecureLogger.logKeyOperation(.delete, keyType: key, success: result) SecureLogger.logKeyOperation("delete", keyType: key, success: result)
return result return result
} }
@@ -112,9 +113,9 @@ final class KeychainManager {
if status == errSecSuccess { return true } if status == errSecSuccess { return true }
if status == -34018 && !triedWithoutGroup { if status == -34018 && !triedWithoutGroup {
SecureLogger.error(NSError(domain: "Keychain", code: -34018), context: "Missing keychain entitlement", category: .keychain) SecureLogger.logError(NSError(domain: "Keychain", code: -34018), context: "Missing keychain entitlement", category: SecureLogger.keychain)
} else if status != errSecDuplicateItem { } else if status != errSecDuplicateItem {
SecureLogger.error(NSError(domain: "Keychain", code: Int(status)), context: "Error saving to keychain", category: .keychain) SecureLogger.logError(NSError(domain: "Keychain", code: Int(status)), context: "Error saving to keychain", category: SecureLogger.keychain)
} }
return false return false
} }
@@ -150,7 +151,7 @@ final class KeychainManager {
if status == errSecSuccess { return result as? Data } if status == errSecSuccess { return result as? Data }
if status == -34018 { if status == -34018 {
SecureLogger.error(NSError(domain: "Keychain", code: -34018), context: "Missing keychain entitlement", category: .keychain) SecureLogger.logError(NSError(domain: "Keychain", code: -34018), context: "Missing keychain entitlement", category: SecureLogger.keychain)
} }
return nil return nil
} }
@@ -197,7 +198,7 @@ final class KeychainManager {
// Delete ALL keychain data for panic mode // Delete ALL keychain data for panic mode
func deleteAllKeychainData() -> Bool { func deleteAllKeychainData() -> Bool {
SecureLogger.warning("Panic mode - deleting all keychain data", category: .security) SecureLogger.log("Panic mode - deleting all keychain data", category: SecureLogger.security, level: .warning)
var totalDeleted = 0 var totalDeleted = 0
@@ -260,7 +261,7 @@ final class KeychainManager {
let deleteStatus = SecItemDelete(deleteQuery as CFDictionary) let deleteStatus = SecItemDelete(deleteQuery as CFDictionary)
if deleteStatus == errSecSuccess { if deleteStatus == errSecSuccess {
totalDeleted += 1 totalDeleted += 1
SecureLogger.info("Deleted keychain item: \(account) from \(service)", category: .keychain) SecureLogger.log("Deleted keychain item: \(account) from \(service)", category: SecureLogger.keychain, level: .info)
} }
} }
} }
@@ -302,7 +303,7 @@ final class KeychainManager {
totalDeleted += 1 totalDeleted += 1
} }
SecureLogger.warning("Panic mode cleanup completed. Total items deleted: \(totalDeleted)", category: .keychain) SecureLogger.log("Panic mode cleanup completed. Total items deleted: \(totalDeleted)", category: SecureLogger.keychain, level: .warning)
return totalDeleted > 0 return totalDeleted > 0
} }
@@ -197,7 +197,8 @@ final class LocationChannelManager: NSObject, CLLocationManagerDelegate, Observa
func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) { func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
// Surface as denied/restricted if relevant; otherwise keep previous state // Surface as denied/restricted if relevant; otherwise keep previous state
SecureLogger.error("LocationChannelManager: location error: \(error.localizedDescription)", category: .session) SecureLogger.log("LocationChannelManager: location error: \(error.localizedDescription)",
category: SecureLogger.session, level: .error)
} }
// MARK: - Helpers // MARK: - Helpers
+18 -9
View File
@@ -39,34 +39,40 @@ final class MessageRouter {
func sendPrivate(_ content: String, to peerID: String, recipientNickname: String, messageID: String) { func sendPrivate(_ content: String, to peerID: String, recipientNickname: String, messageID: String) {
let reachableMesh = mesh.isPeerReachable(peerID) let reachableMesh = mesh.isPeerReachable(peerID)
if reachableMesh { if reachableMesh {
SecureLogger.debug("Routing PM via mesh (reachable) to \(peerID.prefix(8))… id=\(messageID.prefix(8))", category: .session) SecureLogger.log("Routing PM via mesh (reachable) to \(peerID.prefix(8))… id=\(messageID.prefix(8))",
category: SecureLogger.session, level: .debug)
// BLEService will initiate a handshake if needed and queue the message // BLEService will initiate a handshake if needed and queue the message
mesh.sendPrivateMessage(content, to: peerID, recipientNickname: recipientNickname, messageID: messageID) mesh.sendPrivateMessage(content, to: peerID, recipientNickname: recipientNickname, messageID: messageID)
} else if canSendViaNostr(peerID: peerID) { } else if canSendViaNostr(peerID: peerID) {
SecureLogger.debug("Routing PM via Nostr to \(peerID.prefix(8))… id=\(messageID.prefix(8))", category: .session) SecureLogger.log("Routing PM via Nostr to \(peerID.prefix(8))… id=\(messageID.prefix(8))",
category: SecureLogger.session, level: .debug)
nostr.sendPrivateMessage(content, to: peerID, recipientNickname: recipientNickname, messageID: messageID) nostr.sendPrivateMessage(content, to: peerID, recipientNickname: recipientNickname, messageID: messageID)
} else { } else {
// Queue for later (when mesh connects or Nostr mapping appears) // Queue for later (when mesh connects or Nostr mapping appears)
if outbox[peerID] == nil { outbox[peerID] = [] } if outbox[peerID] == nil { outbox[peerID] = [] }
outbox[peerID]?.append((content, recipientNickname, messageID)) outbox[peerID]?.append((content, recipientNickname, messageID))
SecureLogger.debug("Queued PM for \(peerID.prefix(8))… (no mesh, no Nostr mapping) id=\(messageID.prefix(8))", category: .session) SecureLogger.log("Queued PM for \(peerID.prefix(8))… (no mesh, no Nostr mapping) id=\(messageID.prefix(8))",
category: SecureLogger.session, level: .debug)
} }
} }
func sendReadReceipt(_ receipt: ReadReceipt, to peerID: String) { func sendReadReceipt(_ receipt: ReadReceipt, to peerID: String) {
// Prefer mesh for reachable peers; BLE will queue if handshake is needed // Prefer mesh for reachable peers; BLE will queue if handshake is needed
if mesh.isPeerReachable(peerID) { if mesh.isPeerReachable(peerID) {
SecureLogger.debug("Routing READ ack via mesh (reachable) to \(peerID.prefix(8))… id=\(receipt.originalMessageID.prefix(8))", category: .session) SecureLogger.log("Routing READ ack via mesh (reachable) to \(peerID.prefix(8))… id=\(receipt.originalMessageID.prefix(8))",
category: SecureLogger.session, level: .debug)
mesh.sendReadReceipt(receipt, to: peerID) mesh.sendReadReceipt(receipt, to: peerID)
} else { } else {
SecureLogger.debug("Routing READ ack via Nostr to \(peerID.prefix(8))… id=\(receipt.originalMessageID.prefix(8))", category: .session) SecureLogger.log("Routing READ ack via Nostr to \(peerID.prefix(8))… id=\(receipt.originalMessageID.prefix(8))",
category: SecureLogger.session, level: .debug)
nostr.sendReadReceipt(receipt, to: peerID) nostr.sendReadReceipt(receipt, to: peerID)
} }
} }
func sendDeliveryAck(_ messageID: String, to peerID: String) { func sendDeliveryAck(_ messageID: String, to peerID: String) {
if mesh.isPeerReachable(peerID) { if mesh.isPeerReachable(peerID) {
SecureLogger.debug("Routing DELIVERED ack via mesh (reachable) to \(peerID.prefix(8))… id=\(messageID.prefix(8))", category: .session) SecureLogger.log("Routing DELIVERED ack via mesh (reachable) to \(peerID.prefix(8))… id=\(messageID.prefix(8))",
category: SecureLogger.session, level: .debug)
mesh.sendDeliveryAck(for: messageID, to: peerID) mesh.sendDeliveryAck(for: messageID, to: peerID)
} else { } else {
nostr.sendDeliveryAck(for: messageID, to: peerID) nostr.sendDeliveryAck(for: messageID, to: peerID)
@@ -102,15 +108,18 @@ final class MessageRouter {
func flushOutbox(for peerID: String) { func flushOutbox(for peerID: String) {
guard let queued = outbox[peerID], !queued.isEmpty else { return } guard let queued = outbox[peerID], !queued.isEmpty else { return }
SecureLogger.debug("Flushing outbox for \(peerID.prefix(8))… count=\(queued.count)", category: .session) SecureLogger.log("Flushing outbox for \(peerID.prefix(8))… count=\(queued.count)",
category: SecureLogger.session, level: .debug)
var remaining: [(content: String, nickname: String, messageID: String)] = [] var remaining: [(content: String, nickname: String, messageID: String)] = []
// Prefer mesh if connected; else try Nostr if mapping exists // Prefer mesh if connected; else try Nostr if mapping exists
for (content, nickname, messageID) in queued { for (content, nickname, messageID) in queued {
if mesh.isPeerReachable(peerID) { if mesh.isPeerReachable(peerID) {
SecureLogger.debug("Outbox -> mesh for \(peerID.prefix(8))… id=\(messageID.prefix(8))", category: .session) SecureLogger.log("Outbox -> mesh for \(peerID.prefix(8))… id=\(messageID.prefix(8))",
category: SecureLogger.session, level: .debug)
mesh.sendPrivateMessage(content, to: peerID, recipientNickname: nickname, messageID: messageID) mesh.sendPrivateMessage(content, to: peerID, recipientNickname: nickname, messageID: messageID)
} else if canSendViaNostr(peerID: peerID) { } else if canSendViaNostr(peerID: peerID) {
SecureLogger.debug("Outbox -> Nostr for \(peerID.prefix(8))… id=\(messageID.prefix(8))", category: .session) SecureLogger.log("Outbox -> Nostr for \(peerID.prefix(8))… id=\(messageID.prefix(8))",
category: SecureLogger.session, level: .debug)
nostr.sendPrivateMessage(content, to: peerID, recipientNickname: nickname, messageID: messageID) nostr.sendPrivateMessage(content, to: peerID, recipientNickname: nickname, messageID: messageID)
} else { } else {
// Keep unsent items queued // Keep unsent items queued
+20 -19
View File
@@ -85,6 +85,7 @@
import Foundation import Foundation
import CryptoKit import CryptoKit
import os.log
// MARK: - Encryption Status // MARK: - Encryption Status
@@ -134,7 +135,7 @@ enum EncryptionStatus: Equatable {
/// Provides a high-level API for establishing secure channels between peers, /// Provides a high-level API for establishing secure channels between peers,
/// handling all cryptographic operations transparently. /// handling all cryptographic operations transparently.
/// - Important: This service maintains the device's cryptographic identity /// - Important: This service maintains the device's cryptographic identity
final class NoiseEncryptionService { class NoiseEncryptionService {
// Static identity key (persistent across sessions) // Static identity key (persistent across sessions)
private let staticIdentityKey: Curve25519.KeyAgreement.PrivateKey private let staticIdentityKey: Curve25519.KeyAgreement.PrivateKey
public let staticIdentityPublicKey: Curve25519.KeyAgreement.PublicKey public let staticIdentityPublicKey: Curve25519.KeyAgreement.PublicKey
@@ -189,7 +190,7 @@ final class NoiseEncryptionService {
if let identityData = KeychainManager.shared.getIdentityKey(forKey: "noiseStaticKey"), if let identityData = KeychainManager.shared.getIdentityKey(forKey: "noiseStaticKey"),
let key = try? Curve25519.KeyAgreement.PrivateKey(rawRepresentation: identityData) { let key = try? Curve25519.KeyAgreement.PrivateKey(rawRepresentation: identityData) {
loadedKey = key loadedKey = key
SecureLogger.logKeyOperation(.load, keyType: "noiseStaticKey", success: true) SecureLogger.logKeyOperation("load", keyType: "noiseStaticKey", success: true)
} }
// If no identity exists, create new one // If no identity exists, create new one
else { else {
@@ -198,7 +199,7 @@ final class NoiseEncryptionService {
// Save to keychain // Save to keychain
let saved = KeychainManager.shared.saveIdentityKey(keyData, forKey: "noiseStaticKey") let saved = KeychainManager.shared.saveIdentityKey(keyData, forKey: "noiseStaticKey")
SecureLogger.logKeyOperation(.create, keyType: "noiseStaticKey", success: saved) SecureLogger.logKeyOperation("create", keyType: "noiseStaticKey", success: saved)
} }
// Now assign the final value // Now assign the final value
@@ -212,7 +213,7 @@ final class NoiseEncryptionService {
if let signingData = KeychainManager.shared.getIdentityKey(forKey: "ed25519SigningKey"), if let signingData = KeychainManager.shared.getIdentityKey(forKey: "ed25519SigningKey"),
let key = try? Curve25519.Signing.PrivateKey(rawRepresentation: signingData) { let key = try? Curve25519.Signing.PrivateKey(rawRepresentation: signingData) {
loadedSigningKey = key loadedSigningKey = key
SecureLogger.logKeyOperation(.load, keyType: "ed25519SigningKey", success: true) SecureLogger.logKeyOperation("load", keyType: "ed25519SigningKey", success: true)
} }
// If no signing key exists, create new one // If no signing key exists, create new one
else { else {
@@ -221,7 +222,7 @@ final class NoiseEncryptionService {
// Save to keychain // Save to keychain
let saved = KeychainManager.shared.saveIdentityKey(keyData, forKey: "ed25519SigningKey") let saved = KeychainManager.shared.saveIdentityKey(keyData, forKey: "ed25519SigningKey")
SecureLogger.logKeyOperation(.create, keyType: "ed25519SigningKey", success: saved) SecureLogger.logKeyOperation("create", keyType: "ed25519SigningKey", success: saved)
} }
// Now assign the signing keys // Now assign the signing keys
@@ -268,8 +269,8 @@ final class NoiseEncryptionService {
// Clear from keychain // Clear from keychain
let deletedStatic = KeychainManager.shared.deleteIdentityKey(forKey: "noiseStaticKey") let deletedStatic = KeychainManager.shared.deleteIdentityKey(forKey: "noiseStaticKey")
let deletedSigning = KeychainManager.shared.deleteIdentityKey(forKey: "ed25519SigningKey") let deletedSigning = KeychainManager.shared.deleteIdentityKey(forKey: "ed25519SigningKey")
SecureLogger.logKeyOperation(.delete, keyType: "identity keys", success: deletedStatic && deletedSigning) SecureLogger.logKeyOperation("delete", keyType: "identity keys", success: deletedStatic && deletedSigning)
SecureLogger.warning("Panic mode activated - identity cleared", category: .security) SecureLogger.log("Panic mode activated - identity cleared", category: SecureLogger.security, level: .warning)
// Stop rekey timer // Stop rekey timer
stopRekeyTimer() stopRekeyTimer()
} }
@@ -280,7 +281,7 @@ final class NoiseEncryptionService {
let signature = try signingKey.signature(for: data) let signature = try signingKey.signature(for: data)
return signature return signature
} catch { } catch {
SecureLogger.error(error, context: "Failed to sign data") SecureLogger.logError(error, context: "Failed to sign data", category: SecureLogger.noise)
return nil return nil
} }
} }
@@ -291,7 +292,7 @@ final class NoiseEncryptionService {
let signingPublicKey = try Curve25519.Signing.PublicKey(rawRepresentation: publicKey) let signingPublicKey = try Curve25519.Signing.PublicKey(rawRepresentation: publicKey)
return signingPublicKey.isValidSignature(signature, for: data) return signingPublicKey.isValidSignature(signature, for: data)
} catch { } catch {
SecureLogger.error(error, context: "Failed to verify signature") SecureLogger.logError(error, context: "Failed to verify signature", category: SecureLogger.noise)
return false return false
} }
} }
@@ -391,17 +392,17 @@ final class NoiseEncryptionService {
// Validate peer ID // Validate peer ID
guard NoiseSecurityValidator.validatePeerID(peerID) else { guard NoiseSecurityValidator.validatePeerID(peerID) else {
SecureLogger.warning(.authenticationFailed(peerID: peerID)) SecureLogger.logSecurityEvent(.authenticationFailed(peerID: peerID), level: .warning)
throw NoiseSecurityError.invalidPeerID throw NoiseSecurityError.invalidPeerID
} }
// Check rate limit // Check rate limit
guard rateLimiter.allowHandshake(from: peerID) else { guard rateLimiter.allowHandshake(from: peerID) else {
SecureLogger.warning(.authenticationFailed(peerID: "Rate limited: \(peerID)")) SecureLogger.logSecurityEvent(.authenticationFailed(peerID: "Rate limited: \(peerID)"), level: .warning)
throw NoiseSecurityError.rateLimitExceeded throw NoiseSecurityError.rateLimitExceeded
} }
SecureLogger.info(.handshakeStarted(peerID: peerID)) SecureLogger.logSecurityEvent(.handshakeStarted(peerID: peerID))
// Return raw handshake data without wrapper // Return raw handshake data without wrapper
// The Noise protocol handles its own message format // The Noise protocol handles its own message format
@@ -414,19 +415,19 @@ final class NoiseEncryptionService {
// Validate peer ID // Validate peer ID
guard NoiseSecurityValidator.validatePeerID(peerID) else { guard NoiseSecurityValidator.validatePeerID(peerID) else {
SecureLogger.warning(.authenticationFailed(peerID: peerID)) SecureLogger.logSecurityEvent(.authenticationFailed(peerID: peerID), level: .warning)
throw NoiseSecurityError.invalidPeerID throw NoiseSecurityError.invalidPeerID
} }
// Validate message size // Validate message size
guard NoiseSecurityValidator.validateHandshakeMessageSize(message) else { guard NoiseSecurityValidator.validateHandshakeMessageSize(message) else {
SecureLogger.warning(.handshakeFailed(peerID: peerID, error: "Message too large")) SecureLogger.logSecurityEvent(.handshakeFailed(peerID: peerID, error: "Message too large"), level: .warning)
throw NoiseSecurityError.messageTooLarge throw NoiseSecurityError.messageTooLarge
} }
// Check rate limit // Check rate limit
guard rateLimiter.allowHandshake(from: peerID) else { guard rateLimiter.allowHandshake(from: peerID) else {
SecureLogger.warning(.authenticationFailed(peerID: "Rate limited: \(peerID)")) SecureLogger.logSecurityEvent(.authenticationFailed(peerID: "Rate limited: \(peerID)"), level: .warning)
throw NoiseSecurityError.rateLimitExceeded throw NoiseSecurityError.rateLimitExceeded
} }
@@ -520,7 +521,7 @@ final class NoiseEncryptionService {
peerFingerprints.removeValue(forKey: peerID) peerFingerprints.removeValue(forKey: peerID)
} }
SecureLogger.info(.sessionExpired(peerID: peerID)) SecureLogger.logSecurityEvent(.sessionExpired(peerID: peerID))
} }
// MARK: - Private Helpers // MARK: - Private Helpers
@@ -536,7 +537,7 @@ final class NoiseEncryptionService {
} }
// Log security event // Log security event
SecureLogger.info(.handshakeCompleted(peerID: peerID)) SecureLogger.logSecurityEvent(.handshakeCompleted(peerID: peerID))
// Notify all handlers about authentication // Notify all handlers about authentication
serviceQueue.async { [weak self] in serviceQueue.async { [weak self] in
@@ -572,12 +573,12 @@ final class NoiseEncryptionService {
// Attempt to rekey the session // Attempt to rekey the session
do { do {
try sessionManager.initiateRekey(for: peerID) try sessionManager.initiateRekey(for: peerID)
SecureLogger.debug("Key rotation initiated for peer: \(peerID)", category: .security) SecureLogger.log("Key rotation initiated for peer: \(peerID)", category: SecureLogger.security, level: .debug)
// Signal that handshake is needed // Signal that handshake is needed
onHandshakeRequired?(peerID) onHandshakeRequired?(peerID)
} catch { } catch {
SecureLogger.error(error, context: "Failed to initiate rekey for peer: \(peerID)", category: .session) SecureLogger.logError(error, context: "Failed to initiate rekey for peer: \(peerID)", category: SecureLogger.session)
} }
} }
} }
+36 -24
View File
@@ -51,29 +51,31 @@ final class NostrTransport: Transport {
Task { @MainActor in Task { @MainActor in
guard let recipientNpub = resolveRecipientNpub(for: peerID) else { return } guard let recipientNpub = resolveRecipientNpub(for: peerID) else { return }
guard let senderIdentity = try? NostrIdentityBridge.getCurrentNostrIdentity() else { return } guard let senderIdentity = try? NostrIdentityBridge.getCurrentNostrIdentity() else { return }
SecureLogger.debug("NostrTransport: preparing PM to \(recipientNpub.prefix(16))… for peerID \(peerID.prefix(8))… id=\(messageID.prefix(8))", category: .session) SecureLogger.log("NostrTransport: preparing PM to \(recipientNpub.prefix(16))… for peerID \(peerID.prefix(8))… id=\(messageID.prefix(8))",
category: SecureLogger.session, level: .debug)
// Convert recipient npub -> hex (x-only) // Convert recipient npub -> hex (x-only)
let recipientHex: String let recipientHex: String
do { do {
let (hrp, data) = try Bech32.decode(recipientNpub) let (hrp, data) = try Bech32.decode(recipientNpub)
guard hrp == "npub" else { guard hrp == "npub" else {
SecureLogger.error("NostrTransport: recipient key not npub (hrp=\(hrp))", category: .session) SecureLogger.log("NostrTransport: recipient key not npub (hrp=\(hrp))", category: SecureLogger.session, level: .error)
return return
} }
recipientHex = data.hexEncodedString() recipientHex = data.hexEncodedString()
} catch { } catch {
SecureLogger.error("NostrTransport: failed to decode npub -> hex: \(error)", category: .session) SecureLogger.log("NostrTransport: failed to decode npub -> hex: \(error)", category: SecureLogger.session, level: .error)
return return
} }
guard let embedded = NostrEmbeddedBitChat.encodePMForNostr(content: content, messageID: messageID, recipientPeerID: peerID, senderPeerID: senderPeerID) else { guard let embedded = NostrEmbeddedBitChat.encodePMForNostr(content: content, messageID: messageID, recipientPeerID: peerID, senderPeerID: senderPeerID) else {
SecureLogger.error("NostrTransport: failed to embed PM packet", category: .session) SecureLogger.log("NostrTransport: failed to embed PM packet", category: SecureLogger.session, level: .error)
return return
} }
guard let event = try? NostrProtocol.createPrivateMessage(content: embedded, recipientPubkey: recipientHex, senderIdentity: senderIdentity) else { guard let event = try? NostrProtocol.createPrivateMessage(content: embedded, recipientPubkey: recipientHex, senderIdentity: senderIdentity) else {
SecureLogger.error("NostrTransport: failed to build Nostr event for PM", category: .session) SecureLogger.log("NostrTransport: failed to build Nostr event for PM", category: SecureLogger.session, level: .error)
return return
} }
SecureLogger.debug("NostrTransport: sending PM giftWrap id=\(event.id.prefix(16))", category: .session) SecureLogger.log("NostrTransport: sending PM giftWrap id=\(event.id.prefix(16))",
category: SecureLogger.session, level: .debug)
NostrRelayManager.shared.sendEvent(event) NostrRelayManager.shared.sendEvent(event)
} }
} }
@@ -97,7 +99,8 @@ final class NostrTransport: Transport {
Task { @MainActor in Task { @MainActor in
guard let recipientNpub = resolveRecipientNpub(for: item.peerID) else { scheduleNextReadAck(); return } guard let recipientNpub = resolveRecipientNpub(for: item.peerID) else { scheduleNextReadAck(); return }
guard let senderIdentity = try? NostrIdentityBridge.getCurrentNostrIdentity() else { scheduleNextReadAck(); return } guard let senderIdentity = try? NostrIdentityBridge.getCurrentNostrIdentity() else { scheduleNextReadAck(); return }
SecureLogger.debug("NostrTransport: preparing READ ack for id=\(item.receipt.originalMessageID.prefix(8))… to \(recipientNpub.prefix(16))", category: .session) SecureLogger.log("NostrTransport: preparing READ ack for id=\(item.receipt.originalMessageID.prefix(8))… to \(recipientNpub.prefix(16))",
category: SecureLogger.session, level: .debug)
// Convert recipient npub -> hex // Convert recipient npub -> hex
let recipientHex: String let recipientHex: String
do { do {
@@ -106,14 +109,15 @@ final class NostrTransport: Transport {
recipientHex = data.hexEncodedString() recipientHex = data.hexEncodedString()
} catch { scheduleNextReadAck(); return } } catch { scheduleNextReadAck(); return }
guard let ack = NostrEmbeddedBitChat.encodeAckForNostr(type: .readReceipt, messageID: item.receipt.originalMessageID, recipientPeerID: item.peerID, senderPeerID: senderPeerID) else { guard let ack = NostrEmbeddedBitChat.encodeAckForNostr(type: .readReceipt, messageID: item.receipt.originalMessageID, recipientPeerID: item.peerID, senderPeerID: senderPeerID) else {
SecureLogger.error("NostrTransport: failed to embed READ ack", category: .session) SecureLogger.log("NostrTransport: failed to embed READ ack", category: SecureLogger.session, level: .error)
scheduleNextReadAck(); return scheduleNextReadAck(); return
} }
guard let event = try? NostrProtocol.createPrivateMessage(content: ack, recipientPubkey: recipientHex, senderIdentity: senderIdentity) else { guard let event = try? NostrProtocol.createPrivateMessage(content: ack, recipientPubkey: recipientHex, senderIdentity: senderIdentity) else {
SecureLogger.error("NostrTransport: failed to build Nostr event for READ ack", category: .session) SecureLogger.log("NostrTransport: failed to build Nostr event for READ ack", category: SecureLogger.session, level: .error)
scheduleNextReadAck(); return scheduleNextReadAck(); return
} }
SecureLogger.debug("NostrTransport: sending READ ack giftWrap id=\(event.id.prefix(16))", category: .session) SecureLogger.log("NostrTransport: sending READ ack giftWrap id=\(event.id.prefix(16))",
category: SecureLogger.session, level: .debug)
NostrRelayManager.shared.sendEvent(event) NostrRelayManager.shared.sendEvent(event)
scheduleNextReadAck() scheduleNextReadAck()
} }
@@ -132,7 +136,8 @@ final class NostrTransport: Transport {
guard let recipientNpub = resolveRecipientNpub(for: peerID) else { return } guard let recipientNpub = resolveRecipientNpub(for: peerID) else { return }
guard let senderIdentity = try? NostrIdentityBridge.getCurrentNostrIdentity() else { return } guard let senderIdentity = try? NostrIdentityBridge.getCurrentNostrIdentity() else { return }
let content = isFavorite ? "[FAVORITED]:\(senderIdentity.npub)" : "[UNFAVORITED]:\(senderIdentity.npub)" let content = isFavorite ? "[FAVORITED]:\(senderIdentity.npub)" : "[UNFAVORITED]:\(senderIdentity.npub)"
SecureLogger.debug("NostrTransport: preparing FAVORITE(\(isFavorite)) to \(recipientNpub.prefix(16))", category: .session) SecureLogger.log("NostrTransport: preparing FAVORITE(\(isFavorite)) to \(recipientNpub.prefix(16))",
category: SecureLogger.session, level: .debug)
// Convert recipient npub -> hex // Convert recipient npub -> hex
let recipientHex: String let recipientHex: String
do { do {
@@ -141,14 +146,15 @@ final class NostrTransport: Transport {
recipientHex = data.hexEncodedString() recipientHex = data.hexEncodedString()
} catch { return } } catch { return }
guard let embedded = NostrEmbeddedBitChat.encodePMForNostr(content: content, messageID: UUID().uuidString, recipientPeerID: peerID, senderPeerID: senderPeerID) else { guard let embedded = NostrEmbeddedBitChat.encodePMForNostr(content: content, messageID: UUID().uuidString, recipientPeerID: peerID, senderPeerID: senderPeerID) else {
SecureLogger.error("NostrTransport: failed to embed favorite notification", category: .session) SecureLogger.log("NostrTransport: failed to embed favorite notification", category: SecureLogger.session, level: .error)
return return
} }
guard let event = try? NostrProtocol.createPrivateMessage(content: embedded, recipientPubkey: recipientHex, senderIdentity: senderIdentity) else { guard let event = try? NostrProtocol.createPrivateMessage(content: embedded, recipientPubkey: recipientHex, senderIdentity: senderIdentity) else {
SecureLogger.error("NostrTransport: failed to build Nostr event for favorite notification", category: .session) SecureLogger.log("NostrTransport: failed to build Nostr event for favorite notification", category: SecureLogger.session, level: .error)
return return
} }
SecureLogger.debug("NostrTransport: sending favorite giftWrap id=\(event.id.prefix(16))", category: .session) SecureLogger.log("NostrTransport: sending favorite giftWrap id=\(event.id.prefix(16))",
category: SecureLogger.session, level: .debug)
NostrRelayManager.shared.sendEvent(event) NostrRelayManager.shared.sendEvent(event)
} }
} }
@@ -174,7 +180,8 @@ final class NostrTransport: Transport {
Task { @MainActor in Task { @MainActor in
guard let recipientNpub = resolveRecipientNpub(for: peerID) else { return } guard let recipientNpub = resolveRecipientNpub(for: peerID) else { return }
guard let senderIdentity = try? NostrIdentityBridge.getCurrentNostrIdentity() else { return } guard let senderIdentity = try? NostrIdentityBridge.getCurrentNostrIdentity() else { return }
SecureLogger.debug("NostrTransport: preparing DELIVERED ack for id=\(messageID.prefix(8))… to \(recipientNpub.prefix(16))", category: .session) SecureLogger.log("NostrTransport: preparing DELIVERED ack for id=\(messageID.prefix(8))… to \(recipientNpub.prefix(16))",
category: SecureLogger.session, level: .debug)
let recipientHex: String let recipientHex: String
do { do {
let (hrp, data) = try Bech32.decode(recipientNpub) let (hrp, data) = try Bech32.decode(recipientNpub)
@@ -182,14 +189,15 @@ final class NostrTransport: Transport {
recipientHex = data.hexEncodedString() recipientHex = data.hexEncodedString()
} catch { return } } catch { return }
guard let ack = NostrEmbeddedBitChat.encodeAckForNostr(type: .delivered, messageID: messageID, recipientPeerID: peerID, senderPeerID: senderPeerID) else { guard let ack = NostrEmbeddedBitChat.encodeAckForNostr(type: .delivered, messageID: messageID, recipientPeerID: peerID, senderPeerID: senderPeerID) else {
SecureLogger.error("NostrTransport: failed to embed DELIVERED ack", category: .session) SecureLogger.log("NostrTransport: failed to embed DELIVERED ack", category: SecureLogger.session, level: .error)
return return
} }
guard let event = try? NostrProtocol.createPrivateMessage(content: ack, recipientPubkey: recipientHex, senderIdentity: senderIdentity) else { guard let event = try? NostrProtocol.createPrivateMessage(content: ack, recipientPubkey: recipientHex, senderIdentity: senderIdentity) else {
SecureLogger.error("NostrTransport: failed to build Nostr event for DELIVERED ack", category: .session) SecureLogger.log("NostrTransport: failed to build Nostr event for DELIVERED ack", category: SecureLogger.session, level: .error)
return return
} }
SecureLogger.debug("NostrTransport: sending DELIVERED ack giftWrap id=\(event.id.prefix(16))", category: .session) SecureLogger.log("NostrTransport: sending DELIVERED ack giftWrap id=\(event.id.prefix(16))",
category: SecureLogger.session, level: .debug)
NostrRelayManager.shared.sendEvent(event) NostrRelayManager.shared.sendEvent(event)
} }
} }
@@ -197,7 +205,8 @@ final class NostrTransport: Transport {
// MARK: - Geohash ACK helpers // MARK: - Geohash ACK helpers
func sendDeliveryAckGeohash(for messageID: String, toRecipientHex recipientHex: String, from identity: NostrIdentity) { func sendDeliveryAckGeohash(for messageID: String, toRecipientHex recipientHex: String, from identity: NostrIdentity) {
Task { @MainActor in Task { @MainActor in
SecureLogger.debug("GeoDM: send DELIVERED -> recip=\(recipientHex.prefix(8))… mid=\(messageID.prefix(8))… from=\(identity.publicKeyHex.prefix(8))", category: .session) SecureLogger.log("GeoDM: send DELIVERED -> recip=\(recipientHex.prefix(8))… mid=\(messageID.prefix(8))… from=\(identity.publicKeyHex.prefix(8))",
category: SecureLogger.session, level: .debug)
guard let embedded = NostrEmbeddedBitChat.encodeAckForNostrNoRecipient(type: .delivered, messageID: messageID, senderPeerID: senderPeerID) else { return } guard let embedded = NostrEmbeddedBitChat.encodeAckForNostrNoRecipient(type: .delivered, messageID: messageID, senderPeerID: senderPeerID) else { return }
guard let event = try? NostrProtocol.createPrivateMessage(content: embedded, recipientPubkey: recipientHex, senderIdentity: identity) else { return } guard let event = try? NostrProtocol.createPrivateMessage(content: embedded, recipientPubkey: recipientHex, senderIdentity: identity) else { return }
NostrRelayManager.registerPendingGiftWrap(id: event.id) NostrRelayManager.registerPendingGiftWrap(id: event.id)
@@ -207,7 +216,8 @@ final class NostrTransport: Transport {
func sendReadReceiptGeohash(_ messageID: String, toRecipientHex recipientHex: String, from identity: NostrIdentity) { func sendReadReceiptGeohash(_ messageID: String, toRecipientHex recipientHex: String, from identity: NostrIdentity) {
Task { @MainActor in Task { @MainActor in
SecureLogger.debug("GeoDM: send READ -> recip=\(recipientHex.prefix(8))… mid=\(messageID.prefix(8))… from=\(identity.publicKeyHex.prefix(8))", category: .session) SecureLogger.log("GeoDM: send READ -> recip=\(recipientHex.prefix(8))… mid=\(messageID.prefix(8))… from=\(identity.publicKeyHex.prefix(8))",
category: SecureLogger.session, level: .debug)
guard let embedded = NostrEmbeddedBitChat.encodeAckForNostrNoRecipient(type: .readReceipt, messageID: messageID, senderPeerID: senderPeerID) else { return } guard let embedded = NostrEmbeddedBitChat.encodeAckForNostrNoRecipient(type: .readReceipt, messageID: messageID, senderPeerID: senderPeerID) else { return }
guard let event = try? NostrProtocol.createPrivateMessage(content: embedded, recipientPubkey: recipientHex, senderIdentity: identity) else { return } guard let event = try? NostrProtocol.createPrivateMessage(content: embedded, recipientPubkey: recipientHex, senderIdentity: identity) else { return }
NostrRelayManager.registerPendingGiftWrap(id: event.id) NostrRelayManager.registerPendingGiftWrap(id: event.id)
@@ -219,17 +229,19 @@ final class NostrTransport: Transport {
func sendPrivateMessageGeohash(content: String, toRecipientHex recipientHex: String, from identity: NostrIdentity, messageID: String) { func sendPrivateMessageGeohash(content: String, toRecipientHex recipientHex: String, from identity: NostrIdentity, messageID: String) {
Task { @MainActor in Task { @MainActor in
guard !recipientHex.isEmpty else { return } guard !recipientHex.isEmpty else { return }
SecureLogger.debug("GeoDM: send PM -> recip=\(recipientHex.prefix(8))… mid=\(messageID.prefix(8))… from=\(identity.publicKeyHex.prefix(8))", category: .session) SecureLogger.log("GeoDM: send PM -> recip=\(recipientHex.prefix(8))… mid=\(messageID.prefix(8))… from=\(identity.publicKeyHex.prefix(8))",
category: SecureLogger.session, level: .debug)
// Build embedded BitChat packet without recipient peer ID // Build embedded BitChat packet without recipient peer ID
guard let embedded = NostrEmbeddedBitChat.encodePMForNostrNoRecipient(content: content, messageID: messageID, senderPeerID: senderPeerID) else { guard let embedded = NostrEmbeddedBitChat.encodePMForNostrNoRecipient(content: content, messageID: messageID, senderPeerID: senderPeerID) else {
SecureLogger.error("NostrTransport: failed to embed geohash PM packet", category: .session) SecureLogger.log("NostrTransport: failed to embed geohash PM packet", category: SecureLogger.session, level: .error)
return return
} }
guard let event = try? NostrProtocol.createPrivateMessage(content: embedded, recipientPubkey: recipientHex, senderIdentity: identity) else { guard let event = try? NostrProtocol.createPrivateMessage(content: embedded, recipientPubkey: recipientHex, senderIdentity: identity) else {
SecureLogger.error("NostrTransport: failed to build Nostr event for geohash PM", category: .session) SecureLogger.log("NostrTransport: failed to build Nostr event for geohash PM", category: SecureLogger.session, level: .error)
return return
} }
SecureLogger.debug("NostrTransport: sending geohash PM giftWrap id=\(event.id.prefix(16))", category: .session) SecureLogger.log("NostrTransport: sending geohash PM giftWrap id=\(event.id.prefix(16))",
category: SecureLogger.session, level: .debug)
NostrRelayManager.registerPendingGiftWrap(id: event.id) NostrRelayManager.registerPendingGiftWrap(id: event.id)
NostrRelayManager.shared.sendEvent(event) NostrRelayManager.shared.sendEvent(event)
} }
+1 -1
View File
@@ -14,7 +14,7 @@ import UIKit
import AppKit import AppKit
#endif #endif
final class NotificationService { class NotificationService {
static let shared = NotificationService() static let shared = NotificationService()
private init() {} private init() {}
+3 -2
View File
@@ -10,7 +10,7 @@ import Foundation
import SwiftUI import SwiftUI
/// Manages all private chat functionality /// Manages all private chat functionality
final class PrivateChatManager: ObservableObject { class PrivateChatManager: ObservableObject {
@Published var privateChats: [String: [BitchatMessage]] = [:] @Published var privateChats: [String: [BitchatMessage]] = [:]
@Published var selectedPeer: String? = nil @Published var selectedPeer: String? = nil
@Published var unreadMessages: Set<String> = [] @Published var unreadMessages: Set<String> = []
@@ -228,7 +228,8 @@ final class PrivateChatManager: ObservableObject {
// Route via MessageRouter to avoid handshakeRequired spam when session isn't established // Route via MessageRouter to avoid handshakeRequired spam when session isn't established
if let router = messageRouter { if let router = messageRouter {
SecureLogger.debug("PrivateChatManager: sending READ ack for \(message.id.prefix(8))… to \(senderPeerID.prefix(8))… via router", category: .session) SecureLogger.log("PrivateChatManager: sending READ ack for \(message.id.prefix(8))… to \(senderPeerID.prefix(8))… via router",
category: SecureLogger.session, level: .debug)
Task { @MainActor in Task { @MainActor in
router.sendReadReceipt(receipt, to: senderPeerID) router.sendReadReceipt(receipt, to: senderPeerID)
} }
+13 -13
View File
@@ -189,7 +189,7 @@ final class TorManager: ObservableObject {
var started = false var started = false
// If already running (per C glue), treat as started // If already running (per C glue), treat as started
if tor_host_is_running() != 0 { if tor_host_is_running() != 0 {
SecureLogger.info("TorManager: embed reports already running", category: .session) SecureLogger.log("TorManager: embed reports already running", category: SecureLogger.session, level: .info)
return true return true
} }
dir.withCString { dptr in dir.withCString { dptr in
@@ -198,9 +198,9 @@ final class TorManager: ObservableObject {
let rc = tor_host_start(dptr, sptr, cptr, 1) let rc = tor_host_start(dptr, sptr, cptr, 1)
started = (rc == 0) started = (rc == 0)
if rc != 0 { if rc != 0 {
SecureLogger.error("TorManager: tor_host_start failed rc=\(rc)", category: .session) SecureLogger.log("TorManager: tor_host_start failed rc=\(rc)", category: SecureLogger.session, level: .error)
} else { } else {
SecureLogger.info("TorManager: tor_host_start OK (\(socks), control \(control))", category: .session) SecureLogger.log("TorManager: tor_host_start OK (\(socks), control \(control))", category: SecureLogger.session, level: .info)
} }
} }
} }
@@ -215,10 +215,10 @@ final class TorManager: ObservableObject {
await MainActor.run { await MainActor.run {
self.socksReady = ready self.socksReady = ready
if ready { if ready {
SecureLogger.info("TorManager: SOCKS ready at \(self.socksHost):\(self.socksPort) [embed]", category: .session) SecureLogger.log("TorManager: SOCKS ready at \(self.socksHost):\(self.socksPort) [embed]", category: SecureLogger.session, level: .info)
} else { } else {
self.lastError = NSError(domain: "TorManager", code: -14, userInfo: [NSLocalizedDescriptionKey: "Tor SOCKS not reachable after embed start"]) self.lastError = NSError(domain: "TorManager", code: -14, userInfo: [NSLocalizedDescriptionKey: "Tor SOCKS not reachable after embed start"])
SecureLogger.error("TorManager: SOCKS not reachable (timeout) [embed]", category: .session) SecureLogger.log("TorManager: SOCKS not reachable (timeout) [embed]", category: SecureLogger.session, level: .error)
} }
} }
} }
@@ -282,13 +282,13 @@ final class TorManager: ObservableObject {
/// Returns true if the attempt started and port probing was scheduled. /// Returns true if the attempt started and port probing was scheduled.
private func startTorViaDlopen() -> Bool { private func startTorViaDlopen() -> Bool {
guard let fwURL = frameworkBinaryURL() else { guard let fwURL = frameworkBinaryURL() else {
SecureLogger.warning("TorManager: no embedded tor framework found", category: .session) SecureLogger.log("TorManager: no embedded tor framework found", category: SecureLogger.session, level: .warning)
return false return false
} }
// Load the library // Load the library
let mode = RTLD_NOW | RTLD_LOCAL let mode = RTLD_NOW | RTLD_LOCAL
SecureLogger.info("TorManager: dlopen(\(fwURL.lastPathComponent))…", category: .session) SecureLogger.log("TorManager: dlopen(\(fwURL.lastPathComponent))…", category: SecureLogger.session, level: .info)
guard let handle = dlopen(fwURL.path, mode) else { guard let handle = dlopen(fwURL.path, mode) else {
let err = String(cString: dlerror()) let err = String(cString: dlerror())
self.lastError = NSError(domain: "TorManager", code: -10, userInfo: [NSLocalizedDescriptionKey: "dlopen failed: \(err)"]) self.lastError = NSError(domain: "TorManager", code: -10, userInfo: [NSLocalizedDescriptionKey: "dlopen failed: \(err)"])
@@ -314,7 +314,7 @@ final class TorManager: ObservableObject {
argv.append(contentsOf: ["-f", torrc]) argv.append(contentsOf: ["-f", torrc])
} }
// Run Tor on a background thread to avoid blocking the main actor // Run Tor on a background thread to avoid blocking the main actor
SecureLogger.info("TorManager: launching tor_main with torrc", category: .session) SecureLogger.log("TorManager: launching tor_main with torrc", category: SecureLogger.session, level: .info)
let argc = Int32(argv.count) let argc = Int32(argv.count)
DispatchQueue.global(qos: .utility).async { DispatchQueue.global(qos: .utility).async {
// Build stable C argv in this thread // Build stable C argv in this thread
@@ -339,9 +339,9 @@ final class TorManager: ObservableObject {
self.socksReady = ready self.socksReady = ready
if !ready { if !ready {
self.lastError = NSError(domain: "TorManager", code: -12, userInfo: [NSLocalizedDescriptionKey: "Tor SOCKS not reachable after dlopen start"]) self.lastError = NSError(domain: "TorManager", code: -12, userInfo: [NSLocalizedDescriptionKey: "Tor SOCKS not reachable after dlopen start"])
SecureLogger.error("TorManager: SOCKS not reachable (timeout)", category: .session) SecureLogger.log("TorManager: SOCKS not reachable (timeout)", category: SecureLogger.session, level: .error)
} else { } else {
SecureLogger.info("TorManager: SOCKS ready at \(self.socksHost):\(self.socksPort)", category: .session) SecureLogger.log("TorManager: SOCKS ready at \(self.socksHost):\(self.socksPort)", category: SecureLogger.session, level: .info)
} }
// isStarting will be cleared when bootstrap reaches 100% // isStarting will be cleared when bootstrap reaches 100%
} }
@@ -385,7 +385,7 @@ final class TorManager: ObservableObject {
var argv: [String] = ["tor"] var argv: [String] = ["tor"]
if let torrc = torrcURL()?.path { argv.append(contentsOf: ["-f", torrc]) } if let torrc = torrcURL()?.path { argv.append(contentsOf: ["-f", torrc]) }
SecureLogger.info("TorManager: starting tor_main (static)", category: .session) SecureLogger.log("TorManager: starting tor_main (static)", category: SecureLogger.session, level: .info)
let argc = Int32(argv.count) let argc = Int32(argv.count)
DispatchQueue.global(qos: .utility).async { DispatchQueue.global(qos: .utility).async {
// Build stable C argv in this thread // Build stable C argv in this thread
@@ -409,10 +409,10 @@ final class TorManager: ObservableObject {
await MainActor.run { await MainActor.run {
self.socksReady = ready self.socksReady = ready
if ready { if ready {
SecureLogger.info("TorManager: SOCKS ready at \(self.socksHost):\(self.socksPort)", category: .session) SecureLogger.log("TorManager: SOCKS ready at \(self.socksHost):\(self.socksPort)", category: SecureLogger.session, level: .info)
} else { } else {
self.lastError = NSError(domain: "TorManager", code: -13, userInfo: [NSLocalizedDescriptionKey: "Tor SOCKS not reachable after static start"]) self.lastError = NSError(domain: "TorManager", code: -13, userInfo: [NSLocalizedDescriptionKey: "Tor SOCKS not reachable after static start"])
SecureLogger.error("TorManager: SOCKS not reachable (timeout)", category: .session) SecureLogger.log("TorManager: SOCKS not reachable (timeout)", category: SecureLogger.session, level: .error)
} }
// isStarting will be cleared when bootstrap reaches 100% // isStarting will be cleared when bootstrap reaches 100%
} }
-2
View File
@@ -37,8 +37,6 @@ enum TransportConfig {
// UI thresholds // UI thresholds
static let uiLateInsertThreshold: TimeInterval = 15.0 static let uiLateInsertThreshold: TimeInterval = 15.0
// Geohash public chats are more sensitive to ordering; use a tighter threshold
static let uiLateInsertThresholdGeo: TimeInterval = 0.0
static let uiProcessedNostrEventsCap: Int = 2000 static let uiProcessedNostrEventsCap: Int = 2000
static let uiChannelInactivityThresholdSeconds: TimeInterval = 9 * 60 static let uiChannelInactivityThresholdSeconds: TimeInterval = 9 * 60
+14 -6
View File
@@ -13,7 +13,7 @@ import CryptoKit
/// Single source of truth for peer state, combining mesh connectivity and favorites /// Single source of truth for peer state, combining mesh connectivity and favorites
@MainActor @MainActor
final class UnifiedPeerService: ObservableObject, TransportPeerEventsDelegate { class UnifiedPeerService: ObservableObject, TransportPeerEventsDelegate {
// MARK: - Published Properties // MARK: - Published Properties
@@ -203,7 +203,11 @@ final class UnifiedPeerService: ObservableObject, TransportPeerEventsDelegate {
if let favorite = favoriteByNickname, if let favorite = favoriteByNickname,
let noiseKey = peerInfo.noisePublicKey { let noiseKey = peerInfo.noisePublicKey {
SecureLogger.debug("🔄 Found favorite for '\(peerInfo.nickname)' by nickname, updating noise key", category: .session) SecureLogger.log(
"🔄 Found favorite for '\(peerInfo.nickname)' by nickname, updating noise key",
category: SecureLogger.session,
level: .debug
)
// Update the favorite's key in persistence // Update the favorite's key in persistence
favoritesService.updateNoisePublicKey( favoritesService.updateNoisePublicKey(
@@ -278,7 +282,8 @@ final class UnifiedPeerService: ObservableObject, TransportPeerEventsDelegate {
/// Toggle favorite status /// Toggle favorite status
func toggleFavorite(_ peerID: String) { func toggleFavorite(_ peerID: String) {
guard let peer = getPeer(by: peerID) else { guard let peer = getPeer(by: peerID) else {
SecureLogger.warning("⚠️ Cannot toggle favorite - peer not found: \(peerID)", category: .session) SecureLogger.log("⚠️ Cannot toggle favorite - peer not found: \(peerID)",
category: SecureLogger.session, level: .warning)
return return
} }
@@ -288,13 +293,15 @@ final class UnifiedPeerService: ObservableObject, TransportPeerEventsDelegate {
var actualNickname = peer.nickname var actualNickname = peer.nickname
// Debug logging to understand the issue // Debug logging to understand the issue
SecureLogger.debug("🔍 Toggle favorite - peer.nickname: '\(peer.nickname)', peer.displayName: '\(peer.displayName)', peerID: \(peerID)", category: .session) SecureLogger.log("🔍 Toggle favorite - peer.nickname: '\(peer.nickname)', peer.displayName: '\(peer.displayName)', peerID: \(peerID)",
category: SecureLogger.session, level: .debug)
if actualNickname.isEmpty { if actualNickname.isEmpty {
// Try to get from mesh service's current peer list // Try to get from mesh service's current peer list
if let meshPeerNickname = meshService.peerNickname(peerID: peerID) { if let meshPeerNickname = meshService.peerNickname(peerID: peerID) {
actualNickname = meshPeerNickname actualNickname = meshPeerNickname
SecureLogger.debug("🔍 Got nickname from mesh service: '\(actualNickname)'", category: .session) SecureLogger.log("🔍 Got nickname from mesh service: '\(actualNickname)'",
category: SecureLogger.session, level: .debug)
} }
} }
@@ -321,7 +328,8 @@ final class UnifiedPeerService: ObservableObject, TransportPeerEventsDelegate {
} }
// Log the final nickname being saved // Log the final nickname being saved
SecureLogger.debug("⭐️ Toggled favorite for '\(finalNickname)' (peerID: \(peerID), was: \(wasFavorite), now: \(!wasFavorite))", category: .session) SecureLogger.log("⭐️ Toggled favorite for '\(finalNickname)' (peerID: \(peerID), was: \(wasFavorite), now: \(!wasFavorite))",
category: SecureLogger.session, level: .debug)
// Send favorite notification to the peer via router (mesh or Nostr) // Send favorite notification to the peer via router (mesh or Nostr)
if let router = messageRouter { if let router = messageRouter {
-20
View File
@@ -1,20 +0,0 @@
//
// OSLog+Categories.swift
// bitchat
//
// This is free and unencumbered software released into the public domain.
// For more information, see <https://unlicense.org>
//
import os.log
extension OSLog {
private static let subsystem = "chat.bitchat"
static let noise = OSLog(subsystem: subsystem, category: "noise")
static let encryption = OSLog(subsystem: subsystem, category: "encryption")
static let keychain = OSLog(subsystem: subsystem, category: "keychain")
static let session = OSLog(subsystem: subsystem, category: "session")
static let security = OSLog(subsystem: subsystem, category: "security")
static let handshake = OSLog(subsystem: subsystem, category: "handshake")
}
+68 -89
View File
@@ -11,7 +11,18 @@ import os.log
/// Centralized security-aware logging framework /// Centralized security-aware logging framework
/// Provides safe logging that filters sensitive data and security events /// Provides safe logging that filters sensitive data and security events
final class SecureLogger { class SecureLogger {
// MARK: - Log Categories
private static let subsystem = "chat.bitchat"
static let noise = OSLog(subsystem: subsystem, category: "noise")
static let encryption = OSLog(subsystem: subsystem, category: "encryption")
static let keychain = OSLog(subsystem: subsystem, category: "keychain")
static let session = OSLog(subsystem: subsystem, category: "session")
static let security = OSLog(subsystem: subsystem, category: "security")
static let handshake = OSLog(subsystem: subsystem, category: "handshake")
// MARK: - Timestamp Formatter // MARK: - Timestamp Formatter
@@ -113,90 +124,24 @@ final class SecureLogger {
// MARK: - Public Logging Methods // MARK: - Public Logging Methods
static func debug(_ message: @autoclosure () -> String, category: OSLog = .noise, /// Log a security event
file: String = #file, line: Int = #line, function: String = #function) { static func logSecurityEvent(_ event: SecurityEvent, level: LogLevel = .info,
log(message(), category: category, level: .debug, file: file, line: line, function: function) file: String = #file, line: Int = #line, function: String = #function) {
} guard shouldLog(level) else { return }
static func info(_ message: @autoclosure () -> String, category: OSLog = .noise,
file: String = #file, line: Int = #line, function: String = #function) {
log(message(), category: category, level: .info, file: file, line: line, function: function)
}
static func warning(_ message: @autoclosure () -> String, category: OSLog = .noise,
file: String = #file, line: Int = #line, function: String = #function) {
log(message(), category: category, level: .warning, file: file, line: line, function: function)
}
static func error(_ message: @autoclosure () -> String, category: OSLog = .noise,
file: String = #file, line: Int = #line, function: String = #function) {
log(message(), category: category, level: .error, file: file, line: line, function: function)
}
// MARK: Security Event Logging
static func debug(_ event: SecurityEvent, file: String = #file, line: Int = #line, function: String = #function) {
logSecurityEvent(event, level: .debug, file: file, line: line, function: function)
}
static func info(_ event: SecurityEvent, file: String = #file, line: Int = #line, function: String = #function) {
logSecurityEvent(event, level: .info, file: file, line: line, function: function)
}
static func warning(_ event: SecurityEvent, file: String = #file, line: Int = #line, function: String = #function) {
logSecurityEvent(event, level: .warning, file: file, line: line, function: function)
}
static func error(_ event: SecurityEvent, file: String = #file, line: Int = #line, function: String = #function) {
logSecurityEvent(event, level: .error, file: file, line: line, function: function)
}
/// Log errors with context
static func error(_ error: Error, context: @autoclosure () -> String, category: OSLog = .noise,
file: String = #file, line: Int = #line, function: String = #function) {
let location = formatLocation(file: file, line: line, function: function) let location = formatLocation(file: file, line: line, function: function)
let sanitized = sanitize(context()) let message = "\(location) \(event.message)"
let errorDesc = sanitize(error.localizedDescription)
#if DEBUG #if DEBUG
os_log("%{public}@ Error in %{public}@: %{public}@", log: category, type: .error, location, sanitized, errorDesc) os_log("%{public}@", log: security, type: level.osLogType, message)
#else #else
os_log("%{private}@ Error in %{private}@: %{private}@", log: category, type: .error, location, sanitized, errorDesc) // In release, use private logging to prevent sensitive data exposure
os_log("%{private}@", log: security, type: level.osLogType, message)
#endif #endif
} }
}
// MARK: - Convenience Extensions
extension SecureLogger {
enum KeyOperation: String, CustomStringConvertible {
case load
case create
case generate
case delete
case save
var description: String { rawValue }
}
/// Log key management operations
static func logKeyOperation(_ operation: KeyOperation, keyType: String, success: Bool = true,
file: String = #file, line: Int = #line, function: String = #function) {
if success {
debug("Key operation '\(operation)' for \(keyType) succeeded", category: .keychain, file: file, line: line, function: function)
} else {
error("Key operation '\(operation)' for \(keyType) failed", category: .keychain, file: file, line: line, function: function)
}
}
}
// MARK: - Private Helpers
private extension SecureLogger {
/// Log general messages with automatic sensitive data filtering /// Log general messages with automatic sensitive data filtering
static func log(_ message: @autoclosure () -> String, category: OSLog, level: LogLevel, static func log(_ message: @autoclosure () -> String, category: OSLog = noise, level: LogLevel = .debug,
file: String, line: Int, function: String) { file: String = #file, line: Int = #line, function: String = #function) {
guard shouldLog(level) else { return } guard shouldLog(level) else { return }
let location = formatLocation(file: file, line: line, function: function) let location = formatLocation(file: file, line: line, function: function)
let sanitized = sanitize("\(location) \(message())") let sanitized = sanitize("\(location) \(message())")
@@ -211,30 +156,31 @@ private extension SecureLogger {
#endif #endif
} }
/// Log a security event /// Log errors with context
static func logSecurityEvent(_ event: SecurityEvent, level: LogLevel = .info, static func logError(_ error: Error, context: @autoclosure () -> String, category: OSLog = noise,
file: String, line: Int, function: String) { file: String = #file, line: Int = #line, function: String = #function) {
guard shouldLog(level) else { return }
let location = formatLocation(file: file, line: line, function: function) let location = formatLocation(file: file, line: line, function: function)
let message = "\(location) \(event.message)" let sanitized = sanitize(context())
let errorDesc = sanitize(error.localizedDescription)
#if DEBUG #if DEBUG
os_log("%{public}@", log: .security, type: level.osLogType, message) os_log("%{public}@ Error in %{public}@: %{public}@", log: category, type: .error, location, sanitized, errorDesc)
#else #else
// In release, use private logging to prevent sensitive data exposure os_log("%{private}@ Error in %{private}@: %{private}@", log: category, type: .error, location, sanitized, errorDesc)
os_log("%{private}@", log: .security, type: level.osLogType, message)
#endif #endif
} }
// MARK: - Private Helpers
/// Format location information for logging /// Format location information for logging
static func formatLocation(file: String, line: Int, function: String) -> String { private static func formatLocation(file: String, line: Int, function: String) -> String {
let fileName = (file as NSString).lastPathComponent let fileName = (file as NSString).lastPathComponent
let timestamp = timestampFormatter.string(from: Date()) let timestamp = timestampFormatter.string(from: Date())
return "[\(timestamp)] [\(fileName):\(line) \(function)]" return "[\(timestamp)] [\(fileName):\(line) \(function)]"
} }
/// Sanitize strings to remove potentially sensitive data /// Sanitize strings to remove potentially sensitive data
static func sanitize(_ input: String) -> String { private static func sanitize(_ input: String) -> String {
let key = input as NSString let key = input as NSString
// Check cache first // Check cache first
@@ -280,12 +226,45 @@ private extension SecureLogger {
} }
/// Sanitize individual values /// Sanitize individual values
static func sanitize<T>(_ value: T) -> String { private static func sanitize<T>(_ value: T) -> String {
let stringValue = String(describing: value) let stringValue = String(describing: value)
return sanitize(stringValue) return sanitize(stringValue)
} }
} }
// MARK: - Convenience Extensions
extension SecureLogger {
/// Log handshake events
static func logHandshake(_ phase: String, peerID: String, success: Bool = true,
file: String = #file, line: Int = #line, function: String = #function) {
if success {
log("Handshake \(phase) with peer: \(peerID)", category: session, level: .info,
file: file, line: line, function: function)
} else {
log("Handshake \(phase) failed with peer: \(peerID)", category: session, level: .warning,
file: file, line: line, function: function)
}
}
/// Log encryption operations
static func logEncryption(_ operation: String, success: Bool = true,
file: String = #file, line: Int = #line, function: String = #function) {
let level: LogLevel = success ? .debug : .error
log("Encryption operation '\(operation)' \(success ? "succeeded" : "failed")",
category: encryption, level: level, file: file, line: line, function: function)
}
/// Log key management operations
static func logKeyOperation(_ operation: String, keyType: String, success: Bool = true,
file: String = #file, line: Int = #line, function: String = #function) {
let level: LogLevel = success ? .debug : .error
log("Key operation '\(operation)' for \(keyType) \(success ? "succeeded" : "failed")",
category: keychain, level: level, file: file, line: line, function: function)
}
}
// MARK: - Migration Helper // MARK: - Migration Helper
/// Helper to migrate from print statements to SecureLogger /// Helper to migrate from print statements to SecureLogger
@@ -294,6 +273,6 @@ func secureLog(_ items: Any..., separator: String = " ", terminator: String = "\
file: String = #file, line: Int = #line, function: String = #function) { file: String = #file, line: Int = #line, function: String = #function) {
#if DEBUG #if DEBUG
let message = items.map { String(describing: $0) }.joined(separator: separator) let message = items.map { String(describing: $0) }.joined(separator: separator)
SecureLogger.debug(message, file: file, line: line, function: function) SecureLogger.log(message, level: .debug, file: file, line: line, function: function)
#endif #endif
} }
+147 -119
View File
@@ -90,7 +90,7 @@ import UIKit
/// Manages the application state and business logic for BitChat. /// Manages the application state and business logic for BitChat.
/// Acts as the primary coordinator between UI components and backend services, /// Acts as the primary coordinator between UI components and backend services,
/// implementing the BitchatDelegate protocol to handle network events. /// implementing the BitchatDelegate protocol to handle network events.
final class ChatViewModel: ObservableObject, BitchatDelegate { class ChatViewModel: ObservableObject, BitchatDelegate {
// Precompiled regexes and detectors reused across formatting // Precompiled regexes and detectors reused across formatting
private enum Regexes { private enum Regexes {
static let hashtag: NSRegularExpression = { static let hashtag: NSRegularExpression = {
@@ -462,7 +462,8 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
if let data = try? JSONEncoder().encode(Array(sentReadReceipts)) { if let data = try? JSONEncoder().encode(Array(sentReadReceipts)) {
UserDefaults.standard.set(data, forKey: "sentReadReceipts") UserDefaults.standard.set(data, forKey: "sentReadReceipts")
} else { } else {
SecureLogger.error("❌ Failed to encode read receipts for persistence", category: .session) SecureLogger.log("❌ Failed to encode read receipts for persistence",
category: SecureLogger.session, level: .error)
} }
} }
} }
@@ -561,11 +562,11 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
// Wait for Tor readiness before Nostr init // Wait for Tor readiness before Nostr init
let ready = await TorManager.shared.awaitReady(timeout: 60) let ready = await TorManager.shared.awaitReady(timeout: 60)
guard ready else { guard ready else {
SecureLogger.error("Nostr init skipped: Tor not ready", category: .session) SecureLogger.log("Nostr init skipped: Tor not ready", category: SecureLogger.session, level: .error)
return return
} }
nostrRelayManager = NostrRelayManager.shared nostrRelayManager = NostrRelayManager.shared
SecureLogger.debug("Initializing Nostr relay connections", category: .session) SecureLogger.log("Initializing Nostr relay connections", category: SecureLogger.session, level: .debug)
// Connect is managed centrally on scene activation; avoid duplicate connects here // Connect is managed centrally on scene activation; avoid duplicate connects here
// Attempt to flush any queued outbox after Nostr comes online // Attempt to flush any queued outbox after Nostr comes online
@@ -595,7 +596,8 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
if uniquePeers[peer.id] == nil { if uniquePeers[peer.id] == nil {
uniquePeers[peer.id] = peer uniquePeers[peer.id] = peer
} else { } else {
SecureLogger.warning("⚠️ Duplicate peer ID detected: \(peer.id) (\(peer.displayName))", category: .session) SecureLogger.log("⚠️ Duplicate peer ID detected: \(peer.id) (\(peer.displayName))",
category: SecureLogger.session, level: .warning)
} }
} }
self.peerIndex = uniquePeers self.peerIndex = uniquePeers
@@ -897,7 +899,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
if Date().timeIntervalSince(eventTime) < 15 { return } if Date().timeIntervalSince(eventTime) < 15 { return }
} }
if let nickTag = event.tags.first(where: { $0.first == "n" }), nickTag.count >= 2 { if let nickTag = event.tags.first(where: { $0.first == "n" }), nickTag.count >= 2 {
let nick = nickTag[1].trimmingCharacters(in: .whitespacesAndNewlines) let nick = nickTag[1]
self.geoNicknames[event.pubkey.lowercased()] = nick self.geoNicknames[event.pubkey.lowercased()] = nick
} }
// Store mapping for geohash sender IDs used in messages (ensures consistent colors) // Store mapping for geohash sender IDs used in messages (ensures consistent colors)
@@ -928,9 +930,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
} }
let senderName = self.displayNameForNostrPubkey(event.pubkey) let senderName = self.displayNameForNostrPubkey(event.pubkey)
let content = event.content.trimmingCharacters(in: .whitespacesAndNewlines) let content = event.content.trimmingCharacters(in: .whitespacesAndNewlines)
// Clamp future timestamps to now to avoid future-dated messages skewing order let timestamp = Date(timeIntervalSince1970: TimeInterval(event.created_at))
let rawTs = Date(timeIntervalSince1970: TimeInterval(event.created_at))
let timestamp = min(rawTs, Date())
let mentions = self.parseMentions(from: content) let mentions = self.parseMentions(from: content)
let msg = BitchatMessage( let msg = BitchatMessage(
id: event.id, id: event.id,
@@ -1035,9 +1035,11 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
if let idx = self.privateChats[convKey]?.firstIndex(where: { $0.id == messageID }) { if let idx = self.privateChats[convKey]?.firstIndex(where: { $0.id == messageID }) {
self.privateChats[convKey]?[idx].deliveryStatus = .delivered(to: self.displayNameForNostrPubkey(senderPubkey), at: Date()) self.privateChats[convKey]?[idx].deliveryStatus = .delivered(to: self.displayNameForNostrPubkey(senderPubkey), at: Date())
self.objectWillChange.send() self.objectWillChange.send()
SecureLogger.info("GeoDM: recv DELIVERED for mid=\(messageID.prefix(8))… from=\(senderPubkey.prefix(8))", category: .session) SecureLogger.log("GeoDM: recv DELIVERED for mid=\(messageID.prefix(8))… from=\(senderPubkey.prefix(8))",
category: SecureLogger.session, level: .info)
} else { } else {
SecureLogger.warning("GeoDM: delivered ack for unknown mid=\(messageID.prefix(8))… conv=\(convKey)", category: .session) SecureLogger.log("GeoDM: delivered ack for unknown mid=\(messageID.prefix(8))… conv=\(convKey)",
category: SecureLogger.session, level: .warning)
} }
} }
case .readReceipt: case .readReceipt:
@@ -1045,9 +1047,11 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
if let idx = self.privateChats[convKey]?.firstIndex(where: { $0.id == messageID }) { if let idx = self.privateChats[convKey]?.firstIndex(where: { $0.id == messageID }) {
self.privateChats[convKey]?[idx].deliveryStatus = .read(by: self.displayNameForNostrPubkey(senderPubkey), at: Date()) self.privateChats[convKey]?[idx].deliveryStatus = .read(by: self.displayNameForNostrPubkey(senderPubkey), at: Date())
self.objectWillChange.send() self.objectWillChange.send()
SecureLogger.info("GeoDM: recv READ for mid=\(messageID.prefix(8))… from=\(senderPubkey.prefix(8))", category: .session) SecureLogger.log("GeoDM: recv READ for mid=\(messageID.prefix(8))… from=\(senderPubkey.prefix(8))",
category: SecureLogger.session, level: .info)
} else { } else {
SecureLogger.warning("GeoDM: read ack for unknown mid=\(messageID.prefix(8))… conv=\(convKey)", category: .session) SecureLogger.log("GeoDM: read ack for unknown mid=\(messageID.prefix(8))… conv=\(convKey)",
category: SecureLogger.session, level: .warning)
} }
} }
case .verifyChallenge, .verifyResponse: case .verifyChallenge, .verifyResponse:
@@ -1460,13 +1464,14 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
count: TransportConfig.nostrGeoRelayCount count: TransportConfig.nostrGeoRelayCount
) )
if targetRelays.isEmpty { if targetRelays.isEmpty {
SecureLogger.warning("Geo: no geohash relays available for \(ch.geohash); not sending", category: .session) SecureLogger.log("Geo: no geohash relays available for \(ch.geohash); not sending", category: SecureLogger.session, level: .warning)
} else { } else {
NostrRelayManager.shared.sendEvent(event, to: targetRelays) NostrRelayManager.shared.sendEvent(event, to: targetRelays)
} }
// Track ourselves as active participant // Track ourselves as active participant
self.recordGeoParticipant(pubkeyHex: identity.publicKeyHex) self.recordGeoParticipant(pubkeyHex: identity.publicKeyHex)
SecureLogger.debug("GeoTeleport: sent geo message pub=\(identity.publicKeyHex.prefix(8))… teleported=\(LocationChannelManager.shared.teleported)", category: .session) SecureLogger.log("GeoTeleport: sent geo message pub=\(identity.publicKeyHex.prefix(8))… teleported=\(LocationChannelManager.shared.teleported)",
category: SecureLogger.session, level: .debug)
// If we tagged this as teleported, also mark our pubkey in teleportedGeo for UI // If we tagged this as teleported, also mark our pubkey in teleportedGeo for UI
// Only when not in our regional set (and regional list is known) // Only when not in our regional set (and regional list is known)
let hasRegional = !LocationChannelManager.shared.availableChannels.isEmpty let hasRegional = !LocationChannelManager.shared.availableChannels.isEmpty
@@ -1474,10 +1479,11 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
if LocationChannelManager.shared.teleported && hasRegional && !inRegional { if LocationChannelManager.shared.teleported && hasRegional && !inRegional {
let key = identity.publicKeyHex.lowercased() let key = identity.publicKeyHex.lowercased()
self.teleportedGeo = self.teleportedGeo.union([key]) self.teleportedGeo = self.teleportedGeo.union([key])
SecureLogger.info("GeoTeleport: mark self teleported key=\(key.prefix(8))… total=\(self.teleportedGeo.count)", category: .session) SecureLogger.log("GeoTeleport: mark self teleported key=\(key.prefix(8))… total=\(self.teleportedGeo.count)",
category: SecureLogger.session, level: .info)
} }
} catch { } catch {
SecureLogger.error("❌ Failed to send geohash message: \(error)", category: .session) SecureLogger.log("❌ Failed to send geohash message: \(error)", category: SecureLogger.session, level: .error)
self.addSystemMessage("failed to send to location channel") self.addSystemMessage("failed to send to location channel")
} }
} }
@@ -1504,7 +1510,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
// Debug: log if any empty messages are present // Debug: log if any empty messages are present
let emptyMesh = messages.filter { $0.content.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty }.count let emptyMesh = messages.filter { $0.content.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty }.count
if emptyMesh > 0 { if emptyMesh > 0 {
SecureLogger.debug("RenderGuard: mesh timeline contains \(emptyMesh) empty messages", category: .session) SecureLogger.log("RenderGuard: mesh timeline contains \(emptyMesh) empty messages", category: SecureLogger.session, level: .debug)
} }
stopGeoParticipantsTimer() stopGeoParticipantsTimer()
geohashPeople = [] geohashPeople = []
@@ -1531,7 +1537,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
// Debug: log if any empty messages are present post-sanitize // Debug: log if any empty messages are present post-sanitize
let emptyGeo = messages.filter { $0.content.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty }.count let emptyGeo = messages.filter { $0.content.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty }.count
if emptyGeo > 0 { if emptyGeo > 0 {
SecureLogger.debug("RenderGuard: geohash \(ch.geohash) timeline has \(emptyGeo) empty messages after sanitize", category: .session) SecureLogger.log("RenderGuard: geohash \(ch.geohash) timeline has \(emptyGeo) empty messages after sanitize", category: SecureLogger.session, level: .debug)
} }
} }
// If switching to a location channel, flush any pending geohash-only system messages // If switching to a location channel, flush any pending geohash-only system messages
@@ -1563,7 +1569,8 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
let key = id.publicKeyHex.lowercased() let key = id.publicKeyHex.lowercased()
if LocationChannelManager.shared.teleported && hasRegional && !inRegional { if LocationChannelManager.shared.teleported && hasRegional && !inRegional {
teleportedGeo = teleportedGeo.union([key]) teleportedGeo = teleportedGeo.union([key])
SecureLogger.info("GeoTeleport: channel switch mark self teleported key=\(key.prefix(8))… total=\(teleportedGeo.count)", category: .session) SecureLogger.log("GeoTeleport: channel switch mark self teleported key=\(key.prefix(8))… total=\(teleportedGeo.count)",
category: SecureLogger.session, level: .info)
} else { } else {
teleportedGeo.remove(key) teleportedGeo.remove(key)
} }
@@ -1586,7 +1593,8 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
self.recordProcessedEvent(event.id) self.recordProcessedEvent(event.id)
// Log incoming tags for diagnostics // Log incoming tags for diagnostics
let tagSummary = event.tags.map { "[" + $0.joined(separator: ",") + "]" }.joined(separator: ",") let tagSummary = event.tags.map { "[" + $0.joined(separator: ",") + "]" }.joined(separator: ",")
SecureLogger.debug("GeoTeleport: recv pub=\(event.pubkey.prefix(8))… tags=\(tagSummary)", category: .session) SecureLogger.log("GeoTeleport: recv pub=\(event.pubkey.prefix(8))… tags=\(tagSummary)",
category: SecureLogger.session, level: .debug)
// Track teleport tag for participants only our format ["t", "teleport"] // Track teleport tag for participants only our format ["t", "teleport"]
let hasTeleportTag: Bool = event.tags.contains(where: { tag in let hasTeleportTag: Bool = event.tags.contains(where: { tag in
tag.count >= 2 && tag[0].lowercased() == "t" && tag[1].lowercased() == "teleport" tag.count >= 2 && tag[0].lowercased() == "t" && tag[1].lowercased() == "teleport"
@@ -1603,7 +1611,8 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
if !isSelf { if !isSelf {
Task { @MainActor in Task { @MainActor in
self.teleportedGeo = self.teleportedGeo.union([key]) self.teleportedGeo = self.teleportedGeo.union([key])
SecureLogger.info("GeoTeleport: mark peer teleported key=\(key.prefix(8))… total=\(self.teleportedGeo.count)", category: .session) SecureLogger.log("GeoTeleport: mark peer teleported key=\(key.prefix(8))… total=\(self.teleportedGeo.count)",
category: SecureLogger.session, level: .info)
} }
} }
} }
@@ -1616,7 +1625,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
} }
// Cache nickname from tag if present // Cache nickname from tag if present
if let nickTag = event.tags.first(where: { $0.first == "n" }), nickTag.count >= 2 { if let nickTag = event.tags.first(where: { $0.first == "n" }), nickTag.count >= 2 {
let nick = nickTag[1].trimmingCharacters(in: .whitespacesAndNewlines) let nick = nickTag[1]
self.geoNicknames[event.pubkey.lowercased()] = nick self.geoNicknames[event.pubkey.lowercased()] = nick
} }
// If this pubkey is blocked, skip mapping, participants, and timeline // If this pubkey is blocked, skip mapping, participants, and timeline
@@ -1638,9 +1647,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
content.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { content.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
return return
} }
// Clamp future timestamps let timestamp = Date(timeIntervalSince1970: TimeInterval(event.created_at))
let rawTs = Date(timeIntervalSince1970: TimeInterval(event.created_at))
let timestamp = min(rawTs, Date())
let mentions = self.parseMentions(from: content) let mentions = self.parseMentions(from: content)
let msg = BitchatMessage( let msg = BitchatMessage(
id: event.id, id: event.id,
@@ -1669,7 +1676,8 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
// pared back logging: subscribe debug only // pared back logging: subscribe debug only
// Log GeoDM subscribe only when Tor is ready to avoid early noise // Log GeoDM subscribe only when Tor is ready to avoid early noise
if TorManager.shared.isReady { if TorManager.shared.isReady {
SecureLogger.debug("GeoDM: subscribing DMs pub=\(id.publicKeyHex.prefix(8))… sub=\(dmSub)", category: .session) SecureLogger.log("GeoDM: subscribing DMs pub=\(id.publicKeyHex.prefix(8))… sub=\(dmSub)",
category: SecureLogger.session, level: .debug)
} }
let dmFilter = NostrFilter.giftWrapsFor(pubkey: id.publicKeyHex, since: Date().addingTimeInterval(-TransportConfig.nostrDMSubscribeLookbackSeconds)) let dmFilter = NostrFilter.giftWrapsFor(pubkey: id.publicKeyHex, since: Date().addingTimeInterval(-TransportConfig.nostrDMSubscribeLookbackSeconds))
NostrRelayManager.shared.subscribe(filter: dmFilter, id: dmSub) { [weak self] giftWrap in NostrRelayManager.shared.subscribe(filter: dmFilter, id: dmSub) { [weak self] giftWrap in
@@ -1679,10 +1687,12 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
self.recordProcessedEvent(giftWrap.id) self.recordProcessedEvent(giftWrap.id)
// Decrypt with per-geohash identity // Decrypt with per-geohash identity
guard let (content, senderPubkey, rumorTs) = try? NostrProtocol.decryptPrivateMessage(giftWrap: giftWrap, recipientIdentity: id) else { guard let (content, senderPubkey, rumorTs) = try? NostrProtocol.decryptPrivateMessage(giftWrap: giftWrap, recipientIdentity: id) else {
SecureLogger.warning("GeoDM: failed decrypt giftWrap id=\(giftWrap.id.prefix(8))", category: .session) SecureLogger.log("GeoDM: failed decrypt giftWrap id=\(giftWrap.id.prefix(8))",
category: SecureLogger.session, level: .warning)
return return
} }
SecureLogger.debug("GeoDM: decrypted gift-wrap id=\(giftWrap.id.prefix(16))... from=\(senderPubkey.prefix(8))...", category: .session) SecureLogger.log("GeoDM: decrypted gift-wrap id=\(giftWrap.id.prefix(16))... from=\(senderPubkey.prefix(8))...",
category: SecureLogger.session, level: .debug)
guard content.hasPrefix("bitchat1:") else { return } guard content.hasPrefix("bitchat1:") else { return }
guard let packetData = Self.base64URLDecode(String(content.dropFirst("bitchat1:".count))), guard let packetData = Self.base64URLDecode(String(content.dropFirst("bitchat1:".count))),
let packet = BitchatPacket.from(packetData) else { return } let packet = BitchatPacket.from(packetData) else { return }
@@ -1695,7 +1705,8 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
case .privateMessage: case .privateMessage:
guard let pm = PrivateMessagePacket.decode(from: noisePayload.data) else { return } guard let pm = PrivateMessagePacket.decode(from: noisePayload.data) else { return }
let messageId = pm.messageID let messageId = pm.messageID
SecureLogger.info("GeoDM: recv PM <- sender=\(senderPubkey.prefix(8))… mid=\(messageId.prefix(8))", category: .session) SecureLogger.log("GeoDM: recv PM <- sender=\(senderPubkey.prefix(8))… mid=\(messageId.prefix(8))",
category: SecureLogger.session, level: .info)
// Send delivery ACK immediately (even if duplicate), once per messageID // Send delivery ACK immediately (even if duplicate), once per messageID
if !self.sentGeoDeliveryAcks.contains(messageId) { if !self.sentGeoDeliveryAcks.contains(messageId) {
let nostrTransport = NostrTransport() let nostrTransport = NostrTransport()
@@ -1758,9 +1769,11 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
if let idx = self.privateChats[convKey]?.firstIndex(where: { $0.id == messageID }) { if let idx = self.privateChats[convKey]?.firstIndex(where: { $0.id == messageID }) {
self.privateChats[convKey]?[idx].deliveryStatus = .delivered(to: self.displayNameForNostrPubkey(senderPubkey), at: Date()) self.privateChats[convKey]?[idx].deliveryStatus = .delivered(to: self.displayNameForNostrPubkey(senderPubkey), at: Date())
self.objectWillChange.send() self.objectWillChange.send()
SecureLogger.info("GeoDM: recv DELIVERED for mid=\(messageID.prefix(8))… from=\(senderPubkey.prefix(8))", category: .session) SecureLogger.log("GeoDM: recv DELIVERED for mid=\(messageID.prefix(8))… from=\(senderPubkey.prefix(8))",
category: SecureLogger.session, level: .info)
} else { } else {
SecureLogger.warning("GeoDM: delivered ack for unknown mid=\(messageID.prefix(8))… conv=\(convKey)", category: .session) SecureLogger.log("GeoDM: delivered ack for unknown mid=\(messageID.prefix(8))… conv=\(convKey)",
category: SecureLogger.session, level: .warning)
} }
} }
case .readReceipt: case .readReceipt:
@@ -1768,9 +1781,11 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
if let idx = self.privateChats[convKey]?.firstIndex(where: { $0.id == messageID }) { if let idx = self.privateChats[convKey]?.firstIndex(where: { $0.id == messageID }) {
self.privateChats[convKey]?[idx].deliveryStatus = .read(by: self.displayNameForNostrPubkey(senderPubkey), at: Date()) self.privateChats[convKey]?[idx].deliveryStatus = .read(by: self.displayNameForNostrPubkey(senderPubkey), at: Date())
self.objectWillChange.send() self.objectWillChange.send()
SecureLogger.info("GeoDM: recv READ for mid=\(messageID.prefix(8))… from=\(senderPubkey.prefix(8))", category: .session) SecureLogger.log("GeoDM: recv READ for mid=\(messageID.prefix(8))… from=\(senderPubkey.prefix(8))",
category: SecureLogger.session, level: .info)
} else { } else {
SecureLogger.warning("GeoDM: read ack for unknown mid=\(messageID.prefix(8))… conv=\(convKey)", category: .session) SecureLogger.log("GeoDM: read ack for unknown mid=\(messageID.prefix(8))… conv=\(convKey)",
category: SecureLogger.session, level: .warning)
} }
} }
default: default:
@@ -2004,9 +2019,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
let senderSuffix = String(event.pubkey.suffix(4)) let senderSuffix = String(event.pubkey.suffix(4))
let nick = self.geoNicknames[event.pubkey.lowercased()] let nick = self.geoNicknames[event.pubkey.lowercased()]
let senderName = (nick?.isEmpty == false ? nick! : "anon") + "#" + senderSuffix let senderName = (nick?.isEmpty == false ? nick! : "anon") + "#" + senderSuffix
// Clamp future timestamps let ts = Date(timeIntervalSince1970: TimeInterval(event.created_at))
let rawTs = Date(timeIntervalSince1970: TimeInterval(event.created_at))
let ts = min(rawTs, Date())
let mentions = self.parseMentions(from: content) let mentions = self.parseMentions(from: content)
let msg = BitchatMessage( let msg = BitchatMessage(
id: event.id, id: event.id,
@@ -2139,7 +2152,8 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
} }
return return
} }
SecureLogger.debug("GeoDM: local send mid=\(messageID.prefix(8))… to=\(recipientHex.prefix(8))… conv=\(peerID)", category: .session) SecureLogger.log("GeoDM: local send mid=\(messageID.prefix(8))… to=\(recipientHex.prefix(8))… conv=\(peerID)",
category: SecureLogger.session, level: .debug)
let nostrTransport = NostrTransport() let nostrTransport = NostrTransport()
nostrTransport.senderPeerID = meshService.myPeerID nostrTransport.senderPeerID = meshService.myPeerID
nostrTransport.sendPrivateMessageGeohash(content: content, toRecipientHex: recipientHex, from: id, messageID: messageID) nostrTransport.sendPrivateMessageGeohash(content: content, toRecipientHex: recipientHex, from: id, messageID: messageID)
@@ -2459,14 +2473,16 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
// If any temp peer ID had unread messages, mark the consolidated peer as unread // If any temp peer ID had unread messages, mark the consolidated peer as unread
if hadUnreadTemp { if hadUnreadTemp {
unreadPrivateMessages.insert(peerID) unreadPrivateMessages.insert(peerID)
SecureLogger.debug("📬 Transferred unread status from temp peer IDs to \(peerID)", category: .session) SecureLogger.log("📬 Transferred unread status from temp peer IDs to \(peerID)",
category: SecureLogger.session, level: .debug)
} }
if consolidatedCount > 0 { if consolidatedCount > 0 {
// Sort by timestamp // Sort by timestamp
privateChats[peerID]?.sort { $0.timestamp < $1.timestamp } privateChats[peerID]?.sort { $0.timestamp < $1.timestamp }
SecureLogger.info("📥 Consolidated \(consolidatedCount) Nostr messages from temporary peer IDs to \(peerNickname)", category: .session) SecureLogger.log("📥 Consolidated \(consolidatedCount) Nostr messages from temporary peer IDs to \(peerNickname)",
category: SecureLogger.session, level: .info)
} }
} }
@@ -2480,7 +2496,8 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
break break
} }
} else { } else {
SecureLogger.debug("GeoDM: skipping mesh handshake for virtual peerID=\(peerID)", category: .session) SecureLogger.log("GeoDM: skipping mesh handshake for virtual peerID=\(peerID)",
category: SecureLogger.session, level: .debug)
} }
// Delegate to private chat manager but add already-acked messages first // Delegate to private chat manager but add already-acked messages first
@@ -2560,7 +2577,8 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
@objc private func handleNostrReadReceipt(_ notification: Notification) { @objc private func handleNostrReadReceipt(_ notification: Notification) {
guard let receipt = notification.userInfo?["receipt"] as? ReadReceipt else { return } guard let receipt = notification.userInfo?["receipt"] as? ReadReceipt else { return }
SecureLogger.info("📖 Handling read receipt for message \(receipt.originalMessageID) from Nostr", category: .session) SecureLogger.log("📖 Handling read receipt for message \(receipt.originalMessageID) from Nostr",
category: SecureLogger.session, level: .info)
// Process the read receipt through the same flow as Bluetooth read receipts // Process the read receipt through the same flow as Bluetooth read receipts
didReceiveReadReceipt(receipt) didReceiveReadReceipt(receipt)
@@ -2585,7 +2603,8 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
// If we have a private chat open with the old peer ID, update it to the new one // If we have a private chat open with the old peer ID, update it to the new one
if selectedPrivateChatPeer == oldPeerID { if selectedPrivateChatPeer == oldPeerID {
SecureLogger.info("📱 Updating private chat peer ID due to key change: \(oldPeerID) -> \(newPeerID)", category: .session) SecureLogger.log("📱 Updating private chat peer ID due to key change: \(oldPeerID) -> \(newPeerID)",
category: SecureLogger.session, level: .info)
// Transfer private chat messages to new peer ID // Transfer private chat messages to new peer ID
if let messages = privateChats[oldPeerID] { if let messages = privateChats[oldPeerID] {
@@ -2616,7 +2635,8 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
} else { } else {
// Even if the chat isn't open, migrate any existing private chat data // Even if the chat isn't open, migrate any existing private chat data
if let messages = privateChats[oldPeerID] { if let messages = privateChats[oldPeerID] {
SecureLogger.debug("📱 Migrating private chat messages from \(oldPeerID) to \(newPeerID)", category: .session) SecureLogger.log("📱 Migrating private chat messages from \(oldPeerID) to \(newPeerID)",
category: SecureLogger.session, level: .debug)
var chats = privateChats var chats = privateChats
chats[newPeerID] = messages chats[newPeerID] = messages
chats.removeValue(forKey: oldPeerID) chats.removeValue(forKey: oldPeerID)
@@ -2722,7 +2742,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
messageRouter.sendPrivate(screenshotMessage, to: peerID, recipientNickname: peerNickname, messageID: UUID().uuidString) messageRouter.sendPrivate(screenshotMessage, to: peerID, recipientNickname: peerNickname, messageID: UUID().uuidString)
default: default:
// Don't send screenshot notification if no session exists // Don't send screenshot notification if no session exists
SecureLogger.debug("Skipping screenshot notification to \(peerID) - no established session", category: .security) SecureLogger.log("Skipping screenshot notification to \(peerID) - no established session", category: SecureLogger.security, level: .debug)
} }
} }
@@ -2763,14 +2783,14 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
) )
let targetRelays = GeoRelayDirectory.shared.closestRelays(toGeohash: ch.geohash, count: 5) let targetRelays = GeoRelayDirectory.shared.closestRelays(toGeohash: ch.geohash, count: 5)
if targetRelays.isEmpty { if targetRelays.isEmpty {
SecureLogger.warning("Geo: no geohash relays available for \(ch.geohash); not sending", category: .session) SecureLogger.log("Geo: no geohash relays available for \(ch.geohash); not sending", category: SecureLogger.session, level: .warning)
} else { } else {
NostrRelayManager.shared.sendEvent(event, to: targetRelays) NostrRelayManager.shared.sendEvent(event, to: targetRelays)
} }
// Track ourselves as active participant // Track ourselves as active participant
self.recordGeoParticipant(pubkeyHex: identity.publicKeyHex) self.recordGeoParticipant(pubkeyHex: identity.publicKeyHex)
} catch { } catch {
SecureLogger.error("❌ Failed to send geohash screenshot message: \(error)", category: .session) SecureLogger.log("❌ Failed to send geohash screenshot message: \(error)", category: SecureLogger.session, level: .error)
self.addSystemMessage("failed to send to location channel") self.addSystemMessage("failed to send to location channel")
} }
} }
@@ -2828,7 +2848,8 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
// Search for the current peer ID with the same nickname // Search for the current peer ID with the same nickname
for (currentPeerID, currentNickname) in meshService.getPeerNicknames() { for (currentPeerID, currentNickname) in meshService.getPeerNicknames() {
if currentNickname == peerNickname { if currentNickname == peerNickname {
SecureLogger.info("📖 Resolved updated peer ID for read receipt: \(peerID) -> \(currentPeerID)", category: .session) SecureLogger.log("📖 Resolved updated peer ID for read receipt: \(peerID) -> \(currentPeerID)",
category: SecureLogger.session, level: .info)
actualPeerID = currentPeerID actualPeerID = currentPeerID
break break
} }
@@ -2856,7 +2877,8 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
let messages = privateChats[peerID] ?? [] let messages = privateChats[peerID] ?? []
for message in messages where message.senderPeerID == peerID && !message.isRelay { for message in messages where message.senderPeerID == peerID && !message.isRelay {
if !sentReadReceipts.contains(message.id) { if !sentReadReceipts.contains(message.id) {
SecureLogger.debug("GeoDM: sending READ for mid=\(message.id.prefix(8))… to=\(recipientHex.prefix(8))", category: .session) SecureLogger.log("GeoDM: sending READ for mid=\(message.id.prefix(8))… to=\(recipientHex.prefix(8))",
category: SecureLogger.session, level: .debug)
let nostrTransport = NostrTransport() let nostrTransport = NostrTransport()
nostrTransport.senderPeerID = meshService.myPeerID nostrTransport.senderPeerID = meshService.myPeerID
nostrTransport.sendReadReceiptGeohash(message.id, toRecipientHex: recipientHex, from: id) nostrTransport.sendReadReceiptGeohash(message.id, toRecipientHex: recipientHex, from: id)
@@ -2964,14 +2986,6 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
// When in a geohash channel, allow resolving by geohash participant nickname // When in a geohash channel, allow resolving by geohash participant nickname
switch LocationChannelManager.shared.selectedChannel { switch LocationChannelManager.shared.selectedChannel {
case .location: case .location:
// If a disambiguation suffix is present (e.g., "name#abcd"), try exact displayName match first
if nickname.contains("#") {
if let person = visibleGeohashPeople().first(where: { $0.displayName == nickname }) {
let convKey = "nostr_" + String(person.id.prefix(TransportConfig.nostrConvKeyPrefixLength))
nostrKeyMapping[convKey] = person.id
return convKey
}
}
let base: String = { let base: String = {
if let hashIndex = nickname.firstIndex(of: "#") { return String(nickname[..<hashIndex]) } if let hashIndex = nickname.firstIndex(of: "#") { return String(nickname[..<hashIndex]) }
return nickname return nickname
@@ -4308,14 +4322,14 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
verifiedFingerprints = SecureIdentityStateManager.shared.getVerifiedFingerprints() verifiedFingerprints = SecureIdentityStateManager.shared.getVerifiedFingerprints()
// Log snapshot for debugging persistence // Log snapshot for debugging persistence
let sample = Array(verifiedFingerprints.prefix(TransportConfig.uiFingerprintSampleCount)).map { $0.prefix(8) }.joined(separator: ", ") let sample = Array(verifiedFingerprints.prefix(TransportConfig.uiFingerprintSampleCount)).map { $0.prefix(8) }.joined(separator: ", ")
SecureLogger.info("🔐 Verified loaded: \(verifiedFingerprints.count) [\(sample)]", category: .security) SecureLogger.log("🔐 Verified loaded: \(verifiedFingerprints.count) [\(sample)]", category: SecureLogger.security, level: .info)
// Also log any offline favorites and whether we consider them verified // Also log any offline favorites and whether we consider them verified
let offlineFavorites = unifiedPeerService.favorites.filter { !$0.isConnected } let offlineFavorites = unifiedPeerService.favorites.filter { !$0.isConnected }
for fav in offlineFavorites { for fav in offlineFavorites {
let fp = unifiedPeerService.getFingerprint(for: fav.id) let fp = unifiedPeerService.getFingerprint(for: fav.id)
let isVer = fp.flatMap { verifiedFingerprints.contains($0) } ?? false let isVer = fp.flatMap { verifiedFingerprints.contains($0) } ?? false
let fpShort = fp?.prefix(8) ?? "nil" let fpShort = fp?.prefix(8) ?? "nil"
SecureLogger.info("⭐️ Favorite offline: \(fav.nickname) fp=\(fpShort) verified=\(isVer)", category: .security) SecureLogger.log("⭐️ Favorite offline: \(fav.nickname) fp=\(fpShort) verified=\(isVer)", category: SecureLogger.security, level: .info)
} }
// Invalidate cached encryption statuses so offline favorites can show verified badges immediately // Invalidate cached encryption statuses so offline favorites can show verified badges immediately
invalidateEncryptionCache() invalidateEncryptionCache()
@@ -4331,7 +4345,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
DispatchQueue.main.async { DispatchQueue.main.async {
guard let self = self else { return } guard let self = self else { return }
SecureLogger.debug("🔐 Authenticated: \(peerID)", category: .security) SecureLogger.log("🔐 Authenticated: \(peerID)", category: SecureLogger.security, level: .debug)
// Update encryption status // Update encryption status
if self.verifiedFingerprints.contains(fingerprint) { if self.verifiedFingerprints.contains(fingerprint) {
@@ -4350,7 +4364,8 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
let keyData = self.meshService.getNoiseService().getPeerPublicKeyData(peerID) { let keyData = self.meshService.getNoiseService().getPeerPublicKeyData(peerID) {
let stable = keyData.hexEncodedString() let stable = keyData.hexEncodedString()
self.shortIDToNoiseKey[peerID] = stable self.shortIDToNoiseKey[peerID] = stable
SecureLogger.debug("🗺️ Mapped short peerID to Noise key for header continuity: \(peerID) -> \(stable.prefix(8))", category: .session) SecureLogger.log("🗺️ Mapped short peerID to Noise key for header continuity: \(peerID) -> \(stable.prefix(8))",
category: SecureLogger.session, level: .debug)
} }
// If a QR verification is pending but not sent yet, send it now that session is authenticated // If a QR verification is pending but not sent yet, send it now that session is authenticated
@@ -4358,7 +4373,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
self.meshService.sendVerifyChallenge(to: peerID, noiseKeyHex: pending.noiseKeyHex, nonceA: pending.nonceA) self.meshService.sendVerifyChallenge(to: peerID, noiseKeyHex: pending.noiseKeyHex, nonceA: pending.nonceA)
pending.sent = true pending.sent = true
self.pendingQRVerifications[peerID] = pending self.pendingQRVerifications[peerID] = pending
SecureLogger.debug("📤 Sent deferred verify challenge to \(peerID) after handshake", category: .security) SecureLogger.log("📤 Sent deferred verify challenge to \(peerID) after handshake", category: SecureLogger.security, level: .debug)
} }
// Schedule UI update // Schedule UI update
@@ -4504,7 +4519,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
pendingQRVerifications.removeValue(forKey: peerID) pendingQRVerifications.removeValue(forKey: peerID)
if let fp = getFingerprint(for: peerID) { if let fp = getFingerprint(for: peerID) {
let short = fp.prefix(8) let short = fp.prefix(8)
SecureLogger.info("🔐 Marking verified fingerprint: \(short)", category: .security) SecureLogger.log("🔐 Marking verified fingerprint: \(short)", category: SecureLogger.security, level: .info)
SecureIdentityStateManager.shared.setVerified(fingerprint: fp, verified: true) SecureIdentityStateManager.shared.setVerified(fingerprint: fp, verified: true)
SecureIdentityStateManager.shared.forceSave() SecureIdentityStateManager.shared.forceSave()
verifiedFingerprints.insert(fp) verifiedFingerprints.insert(fp)
@@ -4590,7 +4605,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
// MARK: - Peer Connection Events // MARK: - Peer Connection Events
func didConnectToPeer(_ peerID: String) { func didConnectToPeer(_ peerID: String) {
SecureLogger.debug("🤝 Peer connected: \(peerID)", category: .session) SecureLogger.log("🤝 Peer connected: \(peerID)", category: SecureLogger.session, level: .debug)
// Handle all main actor work async // Handle all main actor work async
Task { @MainActor in Task { @MainActor in
@@ -4607,7 +4622,8 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
// Resend favorite notification with our Nostr key after a short delay // Resend favorite notification with our Nostr key after a short delay
try? await Task.sleep(nanoseconds: TransportConfig.uiAsyncMediumSleepNs) // 0.5 seconds try? await Task.sleep(nanoseconds: TransportConfig.uiAsyncMediumSleepNs) // 0.5 seconds
meshService.sendFavoriteNotification(to: peerID, isFavorite: true) meshService.sendFavoriteNotification(to: peerID, isFavorite: true)
SecureLogger.debug("📤 Resent favorite notification to reconnected peer \(peerID)", category: .session) SecureLogger.log("📤 Resent favorite notification to reconnected peer \(peerID)",
category: SecureLogger.session, level: .debug)
} }
// Force UI refresh // Force UI refresh
@@ -4627,7 +4643,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
} }
func didDisconnectFromPeer(_ peerID: String) { func didDisconnectFromPeer(_ peerID: String) {
SecureLogger.debug("👋 Peer disconnected: \(peerID)", category: .session) SecureLogger.log("👋 Peer disconnected: \(peerID)", category: SecureLogger.session, level: .debug)
// Remove ephemeral session from identity manager // Remove ephemeral session from identity manager
SecureIdentityStateManager.shared.removeEphemeralSession(peerID: peerID) SecureIdentityStateManager.shared.removeEphemeralSession(peerID: peerID)
@@ -4722,7 +4738,8 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
self.lastNetworkNotificationTime = Date() self.lastNetworkNotificationTime = Date()
self.recentlySeenPeers = currentPeerSet self.recentlySeenPeers = currentPeerSet
NotificationService.shared.sendNetworkAvailableNotification(peerCount: meshPeers.count) NotificationService.shared.sendNetworkAvailableNotification(peerCount: meshPeers.count)
SecureLogger.info("👥 Sent bitchatters nearby notification for \(meshPeers.count) mesh peers", category: .session) SecureLogger.log("👥 Sent bitchatters nearby notification for \(meshPeers.count) mesh peers",
category: SecureLogger.session, level: .info)
} }
} else { } else {
// No peers immediately reset to allow next rising-edge to notify // No peers immediately reset to allow next rising-edge to notify
@@ -4732,7 +4749,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
self.networkResetTimer?.invalidate() self.networkResetTimer?.invalidate()
self.networkResetTimer = nil self.networkResetTimer = nil
} }
SecureLogger.debug("⏳ Mesh empty — reset network notification state", category: .session) SecureLogger.log("⏳ Mesh empty — reset network notification state", category: SecureLogger.session, level: .debug)
} }
// Register ephemeral sessions for all connected peers // Register ephemeral sessions for all connected peers
@@ -4794,7 +4811,8 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
} }
if !idsToRemove.isEmpty { if !idsToRemove.isEmpty {
SecureLogger.debug("🧹 Cleaned up \(idsToRemove.count) stale unread peer IDs", category: .session) SecureLogger.log("🧹 Cleaned up \(idsToRemove.count) stale unread peer IDs",
category: SecureLogger.session, level: .debug)
} }
} }
@@ -4824,7 +4842,8 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
let removedCount = oldCount - sentReadReceipts.count let removedCount = oldCount - sentReadReceipts.count
if removedCount > 0 { if removedCount > 0 {
SecureLogger.debug("🧹 Cleaned up \(removedCount) old read receipts", category: .session) SecureLogger.log("🧹 Cleaned up \(removedCount) old read receipts",
category: SecureLogger.session, level: .debug)
} }
} }
@@ -4872,7 +4891,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
nostrPublicKey = data.hexEncodedString() nostrPublicKey = data.hexEncodedString()
} }
} catch { } catch {
SecureLogger.error("Failed to decode Nostr npub: \(error)", category: .session) SecureLogger.log("Failed to decode Nostr npub: \(error)", category: SecureLogger.session, level: .error)
} }
} }
@@ -5013,7 +5032,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
NostrRelayManager.shared.sendEvent(event, to: targetRelays) NostrRelayManager.shared.sendEvent(event, to: targetRelays)
} }
} catch { } catch {
SecureLogger.error("❌ Failed to send geohash raw message: \(error)", category: .session) SecureLogger.log("❌ Failed to send geohash raw message: \(error)", category: SecureLogger.session, level: .error)
} }
} }
return return
@@ -5029,11 +5048,12 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
@MainActor @MainActor
private func setupNostrMessageHandling() { private func setupNostrMessageHandling() {
guard let currentIdentity = try? NostrIdentityBridge.getCurrentNostrIdentity() else { guard let currentIdentity = try? NostrIdentityBridge.getCurrentNostrIdentity() else {
SecureLogger.warning("⚠️ No Nostr identity available for message handling", category: .session) SecureLogger.log("⚠️ No Nostr identity available for message handling", category: SecureLogger.session, level: .warning)
return return
} }
SecureLogger.debug("🔑 Setting up Nostr subscription for pubkey: \(currentIdentity.publicKeyHex.prefix(16))...", category: .session) SecureLogger.log("🔑 Setting up Nostr subscription for pubkey: \(currentIdentity.publicKeyHex.prefix(16))...",
category: SecureLogger.session, level: .debug)
// Subscribe to Nostr messages // Subscribe to Nostr messages
let filter = NostrFilter.giftWrapsFor( let filter = NostrFilter.giftWrapsFor(
@@ -5073,19 +5093,19 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
// Expect embedded BitChat packet content // Expect embedded BitChat packet content
guard content.hasPrefix("bitchat1:") else { guard content.hasPrefix("bitchat1:") else {
SecureLogger.debug("Ignoring non-embedded Nostr DM content", category: .session) SecureLogger.log("Ignoring non-embedded Nostr DM content", category: SecureLogger.session, level: .debug)
return return
} }
guard let packetData = Self.base64URLDecode(String(content.dropFirst("bitchat1:".count))), guard let packetData = Self.base64URLDecode(String(content.dropFirst("bitchat1:".count))),
let packet = BitchatPacket.from(packetData) else { let packet = BitchatPacket.from(packetData) else {
SecureLogger.error("Failed to decode embedded BitChat packet from Nostr DM", category: .session) SecureLogger.log("Failed to decode embedded BitChat packet from Nostr DM", category: SecureLogger.session, level: .error)
return return
} }
// Only process typed noiseEncrypted envelope for private messages/receipts // Only process typed noiseEncrypted envelope for private messages/receipts
guard packet.type == MessageType.noiseEncrypted.rawValue else { guard packet.type == MessageType.noiseEncrypted.rawValue else {
SecureLogger.warning("Unsupported embedded packet type: \(packet.type)", category: .session) SecureLogger.log("Unsupported embedded packet type: \(packet.type)", category: SecureLogger.session, level: .warning)
return return
} }
@@ -5099,7 +5119,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
// Parse plaintext typed payload // Parse plaintext typed payload
guard let noisePayload = NoisePayload.decode(packet.payload) else { guard let noisePayload = NoisePayload.decode(packet.payload) else {
SecureLogger.error("Failed to parse embedded NoisePayload", category: .session) SecureLogger.log("Failed to parse embedded NoisePayload", category: SecureLogger.session, level: .error)
return return
} }
@@ -5195,14 +5215,14 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
// Send delivery ack via Nostr embedded // Send delivery ack via Nostr embedded
if !wasReadBefore { if !wasReadBefore {
if let key = actualSenderNoiseKey { if let key = actualSenderNoiseKey {
SecureLogger.debug("Sending DELIVERED ack for \(messageId.prefix(8))… via router", category: .session) SecureLogger.log("Sending DELIVERED ack for \(messageId.prefix(8))… via router", category: SecureLogger.session, level: .debug)
messageRouter.sendDeliveryAck(messageId, to: key.hexEncodedString()) messageRouter.sendDeliveryAck(messageId, to: key.hexEncodedString())
} else if let id = try? NostrIdentityBridge.getCurrentNostrIdentity() { } else if let id = try? NostrIdentityBridge.getCurrentNostrIdentity() {
// Fallback: no Noise mapping yet send directly to sender's Nostr pubkey // Fallback: no Noise mapping yet send directly to sender's Nostr pubkey
let nt = NostrTransport() let nt = NostrTransport()
nt.senderPeerID = meshService.myPeerID nt.senderPeerID = meshService.myPeerID
nt.sendDeliveryAckGeohash(for: messageId, toRecipientHex: senderPubkey, from: id) nt.sendDeliveryAckGeohash(for: messageId, toRecipientHex: senderPubkey, from: id)
SecureLogger.debug("Sent DELIVERED ack directly to Nostr pub=\(senderPubkey.prefix(8))… for mid=\(messageId.prefix(8))", category: .session) SecureLogger.log("Sent DELIVERED ack directly to Nostr pub=\(senderPubkey.prefix(8))… for mid=\(messageId.prefix(8))", category: SecureLogger.session, level: .debug)
} }
} }
@@ -5217,7 +5237,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
if !sentReadReceipts.contains(messageId) { if !sentReadReceipts.contains(messageId) {
if let key = actualSenderNoiseKey { if let key = actualSenderNoiseKey {
let receipt = ReadReceipt(originalMessageID: messageId, readerID: meshService.myPeerID, readerNickname: nickname) let receipt = ReadReceipt(originalMessageID: messageId, readerID: meshService.myPeerID, readerNickname: nickname)
SecureLogger.debug("Viewing chat; sending READ ack for \(messageId.prefix(8))… via router", category: .session) SecureLogger.log("Viewing chat; sending READ ack for \(messageId.prefix(8))… via router", category: SecureLogger.session, level: .debug)
messageRouter.sendReadReceipt(receipt, to: key.hexEncodedString()) messageRouter.sendReadReceipt(receipt, to: key.hexEncodedString())
sentReadReceipts.insert(messageId) sentReadReceipts.insert(messageId)
} else if let id = try? NostrIdentityBridge.getCurrentNostrIdentity() { } else if let id = try? NostrIdentityBridge.getCurrentNostrIdentity() {
@@ -5225,7 +5245,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
nt.senderPeerID = meshService.myPeerID nt.senderPeerID = meshService.myPeerID
nt.sendReadReceiptGeohash(messageId, toRecipientHex: senderPubkey, from: id) nt.sendReadReceiptGeohash(messageId, toRecipientHex: senderPubkey, from: id)
sentReadReceipts.insert(messageId) sentReadReceipts.insert(messageId)
SecureLogger.debug("Viewing chat; sent READ ack directly to Nostr pub=\(senderPubkey.prefix(8))… for mid=\(messageId.prefix(8))", category: .session) SecureLogger.log("Viewing chat; sent READ ack directly to Nostr pub=\(senderPubkey.prefix(8))… for mid=\(messageId.prefix(8))", category: SecureLogger.session, level: .debug)
} }
} }
} else { } else {
@@ -5270,7 +5290,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
} }
} catch { } catch {
SecureLogger.error("Failed to decrypt Nostr message: \(error)", category: .session) SecureLogger.log("Failed to decrypt Nostr message: \(error)", category: SecureLogger.session, level: .error)
} }
} }
@@ -5291,7 +5311,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
// Parse ACK format: "ACK:TYPE:MESSAGE_ID" // Parse ACK format: "ACK:TYPE:MESSAGE_ID"
let parts = content.split(separator: ":", maxSplits: 2) let parts = content.split(separator: ":", maxSplits: 2)
guard parts.count >= 3 else { guard parts.count >= 3 else {
SecureLogger.warning("⚠️ Invalid ACK format: \(content)", category: .session) SecureLogger.log("⚠️ Invalid ACK format: \(content)", category: SecureLogger.session, level: .warning)
return return
} }
@@ -5306,7 +5326,8 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
} }
processedNostrAcks.insert(ackKey) processedNostrAcks.insert(ackKey)
SecureLogger.debug("📨 Received \(ackType) ACK for message \(messageId.prefix(16))... from \(senderPubkey.prefix(16))...", category: .session) SecureLogger.log("📨 Received \(ackType) ACK for message \(messageId.prefix(16))... from \(senderPubkey.prefix(16))...",
category: SecureLogger.session, level: .debug)
// Verify the sender has a valid Noise key // Verify the sender has a valid Noise key
guard findNoiseKey(for: senderPubkey) != nil else { guard findNoiseKey(for: senderPubkey) != nil else {
@@ -5325,11 +5346,12 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
case "READ": case "READ":
privateChats[chatPeerID]?[index].deliveryStatus = .read(by: "recipient", at: Date()) privateChats[chatPeerID]?[index].deliveryStatus = .read(by: "recipient", at: Date())
default: default:
SecureLogger.warning("⚠️ Unknown ACK type: \(ackType)", category: .session) SecureLogger.log("⚠️ Unknown ACK type: \(ackType)", category: SecureLogger.session, level: .warning)
} }
messageFound = true messageFound = true
SecureLogger.info("✅ Updated message \(messageId.prefix(16))... status to \(ackType) in chat \(chatPeerID.prefix(16))...", category: .session) SecureLogger.log("✅ Updated message \(messageId.prefix(16))... status to \(ackType) in chat \(chatPeerID.prefix(16))...",
category: SecureLogger.session, level: .info)
// Don't break - continue to update in all chats where this message exists // Don't break - continue to update in all chats where this message exists
} }
} }
@@ -5337,7 +5359,8 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
if messageFound { if messageFound {
objectWillChange.send() objectWillChange.send()
} else { } else {
SecureLogger.warning("⚠️ Could not find message \(messageId) to update status from ACK", category: .session) SecureLogger.log("⚠️ Could not find message \(messageId) to update status from ACK",
category: SecureLogger.session, level: .warning)
} }
} }
@@ -5363,7 +5386,8 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
var nostrPubkey: String? = nil var nostrPubkey: String? = nil
if parts.count > 1 { if parts.count > 1 {
nostrPubkey = String(parts[1]) nostrPubkey = String(parts[1])
SecureLogger.info("📝 Received Nostr npub in favorite notification: \(nostrPubkey ?? "none")", category: .session) SecureLogger.log("📝 Received Nostr npub in favorite notification: \(nostrPubkey ?? "none")",
category: SecureLogger.session, level: .info)
} }
// Get the noise public key for this peer // Get the noise public key for this peer
@@ -5381,7 +5405,8 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
} }
guard let finalNoiseKey = noiseKey else { guard let finalNoiseKey = noiseKey else {
SecureLogger.warning("⚠️ Cannot get Noise key for peer \(peerID)", category: .session) SecureLogger.log("⚠️ Cannot get Noise key for peer \(peerID)",
category: SecureLogger.session, level: .warning)
return return
} }
@@ -5395,7 +5420,8 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
// If they favorited us and provided their Nostr key, ensure it's stored // If they favorited us and provided their Nostr key, ensure it's stored
if isFavorite && nostrPubkey != nil { if isFavorite && nostrPubkey != nil {
SecureLogger.info("💾 Storing Nostr key association for \(senderNickname): \(nostrPubkey!.prefix(16))...", category: .session) SecureLogger.log("💾 Storing Nostr key association for \(senderNickname): \(nostrPubkey!.prefix(16))...",
category: SecureLogger.session, level: .info)
} }
// Show system message // Show system message
@@ -5506,7 +5532,8 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
// Not notifying for old message // Not notifying for old message
} }
SecureLogger.info("📬 Stored Nostr message from unknown sender \(finalSenderNickname) in temporary peer \(tempPeerID)", category: .session) SecureLogger.log("📬 Stored Nostr message from unknown sender \(finalSenderNickname) in temporary peer \(tempPeerID)",
category: SecureLogger.session, level: .info)
} }
@MainActor @MainActor
@@ -5518,14 +5545,16 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
} else { } else {
// Try to convert hex to npub // Try to convert hex to npub
guard let pubkeyData = Data(hexString: nostrPubkey) else { guard let pubkeyData = Data(hexString: nostrPubkey) else {
SecureLogger.warning("⚠️ Invalid hex public key format: \(nostrPubkey.prefix(16))...", category: .session) SecureLogger.log("⚠️ Invalid hex public key format: \(nostrPubkey.prefix(16))...",
category: SecureLogger.session, level: .warning)
return nil return nil
} }
do { do {
npubToMatch = try Bech32.encode(hrp: "npub", data: pubkeyData) npubToMatch = try Bech32.encode(hrp: "npub", data: pubkeyData)
} catch { } catch {
SecureLogger.warning("⚠️ Failed to convert hex to npub: \(error)", category: .session) SecureLogger.log("⚠️ Failed to convert hex to npub: \(error)",
category: SecureLogger.session, level: .warning)
return nil return nil
} }
} }
@@ -5535,19 +5564,22 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
if let storedNostrKey = relationship.peerNostrPublicKey { if let storedNostrKey = relationship.peerNostrPublicKey {
// Compare npub format // Compare npub format
if storedNostrKey == npubToMatch { if storedNostrKey == npubToMatch {
// SecureLogger.debug(" Found Noise key for Nostr sender (npub match)", category: .session) // SecureLogger.log(" Found Noise key for Nostr sender (npub match)",
// category: SecureLogger.session, level: .debug)
return noiseKey return noiseKey
} }
// Also try hex comparison if stored value is hex // Also try hex comparison if stored value is hex
if !storedNostrKey.hasPrefix("npub") && storedNostrKey == nostrPubkey { if !storedNostrKey.hasPrefix("npub") && storedNostrKey == nostrPubkey {
SecureLogger.debug("✅ Found Noise key for Nostr sender (hex match)", category: .session) SecureLogger.log("✅ Found Noise key for Nostr sender (hex match)",
category: SecureLogger.session, level: .debug)
return noiseKey return noiseKey
} }
} }
} }
SecureLogger.debug("⚠️ No matching Noise key found for Nostr pubkey: \(nostrPubkey.prefix(16))... (tried npub: \(npubToMatch.prefix(16))...)", category: .session) SecureLogger.log("⚠️ No matching Noise key found for Nostr pubkey: \(nostrPubkey.prefix(16))... (tried npub: \(npubToMatch.prefix(16))...)",
category: SecureLogger.session, level: .debug)
return nil return nil
} }
@@ -5575,13 +5607,13 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
// Try mesh first for connected peers // Try mesh first for connected peers
if meshService.isPeerConnected(peerID) { if meshService.isPeerConnected(peerID) {
messageRouter.sendFavoriteNotification(to: peerID, isFavorite: isFavorite) messageRouter.sendFavoriteNotification(to: peerID, isFavorite: isFavorite)
SecureLogger.debug("📤 Sent favorite notification via BLE to \(peerID)", category: .session) SecureLogger.log("📤 Sent favorite notification via BLE to \(peerID)", category: SecureLogger.session, level: .debug)
} else if let key = noiseKey { } else if let key = noiseKey {
// Send via Nostr for offline peers (using router) // Send via Nostr for offline peers (using router)
let recipientPeerID = key.hexEncodedString() let recipientPeerID = key.hexEncodedString()
messageRouter.sendFavoriteNotification(to: recipientPeerID, isFavorite: isFavorite) messageRouter.sendFavoriteNotification(to: recipientPeerID, isFavorite: isFavorite)
} else { } else {
SecureLogger.warning("⚠️ Cannot send favorite notification - peer not connected and no Nostr pubkey", category: .session) SecureLogger.log("⚠️ Cannot send favorite notification - peer not connected and no Nostr pubkey", category: SecureLogger.session, level: .warning)
} }
} }
@@ -5671,10 +5703,12 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
oldPeerIDsToRemove.append(oldPeerID) oldPeerIDsToRemove.append(oldPeerID)
} else { } else {
// Keep old messages in original location but don't show in UI // Keep old messages in original location but don't show in UI
SecureLogger.info("📦 Partially migrating \(recentMessages.count) of \(messages.count) messages from \(oldPeerID)", category: .session) SecureLogger.log("📦 Partially migrating \(recentMessages.count) of \(messages.count) messages from \(oldPeerID)",
category: SecureLogger.session, level: .info)
} }
SecureLogger.info("📦 Migrating \(recentMessages.count) recent messages from old peer ID \(oldPeerID) to \(peerID) (fingerprint match)", category: .session) SecureLogger.log("📦 Migrating \(recentMessages.count) recent messages from old peer ID \(oldPeerID) to \(peerID) (fingerprint match)",
category: SecureLogger.session, level: .info)
} else if currentFingerprint == nil || oldFingerprint == nil { } else if currentFingerprint == nil || oldFingerprint == nil {
// Check if this chat contains messages with this sender by nickname // Check if this chat contains messages with this sender by nickname
let isRelevantChat = recentMessages.contains { msg in let isRelevantChat = recentMessages.contains { msg in
@@ -5690,7 +5724,8 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
oldPeerIDsToRemove.append(oldPeerID) oldPeerIDsToRemove.append(oldPeerID)
} }
SecureLogger.warning("📦 Migrating \(recentMessages.count) recent messages from old peer ID \(oldPeerID) to \(peerID) (nickname match)", category: .session) SecureLogger.log("📦 Migrating \(recentMessages.count) recent messages from old peer ID \(oldPeerID) to \(peerID) (nickname match)",
category: SecureLogger.session, level: .warning)
} }
} }
} }
@@ -5729,7 +5764,8 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
// Update selectedPrivateChatPeer if it was pointing to an old ID // Update selectedPrivateChatPeer if it was pointing to an old ID
if needsSelectedUpdate { if needsSelectedUpdate {
selectedPrivateChatPeer = peerID selectedPrivateChatPeer = peerID
SecureLogger.info("📱 Updated selectedPrivateChatPeer from old ID to \(peerID) during migration", category: .session) SecureLogger.log("📱 Updated selectedPrivateChatPeer from old ID to \(peerID) during migration",
category: SecureLogger.session, level: .info)
} }
} }
} }
@@ -5738,11 +5774,11 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
/// Handle incoming private message /// Handle incoming private message
@MainActor @MainActor
private func handlePrivateMessage(_ message: BitchatMessage) { private func handlePrivateMessage(_ message: BitchatMessage) {
SecureLogger.debug("📥 handlePrivateMessage called for message from \(message.sender)", category: .session) SecureLogger.log("📥 handlePrivateMessage called for message from \(message.sender)", category: SecureLogger.session, level: .debug)
let senderPeerID = message.senderPeerID ?? getPeerIDForNickname(message.sender) let senderPeerID = message.senderPeerID ?? getPeerIDForNickname(message.sender)
guard let peerID = senderPeerID else { guard let peerID = senderPeerID else {
SecureLogger.warning("⚠️ Could not get peer ID for sender \(message.sender)", category: .session) SecureLogger.log("⚠️ Could not get peer ID for sender \(message.sender)", category: SecureLogger.session, level: .warning)
return return
} }
@@ -5782,7 +5818,8 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
// Clean up the stable key storage to avoid duplication // Clean up the stable key storage to avoid duplication
privateChats.removeValue(forKey: stableKeyHex) privateChats.removeValue(forKey: stableKeyHex)
SecureLogger.info("📥 Consolidated \(nostrMessages.count) Nostr messages from stable key to ephemeral peer \(peerID)", category: .session) SecureLogger.log("📥 Consolidated \(nostrMessages.count) Nostr messages from stable key to ephemeral peer \(peerID)",
category: SecureLogger.session, level: .info)
} }
} }
} }
@@ -5984,20 +6021,10 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
isBatchingPublic = true isBatchingPublic = true
// Rough chronological order: sort the batch by timestamp before inserting // Rough chronological order: sort the batch by timestamp before inserting
added.sort { $0.timestamp < $1.timestamp } added.sort { $0.timestamp < $1.timestamp }
// Channel-aware insertion policy: geohash uses strict ordering; mesh allows small out-of-order appends // Insert late arrivals into approximate position; append recent ones
let threshold: TimeInterval = {
switch activeChannel {
case .location: return TransportConfig.uiLateInsertThresholdGeo
case .mesh: return TransportConfig.uiLateInsertThreshold
}
}()
let lastTs = messages.last?.timestamp ?? .distantPast let lastTs = messages.last?.timestamp ?? .distantPast
for m in added { for m in added {
if m.timestamp < lastTs.addingTimeInterval(-threshold) { if m.timestamp < lastTs.addingTimeInterval(-lateInsertThreshold) {
let idx = insertionIndexByTimestamp(m.timestamp)
if idx >= messages.count { messages.append(m) } else { messages.insert(m, at: idx) }
} else if threshold == 0 {
// Strict ordering for geohash: always insert by timestamp
let idx = insertionIndexByTimestamp(m.timestamp) let idx = insertionIndexByTimestamp(m.timestamp)
if idx >= messages.count { messages.append(m) } else { messages.insert(m, at: idx) } if idx >= messages.count { messages.append(m) } else { messages.insert(m, at: idx) }
} else { } else {
@@ -6059,7 +6086,8 @@ private func checkForMentions(_ message: BitchatMessage) {
let isMentioned = (message.mentions?.contains { myTokens.contains($0) } ?? false) let isMentioned = (message.mentions?.contains { myTokens.contains($0) } ?? false)
if isMentioned && message.sender != nickname { if isMentioned && message.sender != nickname {
SecureLogger.info("🔔 Mention from \(message.sender)", category: .session) SecureLogger.log("🔔 Mention from \(message.sender)",
category: SecureLogger.session, level: .info)
NotificationService.shared.sendMentionNotification(from: message.sender, message: message.content) NotificationService.shared.sendMentionNotification(from: message.sender, message: message.content)
} }
} }
+1 -13
View File
@@ -444,19 +444,7 @@ struct ContentView: View {
let id = url.path.trimmingCharacters(in: CharacterSet(charactersIn: "/")) let id = url.path.trimmingCharacters(in: CharacterSet(charactersIn: "/"))
let peerID = id.removingPercentEncoding ?? id let peerID = id.removingPercentEncoding ?? id
selectedMessageSenderID = peerID selectedMessageSenderID = peerID
// Derive a stable display name from the peerID instead of peeking at the last message, selectedMessageSender = viewModel.messages.last(where: { $0.senderPeerID == peerID })?.sender
// which may be a transformed system action (sender == "system").
if peerID.hasPrefix("nostr") {
// For geohash senders, resolve display name via mapping (works for "nostr:" and "nostr_" keys)
selectedMessageSender = viewModel.geohashDisplayName(for: peerID)
} else {
// Mesh sender: use current mesh nickname if available; otherwise fall back to last non-system message
if let name = viewModel.meshService.peerNickname(peerID: peerID) {
selectedMessageSender = name
} else {
selectedMessageSender = viewModel.messages.last(where: { $0.senderPeerID == peerID && $0.sender != "system" })?.sender
}
}
showMessageActions = true showMessageActions = true
} }
.onOpenURL { url in .onOpenURL { url in
+1 -1
View File
@@ -270,7 +270,7 @@ final class BLEServiceTests: XCTestCase {
// MARK: - Mock Delegate Helper // MARK: - Mock Delegate Helper
private final class MockBitchatDelegate: BitchatDelegate { private class MockBitchatDelegate: BitchatDelegate {
private let messageHandler: (BitchatMessage) -> Void private let messageHandler: (BitchatMessage) -> Void
init(_ handler: @escaping (BitchatMessage) -> Void) { init(_ handler: @escaping (BitchatMessage) -> Void) {
-42
View File
@@ -1,42 +0,0 @@
import XCTest
@testable import bitchat
final class CommandProcessorTests: XCTestCase {
@MainActor
func test_slap_notFoundGrammar() {
let processor = CommandProcessor(chatViewModel: nil, meshService: nil)
let result = processor.process("/slap @system")
switch result {
case .error(let message):
XCTAssertEqual(message, "cannot slap system: not found")
default:
XCTFail("Expected error result")
}
}
@MainActor
func test_hug_notFoundGrammar() {
let processor = CommandProcessor(chatViewModel: nil, meshService: nil)
let result = processor.process("/hug @system")
switch result {
case .error(let message):
XCTAssertEqual(message, "cannot hug system: not found")
default:
XCTFail("Expected error result")
}
}
@MainActor
func test_slap_usageMessage() {
let processor = CommandProcessor(chatViewModel: nil, meshService: nil)
let result = processor.process("/slap")
switch result {
case .error(let message):
XCTAssertEqual(message, "usage: /slap <nickname>")
default:
XCTFail("Expected error result for usage message")
}
}
}
+1 -1
View File
@@ -26,7 +26,7 @@ import CoreBluetooth
/// - `autoFloodEnabled` is disabled by default; Integration tests enable it in `setUp()` to /// - `autoFloodEnabled` is disabled by default; Integration tests enable it in `setUp()` to
/// simulate broadcast propagation across the mesh. E2E tests keep it off and perform explicit /// simulate broadcast propagation across the mesh. E2E tests keep it off and perform explicit
/// relays when needed. /// relays when needed.
final class MockBLEService: NSObject { class MockBLEService: NSObject {
// Enable automatic flooding for public messages in integration tests only // Enable automatic flooding for public messages in integration tests only
static var autoFloodEnabled: Bool = false static var autoFloodEnabled: Bool = false
+1 -1
View File
@@ -10,7 +10,7 @@ import Foundation
import CryptoKit import CryptoKit
@testable import bitchat @testable import bitchat
final class TestHelpers { class TestHelpers {
// MARK: - Key Generation // MARK: - Key Generation
-5
View File
@@ -32,8 +32,6 @@ targets:
CFBundleVersion: $(CURRENT_PROJECT_VERSION) CFBundleVersion: $(CURRENT_PROJECT_VERSION)
NSBluetoothAlwaysUsageDescription: bitchat uses Bluetooth to create a secure mesh network for chatting with nearby users. NSBluetoothAlwaysUsageDescription: bitchat uses Bluetooth to create a secure mesh network for chatting with nearby users.
NSBluetoothPeripheralUsageDescription: bitchat uses Bluetooth to discover and connect with other bitchat users nearby. NSBluetoothPeripheralUsageDescription: bitchat uses Bluetooth to discover and connect with other bitchat users nearby.
NSCameraUsageDescription: bitchat uses the camera to scan QR codes to verify peers.
NSLocationWhenInUseUsageDescription: bitchat uses your approximate location to compute local geohash channels for optional public chats. Exact GPS is never shared.
UIBackgroundModes: UIBackgroundModes:
- bluetooth-central - bluetooth-central
- bluetooth-peripheral - bluetooth-peripheral
@@ -91,8 +89,6 @@ targets:
LSMinimumSystemVersion: $(MACOSX_DEPLOYMENT_TARGET) LSMinimumSystemVersion: $(MACOSX_DEPLOYMENT_TARGET)
NSBluetoothAlwaysUsageDescription: bitchat uses Bluetooth to create a secure mesh network for chatting with nearby users. NSBluetoothAlwaysUsageDescription: bitchat uses Bluetooth to create a secure mesh network for chatting with nearby users.
NSBluetoothPeripheralUsageDescription: bitchat uses Bluetooth to discover and connect with other bitchat users nearby. NSBluetoothPeripheralUsageDescription: bitchat uses Bluetooth to discover and connect with other bitchat users nearby.
NSCameraUsageDescription: bitchat uses the camera to scan QR codes to verify peers.
NSLocationWhenInUseUsageDescription: bitchat uses your approximate location to compute local geohash channels for optional public chats. Exact GPS is never shared.
CFBundleURLTypes: CFBundleURLTypes:
- CFBundleURLSchemes: - CFBundleURLSchemes:
- bitchat - bitchat
@@ -130,7 +126,6 @@ targets:
platform: iOS platform: iOS
sources: sources:
- bitchatShareExtension - bitchatShareExtension
- bitchat/Services/TransportConfig.swift
info: info:
path: bitchatShareExtension/Info.plist path: bitchatShareExtension/Info.plist
properties: properties: