Compare 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
66 changed files with 1084 additions and 1833 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)
} }
+12
View File
@@ -0,0 +1,12 @@
Place Tor.xcframework here
Instructions
- Obtain a prebuilt Tor Apple xcframework (iCepa/Onion Browser lineage) or build your own minimal client-only Tor.
- Rename it (if needed) to `Tor.xcframework` and drop it in this `Frameworks/` directory.
- Regenerate the Xcode project if you use XcodeGen (`project.yml` already references `Frameworks/Tor.xcframework`).
- Build the app; `TorManager` will automatically bootstrap Tor and route all networking through it.
Notes
- For iOS, the framework will be embedded and code-signed automatically.
- For macOS, it will be linked and embedded as well (you may prefer a system tor for smaller bundles).
+19 -55
View File
@@ -33,18 +33,16 @@
0481A3472E6D869F00FC845E /* TorURLSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0481A3442E6D869F00FC845E /* TorURLSession.swift */; }; 0481A3472E6D869F00FC845E /* TorURLSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0481A3442E6D869F00FC845E /* TorURLSession.swift */; };
0481A3482E6D869F00FC845E /* TorManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0481A3432E6D869F00FC845E /* TorManager.swift */; }; 0481A3482E6D869F00FC845E /* TorManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0481A3432E6D869F00FC845E /* TorManager.swift */; };
0481A3492E6D869F00FC845E /* TorURLSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0481A3442E6D869F00FC845E /* TorURLSession.swift */; }; 0481A3492E6D869F00FC845E /* TorURLSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0481A3442E6D869F00FC845E /* TorURLSession.swift */; };
0481A3552E6D877600FC845E /* README.md in Resources */ = {isa = PBXBuildFile; fileRef = 0481A3532E6D877600FC845E /* README.md */; };
0481A3562E6D877600FC845E /* README.md in Resources */ = {isa = PBXBuildFile; fileRef = 0481A3532E6D877600FC845E /* README.md */; };
0481A3582E6D929E00FC845E /* tor-nolzma.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0481A3572E6D929E00FC845E /* tor-nolzma.xcframework */; }; 0481A3582E6D929E00FC845E /* 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 */; }; 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 */; };
0481A3A92E74D28800FC845E /* LocationNotesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0481A3A82E74D28800FC845E /* LocationNotesView.swift */; }; 0C0EFA162E6EAABB00ABCDF4 /* TorNotifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C0EFA132E6EAABB00ABCDF1 /* TorNotifications.swift */; };
0481A3AA2E74D28800FC845E /* LocationNotesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0481A3A82E74D28800FC845E /* LocationNotesView.swift */; }; 0C0EFA172E6EAABB00ABCDF5 /* TorNotifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C0EFA132E6EAABB00ABCDF1 /* TorNotifications.swift */; };
0481A3AC2E74D29400FC845E /* LocationNotesManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0481A3AB2E74D29400FC845E /* LocationNotesManager.swift */; };
0481A3AD2E74D29400FC845E /* LocationNotesManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0481A3AB2E74D29400FC845E /* LocationNotesManager.swift */; };
0481A3AF2E74E06300FC845E /* LocationNotesCounter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0481A3AE2E74E06300FC845E /* LocationNotesCounter.swift */; };
0481A3B02E74E06300FC845E /* LocationNotesCounter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0481A3AE2E74E06300FC845E /* LocationNotesCounter.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 */; };
@@ -78,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 */; };
@@ -132,10 +126,6 @@
9CCF09F7527EC681A13FC246 /* NoiseSecurityConsiderations.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43B4548DAFC9F7AA8873DA53 /* NoiseSecurityConsiderations.swift */; }; 9CCF09F7527EC681A13FC246 /* NoiseSecurityConsiderations.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43B4548DAFC9F7AA8873DA53 /* NoiseSecurityConsiderations.swift */; };
A0A1C26EFBFDD5B8EFEEDE57 /* PublicChatE2ETests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D22BF09A49010947CEFE45E2 /* PublicChatE2ETests.swift */; }; A0A1C26EFBFDD5B8EFEEDE57 /* PublicChatE2ETests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D22BF09A49010947CEFE45E2 /* PublicChatE2ETests.swift */; };
A1B2C3D44E5F60718293A4B5 /* XChaCha20Poly1305Compat.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D44E5F60718293A4B4 /* XChaCha20Poly1305Compat.swift */; }; A1B2C3D44E5F60718293A4B5 /* XChaCha20Poly1305Compat.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D44E5F60718293A4B4 /* XChaCha20Poly1305Compat.swift */; };
A1B2C3D4E5F60123456789BA /* MockKeychain.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D4E5F60123456789AA /* MockKeychain.swift */; };
A1B2C3D4E5F60123456789BB /* MockKeychain.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D4E5F60123456789AA /* MockKeychain.swift */; };
A1B2C3D4E5F60123456789BC /* MockIdentityManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D4E5F60123456789AB /* MockIdentityManager.swift */; };
A1B2C3D4E5F60123456789BD /* MockIdentityManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D4E5F60123456789AB /* MockIdentityManager.swift */; };
A1B2C3D54E5F60718293A4B6 /* XChaCha20Poly1305Compat.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D44E5F60718293A4B4 /* XChaCha20Poly1305Compat.swift */; }; A1B2C3D54E5F60718293A4B6 /* XChaCha20Poly1305Compat.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D44E5F60718293A4B4 /* XChaCha20Poly1305Compat.swift */; };
A2977428C1D9EF9944C4BFAF /* BLEServiceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 980B109CBA72BC996455C62B /* BLEServiceTests.swift */; }; A2977428C1D9EF9944C4BFAF /* BLEServiceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 980B109CBA72BC996455C62B /* BLEServiceTests.swift */; };
A7187D48B07C6857DE01D0ED /* NoiseProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43613045E63D21D429396805 /* NoiseProtocol.swift */; }; A7187D48B07C6857DE01D0ED /* NoiseProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43613045E63D21D429396805 /* NoiseProtocol.swift */; };
@@ -176,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 */; };
@@ -236,13 +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>"; };
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>"; };
0481A3A82E74D28800FC845E /* LocationNotesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationNotesView.swift; sourceTree = "<group>"; };
0481A3AB2E74D29400FC845E /* LocationNotesManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationNotesManager.swift; sourceTree = "<group>"; };
0481A3AE2E74E06300FC845E /* LocationNotesCounter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationNotesCounter.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>"; };
@@ -260,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>"; };
@@ -295,8 +280,6 @@
9AB6BE4ABD7F5088E9865E56 /* NoiseSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoiseSession.swift; sourceTree = "<group>"; }; 9AB6BE4ABD7F5088E9865E56 /* NoiseSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoiseSession.swift; sourceTree = "<group>"; };
A08E03AA0C63E97C91749AEC /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; }; A08E03AA0C63E97C91749AEC /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
A1B2C3D44E5F60718293A4B4 /* XChaCha20Poly1305Compat.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XChaCha20Poly1305Compat.swift; sourceTree = "<group>"; }; A1B2C3D44E5F60718293A4B4 /* XChaCha20Poly1305Compat.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XChaCha20Poly1305Compat.swift; sourceTree = "<group>"; };
A1B2C3D4E5F60123456789AA /* MockKeychain.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockKeychain.swift; sourceTree = "<group>"; };
A1B2C3D4E5F60123456789AB /* MockIdentityManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockIdentityManager.swift; sourceTree = "<group>"; };
A2136C3E22D02D4A8DBE7EAB /* BinaryProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BinaryProtocol.swift; sourceTree = "<group>"; }; A2136C3E22D02D4A8DBE7EAB /* BinaryProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BinaryProtocol.swift; sourceTree = "<group>"; };
AA11BB22CC33DD44EE55FF68 /* MessageTextHelpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageTextHelpers.swift; sourceTree = "<group>"; }; AA11BB22CC33DD44EE55FF68 /* MessageTextHelpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageTextHelpers.swift; sourceTree = "<group>"; };
AA77BB10CC22DD33EE44FF55 /* VerificationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VerificationService.swift; sourceTree = "<group>"; }; AA77BB10CC22DD33EE44FF55 /* VerificationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VerificationService.swift; sourceTree = "<group>"; };
@@ -315,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>"; };
@@ -378,6 +360,7 @@
children = ( children = (
0481A35A2E6D9BEF00FC845E /* libz.tbd */, 0481A35A2E6D9BEF00FC845E /* libz.tbd */,
0481A35C2E6DA18600FC845E /* libz.tbd */, 0481A35C2E6DA18600FC845E /* libz.tbd */,
0481A3532E6D877600FC845E /* README.md */,
0481A3572E6D929E00FC845E /* tor-nolzma.xcframework */, 0481A3572E6D929E00FC845E /* tor-nolzma.xcframework */,
); );
path = Frameworks; path = Frameworks;
@@ -475,8 +458,6 @@
children = ( children = (
C27328EE574221395B2B8E87 /* MockBluetoothMeshService.swift */, C27328EE574221395B2B8E87 /* MockBluetoothMeshService.swift */,
FE7CCF2BD78A3F3DAE6DA145 /* MockBLEService.swift */, FE7CCF2BD78A3F3DAE6DA145 /* MockBLEService.swift */,
A1B2C3D4E5F60123456789AA /* MockKeychain.swift */,
A1B2C3D4E5F60123456789AB /* MockIdentityManager.swift */,
); );
path = Mocks; path = Mocks;
sourceTree = "<group>"; sourceTree = "<group>";
@@ -490,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>";
@@ -520,7 +500,6 @@
A55126E93155456CAA8D6656 /* Views */ = { A55126E93155456CAA8D6656 /* Views */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
0481A3A82E74D28800FC845E /* LocationNotesView.swift */,
AA77BB13CC22DD33EE44FF57 /* VerificationViews.swift */, AA77BB13CC22DD33EE44FF57 /* VerificationViews.swift */,
047502B22E55FED60083520F /* GeohashPeopleList.swift */, 047502B22E55FED60083520F /* GeohashPeopleList.swift */,
047502B32E55FED60083520F /* MeshPeerList.swift */, 047502B32E55FED60083520F /* MeshPeerList.swift */,
@@ -559,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 */,
@@ -598,8 +576,6 @@
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
0481A3452E6D869F00FC845E /* Tor */, 0481A3452E6D869F00FC845E /* Tor */,
0481A3AE2E74E06300FC845E /* LocationNotesCounter.swift */,
0481A3AB2E74D29400FC845E /* LocationNotesManager.swift */,
048A4C272E5FCD6600162C4A /* GeohashBookmarksStore.swift */, 048A4C272E5FCD6600162C4A /* GeohashBookmarksStore.swift */,
048A4BE62E5CCCC300162C4A /* TransportConfig.swift */, 048A4BE62E5CCCC300162C4A /* TransportConfig.swift */,
AA77BB10CC22DD33EE44FF55 /* VerificationService.swift */, AA77BB10CC22DD33EE44FF55 /* VerificationService.swift */,
@@ -793,6 +769,7 @@
isa = PBXResourcesBuildPhase; isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
0481A3552E6D877600FC845E /* README.md in Resources */,
7DD72D928FF9DD3CA81B46B0 /* Assets.xcassets in Resources */, 7DD72D928FF9DD3CA81B46B0 /* Assets.xcassets in Resources */,
E0A1B2C3D4E5F6012345678D /* relays/online_relays_gps.csv in Resources */, E0A1B2C3D4E5F6012345678D /* relays/online_relays_gps.csv in Resources */,
); );
@@ -804,6 +781,7 @@
files = ( files = (
BCCFEDC1EBE59323C3C470BF /* Assets.xcassets in Resources */, BCCFEDC1EBE59323C3C470BF /* Assets.xcassets in Resources */,
E65BBB6544FE0159F3C6C3A8 /* LaunchScreen.storyboard in Resources */, E65BBB6544FE0159F3C6C3A8 /* LaunchScreen.storyboard in Resources */,
0481A3562E6D877600FC845E /* README.md in Resources */,
E0A1B2C3D4E5F6012345678E /* relays/online_relays_gps.csv in Resources */, E0A1B2C3D4E5F6012345678E /* relays/online_relays_gps.csv in Resources */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
@@ -824,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 */,
@@ -840,7 +817,6 @@
047502B92E560F690083520F /* RelayController.swift in Sources */, 047502B92E560F690083520F /* RelayController.swift in Sources */,
6E7761E21C99F28AE2F9BE5F /* BitchatApp.swift in Sources */, 6E7761E21C99F28AE2F9BE5F /* BitchatApp.swift in Sources */,
84E3F9B64FB7FB4A140BD0A8 /* BitchatPeer.swift in Sources */, 84E3F9B64FB7FB4A140BD0A8 /* BitchatPeer.swift in Sources */,
0481A3B02E74E06300FC845E /* LocationNotesCounter.swift in Sources */,
923027D6F2F417AFA2488127 /* BitchatProtocol.swift in Sources */, 923027D6F2F417AFA2488127 /* BitchatProtocol.swift in Sources */,
D450CF41F207BDE1A1AAA56E /* ChatViewModel.swift in Sources */, D450CF41F207BDE1A1AAA56E /* ChatViewModel.swift in Sources */,
B0CA7796B2B2AC2B33F84548 /* CompressionUtil.swift in Sources */, B0CA7796B2B2AC2B33F84548 /* CompressionUtil.swift in Sources */,
@@ -871,8 +847,6 @@
049BD3AF2E51ED60001A566B /* Transport.swift in Sources */, 049BD3AF2E51ED60001A566B /* Transport.swift in Sources */,
E2DCF7817344F1CCDB8B7B2F /* SecureIdentityStateManager.swift in Sources */, E2DCF7817344F1CCDB8B7B2F /* SecureIdentityStateManager.swift in Sources */,
049BD3A02E51DBF4001A566B /* Packets.swift in Sources */, 049BD3A02E51DBF4001A566B /* Packets.swift in Sources */,
0481A3AD2E74D29400FC845E /* LocationNotesManager.swift in Sources */,
0481A3AA2E74D28800FC845E /* LocationNotesView.swift in Sources */,
047502892E5416250083520F /* Geohash.swift in Sources */, 047502892E5416250083520F /* Geohash.swift in Sources */,
0475028A2E5416250083520F /* LocationChannel.swift in Sources */, 0475028A2E5416250083520F /* LocationChannel.swift in Sources */,
049BD3A12E51DBF4001A566B /* PeerID.swift in Sources */, 049BD3A12E51DBF4001A566B /* PeerID.swift in Sources */,
@@ -892,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 */,
@@ -908,7 +881,6 @@
047502BA2E560F690083520F /* RelayController.swift in Sources */, 047502BA2E560F690083520F /* RelayController.swift in Sources */,
10E68BB889356219189E38EC /* BitchatApp.swift in Sources */, 10E68BB889356219189E38EC /* BitchatApp.swift in Sources */,
84D13329AB7EE1D65A37438A /* BitchatPeer.swift in Sources */, 84D13329AB7EE1D65A37438A /* BitchatPeer.swift in Sources */,
0481A3AF2E74E06300FC845E /* LocationNotesCounter.swift in Sources */,
6DE056E1EE9850E9FBF50157 /* BitchatProtocol.swift in Sources */, 6DE056E1EE9850E9FBF50157 /* BitchatProtocol.swift in Sources */,
7576A357B278E5733E9D9F33 /* ChatViewModel.swift in Sources */, 7576A357B278E5733E9D9F33 /* ChatViewModel.swift in Sources */,
7DCA0DBCB8884E3B31C7BCE3 /* CompressionUtil.swift in Sources */, 7DCA0DBCB8884E3B31C7BCE3 /* CompressionUtil.swift in Sources */,
@@ -939,8 +911,6 @@
049BD3AE2E51ED60001A566B /* Transport.swift in Sources */, 049BD3AE2E51ED60001A566B /* Transport.swift in Sources */,
68C4BE564735F6E7915274A2 /* SecureIdentityStateManager.swift in Sources */, 68C4BE564735F6E7915274A2 /* SecureIdentityStateManager.swift in Sources */,
049BD3A22E51DBF4001A566B /* Packets.swift in Sources */, 049BD3A22E51DBF4001A566B /* Packets.swift in Sources */,
0481A3AC2E74D29400FC845E /* LocationNotesManager.swift in Sources */,
0481A3A92E74D28800FC845E /* LocationNotesView.swift in Sources */,
047502872E5416250083520F /* Geohash.swift in Sources */, 047502872E5416250083520F /* Geohash.swift in Sources */,
047502882E5416250083520F /* LocationChannel.swift in Sources */, 047502882E5416250083520F /* LocationChannel.swift in Sources */,
049BD3A32E51DBF4001A566B /* PeerID.swift in Sources */, 049BD3A32E51DBF4001A566B /* PeerID.swift in Sources */,
@@ -964,13 +934,10 @@
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 */,
6C803BF930E7E19BE6E99EAA /* MockBLEService.swift in Sources */, 6C803BF930E7E19BE6E99EAA /* MockBLEService.swift in Sources */,
A1B2C3D4E5F60123456789BB /* MockKeychain.swift in Sources */,
A1B2C3D4E5F60123456789BD /* MockIdentityManager.swift in Sources */,
765254F56997F01054699AC0 /* NoiseProtocolTests.swift in Sources */, 765254F56997F01054699AC0 /* NoiseProtocolTests.swift in Sources */,
968181D255CA7A804340B4DA /* NostrProtocolTests.swift in Sources */, 968181D255CA7A804340B4DA /* NostrProtocolTests.swift in Sources */,
ED83C7AC1E6BEF15389C0132 /* PrivateChatE2ETests.swift in Sources */, ED83C7AC1E6BEF15389C0132 /* PrivateChatE2ETests.swift in Sources */,
@@ -990,13 +957,10 @@
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 */,
3849CA6D99B2D536636DF4A6 /* MockBLEService.swift in Sources */, 3849CA6D99B2D536636DF4A6 /* MockBLEService.swift in Sources */,
A1B2C3D4E5F60123456789BA /* MockKeychain.swift in Sources */,
A1B2C3D4E5F60123456789BC /* MockIdentityManager.swift in Sources */,
BC4DC75F4FB823FF40569676 /* NoiseProtocolTests.swift in Sources */, BC4DC75F4FB823FF40569676 /* NoiseProtocolTests.swift in Sources */,
EE8C3ECADAB3083A2687D50B /* NostrProtocolTests.swift in Sources */, EE8C3ECADAB3083A2687D50B /* NostrProtocolTests.swift in Sources */,
0AE840940F21AFC07C226636 /* PrivateChatE2ETests.swift in Sources */, 0AE840940F21AFC07C226636 /* PrivateChatE2ETests.swift in Sources */,
@@ -1112,7 +1076,7 @@
"@executable_path/Frameworks", "@executable_path/Frameworks",
"@executable_path/../../Frameworks", "@executable_path/../../Frameworks",
); );
MARKETING_VERSION = 1.4.0; MARKETING_VERSION = 1.3.4;
PRODUCT_BUNDLE_IDENTIFIER = chat.bitchat.ShareExtension; PRODUCT_BUNDLE_IDENTIFIER = chat.bitchat.ShareExtension;
SDKROOT = iphoneos; SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
@@ -1143,7 +1107,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.4.0; MARKETING_VERSION = 1.3.4;
PRODUCT_BUNDLE_IDENTIFIER = chat.bitchat; PRODUCT_BUNDLE_IDENTIFIER = chat.bitchat;
PRODUCT_NAME = bitchat; PRODUCT_NAME = bitchat;
SDKROOT = iphoneos; SDKROOT = iphoneos;
@@ -1198,7 +1162,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.4.0; MARKETING_VERSION = 1.3.4;
PRODUCT_BUNDLE_IDENTIFIER = chat.bitchat; PRODUCT_BUNDLE_IDENTIFIER = chat.bitchat;
PRODUCT_NAME = bitchat; PRODUCT_NAME = bitchat;
SDKROOT = iphoneos; SDKROOT = iphoneos;
@@ -1230,7 +1194,7 @@
"@executable_path/../Frameworks", "@executable_path/../Frameworks",
); );
MACOSX_DEPLOYMENT_TARGET = 13.0; MACOSX_DEPLOYMENT_TARGET = 13.0;
MARKETING_VERSION = 1.4.0; MARKETING_VERSION = 1.3.4;
PRODUCT_BUNDLE_IDENTIFIER = chat.bitchat; PRODUCT_BUNDLE_IDENTIFIER = chat.bitchat;
PRODUCT_NAME = bitchat; PRODUCT_NAME = bitchat;
REGISTER_APP_GROUPS = YES; REGISTER_APP_GROUPS = YES;
@@ -1319,7 +1283,7 @@
"@executable_path/../Frameworks", "@executable_path/../Frameworks",
); );
MACOSX_DEPLOYMENT_TARGET = 13.0; MACOSX_DEPLOYMENT_TARGET = 13.0;
MARKETING_VERSION = 1.4.0; MARKETING_VERSION = 1.3.4;
PRODUCT_BUNDLE_IDENTIFIER = chat.bitchat; PRODUCT_BUNDLE_IDENTIFIER = chat.bitchat;
PRODUCT_NAME = bitchat; PRODUCT_NAME = bitchat;
REGISTER_APP_GROUPS = YES; REGISTER_APP_GROUPS = YES;
@@ -1412,7 +1376,7 @@
"@executable_path/Frameworks", "@executable_path/Frameworks",
"@executable_path/../../Frameworks", "@executable_path/../../Frameworks",
); );
MARKETING_VERSION = 1.4.0; MARKETING_VERSION = 1.3.4;
PRODUCT_BUNDLE_IDENTIFIER = chat.bitchat.ShareExtension; PRODUCT_BUNDLE_IDENTIFIER = chat.bitchat.ShareExtension;
SDKROOT = iphoneos; SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
@@ -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

