Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3afacca7f6 | ||
|
|
ddadccc554 | ||
|
|
74589539c3 | ||
|
|
860c06c1b1 | ||
|
|
789bbf6862 | ||
|
|
10be302854 | ||
|
|
4894a0b04e | ||
|
|
36b5fdabc4 | ||
|
|
5020190164 | ||
|
|
b0f6568ff5 | ||
|
|
f7b78452d0 | ||
|
|
ca10724e64 | ||
|
|
1a353b216d | ||
|
|
96992c0a59 | ||
|
|
08d1fa7653 | ||
|
|
8dad90685d | ||
|
|
7f829dcbaa | ||
|
|
37d655d065 |
@@ -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 }}
|
|
||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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).
|
||||||
|
|
||||||
@@ -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"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
|
After Width: | Height: | Size: 378 B |
|
After Width: | Height: | Size: 497 B |
|
After Width: | Height: | Size: 570 B |
|
After Width: | Height: | Size: 401 B |
|
After Width: | Height: | Size: 564 B |
|
After Width: | Height: | Size: 668 B |
|
After Width: | Height: | Size: 497 B |
|
After Width: | Height: | Size: 641 B |
|
After Width: | Height: | Size: 765 B |
|
After Width: | Height: | Size: 765 B |
|
After Width: | Height: | Size: 1.0 KiB |
|
After Width: | Height: | Size: 628 B |
|
After Width: | Height: | Size: 930 B |
|
After Width: | Height: | Size: 976 B |
@@ -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?
|
||||||
|
|
||||||
|
|||||||
@@ -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,18 +191,36 @@ 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()
|
||||||
|
if pendingSave {
|
||||||
performSave()
|
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 {
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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,16 +352,16 @@ 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()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 []
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 {
|
||||||
@@ -124,28 +126,6 @@ struct NostrProtocol {
|
|||||||
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
|
||||||
|
|
||||||
private static func createSeal(
|
private static func createSeal(
|
||||||
|
|||||||
@@ -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>()
|
||||||
@@ -46,14 +46,6 @@ final class NostrRelayManager: ObservableObject {
|
|||||||
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.
|
||||||
private struct PendingSend {
|
private struct PendingSend {
|
||||||
@@ -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,13 +382,27 @@ final class NostrRelayManager: ObservableObject {
|
|||||||
|
|
||||||
switch result {
|
switch result {
|
||||||
case .success(let message):
|
case .success(let message):
|
||||||
|
switch message {
|
||||||
|
case .string(let text):
|
||||||
// Parse off-main to reduce UI jank, then hop back for state updates
|
// Parse off-main to reduce UI jank, then hop back for state updates
|
||||||
Task.detached(priority: .utility) {
|
Task.detached(priority: .utility) {
|
||||||
guard let parsed = ParsedInbound(message) else { return }
|
guard let parsed = parseInboundMessage(text) else { return }
|
||||||
await MainActor.run {
|
await MainActor.run {
|
||||||
NostrRelayManager.shared.handleParsedMessage(parsed, from: relayUrl)
|
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
|
||||||
Task { @MainActor in
|
Task { @MainActor in
|
||||||
@@ -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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Off-main JSON parse to avoid UI jank; pure function, not actor-isolated
|
||||||
|
private func parseInboundMessage(_ message: String) -> ParsedInbound? {
|
||||||
|
guard let data = message.data(using: .utf8) else { return nil }
|
||||||
|
do {
|
||||||
|
if let array = try JSONSerialization.jsonObject(with: data) as? [Any],
|
||||||
|
array.count >= 2,
|
||||||
|
let type = array[0] as? String {
|
||||||
switch type {
|
switch type {
|
||||||
case "EVENT":
|
case "EVENT":
|
||||||
if array.count >= 3,
|
if array.count >= 3,
|
||||||
let subId = array[1] as? String,
|
let subId = array[1] as? String,
|
||||||
let eventDict = array[2] as? [String: Any],
|
let eventDict = array[2] as? [String: Any] {
|
||||||
let event = try? NostrEvent(from: eventDict) {
|
let event = try NostrEvent(from: eventDict)
|
||||||
self = .event(subId: subId, event: event)
|
return .event(subId: subId, event: event)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
return nil
|
|
||||||
case "EOSE":
|
case "EOSE":
|
||||||
if let subId = array[1] as? String {
|
if let subId = array[1] as? String { return .eose(subscriptionId: subId) }
|
||||||
self = .eose(subscriptionId: subId)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
case "OK":
|
case "OK":
|
||||||
if array.count >= 3,
|
if array.count >= 3,
|
||||||
let eventId = array[1] as? String,
|
let eventId = array[1] as? String,
|
||||||
let success = array[2] as? Bool {
|
let success = array[2] as? Bool {
|
||||||
let reason = array.count >= 4 ? (array[3] as? String ?? "no reason given") : "no reason given"
|
let reason = array.count >= 4 ? (array[3] as? String ?? "no reason given") : "no reason given"
|
||||||
self = .ok(eventId: eventId, success: success, reason: reason)
|
return .ok(eventId: eventId, success: success, reason: reason)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
return nil
|
|
||||||
case "NOTICE":
|
case "NOTICE":
|
||||||
if array.count >= 2, let msg = array[1] as? String {
|
if array.count >= 2, let msg = array[1] as? String { return .notice(msg) }
|
||||||
self = .notice(msg)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
default:
|
default:
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} catch {
|
||||||
|
// Ignore
|
||||||
}
|
}
|
||||||
|
return nil
|
||||||
private extension URLSessionWebSocketTask.Message {
|
|
||||||
var data: Data? {
|
|
||||||
switch self {
|
|
||||||
case .string(let text): text.data(using: .utf8)
|
|
||||||
case .data(let data): data
|
|
||||||
@unknown default: 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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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: [])
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,16 +21,11 @@ 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 */ }
|
||||||
func emergencyDisconnectAll() { /* no-op */ }
|
func emergencyDisconnectAll() { /* 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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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() {}
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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%
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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")
|
|
||||||
}
|
|
||||||
@@ -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)
|
|
||||||
}
|
|
||||||
|
|
||||||
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) {
|
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 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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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 }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -11,21 +11,6 @@ import XCTest
|
|||||||
|
|
||||||
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)] = []
|
||||||
func didReceiveMessage(_ message: BitchatMessage) {}
|
func didReceiveMessage(_ message: BitchatMessage) {}
|
||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||