mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-27 10:45:20 +00:00
Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
84d1fe9881 | ||
|
|
0b76d76b3d | ||
|
|
2db48a34c0 |
@@ -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,22 @@
|
|||||||
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 */; };
|
0481A3902E734CAE00FC845E /* CommandProcessorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0481A38F2E734CAE00FC845E /* CommandProcessorTests.swift */; };
|
||||||
0481A3912E734CAE00FC845E /* CommandProcessorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0481A38F2E734CAE00FC845E /* CommandProcessorTests.swift */; };
|
0481A3912E734CAE00FC845E /* CommandProcessorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0481A38F2E734CAE00FC845E /* CommandProcessorTests.swift */; };
|
||||||
0481A3A92E74D28800FC845E /* LocationNotesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0481A3A82E74D28800FC845E /* LocationNotesView.swift */; };
|
0481A3932E73730100FC845E /* RequestSyncPacket.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0481A3922E73730100FC845E /* RequestSyncPacket.swift */; };
|
||||||
0481A3AA2E74D28800FC845E /* LocationNotesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0481A3A82E74D28800FC845E /* LocationNotesView.swift */; };
|
0481A3942E73730100FC845E /* RequestSyncPacket.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0481A3922E73730100FC845E /* RequestSyncPacket.swift */; };
|
||||||
0481A3AC2E74D29400FC845E /* LocationNotesManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0481A3AB2E74D29400FC845E /* LocationNotesManager.swift */; };
|
0481A3992E73730F00FC845E /* GossipSyncManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0481A3952E73730F00FC845E /* GossipSyncManager.swift */; };
|
||||||
0481A3AD2E74D29400FC845E /* LocationNotesManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0481A3AB2E74D29400FC845E /* LocationNotesManager.swift */; };
|
0481A39A2E73730F00FC845E /* PacketIdUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0481A3962E73730F00FC845E /* PacketIdUtil.swift */; };
|
||||||
0481A3AF2E74E06300FC845E /* LocationNotesCounter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0481A3AE2E74E06300FC845E /* LocationNotesCounter.swift */; };
|
0481A39B2E73730F00FC845E /* SeenPacketsBloomFilter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0481A3972E73730F00FC845E /* SeenPacketsBloomFilter.swift */; };
|
||||||
0481A3B02E74E06300FC845E /* LocationNotesCounter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0481A3AE2E74E06300FC845E /* LocationNotesCounter.swift */; };
|
0481A39C2E73730F00FC845E /* GossipSyncManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0481A3952E73730F00FC845E /* GossipSyncManager.swift */; };
|
||||||
|
0481A39D2E73730F00FC845E /* PacketIdUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0481A3962E73730F00FC845E /* PacketIdUtil.swift */; };
|
||||||
|
0481A39E2E73730F00FC845E /* SeenPacketsBloomFilter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0481A3972E73730F00FC845E /* SeenPacketsBloomFilter.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 */; };
|
||||||
@@ -132,10 +136,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 */; };
|
||||||
@@ -236,13 +236,15 @@
|
|||||||
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>"; };
|
||||||
|
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>"; };
|
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>"; };
|
0481A3922E73730100FC845E /* RequestSyncPacket.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RequestSyncPacket.swift; sourceTree = "<group>"; };
|
||||||
0481A3AB2E74D29400FC845E /* LocationNotesManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationNotesManager.swift; sourceTree = "<group>"; };
|
0481A3952E73730F00FC845E /* GossipSyncManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GossipSyncManager.swift; sourceTree = "<group>"; };
|
||||||
0481A3AE2E74E06300FC845E /* LocationNotesCounter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationNotesCounter.swift; sourceTree = "<group>"; };
|
0481A3962E73730F00FC845E /* PacketIdUtil.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PacketIdUtil.swift; sourceTree = "<group>"; };
|
||||||
|
0481A3972E73730F00FC845E /* SeenPacketsBloomFilter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SeenPacketsBloomFilter.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>"; };
|
||||||
@@ -295,8 +297,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>"; };
|
||||||
@@ -378,14 +378,26 @@
|
|||||||
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;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
0481A3982E73730F00FC845E /* Sync */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
0481A3952E73730F00FC845E /* GossipSyncManager.swift */,
|
||||||
|
0481A3962E73730F00FC845E /* PacketIdUtil.swift */,
|
||||||
|
0481A3972E73730F00FC845E /* SeenPacketsBloomFilter.swift */,
|
||||||
|
);
|
||||||
|
path = Sync;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
0575DCBD15C7C719ADDCB67E /* Models */ = {
|
0575DCBD15C7C719ADDCB67E /* Models */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
0481A3922E73730100FC845E /* RequestSyncPacket.swift */,
|
||||||
11186E29A064E8D210880E1B /* BitchatPeer.swift */,
|
11186E29A064E8D210880E1B /* BitchatPeer.swift */,
|
||||||
);
|
);
|
||||||
path = Models;
|
path = Models;
|
||||||
@@ -425,6 +437,7 @@
|
|||||||
0575DCBD15C7C719ADDCB67E /* Models */,
|
0575DCBD15C7C719ADDCB67E /* Models */,
|
||||||
637EDFDD042BDB5F2569A501 /* Noise */,
|
637EDFDD042BDB5F2569A501 /* Noise */,
|
||||||
E78C7F4B6769C0A72F5DE544 /* Nostr */,
|
E78C7F4B6769C0A72F5DE544 /* Nostr */,
|
||||||
|
0481A3982E73730F00FC845E /* Sync */,
|
||||||
ADD53BCDA233C02E53458926 /* Protocols */,
|
ADD53BCDA233C02E53458926 /* Protocols */,
|
||||||
D98A3186D7E4C72E35BDF7FE /* Services */,
|
D98A3186D7E4C72E35BDF7FE /* Services */,
|
||||||
9A78348821A7D3374607D4E3 /* Utils */,
|
9A78348821A7D3374607D4E3 /* Utils */,
|
||||||
@@ -475,8 +488,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>";
|
||||||
@@ -520,7 +531,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 */,
|
||||||
@@ -598,8 +608,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 +801,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 +813,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;
|
||||||
@@ -840,10 +850,12 @@
|
|||||||
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 */,
|
||||||
|
0481A3992E73730F00FC845E /* GossipSyncManager.swift in Sources */,
|
||||||
|
0481A39A2E73730F00FC845E /* PacketIdUtil.swift in Sources */,
|
||||||
|
0481A39B2E73730F00FC845E /* SeenPacketsBloomFilter.swift in Sources */,
|
||||||
92D34E7A07C990C8A815B0CE /* ContentView.swift in Sources */,
|
92D34E7A07C990C8A815B0CE /* ContentView.swift in Sources */,
|
||||||
5C93B4FDD0C448C3EDDBF8AE /* FavoritesPersistenceService.swift in Sources */,
|
5C93B4FDD0C448C3EDDBF8AE /* FavoritesPersistenceService.swift in Sources */,
|
||||||
047502B42E55FED60083520F /* MeshPeerList.swift in Sources */,
|
047502B42E55FED60083520F /* MeshPeerList.swift in Sources */,
|
||||||
@@ -869,10 +881,9 @@
|
|||||||
6A85FC357ACD85DBD9020845 /* NostrRelayManager.swift in Sources */,
|
6A85FC357ACD85DBD9020845 /* NostrRelayManager.swift in Sources */,
|
||||||
749D8CF8A362B6CD0786782D /* NotificationService.swift in Sources */,
|
749D8CF8A362B6CD0786782D /* NotificationService.swift in Sources */,
|
||||||
049BD3AF2E51ED60001A566B /* Transport.swift in Sources */,
|
049BD3AF2E51ED60001A566B /* Transport.swift in Sources */,
|
||||||
|
0481A3942E73730100FC845E /* RequestSyncPacket.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 */,
|
||||||
@@ -908,10 +919,12 @@
|
|||||||
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 */,
|
||||||
|
0481A39C2E73730F00FC845E /* GossipSyncManager.swift in Sources */,
|
||||||
|
0481A39D2E73730F00FC845E /* PacketIdUtil.swift in Sources */,
|
||||||
|
0481A39E2E73730F00FC845E /* SeenPacketsBloomFilter.swift in Sources */,
|
||||||
1D9674FA5F998503831DC281 /* ContentView.swift in Sources */,
|
1D9674FA5F998503831DC281 /* ContentView.swift in Sources */,
|
||||||
ACE2ED172C37F01561E50B71 /* FavoritesPersistenceService.swift in Sources */,
|
ACE2ED172C37F01561E50B71 /* FavoritesPersistenceService.swift in Sources */,
|
||||||
047502B62E55FED60083520F /* MeshPeerList.swift in Sources */,
|
047502B62E55FED60083520F /* MeshPeerList.swift in Sources */,
|
||||||
@@ -937,10 +950,9 @@
|
|||||||
BCD0EBACD82AF5E55C2CB2B9 /* NostrRelayManager.swift in Sources */,
|
BCD0EBACD82AF5E55C2CB2B9 /* NostrRelayManager.swift in Sources */,
|
||||||
61C81ED5F679D5E973EE0C07 /* NotificationService.swift in Sources */,
|
61C81ED5F679D5E973EE0C07 /* NotificationService.swift in Sources */,
|
||||||
049BD3AE2E51ED60001A566B /* Transport.swift in Sources */,
|
049BD3AE2E51ED60001A566B /* Transport.swift in Sources */,
|
||||||
|
0481A3932E73730100FC845E /* RequestSyncPacket.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 */,
|
||||||
@@ -969,8 +981,6 @@
|
|||||||
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 */,
|
||||||
@@ -995,8 +1005,6 @@
|
|||||||
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 +1120,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 +1151,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 +1206,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 +1238,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 +1327,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 +1420,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";
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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 {
|
final 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,9 +122,7 @@ 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
|
||||||
|
|
||||||
@@ -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
|
||||||
@@ -205,7 +164,12 @@ final class SecureIdentityStateManager: SecureIdentityStateManagerProtocol {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
||||||
|
|
||||||
@@ -237,8 +201,26 @@ final class SecureIdentityStateManager: SecureIdentityStateManagerProtocol {
|
|||||||
// 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
|
||||||
@@ -476,6 +469,53 @@ 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() {
|
||||||
@@ -485,6 +525,7 @@ final class SecureIdentityStateManager: SecureIdentityStateManagerProtocol {
|
|||||||
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)
|
||||||
@@ -495,6 +536,7 @@ final class SecureIdentityStateManager: SecureIdentityStateManagerProtocol {
|
|||||||
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
// REQUEST_SYNC payload TLV (type, length16, value)
|
||||||
|
// - 0x01: mBytes (uint16)
|
||||||
|
// - 0x02: k (uint8)
|
||||||
|
// - 0x03: bloom filter bits (opaque byte array of length mBytes)
|
||||||
|
struct RequestSyncPacket {
|
||||||
|
let mBytes: Int
|
||||||
|
let k: Int
|
||||||
|
let bits: Data
|
||||||
|
|
||||||
|
func encode() -> Data {
|
||||||
|
var out = Data()
|
||||||
|
func putTLV(_ t: UInt8, _ v: Data) {
|
||||||
|
out.append(t)
|
||||||
|
let len = UInt16(v.count)
|
||||||
|
out.append(UInt8((len >> 8) & 0xFF))
|
||||||
|
out.append(UInt8(len & 0xFF))
|
||||||
|
out.append(v)
|
||||||
|
}
|
||||||
|
// mBytes
|
||||||
|
let mbBE = UInt16(mBytes).bigEndian
|
||||||
|
let mbData = withUnsafeBytes(of: mbBE) { Data($0) }
|
||||||
|
putTLV(0x01, mbData)
|
||||||
|
// k
|
||||||
|
putTLV(0x02, Data([UInt8(k & 0xFF)]))
|
||||||
|
// bits
|
||||||
|
putTLV(0x03, bits)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
static func decode(from data: Data) -> RequestSyncPacket? {
|
||||||
|
var off = 0
|
||||||
|
var mBytes: Int? = nil
|
||||||
|
var k: Int? = nil
|
||||||
|
var bits: Data? = nil
|
||||||
|
|
||||||
|
while off + 3 <= data.count {
|
||||||
|
let t = Int(data[off]); off += 1
|
||||||
|
guard off + 2 <= data.count else { return nil }
|
||||||
|
let len = (Int(data[off]) << 8) | Int(data[off+1]); off += 2
|
||||||
|
guard off + len <= data.count else { return nil }
|
||||||
|
let v = data.subdata(in: off..<(off+len)); off += len
|
||||||
|
switch t {
|
||||||
|
case 0x01:
|
||||||
|
if v.count == 2 {
|
||||||
|
let mb = (Int(v[0]) << 8) | Int(v[1])
|
||||||
|
mBytes = mb
|
||||||
|
}
|
||||||
|
case 0x02:
|
||||||
|
if v.count == 1 { k = Int(v[0]) }
|
||||||
|
case 0x03:
|
||||||
|
bits = v
|
||||||
|
default:
|
||||||
|
break // forward compatible; ignore unknown TLVs
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let mb = mBytes, let kk = k, let bb = bits, mb == bb.count else { return nil }
|
||||||
|
return RequestSyncPacket(mBytes: mb, k: kk, bits: bb)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -490,7 +490,6 @@ final class NoiseSymmetricState {
|
|||||||
final class NoiseHandshakeState {
|
final 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 +505,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 +578,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 +626,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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -722,7 +714,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 +724,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 +737,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 +747,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:
|
||||||
|
|||||||
@@ -36,7 +36,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 +52,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 +71,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
|
||||||
)
|
)
|
||||||
@@ -107,7 +98,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
|
||||||
)
|
)
|
||||||
@@ -240,13 +230,13 @@ 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
|
||||||
|
|
||||||
@@ -262,16 +252,14 @@ class NoiseSession {
|
|||||||
final class NoiseSessionManager {
|
final 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 +269,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
|
||||||
@@ -333,7 +320,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
|
||||||
@@ -384,7 +370,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
|
||||||
|
|||||||
@@ -124,28 +124,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(
|
||||||
|
|||||||
@@ -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 {
|
||||||
@@ -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()
|
||||||
@@ -259,22 +250,6 @@ final class NostrRelayManager: ObservableObject {
|
|||||||
map[id] = messageString
|
map[id] = messageString
|
||||||
self.pendingSubscriptions[url] = map
|
self.pendingSubscriptions[url] = map
|
||||||
}
|
}
|
||||||
// Initialize EOSE tracking if requested
|
|
||||||
if let onEOSE = onEOSE {
|
|
||||||
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)
|
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
|
// Ensure we actually have sockets opening to these relays so queued REQs can flush
|
||||||
ensureConnections(to: urls)
|
ensureConnections(to: urls)
|
||||||
@@ -443,17 +418,9 @@ final class NostrRelayManager: ObservableObject {
|
|||||||
} else {
|
} else {
|
||||||
SecureLogger.warning("⚠️ No handler for subscription \(subId)", category: .session)
|
SecureLogger.warning("⚠️ No handler for subscription \(subId)", category: .session)
|
||||||
}
|
}
|
||||||
case .eose(let subId):
|
case .eose:
|
||||||
if var tracker = eoseTrackers[subId] {
|
// No-op for now
|
||||||
tracker.pendingRelays.remove(relayUrl)
|
break
|
||||||
if tracker.pendingRelays.isEmpty {
|
|
||||||
tracker.timer?.invalidate()
|
|
||||||
eoseTrackers.removeValue(forKey: subId)
|
|
||||||
tracker.callback()
|
|
||||||
} else {
|
|
||||||
eoseTrackers[subId] = tracker
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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)
|
||||||
@@ -788,16 +755,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
|
||||||
|
|||||||
@@ -125,6 +125,7 @@ enum MessageType: UInt8 {
|
|||||||
case announce = 0x01 // "I'm here" with nickname
|
case announce = 0x01 // "I'm here" with nickname
|
||||||
case message = 0x02 // Public chat message
|
case message = 0x02 // Public chat message
|
||||||
case leave = 0x03 // "I'm leaving"
|
case leave = 0x03 // "I'm leaving"
|
||||||
|
case requestSync = 0x21 // Bloom filter-based sync request (local-only)
|
||||||
|
|
||||||
// Noise encryption
|
// Noise encryption
|
||||||
case noiseHandshake = 0x10 // Handshake (init or response determined by payload)
|
case noiseHandshake = 0x10 // Handshake (init or response determined by payload)
|
||||||
@@ -138,6 +139,7 @@ enum MessageType: UInt8 {
|
|||||||
case .announce: return "announce"
|
case .announce: return "announce"
|
||||||
case .message: return "message"
|
case .message: return "message"
|
||||||
case .leave: return "leave"
|
case .leave: return "leave"
|
||||||
|
case .requestSync: return "requestSync"
|
||||||
case .noiseHandshake: return "noiseHandshake"
|
case .noiseHandshake: return "noiseHandshake"
|
||||||
case .noiseEncrypted: return "noiseEncrypted"
|
case .noiseEncrypted: return "noiseEncrypted"
|
||||||
case .fragment: return "fragment"
|
case .fragment: return "fragment"
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -139,6 +138,9 @@ final class BLEService: NSObject {
|
|||||||
// Debounce for 'reconnected' logs
|
// Debounce for 'reconnected' logs
|
||||||
private var lastReconnectLogAt: [String: Date] = [:]
|
private var lastReconnectLogAt: [String: Date] = [:]
|
||||||
|
|
||||||
|
// MARK: - Gossip Sync
|
||||||
|
private var gossipSyncManager: GossipSyncManager?
|
||||||
|
|
||||||
// MARK: - Maintenance Timer
|
// MARK: - Maintenance Timer
|
||||||
|
|
||||||
private var maintenanceTimer: DispatchSourceTimer? // Single timer for all maintenance tasks
|
private var maintenanceTimer: DispatchSourceTimer? // Single timer for all maintenance tasks
|
||||||
@@ -326,9 +328,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)
|
||||||
@@ -401,6 +401,12 @@ final class BLEService: NSObject {
|
|||||||
|
|
||||||
// Publish initial empty state
|
// Publish initial empty state
|
||||||
requestPeerDataPublish()
|
requestPeerDataPublish()
|
||||||
|
|
||||||
|
// Initialize gossip sync manager
|
||||||
|
let sync = GossipSyncManager(myPeerID: myPeerID)
|
||||||
|
sync.delegate = self
|
||||||
|
sync.start()
|
||||||
|
self.gossipSyncManager = sync
|
||||||
}
|
}
|
||||||
|
|
||||||
func setNickname(_ nickname: String) {
|
func setNickname(_ nickname: String) {
|
||||||
@@ -775,6 +781,8 @@ final class BLEService: NSObject {
|
|||||||
self.messageDeduplicator.markProcessed(dedupID)
|
self.messageDeduplicator.markProcessed(dedupID)
|
||||||
// Call synchronously since we're already on background queue
|
// Call synchronously since we're already on background queue
|
||||||
self.broadcastPacket(signedPacket)
|
self.broadcastPacket(signedPacket)
|
||||||
|
// Track our own broadcast for sync
|
||||||
|
self.gossipSyncManager?.onPublicPacketSeen(signedPacket)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1099,10 +1107,13 @@ final class BLEService: NSObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// For broadcast (no directed peer) and non-fragment, choose a subset deterministically
|
// For broadcast (no directed peer) and non-fragment, choose a subset deterministically
|
||||||
// Special-case announces: do NOT subset to maximize reach for presence
|
// Special-case control/presence messages: do NOT subset to maximize immediate coverage
|
||||||
var selectedPeripheralIDs = Set(allowedPeripheralIDs)
|
var selectedPeripheralIDs = Set(allowedPeripheralIDs)
|
||||||
var selectedCentralIDs = Set(allowedCentralIDs)
|
var selectedCentralIDs = Set(allowedCentralIDs)
|
||||||
if directedOnlyPeer == nil && packet.type != MessageType.fragment.rawValue && packet.type != MessageType.announce.rawValue {
|
if directedOnlyPeer == nil
|
||||||
|
&& packet.type != MessageType.fragment.rawValue
|
||||||
|
&& packet.type != MessageType.announce.rawValue
|
||||||
|
&& packet.type != MessageType.requestSync.rawValue {
|
||||||
let kp = subsetSizeForFanout(allowedPeripheralIDs.count)
|
let kp = subsetSizeForFanout(allowedPeripheralIDs.count)
|
||||||
let kc = subsetSizeForFanout(allowedCentralIDs.count)
|
let kc = subsetSizeForFanout(allowedCentralIDs.count)
|
||||||
selectedPeripheralIDs = selectDeterministicSubset(ids: allowedPeripheralIDs, k: kp, seed: messageID)
|
selectedPeripheralIDs = selectDeterministicSubset(ids: allowedPeripheralIDs, k: kp, seed: messageID)
|
||||||
@@ -1133,6 +1144,12 @@ final class BLEService: NSObject {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Directed send helper (unicast to a specific peerID) without altering packet contents
|
||||||
|
private func sendPacketDirected(_ packet: BitchatPacket, to peerID: String) {
|
||||||
|
guard let data = packet.toBinaryData(padding: false) else { return }
|
||||||
|
sendOnAllLinks(packet: packet, data: data, pad: false, directedOnlyPeer: peerID)
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Directed store-and-forward
|
// MARK: - Directed store-and-forward
|
||||||
private func spoolDirectedPacket(_ packet: BitchatPacket, recipientPeerID: String) {
|
private func spoolDirectedPacket(_ packet: BitchatPacket, recipientPeerID: String) {
|
||||||
let msgID = makeMessageID(for: packet)
|
let msgID = makeMessageID(for: packet)
|
||||||
@@ -1383,6 +1400,9 @@ final class BLEService: NSObject {
|
|||||||
case .message:
|
case .message:
|
||||||
handleMessage(packet, from: senderID)
|
handleMessage(packet, from: senderID)
|
||||||
|
|
||||||
|
case .requestSync:
|
||||||
|
handleRequestSync(packet, from: senderID)
|
||||||
|
|
||||||
case .noiseHandshake:
|
case .noiseHandshake:
|
||||||
handleNoiseHandshake(packet, from: senderID)
|
handleNoiseHandshake(packet, from: senderID)
|
||||||
|
|
||||||
@@ -1551,7 +1571,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,
|
||||||
@@ -1583,12 +1603,17 @@ final class BLEService: NSObject {
|
|||||||
// Only notify of connection for new or reconnected peers when it is a direct announce
|
// Only notify of connection for new or reconnected peers when it is a direct announce
|
||||||
if (packet.ttl == self.messageTTL) && (isNewPeer || isReconnectedPeer) {
|
if (packet.ttl == self.messageTTL) && (isNewPeer || isReconnectedPeer) {
|
||||||
self.delegate?.didConnectToPeer(peerID)
|
self.delegate?.didConnectToPeer(peerID)
|
||||||
|
// Schedule initial unicast sync to this peer
|
||||||
|
self.gossipSyncManager?.scheduleInitialSyncToPeer(peerID, delaySeconds: 5.0)
|
||||||
}
|
}
|
||||||
|
|
||||||
self.requestPeerDataPublish()
|
self.requestPeerDataPublish()
|
||||||
self.delegate?.didUpdatePeerList(currentPeerIDs)
|
self.delegate?.didUpdatePeerList(currentPeerIDs)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Track for sync (include our own and others' announces)
|
||||||
|
gossipSyncManager?.onPublicPacketSeen(packet)
|
||||||
|
|
||||||
// Send announce back for bidirectional discovery (only once per peer)
|
// Send announce back for bidirectional discovery (only once per peer)
|
||||||
let announceBackID = "announce-back-\(peerID)"
|
let announceBackID = "announce-back-\(peerID)"
|
||||||
let shouldSendBack = !messageDeduplicator.contains(announceBackID)
|
let shouldSendBack = !messageDeduplicator.contains(announceBackID)
|
||||||
@@ -1611,6 +1636,15 @@ final class BLEService: NSObject {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle REQUEST_SYNC: decode payload and respond with missing packets via sync manager
|
||||||
|
private func handleRequestSync(_ packet: BitchatPacket, from peerID: String) {
|
||||||
|
guard let req = RequestSyncPacket.decode(from: packet.payload) else {
|
||||||
|
SecureLogger.warning("⚠️ Malformed REQUEST_SYNC from \(peerID)", category: .session)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
gossipSyncManager?.handleRequestSync(fromPeerID: peerID, request: req)
|
||||||
|
}
|
||||||
|
|
||||||
// Mention parsing moved to ChatViewModel
|
// Mention parsing moved to ChatViewModel
|
||||||
|
|
||||||
private func handleMessage(_ packet: BitchatPacket, from peerID: String) {
|
private func handleMessage(_ packet: BitchatPacket, from peerID: String) {
|
||||||
@@ -1633,13 +1667,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))
|
||||||
@@ -1650,6 +1684,11 @@ final class BLEService: NSObject {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Track broadcast messages (recipientID == nil indicates broadcast)
|
||||||
|
if packet.recipientID == nil && packet.type == MessageType.message.rawValue {
|
||||||
|
gossipSyncManager?.onPublicPacketSeen(packet)
|
||||||
|
}
|
||||||
|
|
||||||
guard accepted else {
|
guard accepted else {
|
||||||
SecureLogger.warning("🚫 Dropping public message from unverified or unknown peer \(peerID.prefix(8))…", category: .security)
|
SecureLogger.warning("🚫 Dropping public message from unverified or unknown peer \(peerID.prefix(8))…", category: .security)
|
||||||
return
|
return
|
||||||
@@ -1861,6 +1900,8 @@ final class BLEService: NSObject {
|
|||||||
self?.broadcastPacket(signedPacket)
|
self?.broadcastPacket(signedPacket)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Ensure our own announce is included in sync state
|
||||||
|
gossipSyncManager?.onPublicPacketSeen(signedPacket)
|
||||||
}
|
}
|
||||||
|
|
||||||
func sendDeliveryAck(for messageID: String, to peerID: String) {
|
func sendDeliveryAck(for messageID: String, to peerID: String) {
|
||||||
@@ -2249,6 +2290,29 @@ final class BLEService: NSObject {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - GossipSyncManager Delegate
|
||||||
|
extension BLEService: GossipSyncManager.Delegate {
|
||||||
|
func sendPacket(_ packet: BitchatPacket) {
|
||||||
|
if DispatchQueue.getSpecific(key: messageQueueKey) != nil {
|
||||||
|
broadcastPacket(packet)
|
||||||
|
} else {
|
||||||
|
messageQueue.async { [weak self] in self?.broadcastPacket(packet) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func sendPacket(to peerID: String, packet: BitchatPacket) {
|
||||||
|
if DispatchQueue.getSpecific(key: messageQueueKey) != nil {
|
||||||
|
sendPacketDirected(packet, to: peerID)
|
||||||
|
} else {
|
||||||
|
messageQueue.async { [weak self] in self?.sendPacketDirected(packet, to: peerID) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func signPacketForBroadcast(_ packet: BitchatPacket) -> BitchatPacket {
|
||||||
|
return noiseService.signPacket(packet) ?? packet
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - CBCentralManagerDelegate
|
// MARK: - CBCentralManagerDelegate
|
||||||
|
|
||||||
extension BLEService: CBCentralManagerDelegate {
|
extension BLEService: CBCentralManagerDelegate {
|
||||||
|
|||||||
@@ -20,12 +20,10 @@ enum CommandResult {
|
|||||||
final class CommandProcessor {
|
final 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
|
||||||
@@ -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")
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|
||||||
|
|||||||
@@ -9,23 +9,16 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import Security
|
import Security
|
||||||
|
|
||||||
protocol KeychainManagerProtocol {
|
final 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
|
||||||
@@ -317,7 +310,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 +319,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)
|
||||||
|
|||||||
@@ -285,18 +285,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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -74,7 +74,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 {
|
||||||
|
|||||||
@@ -155,7 +155,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,14 +181,12 @@ 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)
|
||||||
@@ -200,7 +197,7 @@ 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)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -212,7 +209,7 @@ 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)
|
||||||
@@ -223,7 +220,7 @@ 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)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -232,7 +229,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,8 +266,8 @@ 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.warning("Panic mode activated - identity cleared", category: .security)
|
||||||
// Stop rekey timer
|
// Stop rekey timer
|
||||||
|
|||||||
@@ -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 */ }
|
||||||
|
|||||||
@@ -103,7 +103,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)
|
||||||
|
|||||||
@@ -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,27 @@ 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.debug("🔄 Found favorite for '\(peerInfo.nickname)' by nickname, updating noise key", category: .session)
|
||||||
|
|
||||||
|
// 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 +268,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
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -326,7 +345,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 +368,7 @@ final class UnifiedPeerService: ObservableObject, TransportPeerEventsDelegate {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
identityManager.updateSocialIdentity(identity)
|
SecureIdentityStateManager.shared.updateSocialIdentity(identity)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get fingerprint for peer ID
|
/// Get fingerprint for peer ID
|
||||||
|
|||||||
@@ -0,0 +1,168 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
// Gossip-based sync manager using rotating Bloom filters
|
||||||
|
final class GossipSyncManager {
|
||||||
|
protocol Delegate: AnyObject {
|
||||||
|
func sendPacket(_ packet: BitchatPacket)
|
||||||
|
func sendPacket(to peerID: String, packet: BitchatPacket)
|
||||||
|
func signPacketForBroadcast(_ packet: BitchatPacket) -> BitchatPacket
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Config {
|
||||||
|
var seenCapacity: Int = 100 // recent broadcast messages kept
|
||||||
|
var bloomMaxBytes: Int = 256 // up to 256 bytes
|
||||||
|
var bloomTargetFpr: Double = 0.01 // 1%
|
||||||
|
}
|
||||||
|
|
||||||
|
private let myPeerID: String
|
||||||
|
private let config: Config
|
||||||
|
weak var delegate: Delegate?
|
||||||
|
|
||||||
|
// Bloom filter
|
||||||
|
private let bloom: SeenPacketsBloomFilter
|
||||||
|
|
||||||
|
// Storage: broadcast messages (ordered by insert), and latest announce per sender
|
||||||
|
private var messages: [String: BitchatPacket] = [:] // idHex -> packet
|
||||||
|
private var messageOrder: [String] = []
|
||||||
|
private var latestAnnouncementByPeer: [String: (id: String, packet: BitchatPacket)] = [:]
|
||||||
|
|
||||||
|
// Timer
|
||||||
|
private var periodicTimer: DispatchSourceTimer?
|
||||||
|
private let queue = DispatchQueue(label: "mesh.sync", qos: .utility)
|
||||||
|
|
||||||
|
init(myPeerID: String, config: Config = Config()) {
|
||||||
|
self.myPeerID = myPeerID
|
||||||
|
self.config = config
|
||||||
|
self.bloom = SeenPacketsBloomFilter(maxBytes: config.bloomMaxBytes, targetFpr: config.bloomTargetFpr)
|
||||||
|
}
|
||||||
|
|
||||||
|
func start() {
|
||||||
|
stop()
|
||||||
|
let timer = DispatchSource.makeTimerSource(queue: queue)
|
||||||
|
timer.schedule(deadline: .now() + 30.0, repeating: 30.0, leeway: .seconds(1))
|
||||||
|
timer.setEventHandler { [weak self] in self?.sendRequestSync() }
|
||||||
|
timer.resume()
|
||||||
|
periodicTimer = timer
|
||||||
|
}
|
||||||
|
|
||||||
|
func stop() {
|
||||||
|
periodicTimer?.cancel(); periodicTimer = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func scheduleInitialSyncToPeer(_ peerID: String, delaySeconds: TimeInterval = 5.0) {
|
||||||
|
queue.asyncAfter(deadline: .now() + delaySeconds) { [weak self] in
|
||||||
|
self?.sendRequestSync(to: peerID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func onPublicPacketSeen(_ packet: BitchatPacket) {
|
||||||
|
let mt = MessageType(rawValue: packet.type)
|
||||||
|
let isBroadcastMessage = (mt == .message && packet.recipientID == nil)
|
||||||
|
let isAnnounce = (mt == .announce)
|
||||||
|
guard isBroadcastMessage || isAnnounce else { return }
|
||||||
|
|
||||||
|
let idBytes = PacketIdUtil.computeId(packet)
|
||||||
|
bloom.add(idBytes)
|
||||||
|
let idHex = idBytes.hexEncodedString()
|
||||||
|
|
||||||
|
if isBroadcastMessage {
|
||||||
|
if messages[idHex] == nil {
|
||||||
|
messages[idHex] = packet
|
||||||
|
messageOrder.append(idHex)
|
||||||
|
// Enforce capacity
|
||||||
|
let cap = max(1, config.seenCapacity)
|
||||||
|
while messageOrder.count > cap {
|
||||||
|
let victim = messageOrder.removeFirst()
|
||||||
|
messages.removeValue(forKey: victim)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if isAnnounce {
|
||||||
|
let sender = packet.senderID.hexEncodedString()
|
||||||
|
latestAnnouncementByPeer[sender] = (id: idHex, packet: packet)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func sendRequestSync() {
|
||||||
|
let snap = bloom.snapshotActive()
|
||||||
|
let payload = RequestSyncPacket(mBytes: snap.mBytes, k: snap.k, bits: snap.bits).encode()
|
||||||
|
let pkt = BitchatPacket(
|
||||||
|
type: MessageType.requestSync.rawValue,
|
||||||
|
senderID: Data(hexString: myPeerID) ?? Data(),
|
||||||
|
recipientID: nil, // broadcast
|
||||||
|
timestamp: UInt64(Date().timeIntervalSince1970 * 1000),
|
||||||
|
payload: payload,
|
||||||
|
signature: nil,
|
||||||
|
ttl: 0 // local-only
|
||||||
|
)
|
||||||
|
let signed = delegate?.signPacketForBroadcast(pkt) ?? pkt
|
||||||
|
delegate?.sendPacket(signed)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func sendRequestSync(to peerID: String) {
|
||||||
|
let snap = bloom.snapshotActive()
|
||||||
|
let payload = RequestSyncPacket(mBytes: snap.mBytes, k: snap.k, bits: snap.bits).encode()
|
||||||
|
var recipient = Data()
|
||||||
|
var temp = peerID
|
||||||
|
while temp.count >= 2 && recipient.count < 8 {
|
||||||
|
let hexByte = String(temp.prefix(2))
|
||||||
|
if let b = UInt8(hexByte, radix: 16) { recipient.append(b) }
|
||||||
|
temp = String(temp.dropFirst(2))
|
||||||
|
}
|
||||||
|
let pkt = BitchatPacket(
|
||||||
|
type: MessageType.requestSync.rawValue,
|
||||||
|
senderID: Data(hexString: myPeerID) ?? Data(),
|
||||||
|
recipientID: recipient,
|
||||||
|
timestamp: UInt64(Date().timeIntervalSince1970 * 1000),
|
||||||
|
payload: payload,
|
||||||
|
signature: nil,
|
||||||
|
ttl: 0 // local-only
|
||||||
|
)
|
||||||
|
let signed = delegate?.signPacketForBroadcast(pkt) ?? pkt
|
||||||
|
delegate?.sendPacket(to: peerID, packet: signed)
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleRequestSync(fromPeerID: String, request: RequestSyncPacket) {
|
||||||
|
// Build membership checker from provided parameters
|
||||||
|
let mBits = request.mBytes * 8
|
||||||
|
let k = request.k
|
||||||
|
func mightContain(_ id: Data) -> Bool {
|
||||||
|
// Same hashing as local bloom; compute indices, check MSB-first bits in request.bits
|
||||||
|
var h1: UInt64 = 1469598103934665603
|
||||||
|
var h2: UInt64 = 0x27d4eb2f165667c5
|
||||||
|
for b in id { h1 = (h1 ^ UInt64(b)) &* 1099511628211; h2 = (h2 ^ UInt64(b)) &* 0x100000001B3 }
|
||||||
|
for i in 0..<k {
|
||||||
|
let combined = h1 &+ (UInt64(i) &* h2)
|
||||||
|
let idx = Int((combined & 0x7fff_ffff_ffff_ffff) % UInt64(mBits))
|
||||||
|
let byteIndex = idx / 8
|
||||||
|
let bitIndex = idx % 8
|
||||||
|
let byte = request.bits[byteIndex]
|
||||||
|
let bit = ((Int(byte) >> (7 - bitIndex)) & 1) == 1
|
||||||
|
if !bit { return false }
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1) Announcements: send latest per peer if requester lacks them
|
||||||
|
for (_, pair) in latestAnnouncementByPeer {
|
||||||
|
let (idHex, pkt) = pair
|
||||||
|
let idBytes = Data(hexString: idHex) ?? Data()
|
||||||
|
if !mightContain(idBytes) {
|
||||||
|
var toSend = pkt
|
||||||
|
toSend.ttl = 0
|
||||||
|
delegate?.sendPacket(to: fromPeerID, packet: toSend)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2) Broadcast messages: send all missing
|
||||||
|
let toSendMsgs = messageOrder.compactMap { messages[$0] }
|
||||||
|
for pkt in toSendMsgs {
|
||||||
|
let idBytes = PacketIdUtil.computeId(pkt)
|
||||||
|
if !mightContain(idBytes) {
|
||||||
|
var toSend = pkt
|
||||||
|
toSend.ttl = 0
|
||||||
|
delegate?.sendPacket(to: fromPeerID, packet: toSend)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import Foundation
|
||||||
|
import CryptoKit
|
||||||
|
|
||||||
|
// Deterministic packet ID used for sync Bloom membership
|
||||||
|
// ID = first 16 bytes of SHA-256 over: [type | senderID | timestamp | payload]
|
||||||
|
enum PacketIdUtil {
|
||||||
|
static func computeId(_ packet: BitchatPacket) -> Data {
|
||||||
|
var hasher = SHA256()
|
||||||
|
hasher.update(data: Data([packet.type]))
|
||||||
|
hasher.update(data: packet.senderID)
|
||||||
|
var tsBE = packet.timestamp.bigEndian
|
||||||
|
withUnsafeBytes(of: &tsBE) { raw in hasher.update(data: Data(raw)) }
|
||||||
|
hasher.update(data: packet.payload)
|
||||||
|
let digest = hasher.finalize()
|
||||||
|
return Data(digest.prefix(16))
|
||||||
|
}
|
||||||
|
|
||||||
|
static func computeIdHex(_ packet: BitchatPacket) -> String {
|
||||||
|
return computeId(packet).hexEncodedString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
// Rotating Bloom filter for recently seen packet IDs (16-byte IDs)
|
||||||
|
final class SeenPacketsBloomFilter {
|
||||||
|
struct Snapshot { let mBytes: Int; let k: Int; let bits: Data }
|
||||||
|
|
||||||
|
private struct Filter { var mBits: Int; var k: Int; var bits: [UInt8]; var count: Int }
|
||||||
|
|
||||||
|
private let maxBytes: Int
|
||||||
|
private let targetFpr: Double
|
||||||
|
private let seed: UInt64 = 0x27d4eb2f165667c5
|
||||||
|
|
||||||
|
private let mBits: Int
|
||||||
|
private let kOptimal: Int
|
||||||
|
private let capacityOptimal: Int
|
||||||
|
|
||||||
|
private var active: Filter
|
||||||
|
private var standby: Filter
|
||||||
|
private var usingStandby: Bool = false
|
||||||
|
private let lock = NSLock()
|
||||||
|
|
||||||
|
init(maxBytes: Int = 256, targetFpr: Double = 0.01) {
|
||||||
|
self.maxBytes = max(1, maxBytes)
|
||||||
|
self.targetFpr = targetFpr
|
||||||
|
self.mBits = max(8, self.maxBytes * 8)
|
||||||
|
let (k, n) = SeenPacketsBloomFilter.deriveParams(mBits: self.mBits, fpr: targetFpr)
|
||||||
|
self.kOptimal = max(1, k)
|
||||||
|
self.capacityOptimal = max(1, n)
|
||||||
|
self.active = Filter(mBits: self.mBits, k: self.kOptimal, bits: [UInt8](repeating: 0, count: self.maxBytes), count: 0)
|
||||||
|
self.standby = Filter(mBits: self.mBits, k: self.kOptimal, bits: [UInt8](repeating: 0, count: self.maxBytes), count: 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func deriveParams(mBits: Int, fpr: Double) -> (Int, Int) {
|
||||||
|
// n ≈ -(m (ln 2)^2) / ln p ; k ≈ (m/n) ln 2
|
||||||
|
let ln2 = log(2.0)
|
||||||
|
let n = max(1, Int(Double(-mBits) * ln2 * ln2 / log(fpr)))
|
||||||
|
let k = max(1, Int(ceil((Double(mBits) / Double(n)) * ln2)))
|
||||||
|
return (k, n)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func indicesFor(id: Data, mBits: Int, k: Int) -> [Int] {
|
||||||
|
var h1: UInt64 = 1469598103934665603 // FNV-1a 64-bit offset
|
||||||
|
var h2: UInt64 = seed
|
||||||
|
for b in id { // treat as unsigned bytes
|
||||||
|
h1 = (h1 ^ UInt64(b)) &* 1099511628211
|
||||||
|
h2 = (h2 ^ UInt64(b)) &* 0x100000001B3
|
||||||
|
}
|
||||||
|
var result = [Int]()
|
||||||
|
result.reserveCapacity(k)
|
||||||
|
for i in 0..<k {
|
||||||
|
let combined = h1 &+ (UInt64(i) &* h2)
|
||||||
|
let idx = Int((combined & 0x7fff_ffff_ffff_ffff) % UInt64(mBits))
|
||||||
|
result.append(idx)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func add(_ id: Data) {
|
||||||
|
lock.lock(); defer { lock.unlock() }
|
||||||
|
let startStandbyAt = capacityOptimal / 2
|
||||||
|
if !usingStandby && active.count >= startStandbyAt {
|
||||||
|
standby = Filter(mBits: mBits, k: kOptimal, bits: [UInt8](repeating: 0, count: maxBytes), count: 0)
|
||||||
|
usingStandby = true
|
||||||
|
}
|
||||||
|
insert(into: &active, id: id)
|
||||||
|
if usingStandby { insert(into: &standby, id: id) }
|
||||||
|
if active.count >= capacityOptimal {
|
||||||
|
active = standby
|
||||||
|
standby = Filter(mBits: mBits, k: kOptimal, bits: [UInt8](repeating: 0, count: maxBytes), count: 0)
|
||||||
|
usingStandby = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func insert(into filter: inout Filter, id: Data) {
|
||||||
|
let idxs = indicesFor(id: id, mBits: filter.mBits, k: filter.k)
|
||||||
|
for i in idxs {
|
||||||
|
let byteIndex = i / 8
|
||||||
|
let bitIndex = i % 8
|
||||||
|
filter.bits[byteIndex] = UInt8(Int(filter.bits[byteIndex]) | (1 << (7 - bitIndex)))
|
||||||
|
}
|
||||||
|
filter.count &+= 1
|
||||||
|
}
|
||||||
|
|
||||||
|
func mightContain(_ id: Data) -> Bool {
|
||||||
|
lock.lock(); defer { lock.unlock() }
|
||||||
|
let a = active
|
||||||
|
let idx = indicesFor(id: id, mBits: a.mBits, k: a.k)
|
||||||
|
var inActive = true
|
||||||
|
for i in idx {
|
||||||
|
let byteIndex = i / 8
|
||||||
|
let bitIndex = i % 8
|
||||||
|
let set = ((Int(a.bits[byteIndex]) >> (7 - bitIndex)) & 1) == 1
|
||||||
|
if !set { inActive = false; break }
|
||||||
|
}
|
||||||
|
if inActive { return true }
|
||||||
|
if usingStandby {
|
||||||
|
let s = standby
|
||||||
|
let idx2 = indicesFor(id: id, mBits: s.mBits, k: s.k)
|
||||||
|
for i in idx2 {
|
||||||
|
let byteIndex = i / 8
|
||||||
|
let bitIndex = i % 8
|
||||||
|
let set = ((Int(s.bits[byteIndex]) >> (7 - bitIndex)) & 1) == 1
|
||||||
|
if !set { return false }
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func snapshotActive() -> Snapshot {
|
||||||
|
lock.lock(); defer { lock.unlock() }
|
||||||
|
let a = active
|
||||||
|
return Snapshot(mBytes: a.bits.count, k: a.k, bits: Data(a.bits))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -344,16 +344,13 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
|
|
||||||
// MARK: - Services and Storage
|
// MARK: - Services and Storage
|
||||||
|
|
||||||
let meshService: Transport
|
var meshService: Transport = BLEService()
|
||||||
let identityManager: SecureIdentityStateManagerProtocol
|
|
||||||
|
|
||||||
private var nostrRelayManager: NostrRelayManager?
|
private var nostrRelayManager: NostrRelayManager?
|
||||||
// PeerManager replaced by UnifiedPeerService
|
// PeerManager replaced by UnifiedPeerService
|
||||||
private var processedNostrEvents = Set<String>() // Simple deduplication
|
private var processedNostrEvents = Set<String>() // Simple deduplication
|
||||||
private var processedNostrEventOrder: [String] = []
|
private var processedNostrEventOrder: [String] = []
|
||||||
private let maxProcessedNostrEvents = TransportConfig.uiProcessedNostrEventsCap
|
private let maxProcessedNostrEvents = TransportConfig.uiProcessedNostrEventsCap
|
||||||
private let userDefaults = UserDefaults.standard
|
private let userDefaults = UserDefaults.standard
|
||||||
private let keychain: KeychainManagerProtocol
|
|
||||||
private let nicknameKey = "bitchat.nickname"
|
private let nicknameKey = "bitchat.nickname"
|
||||||
// Location channel state (macOS supports manual geohash selection)
|
// Location channel state (macOS supports manual geohash selection)
|
||||||
@Published private var activeChannel: ChannelID = .mesh
|
@Published private var activeChannel: ChannelID = .mesh
|
||||||
@@ -423,7 +420,6 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
private var geoSamplingSubs: [String: String] = [:] // subID -> geohash
|
private var geoSamplingSubs: [String: String] = [:] // subID -> geohash
|
||||||
private var lastGeoNotificationAt: [String: Date] = [:] // geohash -> last notify time
|
private var lastGeoNotificationAt: [String: Date] = [:] // geohash -> last notify time
|
||||||
|
|
||||||
|
|
||||||
// MARK: - Message Delivery Tracking
|
// MARK: - Message Delivery Tracking
|
||||||
|
|
||||||
// Delivery tracking
|
// Delivery tracking
|
||||||
@@ -488,14 +484,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
// MARK: - Initialization
|
// MARK: - Initialization
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
init(
|
init() {
|
||||||
keychain: KeychainManagerProtocol,
|
|
||||||
identityManager: SecureIdentityStateManagerProtocol
|
|
||||||
) {
|
|
||||||
self.keychain = keychain
|
|
||||||
self.identityManager = identityManager
|
|
||||||
self.meshService = BLEService(keychain: keychain, identityManager: identityManager)
|
|
||||||
|
|
||||||
// Load persisted read receipts
|
// Load persisted read receipts
|
||||||
if let data = UserDefaults.standard.data(forKey: "sentReadReceipts"),
|
if let data = UserDefaults.standard.data(forKey: "sentReadReceipts"),
|
||||||
let receipts = try? JSONDecoder().decode([String].self, from: data) {
|
let receipts = try? JSONDecoder().decode([String].self, from: data) {
|
||||||
@@ -506,10 +495,10 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Initialize services
|
// Initialize services
|
||||||
self.commandProcessor = CommandProcessor(identityManager: identityManager)
|
self.commandProcessor = CommandProcessor()
|
||||||
self.privateChatManager = PrivateChatManager(meshService: meshService)
|
self.privateChatManager = PrivateChatManager(meshService: meshService)
|
||||||
self.unifiedPeerService = UnifiedPeerService(meshService: meshService, identityManager: identityManager)
|
self.unifiedPeerService = UnifiedPeerService(meshService: meshService)
|
||||||
let nostrTransport = NostrTransport(keychain: keychain)
|
let nostrTransport = NostrTransport()
|
||||||
self.messageRouter = MessageRouter(mesh: meshService, nostr: nostrTransport)
|
self.messageRouter = MessageRouter(mesh: meshService, nostr: nostrTransport)
|
||||||
// Route receipts from PrivateChatManager through MessageRouter
|
// Route receipts from PrivateChatManager through MessageRouter
|
||||||
self.privateChatManager.messageRouter = self.messageRouter
|
self.privateChatManager.messageRouter = self.messageRouter
|
||||||
@@ -858,7 +847,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
Task { @MainActor in
|
Task { @MainActor in
|
||||||
self.torRestartPending = true
|
self.torRestartPending = true
|
||||||
// Post only in geohash channels (queue if not active)
|
// Post only in geohash channels (queue if not active)
|
||||||
self.addGeohashOnlySystemMessage("tor restarting to recover connectivity...")
|
self.addGeohashOnlySystemMessage("tor restarting to recover connectivity…")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -987,7 +976,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
let messageId = pm.messageID
|
let messageId = pm.messageID
|
||||||
// Send delivery ACK immediately (once per message ID)
|
// Send delivery ACK immediately (once per message ID)
|
||||||
if !self.sentGeoDeliveryAcks.contains(messageId) {
|
if !self.sentGeoDeliveryAcks.contains(messageId) {
|
||||||
let nt = NostrTransport(keychain: keychain)
|
let nt = NostrTransport()
|
||||||
nt.senderPeerID = self.meshService.myPeerID
|
nt.senderPeerID = self.meshService.myPeerID
|
||||||
nt.sendDeliveryAckGeohash(for: messageId, toRecipientHex: senderPubkey, from: id)
|
nt.sendDeliveryAckGeohash(for: messageId, toRecipientHex: senderPubkey, from: id)
|
||||||
self.sentGeoDeliveryAcks.insert(messageId)
|
self.sentGeoDeliveryAcks.insert(messageId)
|
||||||
@@ -1015,7 +1004,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
deliveryStatus: .delivered(to: self.nickname, at: Date())
|
deliveryStatus: .delivered(to: self.nickname, at: Date())
|
||||||
)
|
)
|
||||||
// Respect geohash blocks
|
// Respect geohash blocks
|
||||||
if identityManager.isNostrBlocked(pubkeyHexLowercased: senderPubkey) {
|
if SecureIdentityStateManager.shared.isNostrBlocked(pubkeyHexLowercased: senderPubkey) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if self.privateChats[convKey] == nil { self.privateChats[convKey] = [] }
|
if self.privateChats[convKey] == nil { self.privateChats[convKey] = [] }
|
||||||
@@ -1025,7 +1014,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
if isViewing {
|
if isViewing {
|
||||||
// pared back: omit pre-send READ log
|
// pared back: omit pre-send READ log
|
||||||
if !wasReadBefore {
|
if !wasReadBefore {
|
||||||
let nt = NostrTransport(keychain: keychain)
|
let nt = NostrTransport()
|
||||||
nt.senderPeerID = self.meshService.myPeerID
|
nt.senderPeerID = self.meshService.myPeerID
|
||||||
nt.sendReadReceiptGeohash(messageId, toRecipientHex: senderPubkey, from: id)
|
nt.sendReadReceiptGeohash(messageId, toRecipientHex: senderPubkey, from: id)
|
||||||
self.sentReadReceipts.insert(messageId)
|
self.sentReadReceipts.insert(messageId)
|
||||||
@@ -1248,7 +1237,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Update identity state manager with handshake completion
|
// Update identity state manager with handshake completion
|
||||||
identityManager.updateHandshakeState(peerID: peerID, state: .completed(fingerprint: fingerprintStr))
|
SecureIdentityStateManager.shared.updateHandshakeState(peerID: peerID, state: .completed(fingerprint: fingerprintStr))
|
||||||
|
|
||||||
// Update encryption status now that we have the fingerprint
|
// Update encryption status now that we have the fingerprint
|
||||||
updateEncryptionStatus(for: peerID)
|
updateEncryptionStatus(for: peerID)
|
||||||
@@ -1257,9 +1246,9 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
let peerNicknames = meshService.getPeerNicknames()
|
let peerNicknames = meshService.getPeerNicknames()
|
||||||
if let nickname = peerNicknames[peerID], nickname != "Unknown" && nickname != "anon\(peerID.prefix(4))" {
|
if let nickname = peerNicknames[peerID], nickname != "Unknown" && nickname != "anon\(peerID.prefix(4))" {
|
||||||
// Update or create social identity with the claimed nickname
|
// Update or create social identity with the claimed nickname
|
||||||
if var identity = identityManager.getSocialIdentity(for: fingerprintStr) {
|
if var identity = SecureIdentityStateManager.shared.getSocialIdentity(for: fingerprintStr) {
|
||||||
identity.claimedNickname = nickname
|
identity.claimedNickname = nickname
|
||||||
identityManager.updateSocialIdentity(identity)
|
SecureIdentityStateManager.shared.updateSocialIdentity(identity)
|
||||||
} else {
|
} else {
|
||||||
let newIdentity = SocialIdentity(
|
let newIdentity = SocialIdentity(
|
||||||
fingerprint: fingerprintStr,
|
fingerprint: fingerprintStr,
|
||||||
@@ -1270,7 +1259,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
isBlocked: false,
|
isBlocked: false,
|
||||||
notes: nil
|
notes: nil
|
||||||
)
|
)
|
||||||
identityManager.updateSocialIdentity(newIdentity)
|
SecureIdentityStateManager.shared.updateSocialIdentity(newIdentity)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1631,7 +1620,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
self.geoNicknames[event.pubkey.lowercased()] = nick
|
self.geoNicknames[event.pubkey.lowercased()] = nick
|
||||||
}
|
}
|
||||||
// If this pubkey is blocked, skip mapping, participants, and timeline
|
// If this pubkey is blocked, skip mapping, participants, and timeline
|
||||||
if identityManager.isNostrBlocked(pubkeyHexLowercased: event.pubkey) {
|
if SecureIdentityStateManager.shared.isNostrBlocked(pubkeyHexLowercased: event.pubkey) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// Store mapping for geohash DM initiation
|
// Store mapping for geohash DM initiation
|
||||||
@@ -1709,7 +1698,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
SecureLogger.info("GeoDM: recv PM <- sender=\(senderPubkey.prefix(8))… mid=\(messageId.prefix(8))…", category: .session)
|
SecureLogger.info("GeoDM: recv PM <- sender=\(senderPubkey.prefix(8))… mid=\(messageId.prefix(8))…", category: .session)
|
||||||
// Send delivery ACK immediately (even if duplicate), once per messageID
|
// Send delivery ACK immediately (even if duplicate), once per messageID
|
||||||
if !self.sentGeoDeliveryAcks.contains(messageId) {
|
if !self.sentGeoDeliveryAcks.contains(messageId) {
|
||||||
let nostrTransport = NostrTransport(keychain: keychain)
|
let nostrTransport = NostrTransport()
|
||||||
nostrTransport.senderPeerID = self.meshService.myPeerID
|
nostrTransport.senderPeerID = self.meshService.myPeerID
|
||||||
// pared back: omit pre-send log
|
// pared back: omit pre-send log
|
||||||
nostrTransport.sendDeliveryAckGeohash(for: messageId, toRecipientHex: senderPubkey, from: id)
|
nostrTransport.sendDeliveryAckGeohash(for: messageId, toRecipientHex: senderPubkey, from: id)
|
||||||
@@ -1744,7 +1733,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
if isViewing {
|
if isViewing {
|
||||||
// pared back: omit pre-send READ log
|
// pared back: omit pre-send READ log
|
||||||
if !wasReadBefore {
|
if !wasReadBefore {
|
||||||
let nostrTransport = NostrTransport(keychain: keychain)
|
let nostrTransport = NostrTransport()
|
||||||
nostrTransport.senderPeerID = self.meshService.myPeerID
|
nostrTransport.senderPeerID = self.meshService.myPeerID
|
||||||
nostrTransport.sendReadReceiptGeohash(messageId, toRecipientHex: senderPubkey, from: id)
|
nostrTransport.sendReadReceiptGeohash(messageId, toRecipientHex: senderPubkey, from: id)
|
||||||
self.sentReadReceipts.insert(messageId)
|
self.sentReadReceipts.insert(messageId)
|
||||||
@@ -1829,7 +1818,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
// Prune expired entries
|
// Prune expired entries
|
||||||
map = map.filter { $0.value >= cutoff }
|
map = map.filter { $0.value >= cutoff }
|
||||||
// Remove blocked Nostr pubkeys
|
// Remove blocked Nostr pubkeys
|
||||||
map = map.filter { !identityManager.isNostrBlocked(pubkeyHexLowercased: $0.key) }
|
map = map.filter { !SecureIdentityStateManager.shared.isNostrBlocked(pubkeyHexLowercased: $0.key) }
|
||||||
geoParticipants[gh] = map
|
geoParticipants[gh] = map
|
||||||
// Build display list
|
// Build display list
|
||||||
let people = map
|
let people = map
|
||||||
@@ -1862,7 +1851,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
let cutoff = Date().addingTimeInterval(-TransportConfig.uiRecentCutoffFiveMinutesSeconds)
|
let cutoff = Date().addingTimeInterval(-TransportConfig.uiRecentCutoffFiveMinutesSeconds)
|
||||||
let map = (geoParticipants[gh] ?? [:])
|
let map = (geoParticipants[gh] ?? [:])
|
||||||
.filter { $0.value >= cutoff }
|
.filter { $0.value >= cutoff }
|
||||||
.filter { !identityManager.isNostrBlocked(pubkeyHexLowercased: $0.key) }
|
.filter { !SecureIdentityStateManager.shared.isNostrBlocked(pubkeyHexLowercased: $0.key) }
|
||||||
let people = map
|
let people = map
|
||||||
.map { (pub, seen) in GeoPerson(id: pub, displayName: displayNameForNostrPubkey(pub), lastSeen: seen) }
|
.map { (pub, seen) in GeoPerson(id: pub, displayName: displayNameForNostrPubkey(pub), lastSeen: seen) }
|
||||||
.sorted { $0.lastSeen > $1.lastSeen }
|
.sorted { $0.lastSeen > $1.lastSeen }
|
||||||
@@ -1879,12 +1868,12 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
// Geohash block helpers
|
// Geohash block helpers
|
||||||
@MainActor
|
@MainActor
|
||||||
func isGeohashUserBlocked(pubkeyHexLowercased: String) -> Bool {
|
func isGeohashUserBlocked(pubkeyHexLowercased: String) -> Bool {
|
||||||
return identityManager.isNostrBlocked(pubkeyHexLowercased: pubkeyHexLowercased)
|
return SecureIdentityStateManager.shared.isNostrBlocked(pubkeyHexLowercased: pubkeyHexLowercased)
|
||||||
}
|
}
|
||||||
@MainActor
|
@MainActor
|
||||||
func blockGeohashUser(pubkeyHexLowercased: String, displayName: String) {
|
func blockGeohashUser(pubkeyHexLowercased: String, displayName: String) {
|
||||||
let hex = pubkeyHexLowercased.lowercased()
|
let hex = pubkeyHexLowercased.lowercased()
|
||||||
identityManager.setNostrBlocked(hex, isBlocked: true)
|
SecureIdentityStateManager.shared.setNostrBlocked(hex, isBlocked: true)
|
||||||
|
|
||||||
// Remove from participants for all geohashes
|
// Remove from participants for all geohashes
|
||||||
for (gh, var map) in geoParticipants {
|
for (gh, var map) in geoParticipants {
|
||||||
@@ -1932,7 +1921,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
}
|
}
|
||||||
@MainActor
|
@MainActor
|
||||||
func unblockGeohashUser(pubkeyHexLowercased: String, displayName: String) {
|
func unblockGeohashUser(pubkeyHexLowercased: String, displayName: String) {
|
||||||
identityManager.setNostrBlocked(pubkeyHexLowercased, isBlocked: false)
|
SecureIdentityStateManager.shared.setNostrBlocked(pubkeyHexLowercased, isBlocked: false)
|
||||||
addSystemMessage("unblocked \(displayName) in geohash chats")
|
addSystemMessage("unblocked \(displayName) in geohash chats")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1981,7 +1970,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
let content = event.content.trimmingCharacters(in: .whitespacesAndNewlines)
|
let content = event.content.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
guard !content.isEmpty else { return }
|
guard !content.isEmpty else { return }
|
||||||
// Respect geohash blocks
|
// Respect geohash blocks
|
||||||
if identityManager.isNostrBlocked(pubkeyHexLowercased: event.pubkey.lowercased()) { return }
|
if SecureIdentityStateManager.shared.isNostrBlocked(pubkeyHexLowercased: event.pubkey.lowercased()) { return }
|
||||||
// Skip self identity for this geohash
|
// Skip self identity for this geohash
|
||||||
if let my = try? NostrIdentityBridge.deriveIdentity(forGeohash: gh), my.publicKeyHex.lowercased() == event.pubkey.lowercased() { return }
|
if let my = try? NostrIdentityBridge.deriveIdentity(forGeohash: gh), my.publicKeyHex.lowercased() == event.pubkey.lowercased() { return }
|
||||||
// Only trigger when there were zero participants in this geohash recently
|
// Only trigger when there were zero participants in this geohash recently
|
||||||
@@ -2133,7 +2122,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
// Respect geohash blocks
|
// Respect geohash blocks
|
||||||
if identityManager.isNostrBlocked(pubkeyHexLowercased: recipientHex) {
|
if SecureIdentityStateManager.shared.isNostrBlocked(pubkeyHexLowercased: recipientHex) {
|
||||||
if let msgIdx = privateChats[peerID]?.firstIndex(where: { $0.id == messageID }) {
|
if let msgIdx = privateChats[peerID]?.firstIndex(where: { $0.id == messageID }) {
|
||||||
privateChats[peerID]?[msgIdx].deliveryStatus = .failed(reason: "user is blocked")
|
privateChats[peerID]?[msgIdx].deliveryStatus = .failed(reason: "user is blocked")
|
||||||
}
|
}
|
||||||
@@ -2151,7 +2140,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
SecureLogger.debug("GeoDM: local send mid=\(messageID.prefix(8))… to=\(recipientHex.prefix(8))… conv=\(peerID)", category: .session)
|
SecureLogger.debug("GeoDM: local send mid=\(messageID.prefix(8))… to=\(recipientHex.prefix(8))… conv=\(peerID)", category: .session)
|
||||||
let nostrTransport = NostrTransport(keychain: keychain)
|
let nostrTransport = NostrTransport()
|
||||||
nostrTransport.senderPeerID = meshService.myPeerID
|
nostrTransport.senderPeerID = meshService.myPeerID
|
||||||
nostrTransport.sendPrivateMessageGeohash(content: content, toRecipientHex: recipientHex, from: id, messageID: messageID)
|
nostrTransport.sendPrivateMessageGeohash(content: content, toRecipientHex: recipientHex, from: id, messageID: messageID)
|
||||||
if let msgIdx = privateChats[peerID]?.firstIndex(where: { $0.id == messageID }) {
|
if let msgIdx = privateChats[peerID]?.firstIndex(where: { $0.id == messageID }) {
|
||||||
@@ -2809,15 +2798,15 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
meshService.stopServices()
|
meshService.stopServices()
|
||||||
|
|
||||||
// Force save any pending identity changes (verifications, favorites, etc)
|
// Force save any pending identity changes (verifications, favorites, etc)
|
||||||
identityManager.forceSave()
|
SecureIdentityStateManager.shared.forceSave()
|
||||||
|
|
||||||
// Verify identity key is still there
|
// Verify identity key is still there
|
||||||
_ = keychain.verifyIdentityKeyExists()
|
_ = KeychainManager.shared.verifyIdentityKeyExists()
|
||||||
|
|
||||||
// No need to force synchronize here
|
// No need to force synchronize here
|
||||||
|
|
||||||
// Verify identity key after save
|
// Verify identity key after save
|
||||||
_ = keychain.verifyIdentityKeyExists()
|
_ = KeychainManager.shared.verifyIdentityKeyExists()
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc private func appWillTerminate() {
|
@objc private func appWillTerminate() {
|
||||||
@@ -2868,7 +2857,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
for message in messages where message.senderPeerID == peerID && !message.isRelay {
|
for message in messages where message.senderPeerID == peerID && !message.isRelay {
|
||||||
if !sentReadReceipts.contains(message.id) {
|
if !sentReadReceipts.contains(message.id) {
|
||||||
SecureLogger.debug("GeoDM: sending READ for mid=\(message.id.prefix(8))… to=\(recipientHex.prefix(8))…", category: .session)
|
SecureLogger.debug("GeoDM: sending READ for mid=\(message.id.prefix(8))… to=\(recipientHex.prefix(8))…", category: .session)
|
||||||
let nostrTransport = NostrTransport(keychain: keychain)
|
let nostrTransport = NostrTransport()
|
||||||
nostrTransport.senderPeerID = meshService.myPeerID
|
nostrTransport.senderPeerID = meshService.myPeerID
|
||||||
nostrTransport.sendReadReceiptGeohash(message.id, toRecipientHex: recipientHex, from: id)
|
nostrTransport.sendReadReceiptGeohash(message.id, toRecipientHex: recipientHex, from: id)
|
||||||
sentReadReceipts.insert(message.id)
|
sentReadReceipts.insert(message.id)
|
||||||
@@ -3014,7 +3003,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
privateChatManager.unreadMessages.removeAll()
|
privateChatManager.unreadMessages.removeAll()
|
||||||
|
|
||||||
// Delete all keychain data (including Noise and Nostr keys)
|
// Delete all keychain data (including Noise and Nostr keys)
|
||||||
_ = keychain.deleteAllKeychainData()
|
_ = KeychainManager.shared.deleteAllKeychainData()
|
||||||
|
|
||||||
// Clear UserDefaults identity data
|
// Clear UserDefaults identity data
|
||||||
userDefaults.removeObject(forKey: "bitchat.noiseIdentityKey")
|
userDefaults.removeObject(forKey: "bitchat.noiseIdentityKey")
|
||||||
@@ -3030,14 +3019,14 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
|
|
||||||
// Clear favorites and peer mappings
|
// Clear favorites and peer mappings
|
||||||
// Clear through SecureIdentityStateManager instead of directly
|
// Clear through SecureIdentityStateManager instead of directly
|
||||||
identityManager.clearAllIdentityData()
|
SecureIdentityStateManager.shared.clearAllIdentityData()
|
||||||
peerIDToPublicKeyFingerprint.removeAll()
|
peerIDToPublicKeyFingerprint.removeAll()
|
||||||
|
|
||||||
// Clear persistent favorites from keychain
|
// Clear persistent favorites from keychain
|
||||||
FavoritesPersistenceService.shared.clearAllFavorites()
|
FavoritesPersistenceService.shared.clearAllFavorites()
|
||||||
|
|
||||||
// Clear identity data from secure storage
|
// Clear identity data from secure storage
|
||||||
identityManager.clearAllIdentityData()
|
SecureIdentityStateManager.shared.clearAllIdentityData()
|
||||||
|
|
||||||
// Clear autocomplete state
|
// Clear autocomplete state
|
||||||
autocompleteSuggestions.removeAll()
|
autocompleteSuggestions.removeAll()
|
||||||
@@ -4263,7 +4252,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
|
|
||||||
// Try to resolve through fingerprint and social identity
|
// Try to resolve through fingerprint and social identity
|
||||||
if let fingerprint = getFingerprint(for: peerID) {
|
if let fingerprint = getFingerprint(for: peerID) {
|
||||||
if let identity = identityManager.getSocialIdentity(for: fingerprint) {
|
if let identity = SecureIdentityStateManager.shared.getSocialIdentity(for: fingerprint) {
|
||||||
// Prefer local petname if set
|
// Prefer local petname if set
|
||||||
if let petname = identity.localPetname {
|
if let petname = identity.localPetname {
|
||||||
return petname
|
return petname
|
||||||
@@ -4295,7 +4284,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
guard let fingerprint = getFingerprint(for: peerID) else { return }
|
guard let fingerprint = getFingerprint(for: peerID) else { return }
|
||||||
|
|
||||||
// Update secure storage with verified status
|
// Update secure storage with verified status
|
||||||
identityManager.setVerified(fingerprint: fingerprint, verified: true)
|
SecureIdentityStateManager.shared.setVerified(fingerprint: fingerprint, verified: true)
|
||||||
|
|
||||||
// Update local set for UI
|
// Update local set for UI
|
||||||
verifiedFingerprints.insert(fingerprint)
|
verifiedFingerprints.insert(fingerprint)
|
||||||
@@ -4307,8 +4296,8 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
@MainActor
|
@MainActor
|
||||||
func unverifyFingerprint(for peerID: String) {
|
func unverifyFingerprint(for peerID: String) {
|
||||||
guard let fingerprint = getFingerprint(for: peerID) else { return }
|
guard let fingerprint = getFingerprint(for: peerID) else { return }
|
||||||
identityManager.setVerified(fingerprint: fingerprint, verified: false)
|
SecureIdentityStateManager.shared.setVerified(fingerprint: fingerprint, verified: false)
|
||||||
identityManager.forceSave()
|
SecureIdentityStateManager.shared.forceSave()
|
||||||
verifiedFingerprints.remove(fingerprint)
|
verifiedFingerprints.remove(fingerprint)
|
||||||
updateEncryptionStatus(for: peerID)
|
updateEncryptionStatus(for: peerID)
|
||||||
}
|
}
|
||||||
@@ -4316,7 +4305,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
@MainActor
|
@MainActor
|
||||||
func loadVerifiedFingerprints() {
|
func loadVerifiedFingerprints() {
|
||||||
// Load verified fingerprints directly from secure storage
|
// Load verified fingerprints directly from secure storage
|
||||||
verifiedFingerprints = identityManager.getVerifiedFingerprints()
|
verifiedFingerprints = SecureIdentityStateManager.shared.getVerifiedFingerprints()
|
||||||
// Log snapshot for debugging persistence
|
// Log snapshot for debugging persistence
|
||||||
let sample = Array(verifiedFingerprints.prefix(TransportConfig.uiFingerprintSampleCount)).map { $0.prefix(8) }.joined(separator: ", ")
|
let sample = Array(verifiedFingerprints.prefix(TransportConfig.uiFingerprintSampleCount)).map { $0.prefix(8) }.joined(separator: ", ")
|
||||||
SecureLogger.info("🔐 Verified loaded: \(verifiedFingerprints.count) [\(sample)]", category: .security)
|
SecureLogger.info("🔐 Verified loaded: \(verifiedFingerprints.count) [\(sample)]", category: .security)
|
||||||
@@ -4516,8 +4505,8 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
if let fp = getFingerprint(for: peerID) {
|
if let fp = getFingerprint(for: peerID) {
|
||||||
let short = fp.prefix(8)
|
let short = fp.prefix(8)
|
||||||
SecureLogger.info("🔐 Marking verified fingerprint: \(short)", category: .security)
|
SecureLogger.info("🔐 Marking verified fingerprint: \(short)", category: .security)
|
||||||
identityManager.setVerified(fingerprint: fp, verified: true)
|
SecureIdentityStateManager.shared.setVerified(fingerprint: fp, verified: true)
|
||||||
identityManager.forceSave()
|
SecureIdentityStateManager.shared.forceSave()
|
||||||
verifiedFingerprints.insert(fp)
|
verifiedFingerprints.insert(fp)
|
||||||
let name = unifiedPeerService.getPeer(by: peerID)?.nickname ?? resolveNickname(for: peerID)
|
let name = unifiedPeerService.getPeer(by: peerID)?.nickname ?? resolveNickname(for: peerID)
|
||||||
NotificationService.shared.sendLocalNotification(
|
NotificationService.shared.sendLocalNotification(
|
||||||
@@ -4608,10 +4597,18 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
isConnected = true
|
isConnected = true
|
||||||
|
|
||||||
// Register ephemeral session with identity manager
|
// Register ephemeral session with identity manager
|
||||||
identityManager.registerEphemeralSession(peerID: peerID, handshakeState: .none)
|
SecureIdentityStateManager.shared.registerEphemeralSession(peerID: peerID)
|
||||||
|
|
||||||
// Intentionally do not resend favorites on reconnect.
|
// Check if we favorite this peer and resend notification on reconnect
|
||||||
// We only send our npub when a favorite is toggled on, or if our npub changes.
|
// This ensures Nostr key mapping is maintained across reconnections
|
||||||
|
if let peer = unifiedPeerService.getPeer(by: peerID),
|
||||||
|
let favoriteStatus = FavoritesPersistenceService.shared.getFavoriteStatus(for: peer.noisePublicKey),
|
||||||
|
favoriteStatus.isFavorite {
|
||||||
|
// Resend favorite notification with our Nostr key after a short delay
|
||||||
|
try? await Task.sleep(nanoseconds: TransportConfig.uiAsyncMediumSleepNs) // 0.5 seconds
|
||||||
|
meshService.sendFavoriteNotification(to: peerID, isFavorite: true)
|
||||||
|
SecureLogger.debug("📤 Resent favorite notification to reconnected peer \(peerID)", category: .session)
|
||||||
|
}
|
||||||
|
|
||||||
// Force UI refresh
|
// Force UI refresh
|
||||||
objectWillChange.send()
|
objectWillChange.send()
|
||||||
@@ -4633,7 +4630,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
SecureLogger.debug("👋 Peer disconnected: \(peerID)", category: .session)
|
SecureLogger.debug("👋 Peer disconnected: \(peerID)", category: .session)
|
||||||
|
|
||||||
// Remove ephemeral session from identity manager
|
// Remove ephemeral session from identity manager
|
||||||
identityManager.removeEphemeralSession(peerID: peerID)
|
SecureIdentityStateManager.shared.removeEphemeralSession(peerID: peerID)
|
||||||
|
|
||||||
// If the open PM is tied to this short peer ID, switch UI context to the full Noise key (offline favorite)
|
// If the open PM is tied to this short peer ID, switch UI context to the full Noise key (offline favorite)
|
||||||
var derivedStableKeyHex: String? = shortIDToNoiseKey[peerID]
|
var derivedStableKeyHex: String? = shortIDToNoiseKey[peerID]
|
||||||
@@ -4740,7 +4737,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
|
|
||||||
// Register ephemeral sessions for all connected peers
|
// Register ephemeral sessions for all connected peers
|
||||||
for peerID in peers {
|
for peerID in peers {
|
||||||
self.identityManager.registerEphemeralSession(peerID: peerID, handshakeState: .none)
|
SecureIdentityStateManager.shared.registerEphemeralSession(peerID: peerID)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Schedule UI refresh to ensure offline favorites are shown
|
// Schedule UI refresh to ensure offline favorites are shown
|
||||||
@@ -4892,7 +4889,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func isFavorite(fingerprint: String) -> Bool {
|
func isFavorite(fingerprint: String) -> Bool {
|
||||||
return identityManager.isFavorite(fingerprint: fingerprint)
|
return SecureIdentityStateManager.shared.isFavorite(fingerprint: fingerprint)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Delivery Tracking
|
// MARK: - Delivery Tracking
|
||||||
@@ -4960,26 +4957,6 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
messages.append(systemMessage)
|
messages.append(systemMessage)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Add a system message to the mesh timeline only (never geohash).
|
|
||||||
/// If mesh is currently active, also append to the visible `messages`.
|
|
||||||
@MainActor
|
|
||||||
private func addMeshOnlySystemMessage(_ content: String) {
|
|
||||||
let systemMessage = BitchatMessage(
|
|
||||||
sender: "system",
|
|
||||||
content: content,
|
|
||||||
timestamp: Date(),
|
|
||||||
isRelay: false
|
|
||||||
)
|
|
||||||
// Persist to mesh timeline
|
|
||||||
meshTimeline.append(systemMessage)
|
|
||||||
trimMeshTimelineIfNeeded()
|
|
||||||
// Only show inline if mesh is the active channel
|
|
||||||
if case .mesh = activeChannel {
|
|
||||||
messages.append(systemMessage)
|
|
||||||
}
|
|
||||||
objectWillChange.send()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Public helper to add a system message to the public chat timeline.
|
/// Public helper to add a system message to the public chat timeline.
|
||||||
/// Also persists the message into the active channel's backing store so it survives timeline rebinds.
|
/// Also persists the message into the active channel's backing store so it survives timeline rebinds.
|
||||||
@MainActor
|
@MainActor
|
||||||
@@ -5222,7 +5199,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
messageRouter.sendDeliveryAck(messageId, to: key.hexEncodedString())
|
messageRouter.sendDeliveryAck(messageId, to: key.hexEncodedString())
|
||||||
} else if let id = try? NostrIdentityBridge.getCurrentNostrIdentity() {
|
} else if let id = try? NostrIdentityBridge.getCurrentNostrIdentity() {
|
||||||
// Fallback: no Noise mapping yet — send directly to sender's Nostr pubkey
|
// Fallback: no Noise mapping yet — send directly to sender's Nostr pubkey
|
||||||
let nt = NostrTransport(keychain: keychain)
|
let nt = NostrTransport()
|
||||||
nt.senderPeerID = meshService.myPeerID
|
nt.senderPeerID = meshService.myPeerID
|
||||||
nt.sendDeliveryAckGeohash(for: messageId, toRecipientHex: senderPubkey, from: id)
|
nt.sendDeliveryAckGeohash(for: messageId, toRecipientHex: senderPubkey, from: id)
|
||||||
SecureLogger.debug("Sent DELIVERED ack directly to Nostr pub=\(senderPubkey.prefix(8))… for mid=\(messageId.prefix(8))…", category: .session)
|
SecureLogger.debug("Sent DELIVERED ack directly to Nostr pub=\(senderPubkey.prefix(8))… for mid=\(messageId.prefix(8))…", category: .session)
|
||||||
@@ -5244,7 +5221,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
messageRouter.sendReadReceipt(receipt, to: key.hexEncodedString())
|
messageRouter.sendReadReceipt(receipt, to: key.hexEncodedString())
|
||||||
sentReadReceipts.insert(messageId)
|
sentReadReceipts.insert(messageId)
|
||||||
} else if let id = try? NostrIdentityBridge.getCurrentNostrIdentity() {
|
} else if let id = try? NostrIdentityBridge.getCurrentNostrIdentity() {
|
||||||
let nt = NostrTransport(keychain: keychain)
|
let nt = NostrTransport()
|
||||||
nt.senderPeerID = meshService.myPeerID
|
nt.senderPeerID = meshService.myPeerID
|
||||||
nt.sendReadReceiptGeohash(messageId, toRecipientHex: senderPubkey, from: id)
|
nt.sendReadReceiptGeohash(messageId, toRecipientHex: senderPubkey, from: id)
|
||||||
sentReadReceipts.insert(messageId)
|
sentReadReceipts.insert(messageId)
|
||||||
@@ -5407,10 +5384,8 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
SecureLogger.warning("⚠️ Cannot get Noise key for peer \(peerID)", category: .session)
|
SecureLogger.warning("⚠️ Cannot get Noise key for peer \(peerID)", category: .session)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// Determine prior state to avoid duplicate system messages on repeated notifications
|
|
||||||
let prior = FavoritesPersistenceService.shared.getFavoriteStatus(for: finalNoiseKey)?.theyFavoritedUs ?? false
|
|
||||||
|
|
||||||
// Update the favorite relationship (idempotent storage)
|
// Update the favorite relationship
|
||||||
FavoritesPersistenceService.shared.updatePeerFavoritedUs(
|
FavoritesPersistenceService.shared.updatePeerFavoritedUs(
|
||||||
peerNoisePublicKey: finalNoiseKey,
|
peerNoisePublicKey: finalNoiseKey,
|
||||||
favorited: isFavorite,
|
favorited: isFavorite,
|
||||||
@@ -5418,16 +5393,14 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
peerNostrPublicKey: nostrPubkey
|
peerNostrPublicKey: nostrPubkey
|
||||||
)
|
)
|
||||||
|
|
||||||
// If they favorited us and provided their Nostr key, ensure it's stored (log only)
|
// If they favorited us and provided their Nostr key, ensure it's stored
|
||||||
if isFavorite && nostrPubkey != nil {
|
if isFavorite && nostrPubkey != nil {
|
||||||
SecureLogger.info("💾 Storing Nostr key association for \(senderNickname): \(nostrPubkey!.prefix(16))...", category: .session)
|
SecureLogger.info("💾 Storing Nostr key association for \(senderNickname): \(nostrPubkey!.prefix(16))...", category: .session)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only show a system message when the state changes, and only in mesh
|
// Show system message
|
||||||
if prior != isFavorite {
|
|
||||||
let action = isFavorite ? "favorited" : "unfavorited"
|
let action = isFavorite ? "favorited" : "unfavorited"
|
||||||
addMeshOnlySystemMessage("\(senderNickname) \(action) you")
|
addSystemMessage("\(senderNickname) \(action) you")
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
@@ -5623,7 +5596,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
// Check geohash (Nostr) blocks using mapping to full pubkey
|
// Check geohash (Nostr) blocks using mapping to full pubkey
|
||||||
if peerID.hasPrefix("nostr") {
|
if peerID.hasPrefix("nostr") {
|
||||||
if let full = nostrKeyMapping[peerID]?.lowercased() {
|
if let full = nostrKeyMapping[peerID]?.lowercased() {
|
||||||
if identityManager.isNostrBlocked(pubkeyHexLowercased: full) { return true }
|
if SecureIdentityStateManager.shared.isNostrBlocked(pubkeyHexLowercased: full) { return true }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
|
|||||||
@@ -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] = [:]
|
||||||
@@ -1096,7 +1091,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 +1170,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 +1180,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 +1201,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 +1247,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 +1399,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
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
@@ -3,22 +3,9 @@ import XCTest
|
|||||||
|
|
||||||
final class CommandProcessorTests: XCTestCase {
|
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
|
@MainActor
|
||||||
func test_slap_notFoundGrammar() {
|
func test_slap_notFoundGrammar() {
|
||||||
let processor = CommandProcessor(chatViewModel: nil, meshService: nil, identityManager: identityManager)
|
let processor = CommandProcessor(chatViewModel: nil, meshService: nil)
|
||||||
let result = processor.process("/slap @system")
|
let result = processor.process("/slap @system")
|
||||||
switch result {
|
switch result {
|
||||||
case .error(let message):
|
case .error(let message):
|
||||||
@@ -30,7 +17,7 @@ final class CommandProcessorTests: XCTestCase {
|
|||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
func test_hug_notFoundGrammar() {
|
func test_hug_notFoundGrammar() {
|
||||||
let processor = CommandProcessor(chatViewModel: nil, meshService: nil, identityManager: identityManager)
|
let processor = CommandProcessor(chatViewModel: nil, meshService: nil)
|
||||||
let result = processor.process("/hug @system")
|
let result = processor.process("/hug @system")
|
||||||
switch result {
|
switch result {
|
||||||
case .error(let message):
|
case .error(let message):
|
||||||
@@ -42,7 +29,7 @@ final class CommandProcessorTests: XCTestCase {
|
|||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
func test_slap_usageMessage() {
|
func test_slap_usageMessage() {
|
||||||
let processor = CommandProcessor(chatViewModel: nil, meshService: nil, identityManager: identityManager)
|
let processor = CommandProcessor(chatViewModel: nil, meshService: nil)
|
||||||
let result = processor.process("/slap")
|
let result = processor.process("/slap")
|
||||||
switch result {
|
switch result {
|
||||||
case .error(let message):
|
case .error(let message):
|
||||||
@@ -52,3 +39,4 @@ final class CommandProcessorTests: XCTestCase {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user