+4 -12
View File
@@ -11,7 +11,7 @@ import UserNotifications
@main @main
struct BitchatApp: App { struct BitchatApp: App {
@StateObject private var chatViewModel: ChatViewModel @StateObject private var chatViewModel = ChatViewModel()
#if os(iOS) #if os(iOS)
@Environment(\.scenePhase) var scenePhase @Environment(\.scenePhase) var scenePhase
@UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
@@ -23,14 +23,6 @@ struct BitchatApp: App {
#endif #endif
init() { init() {
let keychain = KeychainManager()
_chatViewModel = StateObject(
wrappedValue: ChatViewModel(
keychain: keychain,
identityManager: SecureIdentityStateManager(keychain)
)
)
UNUserNotificationCenter.current().delegate = NotificationDelegate.shared UNUserNotificationCenter.current().delegate = NotificationDelegate.shared
// Warm up georelay directory and refresh if stale (once/day) // Warm up georelay directory and refresh if stale (once/day)
GeoRelayDirectory.shared.prefetchIfNeeded() GeoRelayDirectory.shared.prefetchIfNeeded()
@@ -171,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 {
@@ -183,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) {
@@ -196,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?
+103 -61
View File
@@ -93,51 +93,13 @@
import Foundation import Foundation
import CryptoKit import CryptoKit
protocol SecureIdentityStateManagerProtocol {
// MARK: Secure Loading/Saving
func forceSave()
// MARK: Social Identity Management
func getSocialIdentity(for fingerprint: String) -> SocialIdentity?
// MARK: Cryptographic Identities
func upsertCryptographicIdentity(fingerprint: String, noisePublicKey: Data, signingPublicKey: Data?, claimedNickname: String?)
func getCryptoIdentitiesByPeerIDPrefix(_ peerID: String) -> [CryptographicIdentity]
func updateSocialIdentity(_ identity: SocialIdentity)
// MARK: Favorites Management
func getFavorites() -> Set<String>
func setFavorite(_ fingerprint: String, isFavorite: Bool)
func isFavorite(fingerprint: String) -> Bool
// MARK: Blocked Users Management
func isBlocked(fingerprint: String) -> Bool
func setBlocked(_ fingerprint: String, isBlocked: Bool)
// MARK: Geohash (Nostr) Blocking
func isNostrBlocked(pubkeyHexLowercased: String) -> Bool
func setNostrBlocked(_ pubkeyHexLowercased: String, isBlocked: Bool)
func getBlockedNostrPubkeys() -> Set<String>
// MARK: Ephemeral Session Management
func registerEphemeralSession(peerID: String, handshakeState: HandshakeState)
func updateHandshakeState(peerID: String, state: HandshakeState)
// MARK: Cleanup
func clearAllIdentityData()
func removeEphemeralSession(peerID: String)
// MARK: Verification
func setVerified(fingerprint: String, verified: Bool)
func isVerified(fingerprint: String) -> Bool
func getVerifiedFingerprints() -> Set<String>
}
/// 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: SecureIdentityStateManagerProtocol { class SecureIdentityStateManager {
private let keychain: KeychainManagerProtocol static let shared = SecureIdentityStateManager()
private let keychain = KeychainManager.shared
private let cacheKey = "bitchat.identityCache.v2" private let cacheKey = "bitchat.identityCache.v2"
private let encryptionKeyName = "identityCacheEncryptionKey" private let encryptionKeyName = "identityCacheEncryptionKey"
@@ -146,6 +108,9 @@ final class SecureIdentityStateManager: SecureIdentityStateManagerProtocol {
private var cryptographicIdentities: [String: CryptographicIdentity] = [:] private var cryptographicIdentities: [String: CryptographicIdentity] = [:]
private var cache: IdentityCache = IdentityCache() private var cache: IdentityCache = IdentityCache()
// Pending actions before handshake
private var pendingActions: [String: PendingActions] = [:]
// Thread safety // Thread safety
private let queue = DispatchQueue(label: "bitchat.identity.state", attributes: .concurrent) private let queue = DispatchQueue(label: "bitchat.identity.state", attributes: .concurrent)
@@ -157,16 +122,14 @@ final class SecureIdentityStateManager: SecureIdentityStateManagerProtocol {
// Encryption key // Encryption key
private let encryptionKey: SymmetricKey private let encryptionKey: SymmetricKey
init(_ keychain: KeychainManagerProtocol) { private init() {
self.keychain = keychain
// Generate or retrieve encryption key from keychain // Generate or retrieve encryption key from keychain
let loadedKey: SymmetricKey let loadedKey: SymmetricKey
// 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 {
@@ -174,7 +137,7 @@ final class SecureIdentityStateManager: SecureIdentityStateManagerProtocol {
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
@@ -183,13 +146,9 @@ final class SecureIdentityStateManager: SecureIdentityStateManagerProtocol {
loadIdentityCache() loadIdentityCache()
} }
deinit {
forceSave()
}
// MARK: - Secure Loading/Saving // MARK: - Secure Loading/Saving
private func loadIdentityCache() { func loadIdentityCache() {
guard let encryptedData = keychain.getIdentityKey(forKey: cacheKey) else { guard let encryptedData = keychain.getIdentityKey(forKey: cacheKey) else {
// No existing cache, start fresh // No existing cache, start fresh
return return
@@ -201,11 +160,16 @@ final class SecureIdentityStateManager: SecureIdentityStateManagerProtocol {
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)
} }
} }
private func saveIdentityCache() { deinit {
// Force save any pending changes
forceSave()
}
func saveIdentityCache() {
// Mark that we need to save // Mark that we need to save
pendingSave = true pendingSave = true
@@ -227,17 +191,35 @@ final class SecureIdentityStateManager: SecureIdentityStateManagerProtocol {
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)
} }
} }
// Force immediate save (for app termination) // Force immediate save (for app termination)
func forceSave() { func forceSave() {
saveTimer?.invalidate() saveTimer?.invalidate()
performSave() if pendingSave {
performSave()
}
}
// MARK: - Identity Resolution
func resolveIdentity(peerID: String, claimedNickname: String) -> IdentityHint {
queue.sync {
// Check if we have candidates based on nickname
if let fingerprints = cache.nicknameIndex[claimedNickname] {
if fingerprints.count == 1 {
return .likelyKnown(fingerprint: fingerprints.first!)
} else {
return .ambiguous(candidates: fingerprints)
}
}
return .unknown
}
} }
// MARK: - Social Identity Management // MARK: - Social Identity Management
@@ -319,6 +301,11 @@ final class SecureIdentityStateManager: SecureIdentityStateManagerProtocol {
} }
} }
/// Retrieve cryptographic identity by fingerprint
func getCryptographicIdentity(for fingerprint: String) -> CryptographicIdentity? {
queue.sync { cryptographicIdentities[fingerprint] }
}
/// Find cryptographic identities whose fingerprint prefix matches a peerID (16-hex) short ID /// Find cryptographic identities whose fingerprint prefix matches a peerID (16-hex) short ID
func getCryptoIdentitiesByPeerIDPrefix(_ peerID: String) -> [CryptographicIdentity] { func getCryptoIdentitiesByPeerIDPrefix(_ peerID: String) -> [CryptographicIdentity] {
queue.sync { queue.sync {
@@ -328,6 +315,12 @@ final class SecureIdentityStateManager: SecureIdentityStateManagerProtocol {
} }
} }
func getAllSocialIdentities() -> [SocialIdentity] {
queue.sync {
return Array(cache.socialIdentities.values)
}
}
func updateSocialIdentity(_ identity: SocialIdentity) { func updateSocialIdentity(_ identity: SocialIdentity) {
queue.async(flags: .barrier) { queue.async(flags: .barrier) {
self.cache.socialIdentities[identity.fingerprint] = identity self.cache.socialIdentities[identity.fingerprint] = identity
@@ -402,7 +395,7 @@ final class SecureIdentityStateManager: SecureIdentityStateManagerProtocol {
} }
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] {
@@ -476,32 +469,81 @@ final class SecureIdentityStateManager: SecureIdentityStateManagerProtocol {
} }
} }
func getHandshakeState(peerID: String) -> HandshakeState? {
queue.sync {
return ephemeralSessions[peerID]?.handshakeState
}
}
// MARK: - Pending Actions
func setPendingAction(peerID: String, action: PendingActions) {
queue.async(flags: .barrier) {
self.pendingActions[peerID] = action
}
}
func applyPendingActions(peerID: String, fingerprint: String) {
queue.async(flags: .barrier) {
guard let actions = self.pendingActions[peerID] else { return }
// Get or create social identity
var identity = self.cache.socialIdentities[fingerprint] ?? SocialIdentity(
fingerprint: fingerprint,
localPetname: nil,
claimedNickname: "Unknown",
trustLevel: .unknown,
isFavorite: false,
isBlocked: false,
notes: nil
)
// Apply pending actions
if let toggleFavorite = actions.toggleFavorite {
identity.isFavorite = toggleFavorite
}
if let trustLevel = actions.setTrustLevel {
identity.trustLevel = trustLevel
}
if let petname = actions.setPetname {
identity.localPetname = petname
}
// Save updated identity
self.cache.socialIdentities[fingerprint] = identity
self.pendingActions.removeValue(forKey: peerID)
self.saveIdentityCache()
}
}
// 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()
self.ephemeralSessions.removeAll() self.ephemeralSessions.removeAll()
self.cryptographicIdentities.removeAll() self.cryptographicIdentities.removeAll()
self.pendingActions.removeAll()
// 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)
} }
} }
func removeEphemeralSession(peerID: String) { func removeEphemeralSession(peerID: String) {
queue.async(flags: .barrier) { queue.async(flags: .barrier) {
self.ephemeralSessions.removeValue(forKey: peerID) self.ephemeralSessions.removeValue(forKey: peerID)
self.pendingActions.removeValue(forKey: peerID)
} }
} }
// 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()
} }
} }
} }
+21 -28
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,10 +488,9 @@ 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 let keychain: KeychainManagerProtocol
private var symmetricState: NoiseSymmetricState private var symmetricState: NoiseSymmetricState
// Keys // Keys
@@ -506,16 +506,9 @@ final class NoiseHandshakeState {
private var messagePatterns: [[NoiseMessagePattern]] = [] private var messagePatterns: [[NoiseMessagePattern]] = []
private var currentPattern = 0 private var currentPattern = 0
init( init(role: NoiseRole, pattern: NoisePattern, localStaticKey: Curve25519.KeyAgreement.PrivateKey? = nil, remoteStaticKey: Curve25519.KeyAgreement.PublicKey? = nil) {
role: NoiseRole,
pattern: NoisePattern,
keychain: KeychainManagerProtocol,
localStaticKey: Curve25519.KeyAgreement.PrivateKey? = nil,
remoteStaticKey: Curve25519.KeyAgreement.PublicKey? = nil
) {
self.role = role self.role = role
self.pattern = pattern self.pattern = pattern
self.keychain = keychain
// Initialize static keys // Initialize static keys
if let localKey = localStaticKey { if let localKey = localStaticKey {
@@ -586,7 +579,7 @@ final class NoiseHandshakeState {
var sharedData = shared.withUnsafeBytes { Data($0) } var sharedData = shared.withUnsafeBytes { Data($0) }
symmetricState.mixKey(sharedData) symmetricState.mixKey(sharedData)
// Clear sensitive shared secret // Clear sensitive shared secret
keychain.secureClear(&sharedData) KeychainManager.secureClear(&sharedData)
case .es: case .es:
// DH(ephemeral, static) - direction depends on role // DH(ephemeral, static) - direction depends on role
@@ -634,7 +627,7 @@ final class NoiseHandshakeState {
var sharedData = shared.withUnsafeBytes { Data($0) } var sharedData = shared.withUnsafeBytes { Data($0) }
symmetricState.mixKey(sharedData) symmetricState.mixKey(sharedData)
// Clear sensitive shared secret // Clear sensitive shared secret
keychain.secureClear(&sharedData) KeychainManager.secureClear(&sharedData)
} }
} }
@@ -668,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)
@@ -685,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
} }
@@ -722,7 +715,7 @@ final class NoiseHandshakeState {
var sharedData = shared.withUnsafeBytes { Data($0) } var sharedData = shared.withUnsafeBytes { Data($0) }
symmetricState.mixKey(sharedData) symmetricState.mixKey(sharedData)
// Clear sensitive shared secret // Clear sensitive shared secret
keychain.secureClear(&sharedData) KeychainManager.secureClear(&sharedData)
} else { } else {
guard let localStatic = localStaticPrivate, guard let localStatic = localStaticPrivate,
let remoteEphemeral = remoteEphemeralPublic else { let remoteEphemeral = remoteEphemeralPublic else {
@@ -732,7 +725,7 @@ final class NoiseHandshakeState {
var sharedData = shared.withUnsafeBytes { Data($0) } var sharedData = shared.withUnsafeBytes { Data($0) }
symmetricState.mixKey(sharedData) symmetricState.mixKey(sharedData)
// Clear sensitive shared secret // Clear sensitive shared secret
keychain.secureClear(&sharedData) KeychainManager.secureClear(&sharedData)
} }
case .se: case .se:
@@ -745,7 +738,7 @@ final class NoiseHandshakeState {
var sharedData = shared.withUnsafeBytes { Data($0) } var sharedData = shared.withUnsafeBytes { Data($0) }
symmetricState.mixKey(sharedData) symmetricState.mixKey(sharedData)
// Clear sensitive shared secret // Clear sensitive shared secret
keychain.secureClear(&sharedData) KeychainManager.secureClear(&sharedData)
} else { } else {
guard let localEphemeral = localEphemeralPrivate, guard let localEphemeral = localEphemeralPrivate,
let remoteStatic = remoteStaticPublic else { let remoteStatic = remoteStaticPublic else {
@@ -755,7 +748,7 @@ final class NoiseHandshakeState {
var sharedData = shared.withUnsafeBytes { Data($0) } var sharedData = shared.withUnsafeBytes { Data($0) }
symmetricState.mixKey(sharedData) symmetricState.mixKey(sharedData)
// Clear sensitive shared secret // Clear sensitive shared secret
keychain.secureClear(&sharedData) KeychainManager.secureClear(&sharedData)
} }
case .ss: case .ss:
@@ -884,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
} }
@@ -894,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
} }
+20 -33
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
@@ -36,7 +37,6 @@ enum NoiseSessionState: Equatable {
class NoiseSession { class NoiseSession {
let peerID: String let peerID: String
let role: NoiseRole let role: NoiseRole
private let keychain: KeychainManagerProtocol
private var state: NoiseSessionState = .uninitialized private var state: NoiseSessionState = .uninitialized
private var handshakeState: NoiseHandshakeState? private var handshakeState: NoiseHandshakeState?
private var sendCipher: NoiseCipherState? private var sendCipher: NoiseCipherState?
@@ -53,16 +53,9 @@ class NoiseSession {
// Thread safety // Thread safety
private let sessionQueue = DispatchQueue(label: "chat.bitchat.noise.session", attributes: .concurrent) private let sessionQueue = DispatchQueue(label: "chat.bitchat.noise.session", attributes: .concurrent)
init( init(peerID: String, role: NoiseRole, localStaticKey: Curve25519.KeyAgreement.PrivateKey, remoteStaticKey: Curve25519.KeyAgreement.PublicKey? = nil) {
peerID: String,
role: NoiseRole,
keychain: KeychainManagerProtocol,
localStaticKey: Curve25519.KeyAgreement.PrivateKey,
remoteStaticKey: Curve25519.KeyAgreement.PublicKey? = nil
) {
self.peerID = peerID self.peerID = peerID
self.role = role self.role = role
self.keychain = keychain
self.localStaticKey = localStaticKey self.localStaticKey = localStaticKey
self.remoteStaticPublicKey = remoteStaticKey self.remoteStaticPublicKey = remoteStaticKey
} }
@@ -79,7 +72,6 @@ class NoiseSession {
handshakeState = NoiseHandshakeState( handshakeState = NoiseHandshakeState(
role: role, role: role,
pattern: .XX, pattern: .XX,
keychain: keychain,
localStaticKey: localStaticKey, localStaticKey: localStaticKey,
remoteStaticKey: nil remoteStaticKey: nil
) )
@@ -100,19 +92,18 @@ 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 {
handshakeState = NoiseHandshakeState( handshakeState = NoiseHandshakeState(
role: role, role: role,
pattern: .XX, pattern: .XX,
keychain: keychain,
localStaticKey: localStaticKey, localStaticKey: localStaticKey,
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 {
@@ -121,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() {
@@ -139,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() {
@@ -165,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
@@ -240,18 +231,18 @@ class NoiseSession {
// Clear sent handshake messages // Clear sent handshake messages
for i in 0..<sentHandshakeMessages.count { for i in 0..<sentHandshakeMessages.count {
var message = sentHandshakeMessages[i] var message = sentHandshakeMessages[i]
keychain.secureClear(&message) KeychainManager.secureClear(&message)
} }
sentHandshakeMessages.removeAll() sentHandshakeMessages.removeAll()
// Clear handshake hash // Clear handshake hash
if var hash = handshakeHash { if var hash = handshakeHash {
keychain.secureClear(&hash) KeychainManager.secureClear(&hash)
} }
handshakeHash = nil handshakeHash = nil
if wasEstablished { if wasEstablished {
SecureLogger.info(.sessionExpired(peerID: peerID)) SecureLogger.logSecurityEvent(.sessionExpired(peerID: peerID))
} }
} }
} }
@@ -259,19 +250,17 @@ 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 keychain: KeychainManagerProtocol
private let managerQueue = DispatchQueue(label: "chat.bitchat.noise.manager", attributes: .concurrent) private let managerQueue = DispatchQueue(label: "chat.bitchat.noise.manager", attributes: .concurrent)
// Callbacks // Callbacks
var onSessionEstablished: ((String, Curve25519.KeyAgreement.PublicKey) -> Void)? var onSessionEstablished: ((String, Curve25519.KeyAgreement.PublicKey) -> Void)?
var onSessionFailed: ((String, Error) -> Void)? var onSessionFailed: ((String, Error) -> Void)?
init(localStaticKey: Curve25519.KeyAgreement.PrivateKey, keychain: KeychainManagerProtocol) { init(localStaticKey: Curve25519.KeyAgreement.PrivateKey) {
self.localStaticKey = localStaticKey self.localStaticKey = localStaticKey
self.keychain = keychain
} }
// MARK: - Session Management // MARK: - Session Management
@@ -281,7 +270,6 @@ final class NoiseSessionManager {
let session = SecureNoiseSession( let session = SecureNoiseSession(
peerID: peerID, peerID: peerID,
role: role, role: role,
keychain: keychain,
localStaticKey: localStaticKey localStaticKey: localStaticKey
) )
sessions[peerID] = session sessions[peerID] = session
@@ -299,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()
@@ -333,7 +321,6 @@ final class NoiseSessionManager {
let session = SecureNoiseSession( let session = SecureNoiseSession(
peerID: peerID, peerID: peerID,
role: .initiator, role: .initiator,
keychain: keychain,
localStaticKey: localStaticKey localStaticKey: localStaticKey
) )
sessions[peerID] = session sessions[peerID] = session
@@ -344,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
} }
} }
@@ -361,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 {
@@ -384,7 +372,6 @@ final class NoiseSessionManager {
let newSession = SecureNoiseSession( let newSession = SecureNoiseSession(
peerID: peerID, peerID: peerID,
role: .responder, role: .responder,
keychain: keychain,
localStaticKey: localStaticKey localStaticKey: localStaticKey
) )
sessions[peerID] = newSession sessions[peerID] = newSession
@@ -417,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 -25
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 {
@@ -123,28 +125,6 @@ struct NostrProtocol {
let schnorrKey = try senderIdentity.schnorrSigningKey() let schnorrKey = try senderIdentity.schnorrSigningKey()
return try event.sign(with: schnorrKey) return try event.sign(with: schnorrKey)
} }
/// Create a persistent location note (kind 1: text note) tagged to a street-level geohash.
static func createGeohashTextNote(
content: String,
geohash: String,
senderIdentity: NostrIdentity,
nickname: String? = nil
) throws -> NostrEvent {
var tags = [["g", geohash]]
if let nickname = nickname?.trimmingCharacters(in: .whitespacesAndNewlines), !nickname.isEmpty {
tags.append(["n", nickname])
}
let event = NostrEvent(
pubkey: senderIdentity.publicKeyHex,
createdAt: Date(),
kind: .textNote,
tags: tags,
content: content
)
let schnorrKey = try senderIdentity.schnorrSigningKey()
return try event.sign(with: schnorrKey)
}
// MARK: - Private Methods // MARK: - Private Methods
+91 -130
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>()
@@ -45,14 +45,6 @@ final class NostrRelayManager: ObservableObject {
// Coalesce duplicate subscribe requests for the same id within a short window // Coalesce duplicate subscribe requests for the same id within a short window
private var subscribeCoalesce: [String: Date] = [:] private var subscribeCoalesce: [String: Date] = [:]
private var cancellables = Set<AnyCancellable>() private var cancellables = Set<AnyCancellable>()
// Track EOSE per subscription to signal when initial stored events are done
private struct EOSETracker {
var pendingRelays: Set<String>
var callback: () -> Void
var timer: Timer?
}
private var eoseTrackers: [String: EOSETracker] = [:]
// Message queue for reliability // Message queue for reliability
// Pending sends held only for relays that are not yet connected. // Pending sends held only for relays that are not yet connected.
@@ -90,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)
} }
@@ -209,8 +201,7 @@ final class NostrRelayManager: ObservableObject {
filter: NostrFilter, filter: NostrFilter,
id: String = UUID().uuidString, id: String = UUID().uuidString,
relayUrls: [String]? = nil, relayUrls: [String]? = nil,
handler: @escaping (NostrEvent) -> Void, handler: @escaping (NostrEvent) -> Void
onEOSE: (() -> Void)? = nil
) { ) {
// Coalesce rapid duplicate subscribe requests only if a handler already exists // Coalesce rapid duplicate subscribe requests only if a handler already exists
let now = Date() let now = Date()
@@ -240,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
@@ -259,25 +251,8 @@ final class NostrRelayManager: ObservableObject {
map[id] = messageString map[id] = messageString
self.pendingSubscriptions[url] = map self.pendingSubscriptions[url] = map
} }
// Initialize EOSE tracking if requested SecureLogger.log("📋 Queued subscription id=\(id) for \(urls.count) relay(s)",
if let onEOSE = onEOSE { category: SecureLogger.session, level: .debug)
var tracker = EOSETracker(pendingRelays: Set(urls), callback: onEOSE, timer: nil)
// Fallback timeout to avoid hanging if a relay never sends EOSE
tracker.timer = Timer.scheduledTimer(withTimeInterval: 2.0, repeats: false) { [weak self] _ in
Task { @MainActor in
guard let self = self else { return }
if let t = self.eoseTrackers[id] {
t.timer?.invalidate()
self.eoseTrackers.removeValue(forKey: id)
onEOSE()
}
}
}
eoseTrackers[id] = tracker
}
SecureLogger.debug("📋 Queued subscription id=\(id) for \(urls.count) relay(s)", category: .session)
// Ensure we actually have sockets opening to these relays so queued REQs can flush
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 {
@@ -285,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)
} }
} }
@@ -317,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
} }
@@ -343,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
@@ -362,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))
@@ -384,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>()
@@ -403,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
@@ -433,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
@@ -441,30 +435,21 @@ 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(let subId):
if var tracker = eoseTrackers[subId] {
tracker.pendingRelays.remove(relayUrl)
if tracker.pendingRelays.isEmpty {
tracker.timer?.invalidate()
eoseTrackers.removeValue(forKey: subId)
tracker.callback()
} else {
eoseTrackers[subId] = tracker
}
} }
case .eose:
// No-op for now
break
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
@@ -478,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
@@ -494,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)
} }
} }
@@ -533,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
@@ -551,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
} }
@@ -645,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
@@ -788,16 +759,6 @@ struct NostrFilter: Encodable {
filter.limit = limit filter.limit = limit
return filter return filter
} }
// For location notes: persistent text notes (kind 1) tagged with geohash
static func geohashNotes(_ geohash: String, since: Date? = nil, limit: Int = 200) -> NostrFilter {
var filter = NostrFilter()
filter.kinds = [1]
filter.since = since?.timeIntervalSince1970.toInt()
filter.tagFilters = ["g": [geohash]]
filter.limit = limit
return filter
}
} }
// Dynamic coding key for tag filters // Dynamic coding key for tag filters
+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
-6
View File
@@ -2,7 +2,6 @@ import Foundation
/// Levels of location channels mapped to geohash precisions. /// Levels of location channels mapped to geohash precisions.
enum GeohashChannelLevel: CaseIterable, Codable, Equatable { enum GeohashChannelLevel: CaseIterable, Codable, Equatable {
case building
case block case block
case neighborhood case neighborhood
case city case city
@@ -12,7 +11,6 @@ enum GeohashChannelLevel: CaseIterable, Codable, Equatable {
/// Geohash length used for this level. /// Geohash length used for this level.
var precision: Int { var precision: Int {
switch self { switch self {
case .building: return 8
case .block: return 7 case .block: return 7
case .neighborhood: return 6 case .neighborhood: return 6
case .city: return 5 case .city: return 5
@@ -23,7 +21,6 @@ enum GeohashChannelLevel: CaseIterable, Codable, Equatable {
var displayName: String { var displayName: String {
switch self { switch self {
case .building: return "Building"
case .block: return "Block" case .block: return "Block"
case .neighborhood: return "Neighborhood" case .neighborhood: return "Neighborhood"
case .city: return "City" case .city: return "City"
@@ -38,7 +35,6 @@ extension GeohashChannelLevel {
let container = try decoder.singleValueContainer() let container = try decoder.singleValueContainer()
if let raw = try? container.decode(String.self) { if let raw = try? container.decode(String.self) {
switch raw { switch raw {
case "building": self = .building
case "block": self = .block case "block": self = .block
case "neighborhood": self = .neighborhood case "neighborhood": self = .neighborhood
case "city": self = .city case "city": self = .city
@@ -50,7 +46,6 @@ extension GeohashChannelLevel {
} }
} else if let precision = try? container.decode(Int.self) { } else if let precision = try? container.decode(Int.self) {
switch precision { switch precision {
case 8: self = .building
case 7: self = .block case 7: self = .block
case 6: self = .neighborhood case 6: self = .neighborhood
case 5: self = .city case 5: self = .city
@@ -66,7 +61,6 @@ extension GeohashChannelLevel {
func encode(to encoder: Encoder) throws { func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer() var container = encoder.singleValueContainer()
switch self { switch self {
case .building: try container.encode("building")
case .block: try container.encode("block") case .block: try container.encode("block")
case .neighborhood: try container.encode("neighborhood") case .neighborhood: try container.encode("neighborhood")
case .city: try container.encode("city") case .city: try container.encode("city")
+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: [])
+126 -102
View File
@@ -88,8 +88,7 @@ final class BLEService: NSObject {
var myPeerID: String = "" var myPeerID: String = ""
var myNickname: String = "anon" var myNickname: String = "anon"
private let noiseService: NoiseEncryptionService private let noiseService = NoiseEncryptionService()
private let identityManager: SecureIdentityStateManagerProtocol
private var myPeerIDData: Data = Data() private var myPeerIDData: Data = Data()
// MARK: - Advertising Privacy // MARK: - Advertising Privacy
@@ -231,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 }
@@ -244,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
} }
@@ -326,9 +327,7 @@ final class BLEService: NSObject {
} }
} }
init(keychain: KeychainManagerProtocol, identityManager: SecureIdentityStateManagerProtocol) { override init() {
noiseService = NoiseEncryptionService(keychain: keychain)
self.identityManager = identityManager
super.init() super.init()
// Derive stable peer ID from Noise static public key fingerprint (first 8 bytes 16 hex chars) // Derive stable peer ID from Noise static public key fingerprint (first 8 bytes 16 hex chars)
@@ -342,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)
@@ -587,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]"
@@ -595,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)
} }
@@ -608,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(
@@ -626,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
@@ -635,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)
} }
} }
@@ -677,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)
} }
} }
@@ -743,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
} }
@@ -766,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
@@ -784,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) {
@@ -793,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
} }
@@ -842,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) {
@@ -891,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)
} }
} }
@@ -905,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 {
@@ -913,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
} }
@@ -940,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
@@ -958,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 {
@@ -1029,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)
} }
} }
} }
@@ -1142,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)
} }
} }
} }
@@ -1314,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
@@ -1334,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
@@ -1343,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.
@@ -1396,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
} }
@@ -1436,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
} }
@@ -1444,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)
} }
@@ -1461,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
} }
@@ -1498,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
} }
@@ -1531,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)
} }
} }
} }
@@ -1551,7 +1561,7 @@ final class BLEService: NSObject {
// Derive fingerprint from Noise public key // Derive fingerprint from Noise public key
let hash = SHA256.hash(data: announcement.noisePublicKey) let hash = SHA256.hash(data: announcement.noisePublicKey)
let fingerprint = hash.map { String(format: "%02x", $0) }.joined() let fingerprint = hash.map { String(format: "%02x", $0) }.joined()
identityManager.upsertCryptographicIdentity( SecureIdentityStateManager.shared.upsertCryptographicIdentity(
fingerprint: fingerprint, fingerprint: fingerprint,
noisePublicKey: announcement.noisePublicKey, noisePublicKey: announcement.noisePublicKey,
signingPublicKey: announcement.signingPublicKey, signingPublicKey: announcement.signingPublicKey,
@@ -1633,13 +1643,13 @@ final class BLEService: NSObject {
// Fallback: verify signature using persisted signing key for this peerID's fingerprint prefix // Fallback: verify signature using persisted signing key for this peerID's fingerprint prefix
if let signature = packet.signature, let packetData = packet.toBinaryDataForSigning() { if let signature = packet.signature, let packetData = packet.toBinaryDataForSigning() {
// Find candidate identities by peerID prefix (16 hex) // Find candidate identities by peerID prefix (16 hex)
let candidates = identityManager.getCryptoIdentitiesByPeerIDPrefix(peerID) let candidates = SecureIdentityStateManager.shared.getCryptoIdentitiesByPeerIDPrefix(peerID)
for candidate in candidates { for candidate in candidates {
if let signingKey = candidate.signingPublicKey, if let signingKey = candidate.signingPublicKey,
noiseService.verifySignature(signature, for: packetData, publicKey: signingKey) { noiseService.verifySignature(signature, for: packetData, publicKey: signingKey) {
accepted = true accepted = true
// Prefer persisted social petname or claimed nickname // Prefer persisted social petname or claimed nickname
if let social = identityManager.getSocialIdentity(for: candidate.fingerprint) { if let social = SecureIdentityStateManager.shared.getSocialIdentity(for: candidate.fingerprint) {
senderNickname = social.localPetname ?? social.claimedNickname senderNickname = social.localPetname ?? social.claimedNickname
} else { } else {
senderNickname = "anon" + String(peerID.prefix(4)) senderNickname = "anon" + String(peerID.prefix(4))
@@ -1651,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
@@ -1668,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
@@ -1700,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)
@@ -1710,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
} }
@@ -1761,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)
} }
} }
@@ -1795,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,
@@ -1832,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
} }
@@ -1849,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
} }
@@ -1882,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
@@ -1891,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)
} }
} }
@@ -1902,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)
@@ -1917,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)
} }
} }
} }
@@ -2087,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
} }
@@ -2378,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] = [
@@ -2397,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()
@@ -2430,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])
@@ -2442,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 {
@@ -2497,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() }
@@ -2570,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)
} }
} }
@@ -2615,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 }
@@ -2625,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
} }
@@ -2641,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
} }
@@ -2663,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
@@ -2676,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
@@ -2687,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
} }
@@ -2708,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
} }
@@ -2716,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
@@ -2754,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)
} }
} }
@@ -2767,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
@@ -2784,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 {
@@ -2801,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
@@ -2820,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
@@ -2854,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())
} }
@@ -2893,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
@@ -2912,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
@@ -2929,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)
} }
} }
} }
@@ -2937,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!
@@ -2976,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)
} }
} }
@@ -2986,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)
@@ -3011,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)
} }
} }
} }
+18 -20
View File
@@ -17,15 +17,13 @@ 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?
private let identityManager: SecureIdentityStateManagerProtocol
init(chatViewModel: ChatViewModel? = nil, meshService: Transport? = nil, identityManager: SecureIdentityStateManagerProtocol) { init(chatViewModel: ChatViewModel? = nil, meshService: Transport? = nil) {
self.chatViewModel = chatViewModel self.chatViewModel = chatViewModel
self.meshService = meshService self.meshService = meshService
self.identityManager = identityManager
} }
/// Process a command string /// Process a command string
@@ -52,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":
@@ -131,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) *"
@@ -191,7 +189,7 @@ final class CommandProcessor {
} }
// Geohash blocked names (prefer visible display names; fallback to #suffix) // Geohash blocked names (prefer visible display names; fallback to #suffix)
let geoBlocked = Array(identityManager.getBlockedNostrPubkeys()) let geoBlocked = Array(SecureIdentityStateManager.shared.getBlockedNostrPubkeys())
var geoNames: [String] = [] var geoNames: [String] = []
if let vm = chatViewModel { if let vm = chatViewModel {
let visible = vm.visibleGeohashPeople() let visible = vm.visibleGeohashPeople()
@@ -215,14 +213,14 @@ final class CommandProcessor {
if let peerID = chatViewModel?.getPeerIDForNickname(nickname), if let peerID = chatViewModel?.getPeerIDForNickname(nickname),
let fingerprint = meshService?.getFingerprint(for: peerID) { let fingerprint = meshService?.getFingerprint(for: peerID) {
if identityManager.isBlocked(fingerprint: fingerprint) { if SecureIdentityStateManager.shared.isBlocked(fingerprint: fingerprint) {
return .success(message: "\(nickname) is already blocked") return .success(message: "\(nickname) is already blocked")
} }
// Block the user (mesh/noise identity) // Block the user (mesh/noise identity)
if var identity = identityManager.getSocialIdentity(for: fingerprint) { if var identity = SecureIdentityStateManager.shared.getSocialIdentity(for: fingerprint) {
identity.isBlocked = true identity.isBlocked = true
identity.isFavorite = false identity.isFavorite = false
identityManager.updateSocialIdentity(identity) SecureIdentityStateManager.shared.updateSocialIdentity(identity)
} else { } else {
let blockedIdentity = SocialIdentity( let blockedIdentity = SocialIdentity(
fingerprint: fingerprint, fingerprint: fingerprint,
@@ -233,16 +231,16 @@ final class CommandProcessor {
isBlocked: true, isBlocked: true,
notes: nil notes: nil
) )
identityManager.updateSocialIdentity(blockedIdentity) SecureIdentityStateManager.shared.updateSocialIdentity(blockedIdentity)
} }
return .success(message: "blocked \(nickname). you will no longer receive messages from them") return .success(message: "blocked \(nickname). you will no longer receive messages from them")
} }
// Mesh lookup failed; try geohash (Nostr) participant by display name // Mesh lookup failed; try geohash (Nostr) participant by display name
if let pub = chatViewModel?.nostrPubkeyForDisplayName(nickname) { if let pub = chatViewModel?.nostrPubkeyForDisplayName(nickname) {
if identityManager.isNostrBlocked(pubkeyHexLowercased: pub) { if SecureIdentityStateManager.shared.isNostrBlocked(pubkeyHexLowercased: pub) {
return .success(message: "\(nickname) is already blocked") return .success(message: "\(nickname) is already blocked")
} }
identityManager.setNostrBlocked(pub, isBlocked: true) SecureIdentityStateManager.shared.setNostrBlocked(pub, isBlocked: true)
return .success(message: "blocked \(nickname) in geohash chats") return .success(message: "blocked \(nickname) in geohash chats")
} }
@@ -259,18 +257,18 @@ final class CommandProcessor {
if let peerID = chatViewModel?.getPeerIDForNickname(nickname), if let peerID = chatViewModel?.getPeerIDForNickname(nickname),
let fingerprint = meshService?.getFingerprint(for: peerID) { let fingerprint = meshService?.getFingerprint(for: peerID) {
if !identityManager.isBlocked(fingerprint: fingerprint) { if !SecureIdentityStateManager.shared.isBlocked(fingerprint: fingerprint) {
return .success(message: "\(nickname) is not blocked") return .success(message: "\(nickname) is not blocked")
} }
identityManager.setBlocked(fingerprint, isBlocked: false) SecureIdentityStateManager.shared.setBlocked(fingerprint, isBlocked: false)
return .success(message: "unblocked \(nickname)") return .success(message: "unblocked \(nickname)")
} }
// Try geohash unblock // Try geohash unblock
if let pub = chatViewModel?.nostrPubkeyForDisplayName(nickname) { if let pub = chatViewModel?.nostrPubkeyForDisplayName(nickname) {
if !identityManager.isNostrBlocked(pubkeyHexLowercased: pub) { if !SecureIdentityStateManager.shared.isNostrBlocked(pubkeyHexLowercased: pub) {
return .success(message: "\(nickname) is not blocked") return .success(message: "\(nickname) is not blocked")
} }
identityManager.setNostrBlocked(pub, isBlocked: false) SecureIdentityStateManager.shared.setNostrBlocked(pub, isBlocked: false)
return .success(message: "unblocked \(nickname) in geohash chats") return .success(message: "unblocked \(nickname) in geohash chats")
} }
return .error(message: "cannot unblock \(nickname): not found") return .error(message: "cannot unblock \(nickname): not found")
@@ -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
@@ -13,16 +13,12 @@ final class FavoritesPersistenceService: ObservableObject {
let theyFavoritedUs: Bool let theyFavoritedUs: Bool
let favoritedAt: Date let favoritedAt: Date
let lastUpdated: Date let lastUpdated: Date
// Track what we last sent as OUR npub to this peer, to avoid resending unless it changes
// Note: we do not track which npub we last sent to them; sending happens only on favorite toggle
var isMutual: Bool { var isMutual: Bool {
isFavorite && theyFavoritedUs isFavorite && theyFavoritedUs
} }
} }
// We intentionally do not track when we last sent our npub; sending happens only on favorite toggle.
private static let storageKey = "chat.bitchat.favorites" private static let storageKey = "chat.bitchat.favorites"
private static let keychainService = "chat.bitchat.favorites" private static let keychainService = "chat.bitchat.favorites"
@@ -51,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]
@@ -67,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
@@ -85,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 {
@@ -126,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,
@@ -147,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)
} }
} }
@@ -239,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
@@ -299,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()
@@ -333,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)
} }
} }
@@ -351,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)
} }
} }
@@ -367,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 ||
@@ -408,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)
} }
} }
} }
+16 -22
View File
@@ -8,24 +8,18 @@
import Foundation import Foundation
import Security import Security
import os.log
protocol KeychainManagerProtocol { class KeychainManager {
func saveIdentityKey(_ keyData: Data, forKey key: String) -> Bool static let shared = KeychainManager()
func getIdentityKey(forKey key: String) -> Data?
func deleteIdentityKey(forKey key: String) -> Bool
func deleteAllKeychainData() -> Bool
func secureClear(_ data: inout Data)
func secureClear(_ string: inout String)
func verifyIdentityKeyExists() -> Bool
}
final class KeychainManager: KeychainManagerProtocol {
// Use consistent service name for all keychain items // Use consistent service name for all keychain items
private let service = "chat.bitchat" private let service = "chat.bitchat"
private let appGroup = "group.chat.bitchat" private let appGroup = "group.chat.bitchat"
private init() {}
private func isSandboxed() -> Bool { private func isSandboxed() -> Bool {
#if os(macOS) #if os(macOS)
// More robust sandbox detection using multiple methods // More robust sandbox detection using multiple methods
@@ -59,7 +53,7 @@ final class KeychainManager: KeychainManagerProtocol {
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
} }
@@ -70,7 +64,7 @@ final class KeychainManager: KeychainManagerProtocol {
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
} }
@@ -119,9 +113,9 @@ final class KeychainManager: KeychainManagerProtocol {
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
} }
@@ -157,7 +151,7 @@ final class KeychainManager: KeychainManagerProtocol {
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
} }
@@ -204,7 +198,7 @@ final class KeychainManager: KeychainManagerProtocol {
// 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
@@ -267,7 +261,7 @@ final class KeychainManager: KeychainManagerProtocol {
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)
} }
} }
} }
@@ -309,7 +303,7 @@ final class KeychainManager: KeychainManagerProtocol {
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
} }
@@ -317,7 +311,7 @@ final class KeychainManager: KeychainManagerProtocol {
// MARK: - Security Utilities // MARK: - Security Utilities
/// Securely clear sensitive data from memory /// Securely clear sensitive data from memory
func secureClear(_ data: inout Data) { static func secureClear(_ data: inout Data) {
_ = data.withUnsafeMutableBytes { bytes in _ = data.withUnsafeMutableBytes { bytes in
// Use volatile memset to prevent compiler optimization // Use volatile memset to prevent compiler optimization
memset_s(bytes.baseAddress, bytes.count, 0, bytes.count) memset_s(bytes.baseAddress, bytes.count, 0, bytes.count)
@@ -326,7 +320,7 @@ final class KeychainManager: KeychainManagerProtocol {
} }
/// Securely clear sensitive string from memory /// Securely clear sensitive string from memory
func secureClear(_ string: inout String) { static func secureClear(_ string: inout String) {
// Convert to mutable data and clear // Convert to mutable data and clear
if var data = string.data(using: .utf8) { if var data = string.data(using: .utf8) {
secureClear(&data) secureClear(&data)
@@ -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
@@ -285,18 +286,12 @@ final class LocationChannelManager: NSObject, CLLocationManagerDelegate, Observa
} else if let locality = pm.locality, !locality.isEmpty { } else if let locality = pm.locality, !locality.isEmpty {
dict[.neighborhood] = locality dict[.neighborhood] = locality
} }
// Block: reuse neighborhood/locality granularity // Block: reuse neighborhood/locality granularity without exposing street level
if let subLocality = pm.subLocality, !subLocality.isEmpty { if let subLocality = pm.subLocality, !subLocality.isEmpty {
dict[.block] = subLocality dict[.block] = subLocality
} else if let locality = pm.locality, !locality.isEmpty { } else if let locality = pm.locality, !locality.isEmpty {
dict[.block] = locality dict[.block] = locality
} }
// Building: prefer place name/street/venue when available
if let name = pm.name, !name.isEmpty {
dict[.building] = name
} else if let thoroughfare = pm.thoroughfare, !thoroughfare.isEmpty {
dict[.building] = thoroughfare
}
return dict return dict
} }
} }
@@ -1,52 +0,0 @@
import Foundation
/// Lightweight background counter for location notes (kind 1) at block-level geohash.
@MainActor
final class LocationNotesCounter: ObservableObject {
static let shared = LocationNotesCounter()
@Published private(set) var geohash: String? = nil
@Published private(set) var count: Int? = 0
@Published private(set) var initialLoadComplete: Bool = false
private var subscriptionID: String? = nil
private var noteIDs = Set<String>()
private init() {}
func subscribe(geohash gh: String) {
let norm = gh.lowercased()
if geohash == norm { return }
cancel()
geohash = norm
count = 0
noteIDs.removeAll()
initialLoadComplete = false
// Subscribe only to the building geohash (precision 8)
let subID = "locnotes-count-\(norm)-\(UUID().uuidString.prefix(6))"
subscriptionID = subID
let filter = NostrFilter.geohashNotes(norm, since: nil, limit: 500)
let relays = GeoRelayDirectory.shared.closestRelays(toGeohash: norm, count: TransportConfig.nostrGeoRelayCount)
let relayUrls: [String]? = relays.isEmpty ? nil : relays
NostrRelayManager.shared.subscribe(filter: filter, id: subID, relayUrls: relayUrls, handler: { [weak self] event in
guard let self = self else { return }
guard event.kind == NostrProtocol.EventKind.textNote.rawValue else { return }
guard event.tags.contains(where: { $0.count >= 2 && $0[0].lowercased() == "g" && $0[1].lowercased() == norm }) else { return }
if !self.noteIDs.contains(event.id) {
self.noteIDs.insert(event.id)
self.count = self.noteIDs.count
}
}, onEOSE: { [weak self] in
self?.initialLoadComplete = true
})
}
func cancel() {
if let sub = subscriptionID { NostrRelayManager.shared.unsubscribe(id: sub) }
subscriptionID = nil
geohash = nil
count = 0
noteIDs.removeAll()
}
}
@@ -1,98 +0,0 @@
import Foundation
/// Persistent location notes (Nostr kind 1) scoped to a street-level geohash (precision 7).
/// Subscribes to and publishes notes for a given geohash and provides a send API.
@MainActor
final class LocationNotesManager: ObservableObject {
struct Note: Identifiable, Equatable {
let id: String
let pubkey: String
let content: String
let createdAt: Date
let nickname: String?
var displayName: String {
let suffix = String(pubkey.suffix(4))
if let nick = nickname, !nick.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
return "\(nick)#\(suffix)"
}
return "anon#\(suffix)"
}
}
@Published private(set) var notes: [Note] = [] // reverse-chron sorted
@Published private(set) var geohash: String
@Published private(set) var initialLoadComplete: Bool = false
private var subscriptionID: String?
init(geohash: String) {
self.geohash = geohash.lowercased()
subscribe()
}
func setGeohash(_ newGeohash: String) {
let norm = newGeohash.lowercased()
guard norm != geohash else { return }
if let sub = subscriptionID {
NostrRelayManager.shared.unsubscribe(id: sub)
subscriptionID = nil
}
geohash = norm
notes.removeAll()
subscribe()
}
private func subscribe() {
let subID = "locnotes-\(geohash)-\(UUID().uuidString.prefix(8))"
subscriptionID = subID
// For persistent notes, allow relays to return recent history without an aggressive time cutoff
let filter = NostrFilter.geohashNotes(geohash, since: nil, limit: 200)
let relays = GeoRelayDirectory.shared.closestRelays(toGeohash: geohash, count: TransportConfig.nostrGeoRelayCount)
let relayUrls: [String]? = relays.isEmpty ? nil : relays
initialLoadComplete = false
NostrRelayManager.shared.subscribe(filter: filter, id: subID, relayUrls: relayUrls, handler: { [weak self] event in
guard let self = self else { return }
guard event.kind == NostrProtocol.EventKind.textNote.rawValue else { return }
// Ensure matching tag
guard event.tags.contains(where: { $0.count >= 2 && $0[0].lowercased() == "g" && $0[1].lowercased() == self.geohash }) else { return }
if self.notes.contains(where: { $0.id == event.id }) { return }
let nick = event.tags.first(where: { $0.first?.lowercased() == "n" && $0.count >= 2 })?.dropFirst().first
let ts = Date(timeIntervalSince1970: TimeInterval(event.created_at))
let note = Note(id: event.id, pubkey: event.pubkey, content: event.content, createdAt: ts, nickname: nick)
self.notes.append(note)
self.notes.sort { $0.createdAt > $1.createdAt }
}, onEOSE: { [weak self] in
self?.initialLoadComplete = true
})
}
/// Send a location note for the current geohash using the per-geohash identity.
func send(content: String, nickname: String) {
let trimmed = content.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmed.isEmpty else { return }
do {
let id = try NostrIdentityBridge.deriveIdentity(forGeohash: geohash)
let event = try NostrProtocol.createGeohashTextNote(
content: trimmed,
geohash: geohash,
senderIdentity: id,
nickname: nickname
)
let relays = GeoRelayDirectory.shared.closestRelays(toGeohash: geohash, count: TransportConfig.nostrGeoRelayCount)
NostrRelayManager.shared.sendEvent(event, to: relays)
// Optimistic local-echo
let echo = Note(id: event.id, pubkey: id.publicKeyHex, content: trimmed, createdAt: Date(), nickname: nickname)
self.notes.insert(echo, at: 0)
} catch {
SecureLogger.error("LocationNotesManager: failed to send note: \(error)", category: .session)
}
}
/// Explicitly cancel subscription and release resources.
func cancel() {
if let sub = subscriptionID {
NostrRelayManager.shared.unsubscribe(id: sub)
subscriptionID = nil
}
}
}
+18 -10
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)
@@ -74,7 +80,6 @@ final class MessageRouter {
} }
func sendFavoriteNotification(to peerID: String, isFavorite: Bool) { func sendFavoriteNotification(to peerID: String, isFavorite: Bool) {
// Route via mesh when connected; else use Nostr
if mesh.isPeerConnected(peerID) { if mesh.isPeerConnected(peerID) {
mesh.sendFavoriteNotification(to: peerID, isFavorite: isFavorite) mesh.sendFavoriteNotification(to: peerID, isFavorite: isFavorite)
} else { } else {
@@ -103,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
+28 -30
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
@@ -155,7 +156,6 @@ final class NoiseEncryptionService {
// Security components // Security components
private let rateLimiter = NoiseRateLimiter() private let rateLimiter = NoiseRateLimiter()
private let keychain: KeychainManagerProtocol
// Session maintenance // Session maintenance
private var rekeyTimer: Timer? private var rekeyTimer: Timer?
@@ -182,17 +182,15 @@ final class NoiseEncryptionService {
} }
} }
init(keychain: KeychainManagerProtocol) { init() {
self.keychain = keychain
// Load or create static identity key (ONLY from keychain) // Load or create static identity key (ONLY from keychain)
let loadedKey: Curve25519.KeyAgreement.PrivateKey let loadedKey: Curve25519.KeyAgreement.PrivateKey
// Try to load from keychain // Try to load from keychain
if let identityData = keychain.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 {
@@ -200,8 +198,8 @@ final class NoiseEncryptionService {
let keyData = loadedKey.rawRepresentation let keyData = loadedKey.rawRepresentation
// Save to keychain // Save to keychain
let saved = keychain.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,10 +210,10 @@ final class NoiseEncryptionService {
let loadedSigningKey: Curve25519.Signing.PrivateKey let loadedSigningKey: Curve25519.Signing.PrivateKey
// Try to load from keychain // Try to load from keychain
if let signingData = keychain.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 {
@@ -223,8 +221,8 @@ final class NoiseEncryptionService {
let keyData = loadedSigningKey.rawRepresentation let keyData = loadedSigningKey.rawRepresentation
// Save to keychain // Save to keychain
let saved = keychain.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
@@ -232,7 +230,7 @@ final class NoiseEncryptionService {
self.signingPublicKey = signingKey.publicKey self.signingPublicKey = signingKey.publicKey
// Initialize session manager // Initialize session manager
self.sessionManager = NoiseSessionManager(localStaticKey: staticIdentityKey, keychain: keychain) self.sessionManager = NoiseSessionManager(localStaticKey: staticIdentityKey)
// Set up session callbacks // Set up session callbacks
sessionManager.onSessionEstablished = { [weak self] peerID, remoteStaticKey in sessionManager.onSessionEstablished = { [weak self] peerID, remoteStaticKey in
@@ -269,10 +267,10 @@ final class NoiseEncryptionService {
/// Clear persistent identity (for panic mode) /// Clear persistent identity (for panic mode)
func clearPersistentIdentity() { func clearPersistentIdentity() {
// Clear from keychain // Clear from keychain
let deletedStatic = keychain.deleteIdentityKey(forKey: "noiseStaticKey") let deletedStatic = KeychainManager.shared.deleteIdentityKey(forKey: "noiseStaticKey")
let deletedSigning = keychain.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()
} }
@@ -283,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
} }
} }
@@ -294,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
} }
} }
@@ -394,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
@@ -417,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
} }
@@ -523,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
@@ -539,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
@@ -575,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)
} }
} }
} }
+40 -39
View File
@@ -21,15 +21,10 @@ final class NostrTransport: Transport {
private var readQueue: [QueuedRead] = [] private var readQueue: [QueuedRead] = []
private var isSendingReadAcks = false private var isSendingReadAcks = false
private let readAckInterval: TimeInterval = TransportConfig.nostrReadAckInterval private let readAckInterval: TimeInterval = TransportConfig.nostrReadAckInterval
private let keychain: KeychainManagerProtocol
var myPeerID: String { senderPeerID } var myPeerID: String { senderPeerID }
var myNickname: String { "" } var myNickname: String { "" }
func setNickname(_ nickname: String) { /* not used for Nostr */ } func setNickname(_ nickname: String) { /* not used for Nostr */ }
init(keychain: KeychainManagerProtocol) {
self.keychain = keychain
}
func startServices() { /* no-op */ } func startServices() { /* no-op */ }
func stopServices() { /* no-op */ } func stopServices() { /* no-op */ }
@@ -43,17 +38,11 @@ final class NostrTransport: Transport {
func getFingerprint(for peerID: String) -> String? { nil } func getFingerprint(for peerID: String) -> String? { nil }
func getNoiseSessionState(for peerID: String) -> LazyHandshakeState { .none } func getNoiseSessionState(for peerID: String) -> LazyHandshakeState { .none }
func triggerHandshake(with peerID: String) { /* no-op */ } func triggerHandshake(with peerID: String) { /* no-op */ }
// Nostr does not use Noise sessions here; return a cached placeholder to avoid reallocation // Nostr does not use Noise sessions here; return a cached placeholder to avoid reallocation
private static var cachedNoiseService: NoiseEncryptionService? private static var cachedNoiseService: NoiseEncryptionService = {
func getNoiseService() -> NoiseEncryptionService { NoiseEncryptionService()
if let noiseService = Self.cachedNoiseService { }()
return noiseService func getNoiseService() -> NoiseEncryptionService { Self.cachedNoiseService }
}
let noiseService = NoiseEncryptionService(keychain: keychain)
Self.cachedNoiseService = noiseService
return noiseService
}
// Public broadcast not supported over Nostr here // Public broadcast not supported over Nostr here
func sendMessage(_ content: String, mentions: [String]) { /* no-op */ } func sendMessage(_ content: String, mentions: [String]) { /* no-op */ }
@@ -62,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)
} }
} }
@@ -108,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 {
@@ -117,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()
} }
@@ -143,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 {
@@ -152,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)
} }
} }
@@ -185,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)
@@ -193,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)
} }
} }
@@ -208,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)
@@ -218,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)
@@ -230,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%
} }
+1 -3
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
@@ -103,7 +101,7 @@ enum TransportConfig {
// Location // Location
static let locationDistanceFilterMeters: Double = 1000 static let locationDistanceFilterMeters: Double = 1000
// Live (channel sheet open) distance threshold for meaningful updates // Live (channel sheet open) distance threshold for meaningful updates
static let locationDistanceFilterLiveMeters: Double = 10.0 static let locationDistanceFilterLiveMeters: Double = 21.0
static let locationLiveRefreshInterval: TimeInterval = 5.0 static let locationLiveRefreshInterval: TimeInterval = 5.0
// Notifications (geohash) // Notifications (geohash)
+39 -12
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
@@ -27,16 +27,14 @@ final class UnifiedPeerService: ObservableObject, TransportPeerEventsDelegate {
private var peerIndex: [String: BitchatPeer] = [:] private var peerIndex: [String: BitchatPeer] = [:]
private var fingerprintCache: [String: String] = [:] // peerID -> fingerprint private var fingerprintCache: [String: String] = [:] // peerID -> fingerprint
private let meshService: Transport private let meshService: Transport
private let identityManager: SecureIdentityStateManagerProtocol
weak var messageRouter: MessageRouter? weak var messageRouter: MessageRouter?
private let favoritesService = FavoritesPersistenceService.shared private let favoritesService = FavoritesPersistenceService.shared
private var cancellables = Set<AnyCancellable>() private var cancellables = Set<AnyCancellable>()
// MARK: - Initialization // MARK: - Initialization
init(meshService: Transport, identityManager: SecureIdentityStateManagerProtocol) { init(meshService: Transport) {
self.meshService = meshService self.meshService = meshService
self.identityManager = identityManager
// Subscribe to changes from both services // Subscribe to changes from both services
setupSubscriptions() setupSubscriptions()
@@ -176,7 +174,7 @@ final class UnifiedPeerService: ObservableObject, TransportPeerEventsDelegate {
// Determine reachability based on lastSeen and identity trust // Determine reachability based on lastSeen and identity trust
let now = Date() let now = Date()
let fingerprint = peerInfo.noisePublicKey?.sha256Fingerprint() let fingerprint = peerInfo.noisePublicKey?.sha256Fingerprint()
let isVerified = fingerprint.map { identityManager.isVerified(fingerprint: $0) } ?? false let isVerified = fingerprint.map { SecureIdentityStateManager.shared.isVerified(fingerprint: $0) } ?? false
let isFav = peerInfo.noisePublicKey.flatMap { favorites[$0]?.isFavorite } ?? false let isFav = peerInfo.noisePublicKey.flatMap { favorites[$0]?.isFavorite } ?? false
let retention: TimeInterval = (isVerified || isFav) ? TransportConfig.bleReachabilityRetentionVerifiedSeconds : TransportConfig.bleReachabilityRetentionUnverifiedSeconds let retention: TimeInterval = (isVerified || isFav) ? TransportConfig.bleReachabilityRetentionVerifiedSeconds : TransportConfig.bleReachabilityRetentionUnverifiedSeconds
// A peer is reachable if we recently saw them AND we are attached to the mesh // A peer is reachable if we recently saw them AND we are attached to the mesh
@@ -197,6 +195,31 @@ final class UnifiedPeerService: ObservableObject, TransportPeerEventsDelegate {
let favoriteStatus = favorites[noiseKey] { let favoriteStatus = favorites[noiseKey] {
peer.favoriteStatus = favoriteStatus peer.favoriteStatus = favoriteStatus
peer.nostrPublicKey = favoriteStatus.peerNostrPublicKey peer.nostrPublicKey = favoriteStatus.peerNostrPublicKey
} else {
// Check by nickname for reconnected peers
let favoriteByNickname = favorites.values.first {
$0.peerNickname == peerInfo.nickname
}
if let favorite = favoriteByNickname,
let noiseKey = peerInfo.noisePublicKey {
SecureLogger.log(
"🔄 Found favorite for '\(peerInfo.nickname)' by nickname, updating noise key",
category: SecureLogger.session,
level: .debug
)
// Update the favorite's key in persistence
favoritesService.updateNoisePublicKey(
from: favorite.peerNoisePublicKey,
to: noiseKey,
peerNickname: peerInfo.nickname
)
// Get updated favorite
peer.favoriteStatus = favoritesService.getFavoriteStatus(for: noiseKey)
peer.nostrPublicKey = peer.favoriteStatus?.peerNostrPublicKey ?? favorite.peerNostrPublicKey
}
} }
return peer return peer
@@ -249,7 +272,7 @@ final class UnifiedPeerService: ObservableObject, TransportPeerEventsDelegate {
guard let fingerprint = getFingerprint(for: peerID) else { return false } guard let fingerprint = getFingerprint(for: peerID) else { return false }
// Check SecureIdentityStateManager for block status // Check SecureIdentityStateManager for block status
if let identity = identityManager.getSocialIdentity(for: fingerprint) { if let identity = SecureIdentityStateManager.shared.getSocialIdentity(for: fingerprint) {
return identity.isBlocked return identity.isBlocked
} }
@@ -259,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
} }
@@ -269,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)
} }
} }
@@ -302,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 {
@@ -326,7 +353,7 @@ final class UnifiedPeerService: ObservableObject, TransportPeerEventsDelegate {
guard let fingerprint = getFingerprint(for: peerID) else { return } guard let fingerprint = getFingerprint(for: peerID) else { return }
// Get or create social identity // Get or create social identity
var identity = identityManager.getSocialIdentity(for: fingerprint) var identity = SecureIdentityStateManager.shared.getSocialIdentity(for: fingerprint)
?? SocialIdentity( ?? SocialIdentity(
fingerprint: fingerprint, fingerprint: fingerprint,
localPetname: nil, localPetname: nil,
@@ -349,7 +376,7 @@ final class UnifiedPeerService: ObservableObject, TransportPeerEventsDelegate {
} }
} }
identityManager.updateSocialIdentity(identity) SecureIdentityStateManager.shared.updateSocialIdentity(identity)
} }
/// Get fingerprint for peer ID /// Get fingerprint for peer ID
-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
} }
File diff suppressed because it is too large Load Diff
+10 -158
View File
@@ -25,7 +25,6 @@ struct ContentView: View {
@EnvironmentObject var viewModel: ChatViewModel @EnvironmentObject var viewModel: ChatViewModel
@ObservedObject private var locationManager = LocationChannelManager.shared @ObservedObject private var locationManager = LocationChannelManager.shared
@ObservedObject private var bookmarks = GeohashBookmarksStore.shared @ObservedObject private var bookmarks = GeohashBookmarksStore.shared
@ObservedObject private var notesCounter = LocationNotesCounter.shared
@State private var messageText = "" @State private var messageText = ""
@State private var textFieldSelection: NSRange? = nil @State private var textFieldSelection: NSRange? = nil
@FocusState private var isTextFieldFocused: Bool @FocusState private var isTextFieldFocused: Bool
@@ -50,10 +49,6 @@ struct ContentView: View {
@State private var showLocationChannelsSheet = false @State private var showLocationChannelsSheet = false
@State private var showVerifySheet = false @State private var showVerifySheet = false
@State private var expandedMessageIDs: Set<String> = [] @State private var expandedMessageIDs: Set<String> = []
@State private var showLocationNotes = false
@State private var notesGeohash: String? = nil
@State private var sheetNotesCount: Int = 0
// Timer-based refresh removed; use LocationChannelManager live updates instead
// Window sizes for rendering (infinite scroll up) // Window sizes for rendering (infinite scroll up)
@State private var windowCountPublic: Int = 300 @State private var windowCountPublic: Int = 300
@State private var windowCountPrivate: [String: Int] = [:] @State private var windowCountPrivate: [String: Int] = [:]
@@ -449,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
@@ -471,14 +454,14 @@ struct ContentView: View {
guard (2...12).contains(gh.count), gh.allSatisfy({ allowed.contains($0) }) else { return } guard (2...12).contains(gh.count), gh.allSatisfy({ allowed.contains($0) }) else { return }
func levelForLength(_ len: Int) -> GeohashChannelLevel { func levelForLength(_ len: Int) -> GeohashChannelLevel {
switch len { switch len {
case 0...2: return .region case 0...2: return .region
case 3...4: return .province case 3...4: return .province
case 5: return .city case 5: return .city
case 6: return .neighborhood case 6: return .neighborhood
case 7: return .block case 7: return .block
default: return .block default: return .block
}
} }
}
let level = levelForLength(gh.count) let level = levelForLength(gh.count)
let ch = GeohashChannel(level: level, geohash: gh) let ch = GeohashChannel(level: level, geohash: gh)
// Do not mark teleported when opening a geohash that is in our regional set. // Do not mark teleported when opening a geohash that is in our regional set.
@@ -1096,7 +1079,7 @@ struct ContentView: View {
TextField("nickname", text: $viewModel.nickname) TextField("nickname", text: $viewModel.nickname)
.textFieldStyle(.plain) .textFieldStyle(.plain)
.font(.system(size: 14, design: .monospaced)) .font(.system(size: 14, design: .monospaced))
.frame(maxWidth: 80) .frame(maxWidth: 100)
.foregroundColor(textColor) .foregroundColor(textColor)
.focused($isNicknameFieldFocused) .focused($isNicknameFieldFocused)
.autocorrectionDisabled(true) .autocorrectionDisabled(true)
@@ -1175,29 +1158,6 @@ struct ContentView: View {
} }
.buttonStyle(.plain) .buttonStyle(.plain)
// Notes icon (mesh only and when location is authorized), to the right of #mesh
if case .mesh = locationManager.selectedChannel, locationManager.permissionState == .authorized {
Button(action: {
// Kick a one-shot refresh and show the sheet immediately.
LocationChannelManager.shared.enableLocationChannels()
LocationChannelManager.shared.refreshChannels()
// If we already have a block geohash, pass it; otherwise wait in the sheet.
notesGeohash = LocationChannelManager.shared.availableChannels.first(where: { $0.level == .building })?.geohash
showLocationNotes = true
}) {
HStack(alignment: .center, spacing: 4) {
let hasNotes = ((notesCounter.count ?? 0) > 0) || (sheetNotesCount > 0)
Image(systemName: "long.text.page.and.pencil")
.font(.system(size: 12))
.foregroundColor(hasNotes ? Color(hue: 0.60, saturation: 0.85, brightness: 0.82) : Color.gray)
.padding(.top, 1)
}
.fixedSize(horizontal: true, vertical: false)
}
.buttonStyle(.plain)
.accessibilityLabel("Location notes for this place")
}
HStack(spacing: 4) { HStack(spacing: 4) {
// People icon with count // People icon with count
Image(systemName: "person.2.fill") Image(systemName: "person.2.fill")
@@ -1208,12 +1168,9 @@ struct ContentView: View {
.accessibilityHidden(true) .accessibilityHidden(true)
} }
.foregroundColor(headerCountColor) .foregroundColor(headerCountColor)
.lineLimit(1)
.fixedSize(horizontal: true, vertical: false)
// QR moved to the PEOPLE header in the sidebar when on mesh channel // QR moved to the PEOPLE header in the sidebar when on mesh channel
} }
.layoutPriority(3)
.onTapGesture { .onTapGesture {
withAnimation(.easeInOut(duration: TransportConfig.uiAnimationMediumSeconds)) { withAnimation(.easeInOut(duration: TransportConfig.uiAnimationMediumSeconds)) {
showSidebar.toggle() showSidebar.toggle()
@@ -1232,91 +1189,6 @@ struct ContentView: View {
.onAppear { viewModel.isLocationChannelsSheetPresented = true } .onAppear { viewModel.isLocationChannelsSheetPresented = true }
.onDisappear { viewModel.isLocationChannelsSheetPresented = false } .onDisappear { viewModel.isLocationChannelsSheetPresented = false }
} }
.sheet(isPresented: $showLocationNotes) {
Group {
if let gh = notesGeohash ?? LocationChannelManager.shared.availableChannels.first(where: { $0.level == .building })?.geohash {
LocationNotesView(geohash: gh, onNotesCountChanged: { cnt in sheetNotesCount = cnt })
.environmentObject(viewModel)
} else {
VStack(spacing: 12) {
HStack {
Text("notes")
.font(.system(size: 16, weight: .bold, design: .monospaced))
Spacer()
Button(action: { showLocationNotes = false }) {
Image(systemName: "xmark")
.font(.system(size: 13, weight: .semibold, design: .monospaced))
.foregroundColor(textColor)
.frame(width: 32, height: 32)
}
.buttonStyle(.plain)
.accessibilityLabel("Close")
}
.frame(height: 44)
.padding(.horizontal, 12)
.background(backgroundColor.opacity(0.95))
Text("location unavailable")
.font(.system(size: 14, design: .monospaced))
.foregroundColor(secondaryTextColor)
Button("enable location") {
LocationChannelManager.shared.enableLocationChannels()
LocationChannelManager.shared.refreshChannels()
}
.buttonStyle(.bordered)
Spacer()
}
.background(backgroundColor)
.foregroundColor(textColor)
// per-sheet global onChange added below
}
}
.onAppear {
// Ensure we are authorized and start live location updates (distance-filtered)
LocationChannelManager.shared.enableLocationChannels()
LocationChannelManager.shared.beginLiveRefresh()
}
.onDisappear {
LocationChannelManager.shared.endLiveRefresh()
sheetNotesCount = 0
}
.onChange(of: locationManager.availableChannels) { channels in
if let current = channels.first(where: { $0.level == .building })?.geohash,
notesGeohash != current {
notesGeohash = current
#if os(iOS)
// Light taptic when geohash changes while the sheet is open
let generator = UIImpactFeedbackGenerator(style: .light)
generator.prepare()
generator.impactOccurred()
#endif
}
}
}
.onAppear {
updateNotesCounterSubscription()
if case .mesh = locationManager.selectedChannel,
locationManager.permissionState == .authorized,
LocationChannelManager.shared.availableChannels.isEmpty {
LocationChannelManager.shared.refreshChannels()
}
}
.onChange(of: locationManager.selectedChannel) { _ in
updateNotesCounterSubscription()
if case .mesh = locationManager.selectedChannel,
locationManager.permissionState == .authorized,
LocationChannelManager.shared.availableChannels.isEmpty {
LocationChannelManager.shared.refreshChannels()
}
}
.onChange(of: locationManager.availableChannels) { _ in updateNotesCounterSubscription() }
.onChange(of: locationManager.permissionState) { _ in
updateNotesCounterSubscription()
if case .mesh = locationManager.selectedChannel,
locationManager.permissionState == .authorized,
LocationChannelManager.shared.availableChannels.isEmpty {
LocationChannelManager.shared.refreshChannels()
}
}
.alert("heads up", isPresented: $viewModel.showScreenshotPrivacyWarning) { .alert("heads up", isPresented: $viewModel.showScreenshotPrivacyWarning) {
Button("ok", role: .cancel) {} Button("ok", role: .cancel) {}
} message: { } message: {
@@ -1363,15 +1235,15 @@ struct ContentView: View {
!fav.peerNickname.isEmpty { return fav.peerNickname } !fav.peerNickname.isEmpty { return fav.peerNickname }
// Fallback: resolve from persisted social identity via fingerprint mapping // Fallback: resolve from persisted social identity via fingerprint mapping
if headerPeerID.count == 16 { if headerPeerID.count == 16 {
let candidates = viewModel.identityManager.getCryptoIdentitiesByPeerIDPrefix(headerPeerID) let candidates = SecureIdentityStateManager.shared.getCryptoIdentitiesByPeerIDPrefix(headerPeerID)
if let id = candidates.first, if let id = candidates.first,
let social = viewModel.identityManager.getSocialIdentity(for: id.fingerprint) { let social = SecureIdentityStateManager.shared.getSocialIdentity(for: id.fingerprint) {
if let pet = social.localPetname, !pet.isEmpty { return pet } if let pet = social.localPetname, !pet.isEmpty { return pet }
if !social.claimedNickname.isEmpty { return social.claimedNickname } if !social.claimedNickname.isEmpty { return social.claimedNickname }
} }
} else if headerPeerID.count == 64, let keyData = Data(hexString: headerPeerID) { } else if headerPeerID.count == 64, let keyData = Data(hexString: headerPeerID) {
let fp = keyData.sha256Fingerprint() let fp = keyData.sha256Fingerprint()
if let social = viewModel.identityManager.getSocialIdentity(for: fp) { if let social = SecureIdentityStateManager.shared.getSocialIdentity(for: fp) {
if let pet = social.localPetname, !pet.isEmpty { return pet } if let pet = social.localPetname, !pet.isEmpty { return pet }
if !social.claimedNickname.isEmpty { return social.claimedNickname } if !social.claimedNickname.isEmpty { return social.claimedNickname }
} }
@@ -1515,26 +1387,6 @@ struct ContentView: View {
} }
// MARK: - Notes Counter Subscription Helper
extension ContentView {
private func updateNotesCounterSubscription() {
switch locationManager.selectedChannel {
case .mesh:
// Ensure we have a fresh one-shot location fix so building geohash is current
if locationManager.permissionState == .authorized {
LocationChannelManager.shared.refreshChannels()
}
if let building = LocationChannelManager.shared.availableChannels.first(where: { $0.level == .building })?.geohash {
LocationNotesCounter.shared.subscribe(geohash: building)
} else {
LocationNotesCounter.shared.cancel()
}
case .location:
LocationNotesCounter.shared.cancel()
}
}
}
// MARK: - Helper Views // MARK: - Helper Views
// Rounded payment chip button // Rounded payment chip button
+1 -1
View File
@@ -53,7 +53,7 @@ struct FingerprintView: View {
if peerID.count == 64, let data = Data(hexString: peerID) { if peerID.count == 64, let data = Data(hexString: peerID) {
if let fav = FavoritesPersistenceService.shared.getFavoriteStatus(for: data), !fav.peerNickname.isEmpty { return fav.peerNickname } if let fav = FavoritesPersistenceService.shared.getFavoriteStatus(for: data), !fav.peerNickname.isEmpty { return fav.peerNickname }
let fp = data.sha256Fingerprint() let fp = data.sha256Fingerprint()
if let social = viewModel.identityManager.getSocialIdentity(for: fp) { if let social = SecureIdentityStateManager.shared.getSocialIdentity(for: fp) {
if let pet = social.localPetname, !pet.isEmpty { return pet } if let pet = social.localPetname, !pet.isEmpty { return pet }
if !social.claimedNickname.isEmpty { return social.claimedNickname } if !social.claimedNickname.isEmpty { return social.claimedNickname }
} }
+1 -2
View File
@@ -107,7 +107,7 @@ struct LocationChannelsSheet: View {
// Nearby options // Nearby options
if !manager.availableChannels.isEmpty { if !manager.availableChannels.isEmpty {
ForEach(manager.availableChannels.filter { $0.level != .building }) { channel in ForEach(manager.availableChannels) { channel in
let coverage = coverageString(forPrecision: channel.geohash.count) let coverage = coverageString(forPrecision: channel.geohash.count)
let nameBase = locationName(for: channel.level) let nameBase = locationName(for: channel.level)
let namePart = nameBase.map { formattedNamePrefix(for: channel.level) + $0 } let namePart = nameBase.map { formattedNamePrefix(for: channel.level) + $0 }
@@ -381,7 +381,6 @@ struct LocationChannelsSheet: View {
case 5: return .city case 5: return .city
case 6: return .neighborhood case 6: return .neighborhood
case 7: return .block case 7: return .block
case 8: return .building
default: return .block default: return .block
} }
} }
-65
View File
@@ -1,65 +0,0 @@
import SwiftUI
struct LocationNotesSheet: View {
@EnvironmentObject var viewModel: ChatViewModel
@ObservedObject private var locationManager = LocationChannelManager.shared
@Binding var notesGeohash: String?
@Environment(\.dismiss) private var dismiss
@Environment(\.colorScheme) private var colorScheme
private var backgroundColor: Color { colorScheme == .dark ? .black : .white }
private var textColor: Color { colorScheme == .dark ? .green : Color(red: 0, green: 0.5, blue: 0) }
private var secondaryTextColor: Color { textColor.opacity(0.8) }
var body: some View {
Group {
if let gh = notesGeohash ?? locationManager.availableChannels.first(where: { $0.level == .block })?.geohash {
// Found block geohash: show notes view
LocationNotesView(geohash: gh)
.environmentObject(viewModel)
} else {
// Acquire location: keep a loading overlay (Matrix) until we either get a block geohash
ZStack {
VStack(spacing: 0) {
HStack {
VStack(alignment: .leading, spacing: 2) {
Text("notes")
.font(.system(size: 16, weight: .bold, design: .monospaced))
Text("acquiring location…")
.font(.system(size: 12, design: .monospaced))
.foregroundColor(secondaryTextColor)
}
Spacer()
Button(action: { dismiss() }) {
Image(systemName: "xmark")
.font(.system(size: 13, weight: .semibold, design: .monospaced))
.foregroundColor(textColor)
.frame(width: 32, height: 32)
}
.buttonStyle(.plain)
.accessibilityLabel("Close")
}
.frame(height: 44)
.padding(.horizontal, 12)
.background(backgroundColor.opacity(0.95))
Spacer()
}
MatrixRainView()
.transition(.opacity)
}
.background(backgroundColor)
.foregroundColor(textColor)
.onAppear {
LocationChannelManager.shared.enableLocationChannels()
// Nudge a fresh fix
LocationChannelManager.shared.refreshChannels()
}
.onChange(of: locationManager.availableChannels) { channels in
if notesGeohash == nil, let block = channels.first(where: { $0.level == .block }) {
notesGeohash = block.geohash
}
}
}
}
}
}
-176
View File
@@ -1,176 +0,0 @@
import SwiftUI
struct LocationNotesView: View {
@EnvironmentObject var viewModel: ChatViewModel
@StateObject private var manager: LocationNotesManager
let geohash: String
let onNotesCountChanged: ((Int) -> Void)?
@Environment(\.colorScheme) var colorScheme
@ObservedObject private var locationManager = LocationChannelManager.shared
@Environment(\.dismiss) private var dismiss
@State private var draft: String = ""
init(geohash: String, onNotesCountChanged: ((Int) -> Void)? = nil) {
let gh = geohash.lowercased()
self.geohash = gh
self.onNotesCountChanged = onNotesCountChanged
_manager = StateObject(wrappedValue: LocationNotesManager(geohash: gh))
}
private var backgroundColor: Color {
colorScheme == .dark ? Color.black : Color.white
}
private var textColor: Color {
colorScheme == .dark ? Color.green : Color(red: 0, green: 0.5, blue: 0)
}
private var secondaryTextColor: Color {
colorScheme == .dark ? Color.green.opacity(0.8) : Color(red: 0, green: 0.5, blue: 0).opacity(0.8)
}
var body: some View {
VStack(spacing: 0) {
header
Divider()
list
Divider()
input
}
.background(backgroundColor)
.foregroundColor(textColor)
.onDisappear { manager.cancel() }
.onChange(of: geohash) { newValue in
manager.setGeohash(newValue)
}
.onAppear { onNotesCountChanged?(manager.notes.count) }
.onChange(of: manager.notes.count) { newValue in
onNotesCountChanged?(newValue)
}
}
private var header: some View {
HStack {
VStack(alignment: .leading, spacing: 2) {
HStack(spacing: 4) {
let c = manager.notes.count
Text("\(c) \(c == 1 ? "note" : "notes") ")
.font(.system(size: 16, weight: .bold, design: .monospaced))
Text("@ #\(geohash)")
.font(.system(size: 16, weight: .bold, design: .monospaced))
}
if let buildingName = locationManager.locationNames[.building], !buildingName.isEmpty {
Text(buildingName)
.font(.system(size: 12, design: .monospaced))
.foregroundColor(secondaryTextColor)
} else if let blockName = locationManager.locationNames[.block], !blockName.isEmpty {
Text(blockName)
.font(.system(size: 12, design: .monospaced))
.foregroundColor(secondaryTextColor)
}
}
Spacer()
Button(action: { dismiss() }) {
Image(systemName: "xmark")
.font(.system(size: 13, weight: .semibold, design: .monospaced))
.foregroundColor(textColor)
.frame(width: 32, height: 32)
.contentShape(Rectangle())
}
.buttonStyle(.plain)
.accessibilityLabel("Close")
}
.frame(height: 44)
.padding(.horizontal, 12)
.background(backgroundColor.opacity(0.95))
}
private var list: some View {
ScrollView {
LazyVStack(alignment: .leading, spacing: 8) {
ForEach(manager.notes) { note in
VStack(alignment: .leading, spacing: 2) {
HStack(spacing: 6) {
Text(note.displayName)
.font(.system(size: 12, weight: .semibold, design: .monospaced))
.foregroundColor(secondaryTextColor)
Text(timestampText(for: note.createdAt))
.font(.system(size: 11, design: .monospaced))
.foregroundColor(secondaryTextColor.opacity(0.8))
}
Text(note.content)
.font(.system(size: 14, design: .monospaced))
.fixedSize(horizontal: false, vertical: true)
}
.padding(.horizontal, 12)
}
}
.padding(.vertical, 8)
}
.background(backgroundColor)
}
private var input: some View {
HStack(alignment: .center, spacing: 8) {
TextField("add a note for this place", text: $draft, axis: .vertical)
.textFieldStyle(.plain)
.font(.system(size: 14, design: .monospaced))
.lineLimit(3, reservesSpace: true)
.padding(.horizontal, 12)
Button(action: send) {
Image(systemName: "arrow.up.circle.fill")
.font(.system(size: 20))
.foregroundColor(draft.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty ? Color.gray : textColor)
}
.buttonStyle(.plain)
.disabled(draft.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty)
.padding(.trailing, 12)
}
.frame(minHeight: 44)
.padding(.vertical, 8)
.background(backgroundColor.opacity(0.95))
}
private func send() {
let content = draft.trimmingCharacters(in: .whitespacesAndNewlines)
guard !content.isEmpty else { return }
manager.send(content: content, nickname: viewModel.nickname)
draft = ""
}
// MARK: - Timestamp Formatting
private func timestampText(for date: Date) -> String {
let now = Date()
if let days = Calendar.current.dateComponents([.day], from: date, to: now).day, days < 7 {
// Relative (minute/hour/day), no seconds
let rel = Self.relativeFormatter.string(from: date, to: now) ?? ""
return rel.isEmpty ? "" : "\(rel) ago"
} else {
// Absolute date (MMM d or MMM d, yyyy if different year)
let sameYear = Calendar.current.isDate(date, equalTo: now, toGranularity: .year)
let fmt = sameYear ? Self.absDateFormatter : Self.absDateYearFormatter
return fmt.string(from: date)
}
}
private static let relativeFormatter: DateComponentsFormatter = {
let f = DateComponentsFormatter()
f.allowedUnits = [.day, .hour, .minute]
f.maximumUnitCount = 1
f.unitsStyle = .abbreviated
f.collapsesLargestUnit = true
return f
}()
private static let absDateFormatter: DateFormatter = {
let f = DateFormatter()
f.setLocalizedDateFormatFromTemplate("MMM d")
return f
}()
private static let absDateYearFormatter: DateFormatter = {
let f = DateFormatter()
f.setLocalizedDateFormatFromTemplate("MMM d, y")
return f
}()
}
+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) {
-54
View File
@@ -1,54 +0,0 @@
import XCTest
@testable import bitchat
final class CommandProcessorTests: XCTestCase {
var identityManager: MockIdentityManager!
override func setUp() {
super.setUp()
// Provide a minimal identity manager for commands that query identity/block lists
identityManager = MockIdentityManager(MockKeychain())
}
override func tearDown() {
identityManager = nil
super.tearDown()
}
@MainActor
func test_slap_notFoundGrammar() {
let processor = CommandProcessor(chatViewModel: nil, meshService: nil, identityManager: identityManager)
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, identityManager: identityManager)
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, identityManager: identityManager)
let result = processor.process("/slap")
switch result {
case .error(let message):
XCTAssertEqual(message, "usage: /slap <nickname>")
default:
XCTFail("Expected error result for usage message")
}
}
}
@@ -16,8 +16,6 @@ final class PrivateChatE2ETests: XCTestCase {
var bob: MockBluetoothMeshService! var bob: MockBluetoothMeshService!
var charlie: MockBluetoothMeshService! var charlie: MockBluetoothMeshService!
private var mockKeychain: MockKeychain!
override func setUp() { override func setUp() {
super.setUp() super.setUp()
MockBLEService.resetTestBus() MockBLEService.resetTestBus()
@@ -26,7 +24,6 @@ final class PrivateChatE2ETests: XCTestCase {
alice = createMockService(peerID: TestConstants.testPeerID1, nickname: TestConstants.testNickname1) alice = createMockService(peerID: TestConstants.testPeerID1, nickname: TestConstants.testNickname1)
bob = createMockService(peerID: TestConstants.testPeerID2, nickname: TestConstants.testNickname2) bob = createMockService(peerID: TestConstants.testPeerID2, nickname: TestConstants.testNickname2)
charlie = createMockService(peerID: TestConstants.testPeerID3, nickname: TestConstants.testNickname3) charlie = createMockService(peerID: TestConstants.testPeerID3, nickname: TestConstants.testNickname3)
mockKeychain = MockKeychain()
// Delivery tracking is now handled internally by BLEService // Delivery tracking is now handled internally by BLEService
} }
@@ -35,7 +32,6 @@ final class PrivateChatE2ETests: XCTestCase {
alice = nil alice = nil
bob = nil bob = nil
charlie = nil charlie = nil
mockKeychain = nil
super.tearDown() super.tearDown()
} }
@@ -120,8 +116,8 @@ final class PrivateChatE2ETests: XCTestCase {
let aliceKey = Curve25519.KeyAgreement.PrivateKey() let aliceKey = Curve25519.KeyAgreement.PrivateKey()
let bobKey = Curve25519.KeyAgreement.PrivateKey() let bobKey = Curve25519.KeyAgreement.PrivateKey()
let aliceManager = NoiseSessionManager(localStaticKey: aliceKey, keychain: mockKeychain) let aliceManager = NoiseSessionManager(localStaticKey: aliceKey)
let bobManager = NoiseSessionManager(localStaticKey: bobKey, keychain: mockKeychain) let bobManager = NoiseSessionManager(localStaticKey: bobKey)
// Establish encrypted session // Establish encrypted session
do { do {
@@ -10,21 +10,6 @@ import XCTest
@testable import bitchat @testable import bitchat
final class FragmentationTests: XCTestCase { final class FragmentationTests: XCTestCase {
private var mockKeychain: MockKeychain!
private var mockIdentityManager: MockIdentityManager!
override func setUp() {
super.setUp()
mockKeychain = MockKeychain()
mockIdentityManager = MockIdentityManager(mockKeychain)
}
override func tearDown() {
mockKeychain = nil
mockIdentityManager = nil
super.tearDown()
}
private final class CaptureDelegate: BitchatDelegate { private final class CaptureDelegate: BitchatDelegate {
var publicMessages: [(peerID: String, nickname: String, content: String)] = [] var publicMessages: [(peerID: String, nickname: String, content: String)] = []
@@ -90,7 +75,7 @@ final class FragmentationTests: XCTestCase {
} }
func test_reassembly_from_fragments_delivers_public_message() { func test_reassembly_from_fragments_delivers_public_message() {
let ble = BLEService(keychain: mockKeychain, identityManager: mockIdentityManager) let ble = BLEService()
let capture = CaptureDelegate() let capture = CaptureDelegate()
ble.delegate = capture ble.delegate = capture
@@ -121,7 +106,7 @@ final class FragmentationTests: XCTestCase {
} }
func test_duplicate_fragment_does_not_break_reassembly() { func test_duplicate_fragment_does_not_break_reassembly() {
let ble = BLEService(keychain: mockKeychain, identityManager: mockIdentityManager) let ble = BLEService()
let capture = CaptureDelegate() let capture = CaptureDelegate()
ble.delegate = capture ble.delegate = capture
@@ -147,7 +132,7 @@ final class FragmentationTests: XCTestCase {
} }
func test_invalid_fragment_header_is_ignored() { func test_invalid_fragment_header_is_ignored() {
let ble = BLEService(keychain: mockKeychain, identityManager: mockIdentityManager) let ble = BLEService()
let capture = CaptureDelegate() let capture = CaptureDelegate()
ble.delegate = capture ble.delegate = capture
@@ -14,7 +14,6 @@ final class IntegrationTests: XCTestCase {
var nodes: [String: MockBluetoothMeshService] = [:] var nodes: [String: MockBluetoothMeshService] = [:]
var noiseManagers: [String: NoiseSessionManager] = [:] var noiseManagers: [String: NoiseSessionManager] = [:]
private var mockKeychain: MockKeychain!
override func setUp() { override func setUp() {
super.setUp() super.setUp()
@@ -22,7 +21,6 @@ final class IntegrationTests: XCTestCase {
// broadcast propagation across a larger mesh. Integration-only. // broadcast propagation across a larger mesh. Integration-only.
MockBLEService.resetTestBus() MockBLEService.resetTestBus()
MockBLEService.autoFloodEnabled = true MockBLEService.autoFloodEnabled = true
mockKeychain = MockKeychain()
// Create a network of nodes // Create a network of nodes
createNode("Alice", peerID: TestConstants.testPeerID1) createNode("Alice", peerID: TestConstants.testPeerID1)
@@ -36,7 +34,6 @@ final class IntegrationTests: XCTestCase {
MockBLEService.autoFloodEnabled = false MockBLEService.autoFloodEnabled = false
nodes.removeAll() nodes.removeAll()
noiseManagers.removeAll() noiseManagers.removeAll()
mockKeychain = nil
super.tearDown() super.tearDown()
} }
@@ -310,7 +307,7 @@ final class IntegrationTests: XCTestCase {
// Simulate Bob restart by recreating his Noise manager // Simulate Bob restart by recreating his Noise manager
let bobKey = Curve25519.KeyAgreement.PrivateKey() let bobKey = Curve25519.KeyAgreement.PrivateKey()
noiseManagers["Bob"] = NoiseSessionManager(localStaticKey: bobKey, keychain: mockKeychain) noiseManagers["Bob"] = NoiseSessionManager(localStaticKey: bobKey)
// Re-establish Noise handshake explicitly via managers // Re-establish Noise handshake explicitly via managers
do { do {
@@ -596,7 +593,7 @@ final class IntegrationTests: XCTestCase {
// Create Noise manager // Create Noise manager
let key = Curve25519.KeyAgreement.PrivateKey() let key = Curve25519.KeyAgreement.PrivateKey()
noiseManagers[name] = NoiseSessionManager(localStaticKey: key, keychain: mockKeychain) noiseManagers[name] = NoiseSessionManager(localStaticKey: key)
} }
private func connect(_ node1: String, _ node2: String) { private func connect(_ node1: String, _ node2: String) {
+2 -4
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
@@ -36,8 +36,6 @@ final class MockBLEService: NSObject {
var myPeerID: String = "MOCK1234" var myPeerID: String = "MOCK1234"
var myNickname: String = "MockUser" var myNickname: String = "MockUser"
private let mockKeychain = MockKeychain()
// Test-specific properties // Test-specific properties
var sentMessages: [(message: BitchatMessage, packet: BitchatPacket)] = [] var sentMessages: [(message: BitchatMessage, packet: BitchatPacket)] = []
var sentPackets: [BitchatPacket] = [] var sentPackets: [BitchatPacket] = []
@@ -274,7 +272,7 @@ final class MockBLEService: NSObject {
} }
func getNoiseService() -> NoiseEncryptionService { func getNoiseService() -> NoiseEncryptionService {
return NoiseEncryptionService(keychain: mockKeychain) return NoiseEncryptionService()
} }
func getFingerprint(for peerID: String) -> String? { func getFingerprint(for peerID: String) -> String? {
@@ -1,80 +0,0 @@
//
// MockIdentityManager.swift
// bitchat
//
// This is free and unencumbered software released into the public domain.
// For more information, see <https://unlicense.org>
//
import Foundation
@testable import bitchat
final class MockIdentityManager: SecureIdentityStateManagerProtocol {
private let keychain: KeychainManagerProtocol
init(_ keychain: KeychainManagerProtocol) {
self.keychain = keychain
}
func loadIdentityCache() {}
func saveIdentityCache() {}
func forceSave() {}
func getSocialIdentity(for fingerprint: String) -> SocialIdentity? {
nil
}
func upsertCryptographicIdentity(fingerprint: String, noisePublicKey: Data, signingPublicKey: Data?, claimedNickname: String?) {}
func getCryptoIdentitiesByPeerIDPrefix(_ peerID: String) -> [CryptographicIdentity] {
[]
}
func updateSocialIdentity(_ identity: SocialIdentity) {}
func getFavorites() -> Set<String> {
Set()
}
func setFavorite(_ fingerprint: String, isFavorite: Bool) {}
func isFavorite(fingerprint: String) -> Bool {
false
}
func isBlocked(fingerprint: String) -> Bool {
false
}
func setBlocked(_ fingerprint: String, isBlocked: Bool) {}
func isNostrBlocked(pubkeyHexLowercased: String) -> Bool {
true
}
func setNostrBlocked(_ pubkeyHexLowercased: String, isBlocked: Bool) {}
func getBlockedNostrPubkeys() -> Set<String> {
Set()
}
func registerEphemeralSession(peerID: String, handshakeState: HandshakeState) {}
func updateHandshakeState(peerID: String, state: HandshakeState) {}
func clearAllIdentityData() {}
func removeEphemeralSession(peerID: String) {}
func setVerified(fingerprint: String, verified: Bool) {}
func isVerified(fingerprint: String) -> Bool {
true
}
func getVerifiedFingerprints() -> Set<String> {
Set()
}
}
-46
View File
@@ -1,46 +0,0 @@
//
// MockKeychain.swift
// bitchat
//
// This is free and unencumbered software released into the public domain.
// For more information, see <https://unlicense.org>
//
import Foundation
@testable import bitchat
final class MockKeychain: KeychainManagerProtocol {
private var storage: [String: Data] = [:]
func saveIdentityKey(_ keyData: Data, forKey key: String) -> Bool {
storage[key] = keyData
return true
}
func getIdentityKey(forKey key: String) -> Data? {
storage[key]
}
func deleteIdentityKey(forKey key: String) -> Bool {
storage.removeValue(forKey: key)
return true
}
func deleteAllKeychainData() -> Bool {
storage.removeAll()
return true
}
func secureClear(_ data: inout Data) {
//
data = Data()
}
func secureClear(_ string: inout String) {
string = ""
}
func verifyIdentityKeyExists() -> Bool {
storage["identity_noiseStaticKey"] != nil
}
}
+27 -36
View File
@@ -16,19 +16,16 @@ final class NoiseProtocolTests: XCTestCase {
var bobKey: Curve25519.KeyAgreement.PrivateKey! var bobKey: Curve25519.KeyAgreement.PrivateKey!
var aliceSession: NoiseSession! var aliceSession: NoiseSession!
var bobSession: NoiseSession! var bobSession: NoiseSession!
private var mockKeychain: MockKeychain!
override func setUp() { override func setUp() {
super.setUp() super.setUp()
aliceKey = Curve25519.KeyAgreement.PrivateKey() aliceKey = Curve25519.KeyAgreement.PrivateKey()
bobKey = Curve25519.KeyAgreement.PrivateKey() bobKey = Curve25519.KeyAgreement.PrivateKey()
mockKeychain = MockKeychain()
} }
override func tearDown() { override func tearDown() {
aliceSession = nil aliceSession = nil
bobSession = nil bobSession = nil
mockKeychain = nil
super.tearDown() super.tearDown()
} }
@@ -39,14 +36,12 @@ final class NoiseProtocolTests: XCTestCase {
aliceSession = NoiseSession( aliceSession = NoiseSession(
peerID: TestConstants.testPeerID2, peerID: TestConstants.testPeerID2,
role: .initiator, role: .initiator,
keychain: mockKeychain,
localStaticKey: aliceKey localStaticKey: aliceKey
) )
bobSession = NoiseSession( bobSession = NoiseSession(
peerID: TestConstants.testPeerID1, peerID: TestConstants.testPeerID1,
role: .responder, role: .responder,
keychain: mockKeychain,
localStaticKey: bobKey localStaticKey: bobKey
) )
@@ -85,7 +80,6 @@ final class NoiseProtocolTests: XCTestCase {
aliceSession = NoiseSession( aliceSession = NoiseSession(
peerID: TestConstants.testPeerID2, peerID: TestConstants.testPeerID2,
role: .initiator, role: .initiator,
keychain: mockKeychain,
localStaticKey: aliceKey localStaticKey: aliceKey
) )
@@ -150,7 +144,6 @@ final class NoiseProtocolTests: XCTestCase {
aliceSession = NoiseSession( aliceSession = NoiseSession(
peerID: TestConstants.testPeerID2, peerID: TestConstants.testPeerID2,
role: .initiator, role: .initiator,
keychain: mockKeychain,
localStaticKey: aliceKey localStaticKey: aliceKey
) )
@@ -164,7 +157,7 @@ final class NoiseProtocolTests: XCTestCase {
// MARK: - Session Manager Tests // MARK: - Session Manager Tests
func testSessionManagerBasicOperations() throws { func testSessionManagerBasicOperations() throws {
let manager = NoiseSessionManager(localStaticKey: aliceKey, keychain: mockKeychain) let manager = NoiseSessionManager(localStaticKey: aliceKey)
// Create session // Create session
let session = manager.createSession(for: TestConstants.testPeerID2, role: .initiator) let session = manager.createSession(for: TestConstants.testPeerID2, role: .initiator)
@@ -181,7 +174,7 @@ final class NoiseProtocolTests: XCTestCase {
} }
func testSessionManagerHandshakeInitiation() throws { func testSessionManagerHandshakeInitiation() throws {
let manager = NoiseSessionManager(localStaticKey: aliceKey, keychain: mockKeychain) let manager = NoiseSessionManager(localStaticKey: aliceKey)
// Initiate handshake // Initiate handshake
let handshakeData = try manager.initiateHandshake(with: TestConstants.testPeerID2) let handshakeData = try manager.initiateHandshake(with: TestConstants.testPeerID2)
@@ -194,8 +187,8 @@ final class NoiseProtocolTests: XCTestCase {
} }
func testSessionManagerIncomingHandshake() throws { func testSessionManagerIncomingHandshake() throws {
let aliceManager = NoiseSessionManager(localStaticKey: aliceKey, keychain: mockKeychain) let aliceManager = NoiseSessionManager(localStaticKey: aliceKey)
let bobManager = NoiseSessionManager(localStaticKey: bobKey, keychain: mockKeychain) let bobManager = NoiseSessionManager(localStaticKey: bobKey)
// Alice initiates // Alice initiates
let message1 = try aliceManager.initiateHandshake(with: TestConstants.testPeerID2) let message1 = try aliceManager.initiateHandshake(with: TestConstants.testPeerID2)
@@ -218,8 +211,8 @@ final class NoiseProtocolTests: XCTestCase {
} }
func testSessionManagerEncryptionDecryption() throws { func testSessionManagerEncryptionDecryption() throws {
let aliceManager = NoiseSessionManager(localStaticKey: aliceKey, keychain: mockKeychain) let aliceManager = NoiseSessionManager(localStaticKey: aliceKey)
let bobManager = NoiseSessionManager(localStaticKey: bobKey, keychain: mockKeychain) let bobManager = NoiseSessionManager(localStaticKey: bobKey)
// Establish sessions // Establish sessions
try establishManagerSessions(aliceManager: aliceManager, bobManager: bobManager) try establishManagerSessions(aliceManager: aliceManager, bobManager: bobManager)
@@ -263,11 +256,11 @@ final class NoiseProtocolTests: XCTestCase {
func testSessionIsolation() throws { func testSessionIsolation() throws {
// Create two separate session pairs // Create two separate session pairs
let aliceSession1 = NoiseSession(peerID: "peer1", role: .initiator, keychain: mockKeychain, localStaticKey: aliceKey) let aliceSession1 = NoiseSession(peerID: "peer1", role: .initiator, localStaticKey: aliceKey)
let bobSession1 = NoiseSession(peerID: "alice1", role: .responder, keychain: mockKeychain, localStaticKey: bobKey) let bobSession1 = NoiseSession(peerID: "alice1", role: .responder, localStaticKey: bobKey)
let aliceSession2 = NoiseSession(peerID: "peer2", role: .initiator, keychain: mockKeychain, localStaticKey: aliceKey) let aliceSession2 = NoiseSession(peerID: "peer2", role: .initiator, localStaticKey: aliceKey)
let bobSession2 = NoiseSession(peerID: "alice2", role: .responder, keychain: mockKeychain, localStaticKey: bobKey) let bobSession2 = NoiseSession(peerID: "alice2", role: .responder, localStaticKey: bobKey)
// Establish both pairs // Establish both pairs
try performHandshake(initiator: aliceSession1, responder: bobSession1) try performHandshake(initiator: aliceSession1, responder: bobSession1)
@@ -289,8 +282,8 @@ final class NoiseProtocolTests: XCTestCase {
func testPeerRestartDetection() throws { func testPeerRestartDetection() throws {
// Establish initial sessions // Establish initial sessions
let aliceManager = NoiseSessionManager(localStaticKey: aliceKey, keychain: mockKeychain) let aliceManager = NoiseSessionManager(localStaticKey: aliceKey)
let bobManager = NoiseSessionManager(localStaticKey: bobKey, keychain: mockKeychain) let bobManager = NoiseSessionManager(localStaticKey: bobKey)
try establishManagerSessions(aliceManager: aliceManager, bobManager: bobManager) try establishManagerSessions(aliceManager: aliceManager, bobManager: bobManager)
@@ -302,7 +295,7 @@ final class NoiseProtocolTests: XCTestCase {
_ = try aliceManager.decrypt(message2, from: TestConstants.testPeerID2) _ = try aliceManager.decrypt(message2, from: TestConstants.testPeerID2)
// Simulate Bob restart by creating new manager with same key // Simulate Bob restart by creating new manager with same key
let bobManagerRestarted = NoiseSessionManager(localStaticKey: bobKey, keychain: mockKeychain) let bobManagerRestarted = NoiseSessionManager(localStaticKey: bobKey)
// Bob initiates new handshake after restart // Bob initiates new handshake after restart
let newHandshake1 = try bobManagerRestarted.initiateHandshake(with: TestConstants.testPeerID1) let newHandshake1 = try bobManagerRestarted.initiateHandshake(with: TestConstants.testPeerID1)
@@ -325,8 +318,8 @@ final class NoiseProtocolTests: XCTestCase {
func testNonceDesynchronizationRecovery() throws { func testNonceDesynchronizationRecovery() throws {
// Create two sessions // Create two sessions
aliceSession = NoiseSession(peerID: TestConstants.testPeerID2, role: .initiator, keychain: mockKeychain, localStaticKey: aliceKey) aliceSession = NoiseSession(peerID: TestConstants.testPeerID2, role: .initiator, localStaticKey: aliceKey)
bobSession = NoiseSession(peerID: TestConstants.testPeerID1, role: .responder, keychain: mockKeychain, localStaticKey: bobKey) bobSession = NoiseSession(peerID: TestConstants.testPeerID1, role: .responder, localStaticKey: bobKey)
// Establish sessions // Establish sessions
try performHandshake(initiator: aliceSession, responder: bobSession) try performHandshake(initiator: aliceSession, responder: bobSession)
@@ -349,8 +342,8 @@ final class NoiseProtocolTests: XCTestCase {
func testConcurrentEncryption() throws { func testConcurrentEncryption() throws {
// Test thread safety of encryption operations // Test thread safety of encryption operations
let aliceManager = NoiseSessionManager(localStaticKey: aliceKey, keychain: mockKeychain) let aliceManager = NoiseSessionManager(localStaticKey: aliceKey)
let bobManager = NoiseSessionManager(localStaticKey: bobKey, keychain: mockKeychain) let bobManager = NoiseSessionManager(localStaticKey: bobKey)
try establishManagerSessions(aliceManager: aliceManager, bobManager: bobManager) try establishManagerSessions(aliceManager: aliceManager, bobManager: bobManager)
@@ -387,8 +380,8 @@ final class NoiseProtocolTests: XCTestCase {
func testSessionStaleDetection() throws { func testSessionStaleDetection() throws {
// Test that sessions are properly marked as stale // Test that sessions are properly marked as stale
let aliceManager = NoiseSessionManager(localStaticKey: aliceKey, keychain: mockKeychain) let aliceManager = NoiseSessionManager(localStaticKey: aliceKey)
let bobManager = NoiseSessionManager(localStaticKey: bobKey, keychain: mockKeychain) let bobManager = NoiseSessionManager(localStaticKey: bobKey)
try establishManagerSessions(aliceManager: aliceManager, bobManager: bobManager) try establishManagerSessions(aliceManager: aliceManager, bobManager: bobManager)
@@ -401,8 +394,8 @@ final class NoiseProtocolTests: XCTestCase {
func testHandshakeAfterDecryptionFailure() throws { func testHandshakeAfterDecryptionFailure() throws {
// Test that handshake is properly initiated after decryption failure // Test that handshake is properly initiated after decryption failure
let aliceManager = NoiseSessionManager(localStaticKey: aliceKey, keychain: mockKeychain) let aliceManager = NoiseSessionManager(localStaticKey: aliceKey)
let bobManager = NoiseSessionManager(localStaticKey: bobKey, keychain: mockKeychain) let bobManager = NoiseSessionManager(localStaticKey: bobKey)
// Establish sessions // Establish sessions
try establishManagerSessions(aliceManager: aliceManager, bobManager: bobManager) try establishManagerSessions(aliceManager: aliceManager, bobManager: bobManager)
@@ -420,8 +413,8 @@ final class NoiseProtocolTests: XCTestCase {
func testHandshakeAlwaysAcceptedWithExistingSession() throws { func testHandshakeAlwaysAcceptedWithExistingSession() throws {
// Test that handshake is always accepted even with existing valid session // Test that handshake is always accepted even with existing valid session
let aliceManager = NoiseSessionManager(localStaticKey: aliceKey, keychain: mockKeychain) let aliceManager = NoiseSessionManager(localStaticKey: aliceKey)
let bobManager = NoiseSessionManager(localStaticKey: bobKey, keychain: mockKeychain) let bobManager = NoiseSessionManager(localStaticKey: bobKey)
// Establish sessions // Establish sessions
try establishManagerSessions(aliceManager: aliceManager, bobManager: bobManager) try establishManagerSessions(aliceManager: aliceManager, bobManager: bobManager)
@@ -460,8 +453,8 @@ final class NoiseProtocolTests: XCTestCase {
func testNonceDesynchronizationCausesRehandshake() throws { func testNonceDesynchronizationCausesRehandshake() throws {
// Test that nonce desynchronization leads to proper re-handshake // Test that nonce desynchronization leads to proper re-handshake
let aliceManager = NoiseSessionManager(localStaticKey: aliceKey, keychain: mockKeychain) let aliceManager = NoiseSessionManager(localStaticKey: aliceKey)
let bobManager = NoiseSessionManager(localStaticKey: bobKey, keychain: mockKeychain) let bobManager = NoiseSessionManager(localStaticKey: bobKey)
// Establish sessions // Establish sessions
try establishManagerSessions(aliceManager: aliceManager, bobManager: bobManager) try establishManagerSessions(aliceManager: aliceManager, bobManager: bobManager)
@@ -506,8 +499,8 @@ final class NoiseProtocolTests: XCTestCase {
func testHandshakePerformance() throws { func testHandshakePerformance() throws {
measure { measure {
do { do {
let alice = NoiseSession(peerID: "bob", role: .initiator, keychain: mockKeychain, localStaticKey: aliceKey) let alice = NoiseSession(peerID: "bob", role: .initiator, localStaticKey: aliceKey)
let bob = NoiseSession(peerID: "alice", role: .responder, keychain: mockKeychain, localStaticKey: bobKey) let bob = NoiseSession(peerID: "alice", role: .responder, localStaticKey: bobKey)
try performHandshake(initiator: alice, responder: bob) try performHandshake(initiator: alice, responder: bob)
} catch { } catch {
XCTFail("Handshake failed: \(error)") XCTFail("Handshake failed: \(error)")
@@ -537,14 +530,12 @@ final class NoiseProtocolTests: XCTestCase {
aliceSession = NoiseSession( aliceSession = NoiseSession(
peerID: TestConstants.testPeerID2, peerID: TestConstants.testPeerID2,
role: .initiator, role: .initiator,
keychain: mockKeychain,
localStaticKey: aliceKey localStaticKey: aliceKey
) )
bobSession = NoiseSession( bobSession = NoiseSession(
peerID: TestConstants.testPeerID1, peerID: TestConstants.testPeerID1,
role: .responder, role: .responder,
keychain: mockKeychain,
localStaticKey: bobKey localStaticKey: bobKey
) )
+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: