Remove all channel functionality and clean up test suite

- Remove channel UI elements from ContentView
- Remove channel data structures and methods from ChatViewModel
- Remove channel commands (/j, /leave, /channels)
- Remove channel field from BitchatMessage protocol
- Remove channel message types and handling
- Remove NoiseChannelEncryption.swift entirely
- Clean up all channel references across the codebase
- Fix compilation warnings (var to let conversions)
- Remove all outdated test files that used incorrect APIs
- Simplify app to only support public broadcast and 1:1 private messages
This commit is contained in:
jack 2025-07-23 01:33:54 +02:00
parent 36bda0821f
commit f53e163d25
40 changed files with 112 additions and 9357 deletions

View File

@ -16,61 +16,22 @@
04891CAA2E22971E0064A111 /* LRUCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04891CA82E22971E0064A111 /* LRUCache.swift */; };
04AD0B4E2E25B9580002A40A /* IdentityModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E2446380E7A44E49A35B664 /* IdentityModels.swift */; };
04AD0B4F2E25B9580002A40A /* SecureIdentityStateManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2EE9D4FA625C4671ACD371D4 /* SecureIdentityStateManager.swift */; };
04AD0B542E2678220002A40A /* VersionNegotiationIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04AD0B522E2678220002A40A /* VersionNegotiationIntegrationTests.swift */; };
04AD0B552E2678220002A40A /* BinaryProtocolVersionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04AD0B502E2678220002A40A /* BinaryProtocolVersionTests.swift */; };
04AD0B562E2678220002A40A /* ProtocolVersionNegotiationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04AD0B512E2678220002A40A /* ProtocolVersionNegotiationTests.swift */; };
04AD0B572E2678220002A40A /* VersionNegotiationScenarioTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04AD0B532E2678220002A40A /* VersionNegotiationScenarioTests.swift */; };
04AD0B582E2678220002A40A /* VersionNegotiationIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04AD0B522E2678220002A40A /* VersionNegotiationIntegrationTests.swift */; };
04AD0B592E2678220002A40A /* BinaryProtocolVersionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04AD0B502E2678220002A40A /* BinaryProtocolVersionTests.swift */; };
04AD0B5A2E2678220002A40A /* ProtocolVersionNegotiationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04AD0B512E2678220002A40A /* ProtocolVersionNegotiationTests.swift */; };
04AD0B5B2E2678220002A40A /* VersionNegotiationScenarioTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04AD0B532E2678220002A40A /* VersionNegotiationScenarioTests.swift */; };
04B6BA3E2E2035220090FE39 /* NoiseProtocolTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04B6BA3D2E2035220090FE39 /* NoiseProtocolTests.swift */; };
04B6BA3F2E2035220090FE39 /* NoiseProtocolTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04B6BA3D2E2035220090FE39 /* NoiseProtocolTests.swift */; };
04B6BA452E2035530090FE39 /* NoiseSecurityConsiderations.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04B6BA422E2035530090FE39 /* NoiseSecurityConsiderations.swift */; };
04B6BA462E2035530090FE39 /* NoiseSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04B6BA432E2035530090FE39 /* NoiseSession.swift */; };
04B6BA472E2035530090FE39 /* NoiseProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04B6BA412E2035530090FE39 /* NoiseProtocol.swift */; };
04B6BA482E2035530090FE39 /* NoiseChannelEncryption.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04B6BA402E2035530090FE39 /* NoiseChannelEncryption.swift */; };
04B6BA492E2035530090FE39 /* NoiseSecurityConsiderations.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04B6BA422E2035530090FE39 /* NoiseSecurityConsiderations.swift */; };
04B6BA4A2E2035530090FE39 /* NoiseSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04B6BA432E2035530090FE39 /* NoiseSession.swift */; };
04B6BA4B2E2035530090FE39 /* NoiseProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04B6BA412E2035530090FE39 /* NoiseProtocol.swift */; };
04B6BA4C2E2035530090FE39 /* NoiseChannelEncryption.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04B6BA402E2035530090FE39 /* NoiseChannelEncryption.swift */; };
04B6BA4E2E2038A70090FE39 /* NoiseEncryptionService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04B6BA4D2E2038A70090FE39 /* NoiseEncryptionService.swift */; };
04B6BA4F2E2038A70090FE39 /* NoiseEncryptionService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04B6BA4D2E2038A70090FE39 /* NoiseEncryptionService.swift */; };
04B6BA552E203D6C0090FE39 /* FingerprintView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04B6BA532E203D6C0090FE39 /* FingerprintView.swift */; };
04B6BA562E203D6C0090FE39 /* NoiseTestingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04B6BA542E203D6C0090FE39 /* NoiseTestingView.swift */; };
04B6BA572E203D6C0090FE39 /* FingerprintView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04B6BA532E203D6C0090FE39 /* FingerprintView.swift */; };
04B6BA582E203D6C0090FE39 /* NoiseTestingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04B6BA542E203D6C0090FE39 /* NoiseTestingView.swift */; };
04B6BA5B2E2041220090FE39 /* NoiseIdentityPersistenceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04B6BA5A2E2041220090FE39 /* NoiseIdentityPersistenceTests.swift */; };
04B6BA5C2E2041220090FE39 /* NoiseIdentityPersistenceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04B6BA5A2E2041220090FE39 /* NoiseIdentityPersistenceTests.swift */; };
04B6BA5C2E215FDA0090FE39 /* NoiseChannelKeyRotation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04B6BA592E215FDA0090FE39 /* NoiseChannelKeyRotation.swift */; };
04B6BA5E2E215FDA0090FE39 /* NoiseChannelKeyRotation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04B6BA592E215FDA0090FE39 /* NoiseChannelKeyRotation.swift */; };
04B6BA672E21601B0090FE39 /* ChannelVerificationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04B6BA5F2E21601B0090FE39 /* ChannelVerificationTests.swift */; };
04B6BA682E21601B0090FE39 /* NoiseKeyRotationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04B6BA622E21601B0090FE39 /* NoiseKeyRotationTests.swift */; };
04B6BA692E21601B0090FE39 /* NoiseRateLimiterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04B6BA632E21601B0090FE39 /* NoiseRateLimiterTests.swift */; };
04B6BA6A2E21601B0090FE39 /* NoiseSecurityTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04B6BA642E21601B0090FE39 /* NoiseSecurityTests.swift */; };
04B6BA6B2E21601B0090FE39 /* NoiseSecurityValidatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04B6BA652E21601B0090FE39 /* NoiseSecurityValidatorTests.swift */; };
04B6BA6C2E21601B0090FE39 /* SecureNoiseSessionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04B6BA662E21601B0090FE39 /* SecureNoiseSessionTests.swift */; };
04B6BA6D2E21601B0090FE39 /* KeychainIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04B6BA602E21601B0090FE39 /* KeychainIntegrationTests.swift */; };
04B6BA6E2E21601B0090FE39 /* NoiseChannelEncryptionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04B6BA612E21601B0090FE39 /* NoiseChannelEncryptionTests.swift */; };
04B6BA6F2E21601B0090FE39 /* ChannelVerificationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04B6BA5F2E21601B0090FE39 /* ChannelVerificationTests.swift */; };
04B6BA702E21601B0090FE39 /* NoiseKeyRotationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04B6BA622E21601B0090FE39 /* NoiseKeyRotationTests.swift */; };
04B6BA712E21601B0090FE39 /* NoiseRateLimiterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04B6BA632E21601B0090FE39 /* NoiseRateLimiterTests.swift */; };
04B6BA722E21601B0090FE39 /* NoiseSecurityTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04B6BA642E21601B0090FE39 /* NoiseSecurityTests.swift */; };
04B6BA732E21601B0090FE39 /* NoiseSecurityValidatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04B6BA652E21601B0090FE39 /* NoiseSecurityValidatorTests.swift */; };
04B6BA742E21601B0090FE39 /* SecureNoiseSessionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04B6BA662E21601B0090FE39 /* SecureNoiseSessionTests.swift */; };
04B6BA752E21601B0090FE39 /* KeychainIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04B6BA602E21601B0090FE39 /* KeychainIntegrationTests.swift */; };
04B6BA762E21601B0090FE39 /* NoiseChannelEncryptionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04B6BA612E21601B0090FE39 /* NoiseChannelEncryptionTests.swift */; };
04B6BA792E2166A50090FE39 /* NoiseTestingHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04B6BA772E2166A50090FE39 /* NoiseTestingHelper.swift */; };
04B6BA7B2E2166A50090FE39 /* NoiseTestingHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04B6BA772E2166A50090FE39 /* NoiseTestingHelper.swift */; };
0DAFF1DDE9BA83FF648D5AB3 /* BitchatMessageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3FA8FF26ABDC1C642A8C7AE5 /* BitchatMessageTests.swift */; };
0FBC81FF78CF4711B78E092A /* IdentityModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E2446380E7A44E49A35B664 /* IdentityModels.swift */; };
10E68BB889356219189E38EC /* BitchatApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = EF625BB3AD919322C01A46B2 /* BitchatApp.swift */; };
17901751FD8010AFC8E750F2 /* bitchatShareExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 61F92EBA29C47C0FCC482F1F /* bitchatShareExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
1AF9F9036DEE42408D557A87 /* SecureIdentityStateManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2EE9D4FA625C4671ACD371D4 /* SecureIdentityStateManager.swift */; };
1D9674FA5F998503831DC281 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A08E03AA0C63E97C91749AEC /* ContentView.swift */; };
1F48A8CEEE9399D1EBD08F0C /* OptimizedBloomFilter.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB043CA5EEB9AC8B07D61E97 /* OptimizedBloomFilter.swift */; };
24F17B1446E13F42652B7B08 /* PasswordProtectedChannelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 036A1A705AAF9EC21F4354BE /* PasswordProtectedChannelTests.swift */; };
2E71E320EA921498C57E023B /* BitchatMessageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3FA8FF26ABDC1C642A8C7AE5 /* BitchatMessageTests.swift */; };
31D147471B9F4E2815352DDA /* LinkPreviewView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9AC141774F6671FCDC347DC7 /* LinkPreviewView.swift */; };
4B747085D07A1BCE0F5BA612 /* BinaryProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2136C3E22D02D4A8DBE7EAB /* BinaryProtocol.swift */; };
5D95F2BFBE257A1225998389 /* BatteryOptimizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED176FF3B274E35C2D827894 /* BatteryOptimizer.swift */; };
@ -83,29 +44,22 @@
7A5B1AB5642FEC168E917949 /* LinkPreviewView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9AC141774F6671FCDC347DC7 /* LinkPreviewView.swift */; };
7DCA0DBCB8884E3B31C7BCE3 /* CompressionUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32F149C43D1915831B60FE09 /* CompressionUtil.swift */; };
7DD72D928FF9DD3CA81B46B0 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 3A69677D382F1C3D5ED03F7D /* Assets.xcassets */; };
846E2B446E36639159704730 /* BloomFilterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EB3A8FE16333ED12FCB8ACB /* BloomFilterTests.swift */; };
8DCEEA289EF7C49E7CD38B08 /* DeliveryTracker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 12B9C3EDF3BC73D3BC106DA4 /* DeliveryTracker.swift */; };
8F0BFC2D2B2A5E7B70C3B485 /* BinaryProtocolTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53D535D9CE0B875F47402290 /* BinaryProtocolTests.swift */; };
8F737CE0435792CC2AD65FCB /* KeychainManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 136696FC4436A02D98CE6A77 /* KeychainManager.swift */; };
923027D6F2F417AFA2488127 /* BitchatProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 229F17B68CFF7AB1BC91C847 /* BitchatProtocol.swift */; };
9269B4230187A9EA969BEDB7 /* PasswordProtectedChannelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 036A1A705AAF9EC21F4354BE /* PasswordProtectedChannelTests.swift */; };
92D34E7A07C990C8A815B0CE /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A08E03AA0C63E97C91749AEC /* ContentView.swift */; };
9C7D287C8E67AAE576A5ECB7 /* ShareViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1B378C16594575FCC7F9C75 /* ShareViewController.swift */; };
ABAF130D88561F4A646F0430 /* AppInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 763E0DBA9492A654FC0CDCB9 /* AppInfoView.swift */; };
AD11E46940D742AEAF547EB2 /* AppInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 763E0DBA9492A654FC0CDCB9 /* AppInfoView.swift */; };
ADC66F95FBD513B10411ADB3 /* MessagePaddingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8DE9CDF66D4E52D268851048 /* MessagePaddingTests.swift */; };
B0CA7796B2B2AC2B33F84548 /* CompressionUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32F149C43D1915831B60FE09 /* CompressionUtil.swift */; };
BCCFEDC1EBE59323C3C470BF /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 3A69677D382F1C3D5ED03F7D /* Assets.xcassets */; };
C0A80BA73EC1A372B9338E3C /* BatteryOptimizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED176FF3B274E35C2D827894 /* BatteryOptimizer.swift */; };
C91FDE97070433E6CFE95C55 /* BloomFilterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EB3A8FE16333ED12FCB8ACB /* BloomFilterTests.swift */; };
C99763A4761567F587D21688 /* MessageRetryService.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA4D7595A613F7ED3B386132 /* MessageRetryService.swift */; };
CD0AE423F03AC52BAFC16834 /* DeliveryTracker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 12B9C3EDF3BC73D3BC106DA4 /* DeliveryTracker.swift */; };
CDAD6629EB69916B95C80DAF /* BinaryProtocolTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53D535D9CE0B875F47402290 /* BinaryProtocolTests.swift */; };
CEAE115C9C3EB3C4ED82F128 /* MessageRetryService.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA4D7595A613F7ED3B386132 /* MessageRetryService.swift */; };
D450CF41F207BDE1A1AAA56E /* ChatViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6B8F7B7D55092C2540A7996 /* ChatViewModel.swift */; };
D948085736ED8E736C1DE3B0 /* BluetoothMeshService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5C3D880FF8AE1673B20E1E3 /* BluetoothMeshService.swift */; };
E65BBB6544FE0159F3C6C3A8 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 95F16C3A4A5621C74461D8D3 /* LaunchScreen.storyboard */; };
F00B713D5053617FB5F3F1BE /* MessagePaddingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8DE9CDF66D4E52D268851048 /* MessagePaddingTests.swift */; };
F455F011B3B648ADA233F998 /* BinaryProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2136C3E22D02D4A8DBE7EAB /* BinaryProtocol.swift */; };
FB8819B4C84FAFEF5C36B216 /* KeychainManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 136696FC4436A02D98CE6A77 /* KeychainManager.swift */; };
/* End PBXBuildFile section */
@ -149,37 +103,17 @@
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
036A1A705AAF9EC21F4354BE /* PasswordProtectedChannelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PasswordProtectedChannelTests.swift; sourceTree = "<group>"; };
03C57F452B55FD0FD8F51421 /* bitchatTests_macOS.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = bitchatTests_macOS.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
04636BB82E2FAA1700FBCFA8 /* BinaryEncodingUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BinaryEncodingUtils.swift; sourceTree = "<group>"; };
04636BBE2E2FCA8A00FBCFA8 /* SecureLogger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureLogger.swift; sourceTree = "<group>"; };
04891CA82E22971E0064A111 /* LRUCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LRUCache.swift; sourceTree = "<group>"; };
04AD0B502E2678220002A40A /* BinaryProtocolVersionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BinaryProtocolVersionTests.swift; sourceTree = "<group>"; };
04AD0B512E2678220002A40A /* ProtocolVersionNegotiationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProtocolVersionNegotiationTests.swift; sourceTree = "<group>"; };
04AD0B522E2678220002A40A /* VersionNegotiationIntegrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VersionNegotiationIntegrationTests.swift; sourceTree = "<group>"; };
04AD0B532E2678220002A40A /* VersionNegotiationScenarioTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VersionNegotiationScenarioTests.swift; sourceTree = "<group>"; };
04B6BA3D2E2035220090FE39 /* NoiseProtocolTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoiseProtocolTests.swift; sourceTree = "<group>"; };
04B6BA402E2035530090FE39 /* NoiseChannelEncryption.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoiseChannelEncryption.swift; sourceTree = "<group>"; };
04B6BA412E2035530090FE39 /* NoiseProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoiseProtocol.swift; sourceTree = "<group>"; };
04B6BA422E2035530090FE39 /* NoiseSecurityConsiderations.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoiseSecurityConsiderations.swift; sourceTree = "<group>"; };
04B6BA432E2035530090FE39 /* NoiseSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoiseSession.swift; sourceTree = "<group>"; };
04B6BA4D2E2038A70090FE39 /* NoiseEncryptionService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoiseEncryptionService.swift; sourceTree = "<group>"; };
04B6BA532E203D6C0090FE39 /* FingerprintView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FingerprintView.swift; sourceTree = "<group>"; };
04B6BA542E203D6C0090FE39 /* NoiseTestingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoiseTestingView.swift; sourceTree = "<group>"; };
04B6BA592E215FDA0090FE39 /* NoiseChannelKeyRotation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoiseChannelKeyRotation.swift; sourceTree = "<group>"; };
04B6BA5A2E2041220090FE39 /* NoiseIdentityPersistenceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoiseIdentityPersistenceTests.swift; sourceTree = "<group>"; };
04B6BA5F2E21601B0090FE39 /* ChannelVerificationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChannelVerificationTests.swift; sourceTree = "<group>"; };
04B6BA602E21601B0090FE39 /* KeychainIntegrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainIntegrationTests.swift; sourceTree = "<group>"; };
04B6BA612E21601B0090FE39 /* NoiseChannelEncryptionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoiseChannelEncryptionTests.swift; sourceTree = "<group>"; };
04B6BA622E21601B0090FE39 /* NoiseKeyRotationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoiseKeyRotationTests.swift; sourceTree = "<group>"; };
04B6BA632E21601B0090FE39 /* NoiseRateLimiterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoiseRateLimiterTests.swift; sourceTree = "<group>"; };
04B6BA642E21601B0090FE39 /* NoiseSecurityTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoiseSecurityTests.swift; sourceTree = "<group>"; };
04B6BA652E21601B0090FE39 /* NoiseSecurityValidatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoiseSecurityValidatorTests.swift; sourceTree = "<group>"; };
04B6BA662E21601B0090FE39 /* SecureNoiseSessionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureNoiseSessionTests.swift; sourceTree = "<group>"; };
04B6BA772E2166A50090FE39 /* NoiseTestingHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoiseTestingHelper.swift; sourceTree = "<group>"; };
12B9C3EDF3BC73D3BC106DA4 /* DeliveryTracker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeliveryTracker.swift; sourceTree = "<group>"; };
136696FC4436A02D98CE6A77 /* KeychainManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainManager.swift; sourceTree = "<group>"; };
1EB3A8FE16333ED12FCB8ACB /* BloomFilterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BloomFilterTests.swift; sourceTree = "<group>"; };
229F17B68CFF7AB1BC91C847 /* BitchatProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BitchatProtocol.swift; sourceTree = "<group>"; };
2EE9D4FA625C4671ACD371D4 /* SecureIdentityStateManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureIdentityStateManager.swift; sourceTree = "<group>"; };
32F149C43D1915831B60FE09 /* CompressionUtil.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompressionUtil.swift; sourceTree = "<group>"; };
@ -187,13 +121,10 @@
3668EEBB42FD4A24D5D83B7B /* bitchatShareExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = bitchatShareExtension.entitlements; sourceTree = "<group>"; };
3A556661F74B7D5AE2F0521B /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; };
3A69677D382F1C3D5ED03F7D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
3FA8FF26ABDC1C642A8C7AE5 /* BitchatMessageTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BitchatMessageTests.swift; sourceTree = "<group>"; };
527EB217EFDFAD4CF1C91F07 /* bitchat.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = bitchat.entitlements; sourceTree = "<group>"; };
53D535D9CE0B875F47402290 /* BinaryProtocolTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BinaryProtocolTests.swift; sourceTree = "<group>"; };
61F92EBA29C47C0FCC482F1F /* bitchatShareExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = bitchatShareExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
6E2446380E7A44E49A35B664 /* IdentityModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IdentityModels.swift; sourceTree = "<group>"; };
763E0DBA9492A654FC0CDCB9 /* AppInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppInfoView.swift; sourceTree = "<group>"; };
8DE9CDF66D4E52D268851048 /* MessagePaddingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessagePaddingTests.swift; sourceTree = "<group>"; };
8F3A7C058C2C8E1A06C8CF8B /* bitchat.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = bitchat.app; sourceTree = BUILT_PRODUCTS_DIR; };
95F16C3A4A5621C74461D8D3 /* LaunchScreen.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = LaunchScreen.storyboard; sourceTree = "<group>"; };
96D0D41CA19EE5A772AA8434 /* bitchat.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = bitchat.app; sourceTree = BUILT_PRODUCTS_DIR; };
@ -217,8 +148,6 @@
04B6BA442E2035530090FE39 /* Noise */ = {
isa = PBXGroup;
children = (
04B6BA592E215FDA0090FE39 /* NoiseChannelKeyRotation.swift */,
04B6BA402E2035530090FE39 /* NoiseChannelEncryption.swift */,
04B6BA412E2035530090FE39 /* NoiseProtocol.swift */,
04B6BA422E2035530090FE39 /* NoiseSecurityConsiderations.swift */,
04B6BA432E2035530090FE39 /* NoiseSession.swift */,
@ -278,7 +207,6 @@
children = (
04636BBE2E2FCA8A00FBCFA8 /* SecureLogger.swift */,
04891CA82E22971E0064A111 /* LRUCache.swift */,
04B6BA772E2166A50090FE39 /* NoiseTestingHelper.swift */,
ED176FF3B274E35C2D827894 /* BatteryOptimizer.swift */,
32F149C43D1915831B60FE09 /* CompressionUtil.swift */,
CB043CA5EEB9AC8B07D61E97 /* OptimizedBloomFilter.swift */,
@ -312,7 +240,6 @@
isa = PBXGroup;
children = (
04B6BA532E203D6C0090FE39 /* FingerprintView.swift */,
04B6BA542E203D6C0090FE39 /* NoiseTestingView.swift */,
763E0DBA9492A654FC0CDCB9 /* AppInfoView.swift */,
A08E03AA0C63E97C91749AEC /* ContentView.swift */,
9AC141774F6671FCDC347DC7 /* LinkPreviewView.swift */,
@ -333,26 +260,7 @@
C3D98EB3E1B455E321F519F4 /* bitchatTests */ = {
isa = PBXGroup;
children = (
04AD0B502E2678220002A40A /* BinaryProtocolVersionTests.swift */,
04AD0B512E2678220002A40A /* ProtocolVersionNegotiationTests.swift */,
04AD0B522E2678220002A40A /* VersionNegotiationIntegrationTests.swift */,
04AD0B532E2678220002A40A /* VersionNegotiationScenarioTests.swift */,
04B6BA5F2E21601B0090FE39 /* ChannelVerificationTests.swift */,
04B6BA602E21601B0090FE39 /* KeychainIntegrationTests.swift */,
04B6BA612E21601B0090FE39 /* NoiseChannelEncryptionTests.swift */,
04B6BA622E21601B0090FE39 /* NoiseKeyRotationTests.swift */,
04B6BA632E21601B0090FE39 /* NoiseRateLimiterTests.swift */,
04B6BA642E21601B0090FE39 /* NoiseSecurityTests.swift */,
04B6BA652E21601B0090FE39 /* NoiseSecurityValidatorTests.swift */,
04B6BA662E21601B0090FE39 /* SecureNoiseSessionTests.swift */,
04B6BA3D2E2035220090FE39 /* NoiseProtocolTests.swift */,
04B6BA5A2E2041220090FE39 /* NoiseIdentityPersistenceTests.swift */,
53D535D9CE0B875F47402290 /* BinaryProtocolTests.swift */,
3FA8FF26ABDC1C642A8C7AE5 /* BitchatMessageTests.swift */,
1EB3A8FE16333ED12FCB8ACB /* BloomFilterTests.swift */,
D69A18D27F9A565FD6041E12 /* Info.plist */,
8DE9CDF66D4E52D268851048 /* MessagePaddingTests.swift */,
036A1A705AAF9EC21F4354BE /* PasswordProtectedChannelTests.swift */,
);
path = bitchatTests;
sourceTree = "<group>";
@ -555,26 +463,22 @@
4B747085D07A1BCE0F5BA612 /* BinaryProtocol.swift in Sources */,
6E7761E21C99F28AE2F9BE5F /* BitchatApp.swift in Sources */,
04891CAA2E22971E0064A111 /* LRUCache.swift in Sources */,
04B6BA5C2E215FDA0090FE39 /* NoiseChannelKeyRotation.swift in Sources */,
923027D6F2F417AFA2488127 /* BitchatProtocol.swift in Sources */,
04B6BA452E2035530090FE39 /* NoiseSecurityConsiderations.swift in Sources */,
04B6BA462E2035530090FE39 /* NoiseSession.swift in Sources */,
04B6BA472E2035530090FE39 /* NoiseProtocol.swift in Sources */,
04B6BA482E2035530090FE39 /* NoiseChannelEncryption.swift in Sources */,
7A50E2F04A3515A7E90EEAE4 /* BluetoothMeshService.swift in Sources */,
D450CF41F207BDE1A1AAA56E /* ChatViewModel.swift in Sources */,
B0CA7796B2B2AC2B33F84548 /* CompressionUtil.swift in Sources */,
92D34E7A07C990C8A815B0CE /* ContentView.swift in Sources */,
04B6BA4F2E2038A70090FE39 /* NoiseEncryptionService.swift in Sources */,
8DCEEA289EF7C49E7CD38B08 /* DeliveryTracker.swift in Sources */,
04B6BA792E2166A50090FE39 /* NoiseTestingHelper.swift in Sources */,
FB8819B4C84FAFEF5C36B216 /* KeychainManager.swift in Sources */,
04636BBA2E2FAA1700FBCFA8 /* BinaryEncodingUtils.swift in Sources */,
04AD0B4E2E25B9580002A40A /* IdentityModels.swift in Sources */,
04AD0B4F2E25B9580002A40A /* SecureIdentityStateManager.swift in Sources */,
31D147471B9F4E2815352DDA /* LinkPreviewView.swift in Sources */,
04B6BA572E203D6C0090FE39 /* FingerprintView.swift in Sources */,
04B6BA582E203D6C0090FE39 /* NoiseTestingView.swift in Sources */,
C99763A4761567F587D21688 /* MessageRetryService.swift in Sources */,
749D8CF8A362B6CD0786782D /* NotificationService.swift in Sources */,
04636BBF2E2FCA8A00FBCFA8 /* SecureLogger.swift in Sources */,
@ -591,26 +495,22 @@
F455F011B3B648ADA233F998 /* BinaryProtocol.swift in Sources */,
10E68BB889356219189E38EC /* BitchatApp.swift in Sources */,
04891CA92E22971E0064A111 /* LRUCache.swift in Sources */,
04B6BA5E2E215FDA0090FE39 /* NoiseChannelKeyRotation.swift in Sources */,
6DE056E1EE9850E9FBF50157 /* BitchatProtocol.swift in Sources */,
04B6BA492E2035530090FE39 /* NoiseSecurityConsiderations.swift in Sources */,
04B6BA4A2E2035530090FE39 /* NoiseSession.swift in Sources */,
04B6BA4B2E2035530090FE39 /* NoiseProtocol.swift in Sources */,
04B6BA4C2E2035530090FE39 /* NoiseChannelEncryption.swift in Sources */,
D948085736ED8E736C1DE3B0 /* BluetoothMeshService.swift in Sources */,
7576A357B278E5733E9D9F33 /* ChatViewModel.swift in Sources */,
7DCA0DBCB8884E3B31C7BCE3 /* CompressionUtil.swift in Sources */,
1D9674FA5F998503831DC281 /* ContentView.swift in Sources */,
04B6BA4E2E2038A70090FE39 /* NoiseEncryptionService.swift in Sources */,
CD0AE423F03AC52BAFC16834 /* DeliveryTracker.swift in Sources */,
04B6BA7B2E2166A50090FE39 /* NoiseTestingHelper.swift in Sources */,
8F737CE0435792CC2AD65FCB /* KeychainManager.swift in Sources */,
04636BBC2E2FAA1700FBCFA8 /* BinaryEncodingUtils.swift in Sources */,
0FBC81FF78CF4711B78E092A /* IdentityModels.swift in Sources */,
1AF9F9036DEE42408D557A87 /* SecureIdentityStateManager.swift in Sources */,
7A5B1AB5642FEC168E917949 /* LinkPreviewView.swift in Sources */,
04B6BA552E203D6C0090FE39 /* FingerprintView.swift in Sources */,
04B6BA562E203D6C0090FE39 /* NoiseTestingView.swift in Sources */,
CEAE115C9C3EB3C4ED82F128 /* MessageRetryService.swift in Sources */,
61C81ED5F679D5E973EE0C07 /* NotificationService.swift in Sources */,
04636BC02E2FCA8A00FBCFA8 /* SecureLogger.swift in Sources */,
@ -622,25 +522,6 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
8F0BFC2D2B2A5E7B70C3B485 /* BinaryProtocolTests.swift in Sources */,
04AD0B582E2678220002A40A /* VersionNegotiationIntegrationTests.swift in Sources */,
04AD0B592E2678220002A40A /* BinaryProtocolVersionTests.swift in Sources */,
04AD0B5A2E2678220002A40A /* ProtocolVersionNegotiationTests.swift in Sources */,
04AD0B5B2E2678220002A40A /* VersionNegotiationScenarioTests.swift in Sources */,
04B6BA6F2E21601B0090FE39 /* ChannelVerificationTests.swift in Sources */,
04B6BA702E21601B0090FE39 /* NoiseKeyRotationTests.swift in Sources */,
04B6BA712E21601B0090FE39 /* NoiseRateLimiterTests.swift in Sources */,
04B6BA722E21601B0090FE39 /* NoiseSecurityTests.swift in Sources */,
04B6BA732E21601B0090FE39 /* NoiseSecurityValidatorTests.swift in Sources */,
04B6BA742E21601B0090FE39 /* SecureNoiseSessionTests.swift in Sources */,
04B6BA752E21601B0090FE39 /* KeychainIntegrationTests.swift in Sources */,
04B6BA762E21601B0090FE39 /* NoiseChannelEncryptionTests.swift in Sources */,
2E71E320EA921498C57E023B /* BitchatMessageTests.swift in Sources */,
04B6BA3F2E2035220090FE39 /* NoiseProtocolTests.swift in Sources */,
04B6BA5C2E2041220090FE39 /* NoiseIdentityPersistenceTests.swift in Sources */,
C91FDE97070433E6CFE95C55 /* BloomFilterTests.swift in Sources */,
ADC66F95FBD513B10411ADB3 /* MessagePaddingTests.swift in Sources */,
24F17B1446E13F42652B7B08 /* PasswordProtectedChannelTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -648,25 +529,6 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
CDAD6629EB69916B95C80DAF /* BinaryProtocolTests.swift in Sources */,
04AD0B542E2678220002A40A /* VersionNegotiationIntegrationTests.swift in Sources */,
04AD0B552E2678220002A40A /* BinaryProtocolVersionTests.swift in Sources */,
04AD0B562E2678220002A40A /* ProtocolVersionNegotiationTests.swift in Sources */,
04AD0B572E2678220002A40A /* VersionNegotiationScenarioTests.swift in Sources */,
04B6BA672E21601B0090FE39 /* ChannelVerificationTests.swift in Sources */,
04B6BA682E21601B0090FE39 /* NoiseKeyRotationTests.swift in Sources */,
04B6BA692E21601B0090FE39 /* NoiseRateLimiterTests.swift in Sources */,
04B6BA6A2E21601B0090FE39 /* NoiseSecurityTests.swift in Sources */,
04B6BA6B2E21601B0090FE39 /* NoiseSecurityValidatorTests.swift in Sources */,
04B6BA6C2E21601B0090FE39 /* SecureNoiseSessionTests.swift in Sources */,
04B6BA6D2E21601B0090FE39 /* KeychainIntegrationTests.swift in Sources */,
04B6BA6E2E21601B0090FE39 /* NoiseChannelEncryptionTests.swift in Sources */,
0DAFF1DDE9BA83FF648D5AB3 /* BitchatMessageTests.swift in Sources */,
04B6BA3E2E2035220090FE39 /* NoiseProtocolTests.swift in Sources */,
04B6BA5B2E2041220090FE39 /* NoiseIdentityPersistenceTests.swift in Sources */,
846E2B446E36639159704730 /* BloomFilterTests.swift in Sources */,
F00B713D5053617FB5F3F1BE /* MessagePaddingTests.swift in Sources */,
9269B4230187A9EA969BEDB7 /* PasswordProtectedChannelTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};

View File

@ -286,7 +286,7 @@ class SecureIdentityStateManager {
// MARK: - Cleanup
func clearAllIdentityData() {
SecureLogger.logSecurityEvent(.invalidKey(reason: "Clearing all identity data"), level: .warning)
SecureLogger.log("Clearing all identity data", category: SecureLogger.security, level: .warning)
queue.async(flags: .barrier) {
self.cache = IdentityCache()

View File

@ -1,285 +0,0 @@
//
// NoiseChannelEncryption.swift
// bitchat
//
// This is free and unencumbered software released into the public domain.
// For more information, see <https://unlicense.org>
//
import Foundation
import CryptoKit
import os.log
// MARK: - Noise Channel Encryption
class NoiseChannelEncryption {
// Channel keys derived from passwords
private var channelKeys: [String: SymmetricKey] = [:]
private let keyQueue = DispatchQueue(label: "chat.bitchat.noise.channels", attributes: .concurrent)
// Key rotation support
private let keyRotation = NoiseChannelKeyRotation()
private var rotationEnabled: [String: Bool] = [:] // channel -> enabled
// Replay protection
private var receivedNonces: Set<String> = []
private let nonceExpirationTime: TimeInterval = 600 // 10 minutes
private var nonceCleanupTimer: Timer?
// MARK: - Channel Key Management
/// Derive a channel key from password
func deriveChannelKey(from password: String, channel: String, creatorFingerprint: String? = nil) -> SymmetricKey {
// Use PBKDF2 with channel name + creator fingerprint as salt
// This prevents rainbow table attacks across different channel instances
var saltComponents = "bitchat-channel-\(channel)"
if let fingerprint = creatorFingerprint {
saltComponents += "-\(fingerprint)"
}
let salt = saltComponents.data(using: .utf8)!
// Increased iterations for better security (OWASP recommends 210,000 for PBKDF2-SHA256)
let keyData = PBKDF2<SHA256>(
password: password.data(using: .utf8)!,
salt: salt,
iterations: 210_000,
keyByteCount: 32
).makeIterator()
return SymmetricKey(data: keyData)
}
/// Set password for a channel
func setChannelPassword(_ password: String, for channel: String, creatorFingerprint: String? = nil) {
let key = deriveChannelKey(from: password, channel: channel, creatorFingerprint: creatorFingerprint)
keyQueue.async(flags: .barrier) {
self.channelKeys[channel] = key
}
// Store in keychain
let saved = KeychainManager.shared.saveChannelPassword(password, for: channel)
if saved {
SecureLogger.logKeyOperation("set", keyType: "channel key for \(channel)", success: true)
}
}
/// Get channel key
func getChannelKey(for channel: String) -> SymmetricKey? {
return keyQueue.sync {
return channelKeys[channel]
}
}
/// Load channel password from keychain
func loadChannelPassword(for channel: String) -> Bool {
guard let password = KeychainManager.shared.getChannelPassword(for: channel) else {
return false
}
setChannelPassword(password, for: channel)
return true
}
/// Remove channel password
func removeChannelPassword(for channel: String) {
keyQueue.async(flags: .barrier) {
self.channelKeys.removeValue(forKey: channel)
}
let deleted = KeychainManager.shared.deleteChannelPassword(for: channel)
if deleted {
SecureLogger.logKeyOperation("remove", keyType: "channel key for \(channel)", success: true)
}
}
// MARK: - Replay Protection
private func scheduleNonceCleanup() {
DispatchQueue.main.async { [weak self] in
self?.nonceCleanupTimer?.invalidate()
self?.nonceCleanupTimer = Timer.scheduledTimer(withTimeInterval: 300, repeats: true) { [weak self] _ in
self?.cleanupExpiredNonces()
}
}
}
private func cleanupExpiredNonces() {
keyQueue.async(flags: .barrier) { [weak self] in
guard let self = self else { return }
// In production, we'd need to store timestamps with nonces
// For now, we'll clear all nonces periodically
if self.receivedNonces.count > 1000 {
self.receivedNonces.removeAll()
}
}
}
deinit {
nonceCleanupTimer?.invalidate()
}
// MARK: - Channel Message Encryption
/// Encrypt message for a channel
func encryptChannelMessage(_ message: String, for channel: String) throws -> Data {
guard let key = getChannelKey(for: channel) else {
SecureLogger.log("Channel encryption failed - no key for channel: \(channel)", category: SecureLogger.encryption, level: .error)
throw NoiseChannelError.noChannelKey
}
let messageData = message.data(using: .utf8)!
// Generate random nonce
let nonce = ChaChaPoly.Nonce()
// Encrypt with channel key
let sealedBox = try ChaChaPoly.seal(messageData, using: key, nonce: nonce)
// Return nonce + ciphertext + tag
return nonce.withUnsafeBytes { Data($0) } + sealedBox.ciphertext + sealedBox.tag
}
/// Decrypt channel message
func decryptChannelMessage(_ encryptedData: Data, for channel: String) throws -> String {
guard let key = getChannelKey(for: channel) else {
SecureLogger.log("Channel decryption failed - no key for channel: \(channel)", category: SecureLogger.encryption, level: .error)
throw NoiseChannelError.noChannelKey
}
guard encryptedData.count >= 12 + 16 else { // nonce + tag minimum
throw NoiseChannelError.invalidCiphertext
}
// Extract components
let nonceData = encryptedData.prefix(12)
let ciphertext = encryptedData.dropFirst(12).dropLast(16)
let tag = encryptedData.suffix(16)
// Create sealed box
let nonce = try ChaChaPoly.Nonce(data: nonceData)
let sealedBox = try ChaChaPoly.SealedBox(nonce: nonce, ciphertext: ciphertext, tag: tag)
// Decrypt
let decryptedData = try ChaChaPoly.open(sealedBox, using: key)
guard let message = String(data: decryptedData, encoding: .utf8) else {
SecureLogger.log("Channel decryption failed - invalid UTF8 for channel: \(channel)", category: SecureLogger.encryption, level: .error)
throw NoiseChannelError.decryptionFailed
}
return message
}
// MARK: - Channel Key Sharing
/// Create encrypted channel key packet for sharing via Noise session
func createChannelKeyPacket(password: String, channel: String) -> Data? {
// Generate a unique nonce for replay protection
var nonceData = Data(count: 16)
_ = nonceData.withUnsafeMutableBytes { bytes in
SecRandomCopyBytes(kSecRandomDefault, 16, bytes.baseAddress!)
}
let nonce = nonceData.base64EncodedString()
let packet = ChannelKeyPacket(
channel: channel,
password: password,
timestamp: Date(),
nonce: nonce
)
return try? JSONEncoder().encode(packet)
}
/// Process received channel key packet
func processChannelKeyPacket(_ data: Data) -> (channel: String, password: String)? {
guard let packet = try? JSONDecoder().decode(ChannelKeyPacket.self, from: data) else {
return nil
}
// Verify timestamp is recent (within 5 minutes)
let age = Date().timeIntervalSince(packet.timestamp)
guard age < 300 else {
SecureLogger.log("Expired channel key packet for channel: \(packet.channel), age: \(age)s", category: SecureLogger.security, level: .warning)
return nil
}
return keyQueue.sync(flags: .barrier) {
// Check for replay attack
if receivedNonces.contains(packet.nonce) {
SecureLogger.logSecurityEvent(.replayAttackDetected(channel: packet.channel), level: .warning)
return nil // This nonce was already processed
}
// Add nonce to received set
receivedNonces.insert(packet.nonce)
// Schedule cleanup if not already scheduled
if nonceCleanupTimer == nil {
scheduleNonceCleanup()
}
return (packet.channel, packet.password)
}
}
}
// MARK: - Supporting Types
private struct ChannelKeyPacket: Codable {
let channel: String
let password: String
let timestamp: Date
let nonce: String
}
enum NoiseChannelError: Error {
case noChannelKey
case invalidCiphertext
case decryptionFailed
}
// MARK: - PBKDF2 Implementation
private struct PBKDF2<H: HashFunction> {
let password: Data
let salt: Data
let iterations: Int
let keyByteCount: Int
init(password: Data, salt: Data, iterations: Int, keyByteCount: Int) {
self.password = password
self.salt = salt
self.iterations = iterations
self.keyByteCount = keyByteCount
}
func makeIterator() -> Data {
var derivedKey = Data()
var blockNum: UInt32 = 1
while derivedKey.count < keyByteCount {
var block = salt
withUnsafeBytes(of: blockNum.bigEndian) { bytes in
block.append(contentsOf: bytes)
}
var u = Data(HMAC<H>.authenticationCode(for: block, using: SymmetricKey(data: password)))
var xor = u
for _ in 1..<iterations {
u = Data(HMAC<H>.authenticationCode(for: u, using: SymmetricKey(data: password)))
for i in 0..<xor.count {
xor[i] ^= u[i]
}
}
derivedKey.append(xor)
blockNum += 1
}
return derivedKey.prefix(keyByteCount)
}
}

View File

@ -1,296 +0,0 @@
//
// NoiseChannelKeyRotation.swift
// bitchat
//
// This is free and unencumbered software released into the public domain.
// For more information, see <https://unlicense.org>
//
import Foundation
import CryptoKit
// MARK: - Channel Key Rotation for Forward Secrecy
/// Implements key rotation for channels to provide forward secrecy
/// This is a stepping stone toward full Double Ratchet implementation
class NoiseChannelKeyRotation {
// MARK: - Types
struct KeyEpoch: Codable {
let epochNumber: UInt64
let startTime: Date
let endTime: Date
let keyCommitment: String
let previousEpochCommitment: String?
}
struct RotatedChannelKey {
let epoch: KeyEpoch
let key: SymmetricKey
let isActive: Bool
}
// MARK: - Constants
private static let epochDuration: TimeInterval = 24 * 60 * 60 // 24 hours
private static let epochOverlap: TimeInterval = 60 * 60 // 1 hour overlap for late messages
private static let maxStoredEpochs = 7 // Keep 1 week of history
// MARK: - Properties
private var channelEpochs: [String: [KeyEpoch]] = [:] // channel -> epochs
private let keychainPrefix = "channel.epoch."
// Thread safety
private let queue = DispatchQueue(label: "chat.bitchat.noise.keyrotation", attributes: .concurrent)
// MARK: - Public Interface
/// Get the current key for a channel with rotation
func getCurrentKey(for channel: String, basePassword: String, creatorFingerprint: String) -> RotatedChannelKey? {
let currentTime = Date()
return queue.sync {
// Get or create current epoch
let epoch = getCurrentOrCreateEpoch(for: channel, at: currentTime)
// Derive key for this epoch
let epochKey = deriveEpochKey(
basePassword: basePassword,
channel: channel,
creatorFingerprint: creatorFingerprint,
epochNumber: epoch.epochNumber
)
return RotatedChannelKey(
epoch: epoch,
key: epochKey,
isActive: true
)
}
}
/// Get valid keys for decryption (current + recent epochs)
func getValidKeysForDecryption(channel: String, basePassword: String, creatorFingerprint: String, messageTime: Date? = nil) -> [RotatedChannelKey] {
let checkTime = messageTime ?? Date()
return queue.sync {
let epochs = getValidEpochs(for: channel, at: checkTime)
return epochs.map { epoch in
let key = deriveEpochKey(
basePassword: basePassword,
channel: channel,
creatorFingerprint: creatorFingerprint,
epochNumber: epoch.epochNumber
)
let isActive = checkTime >= epoch.startTime && checkTime < epoch.endTime
return RotatedChannelKey(
epoch: epoch,
key: key,
isActive: isActive
)
}
}
}
/// Rotate key for a channel (channel owner only)
func rotateChannelKey(for channel: String, basePassword: String, creatorFingerprint: String) -> KeyEpoch {
return queue.sync(flags: .barrier) {
let currentTime = Date()
let epochs = channelEpochs[channel] ?? []
// Get current epoch
let currentEpoch = epochs.last
let nextEpochNumber = (currentEpoch?.epochNumber ?? 0) + 1
// Create new epoch
let newEpoch = KeyEpoch(
epochNumber: nextEpochNumber,
startTime: currentTime,
endTime: currentTime.addingTimeInterval(Self.epochDuration),
keyCommitment: computeEpochKeyCommitment(
basePassword: basePassword,
channel: channel,
creatorFingerprint: creatorFingerprint,
epochNumber: nextEpochNumber
),
previousEpochCommitment: currentEpoch?.keyCommitment
)
// Add to epochs
var updatedEpochs = epochs
updatedEpochs.append(newEpoch)
// Trim old epochs
if updatedEpochs.count > Self.maxStoredEpochs {
updatedEpochs.removeFirst(updatedEpochs.count - Self.maxStoredEpochs)
}
channelEpochs[channel] = updatedEpochs
// Persist epochs
saveEpochs(updatedEpochs, for: channel)
return newEpoch
}
}
/// Check if a channel needs key rotation
func needsKeyRotation(for channel: String) -> Bool {
return queue.sync {
guard let epochs = channelEpochs[channel],
let currentEpoch = epochs.last else {
return true // No epochs, needs initial key
}
// Check if current epoch is near expiration (within 2 hours)
let timeUntilExpiration = currentEpoch.endTime.timeIntervalSinceNow
return timeUntilExpiration < 2 * 60 * 60
}
}
// MARK: - Private Methods
private func getCurrentOrCreateEpoch(for channel: String, at time: Date) -> KeyEpoch {
var epochs = channelEpochs[channel] ?? []
// Find current epoch
if let currentEpoch = epochs.first(where: { epoch in
time >= epoch.startTime && time < epoch.endTime.addingTimeInterval(Self.epochOverlap)
}) {
return currentEpoch
}
// No valid epoch, create initial one
let initialEpoch = KeyEpoch(
epochNumber: 1,
startTime: time,
endTime: time.addingTimeInterval(Self.epochDuration),
keyCommitment: "", // Will be computed when key is derived
previousEpochCommitment: nil
)
epochs.append(initialEpoch)
channelEpochs[channel] = epochs
return initialEpoch
}
private func getValidEpochs(for channel: String, at time: Date) -> [KeyEpoch] {
let epochs = channelEpochs[channel] ?? []
// Return epochs that are valid at the given time (including overlap period)
return epochs.filter { epoch in
time >= epoch.startTime.addingTimeInterval(-Self.epochOverlap) &&
time < epoch.endTime.addingTimeInterval(Self.epochOverlap)
}
}
private func deriveEpochKey(basePassword: String, channel: String, creatorFingerprint: String, epochNumber: UInt64) -> SymmetricKey {
// Derive epoch-specific key using base password + epoch number
let epochSalt = "\(channel)-\(creatorFingerprint)-epoch-\(epochNumber)".data(using: .utf8)!
let keyData = pbkdf2(
password: basePassword,
salt: epochSalt,
iterations: 210_000, // Same as channel encryption
keyLength: 32
)
return SymmetricKey(data: keyData)
}
private func computeEpochKeyCommitment(basePassword: String, channel: String, creatorFingerprint: String, epochNumber: UInt64) -> String {
let epochKey = deriveEpochKey(
basePassword: basePassword,
channel: channel,
creatorFingerprint: creatorFingerprint,
epochNumber: epochNumber
)
let commitment = SHA256.hash(data: epochKey.withUnsafeBytes { Data($0) })
return commitment.map { String(format: "%02x", $0) }.joined()
}
private func pbkdf2(password: String, salt: Data, iterations: Int, keyLength: Int) -> Data {
guard let passwordData = password.data(using: .utf8) else {
return Data()
}
// Use CryptoKit's safer implementation instead of CommonCrypto
var derivedKey = Data()
var blockNum: UInt32 = 1
while derivedKey.count < keyLength {
var block = salt
withUnsafeBytes(of: blockNum.bigEndian) { bytes in
block.append(contentsOf: bytes)
}
var u = Data(HMAC<SHA256>.authenticationCode(for: block, using: SymmetricKey(data: passwordData)))
var xor = u
for _ in 1..<iterations {
u = Data(HMAC<SHA256>.authenticationCode(for: u, using: SymmetricKey(data: passwordData)))
for i in 0..<xor.count {
xor[i] ^= u[i]
}
}
derivedKey.append(xor)
blockNum += 1
}
return Data(derivedKey.prefix(keyLength))
}
// MARK: - Persistence
private func saveEpochs(_ epochs: [KeyEpoch], for channel: String) {
// Use channel password storage with special prefix for epoch data
let epochKey = "epoch::\(channel)"
if let data = try? JSONEncoder().encode(epochs),
let epochString = String(data: data, encoding: .utf8) {
_ = KeychainManager.shared.saveChannelPassword(epochString, for: epochKey)
}
}
private func loadEpochs(for channel: String) -> [KeyEpoch]? {
let epochKey = "epoch::\(channel)"
guard let epochString = KeychainManager.shared.getChannelPassword(for: epochKey),
let data = epochString.data(using: .utf8),
let epochs = try? JSONDecoder().decode([KeyEpoch].self, from: data) else {
return nil
}
return epochs
}
/// Load all saved epochs on initialization
func loadSavedEpochs() {
queue.sync(flags: .barrier) {
// Get all channel passwords and filter for epoch data
let allPasswords = KeychainManager.shared.getAllChannelPasswords()
for (key, epochString) in allPasswords where key.hasPrefix("epoch::") {
let channel = String(key.dropFirst(7)) // Remove "epoch::" prefix
if let data = epochString.data(using: .utf8),
let epochs = try? JSONDecoder().decode([KeyEpoch].self, from: data) {
channelEpochs[channel] = epochs
}
}
}
}
/// Clear all epochs for a channel
func clearEpochs(for channel: String) {
queue.sync(flags: .barrier) {
channelEpochs.removeValue(forKey: channel)
let epochKey = "epoch::\(channel)"
_ = KeychainManager.shared.deleteChannelPassword(for: epochKey)
}
}
}

View File

@ -609,7 +609,7 @@ extension NoiseHandshakeState {
// Check against known bad points
if lowOrderPoints.contains(keyData) {
SecureLogger.logSecurityEvent(.invalidKey(reason: "Low-order point detected"), level: .warning)
SecureLogger.log("Low-order point detected", category: SecureLogger.security, level: .warning)
throw NoiseError.invalidPublicKey
}
@ -619,7 +619,7 @@ extension NoiseHandshakeState {
return publicKey
} catch {
// If CryptoKit rejects it, it's invalid
SecureLogger.logSecurityEvent(.invalidKey(reason: "CryptoKit validation failed"), level: .warning)
SecureLogger.log("CryptoKit validation failed", category: SecureLogger.security, level: .warning)
throw NoiseError.invalidPublicKey
}
}

View File

@ -61,16 +61,6 @@ struct NoiseSecurityValidator {
peerID.count <= 64 &&
peerID.rangeOfCharacter(from: validCharset.inverted) == nil
}
/// Validate channel name format
static func validateChannelName(_ channel: String) -> Bool {
// Channel should start with # and contain valid characters
let validCharset = CharacterSet.alphanumerics.union(CharacterSet(charactersIn: "-_"))
return channel.hasPrefix("#") &&
channel.count > 1 &&
channel.count <= 32 &&
channel.dropFirst().rangeOfCharacter(from: validCharset.inverted) == nil
}
}
// MARK: - Enhanced Noise Session with Security
@ -232,54 +222,6 @@ enum NoiseSecurityError: Error {
case sessionExhausted
case messageTooLarge
case invalidPeerID
case invalidChannelName
case rateLimitExceeded
case handshakeTimeout
}
// MARK: - Security Audit Checklist
/*
SECURITY AUDIT CHECKLIST:
1. KEY MANAGEMENT
✓ Static keys stored in Keychain (most secure iOS storage)
✓ Keys cleared on panic mode
✓ Ephemeral keys generated per session
✓ No key reuse across sessions
2. PROTOCOL SECURITY
✓ Using Noise XX pattern for mutual authentication
✓ Forward secrecy via ephemeral keys
✓ Replay protection via nonce counter
✓ AEAD encryption (ChaCha20-Poly1305)
✓ SHA-256 for hashing
3. IMPLEMENTATION SECURITY
✓ Message size limits to prevent DoS
✓ Session timeout to limit exposure
✓ Message count limits to prevent nonce reuse
✓ Rate limiting for handshakes and messages
✓ Input validation for all user data
✓ Thread-safe operations
4. NETWORK SECURITY
✓ Messages padded to standard sizes for traffic analysis resistance
✓ Cover traffic for anonymity
✓ No metadata leakage in protocol
✓ Fingerprint verification for identity
5. EDGE CASES HANDLED
✓ Incomplete handshakes timeout
✓ Duplicate handshake messages ignored
✓ Session renegotiation when needed
✓ Graceful handling of decryption failures
✓ Memory limits on message sizes
6. FUTURE IMPROVEMENTS
- Implement post-quantum key exchange (when available)
- Add perfect forward secrecy for channel keys
- Implement key rotation for long-lived channels
- Add security event logging
- Implement secure key backup/restore
*/

View File

@ -443,8 +443,6 @@ class NoiseSessionManager {
}
func initiateRekey(for peerID: String) throws {
SecureLogger.logSecurityEvent(.keyRotation(channel: peerID))
// Remove old session
removeSession(for: peerID)

View File

@ -233,10 +233,6 @@ protocol BinaryEncodable {
enum BinaryMessageType: UInt8 {
case deliveryAck = 0x01
case readReceipt = 0x02
case channelKeyVerifyRequest = 0x03
case channelKeyVerifyResponse = 0x04
case channelPasswordUpdate = 0x05
case channelMetadata = 0x06
case versionHello = 0x07
case versionAck = 0x08
case noiseIdentityAnnouncement = 0x09

View File

@ -251,20 +251,19 @@ extension BitchatMessage {
var data = Data()
// Message format:
// - Flags: 1 byte (bit 0: isRelay, bit 1: isPrivate, bit 2: hasOriginalSender, bit 3: hasRecipientNickname, bit 4: hasSenderPeerID, bit 5: hasMentions, bit 6: hasChannel, bit 7: isEncrypted)
// - Flags: 1 byte (bit 0: isRelay, bit 1: isPrivate, bit 2: hasOriginalSender, bit 3: hasRecipientNickname, bit 4: hasSenderPeerID, bit 5: hasMentions)
// - Timestamp: 8 bytes (seconds since epoch)
// - ID length: 1 byte
// - ID: variable
// - Sender length: 1 byte
// - Sender: variable
// - Content length: 2 bytes
// - Content: variable (or encrypted content if isEncrypted)
// - Content: variable
// Optional fields based on flags:
// - Original sender length + data
// - Recipient nickname length + data
// - Sender peer ID length + data
// - Mentions array
// - Channel hashtag
var flags: UInt8 = 0
if isRelay { flags |= 0x01 }
@ -273,8 +272,6 @@ extension BitchatMessage {
if recipientNickname != nil { flags |= 0x08 }
if senderPeerID != nil { flags |= 0x10 }
if mentions != nil && !mentions!.isEmpty { flags |= 0x20 }
if channel != nil { flags |= 0x40 }
if isEncrypted { flags |= 0x80 }
data.append(flags)
@ -301,14 +298,8 @@ extension BitchatMessage {
data.append(0)
}
// Content or encrypted content
if isEncrypted, let encryptedContent = encryptedContent {
let length = UInt16(min(encryptedContent.count, 65535))
// Encode length as 2 bytes, big-endian
data.append(UInt8((length >> 8) & 0xFF))
data.append(UInt8(length & 0xFF))
data.append(encryptedContent.prefix(Int(length)))
} else if let contentData = content.data(using: .utf8) {
// Content
if let contentData = content.data(using: .utf8) {
let length = UInt16(min(contentData.count, 65535))
// Encode length as 2 bytes, big-endian
data.append(UInt8((length >> 8) & 0xFF))
@ -347,11 +338,6 @@ extension BitchatMessage {
}
}
// Channel hashtag
if let channel = channel, let channelData = channel.data(using: .utf8) {
data.append(UInt8(min(channelData.count, 255)))
data.append(channelData.prefix(255))
}
return data
}
@ -378,8 +364,6 @@ extension BitchatMessage {
let hasRecipientNickname = (flags & 0x08) != 0
let hasSenderPeerID = (flags & 0x10) != 0
let hasMentions = (flags & 0x20) != 0
let hasChannel = (flags & 0x40) != 0
let isEncrypted = (flags & 0x80) != 0
// Timestamp
guard offset + 8 <= dataCopy.count else {
@ -427,18 +411,7 @@ extension BitchatMessage {
return nil
}
let content: String
let encryptedContent: Data?
if isEncrypted {
// Content is encrypted, store as Data
encryptedContent = dataCopy[offset..<offset+contentLength]
content = "" // Empty placeholder
} else {
// Normal string content
content = String(data: dataCopy[offset..<offset+contentLength], encoding: .utf8) ?? ""
encryptedContent = nil
}
let content = String(data: dataCopy[offset..<offset+contentLength], encoding: .utf8) ?? ""
offset += contentLength
// Optional fields
@ -489,16 +462,6 @@ extension BitchatMessage {
}
}
// Channel
var channel: String? = nil
if hasChannel && offset < dataCopy.count {
let length = Int(dataCopy[offset]); offset += 1
if offset + length <= dataCopy.count {
channel = String(data: dataCopy[offset..<offset+length], encoding: .utf8)
offset += length
}
}
let message = BitchatMessage(
id: id,
sender: sender,
@ -509,10 +472,7 @@ extension BitchatMessage {
isPrivate: isPrivate,
recipientNickname: recipientNickname,
senderPeerID: senderPeerID,
mentions: mentions,
channel: channel,
encryptedContent: encryptedContent,
isEncrypted: isEncrypted
mentions: mentions
)
return message
}

View File

@ -82,7 +82,6 @@ enum MessageType: UInt8 {
case fragmentStart = 0x05
case fragmentContinue = 0x06
case fragmentEnd = 0x07
case channelAnnounce = 0x08 // Announce password-protected channel status
case deliveryAck = 0x0A // Acknowledge message received
case deliveryStatusRequest = 0x0B // Request delivery status update
case readReceipt = 0x0C // Message has been read/viewed
@ -92,10 +91,6 @@ enum MessageType: UInt8 {
case noiseHandshakeResp = 0x11 // Noise handshake response
case noiseEncrypted = 0x12 // Noise encrypted transport message
case noiseIdentityAnnounce = 0x13 // Announce static public key for discovery
case channelKeyVerifyRequest = 0x14 // Request key verification for a channel
case channelKeyVerifyResponse = 0x15 // Response to key verification request
case channelPasswordUpdate = 0x16 // Distribute new password to channel members
case channelMetadata = 0x17 // Announce channel creator and metadata
// Protocol version negotiation
case versionHello = 0x20 // Initial version announcement
@ -109,7 +104,6 @@ enum MessageType: UInt8 {
case .fragmentStart: return "fragmentStart"
case .fragmentContinue: return "fragmentContinue"
case .fragmentEnd: return "fragmentEnd"
case .channelAnnounce: return "channelAnnounce"
case .deliveryAck: return "deliveryAck"
case .deliveryStatusRequest: return "deliveryStatusRequest"
case .readReceipt: return "readReceipt"
@ -117,10 +111,6 @@ enum MessageType: UInt8 {
case .noiseHandshakeResp: return "noiseHandshakeResp"
case .noiseEncrypted: return "noiseEncrypted"
case .noiseIdentityAnnounce: return "noiseIdentityAnnounce"
case .channelKeyVerifyRequest: return "channelKeyVerifyRequest"
case .channelKeyVerifyResponse: return "channelKeyVerifyResponse"
case .channelPasswordUpdate: return "channelPasswordUpdate"
case .channelMetadata: return "channelMetadata"
case .versionHello: return "versionHello"
case .versionAck: return "versionAck"
}
@ -362,350 +352,6 @@ struct ReadReceipt: Codable {
}
}
// Channel key verification request
struct ChannelKeyVerifyRequest: Codable {
let channel: String
let requesterID: String
let keyCommitment: String // SHA256 hash of the key they have
let timestamp: Date
init(channel: String, requesterID: String, keyCommitment: String) {
self.channel = channel
self.requesterID = requesterID
self.keyCommitment = keyCommitment
self.timestamp = Date()
}
// For binary decoding
private init(channel: String, requesterID: String, keyCommitment: String, timestamp: Date) {
self.channel = channel
self.requesterID = requesterID
self.keyCommitment = keyCommitment
self.timestamp = timestamp
}
func encode() -> Data? {
return try? JSONEncoder().encode(self)
}
static func decode(from data: Data) -> ChannelKeyVerifyRequest? {
try? JSONDecoder().decode(ChannelKeyVerifyRequest.self, from: data)
}
// MARK: - Binary Encoding
func toBinaryData() -> Data {
var data = Data()
data.appendString(channel)
// RequesterID as 8-byte hex string
var requesterData = Data()
var tempID = requesterID
while tempID.count >= 2 && requesterData.count < 8 {
let hexByte = String(tempID.prefix(2))
if let byte = UInt8(hexByte, radix: 16) {
requesterData.append(byte)
}
tempID = String(tempID.dropFirst(2))
}
while requesterData.count < 8 {
requesterData.append(0)
}
data.append(requesterData)
data.appendString(keyCommitment)
data.appendDate(timestamp)
return data
}
static func fromBinaryData(_ data: Data) -> ChannelKeyVerifyRequest? {
// Create defensive copy
let dataCopy = Data(data)
var offset = 0
guard let channel = dataCopy.readString(at: &offset) else { return nil }
guard let requesterIDData = dataCopy.readFixedBytes(at: &offset, count: 8) else { return nil }
let requesterID = requesterIDData.hexEncodedString()
guard let keyCommitment = dataCopy.readString(at: &offset),
let timestamp = dataCopy.readDate(at: &offset) else { return nil }
return ChannelKeyVerifyRequest(channel: channel,
requesterID: requesterID,
keyCommitment: keyCommitment,
timestamp: timestamp)
}
}
// Channel key verification response
struct ChannelKeyVerifyResponse: Codable {
let channel: String
let responderID: String
let verified: Bool // Whether the key commitment matches
let timestamp: Date
init(channel: String, responderID: String, verified: Bool) {
self.channel = channel
self.responderID = responderID
self.verified = verified
self.timestamp = Date()
}
// For binary decoding
private init(channel: String, responderID: String, verified: Bool, timestamp: Date) {
self.channel = channel
self.responderID = responderID
self.verified = verified
self.timestamp = timestamp
}
func encode() -> Data? {
return try? JSONEncoder().encode(self)
}
static func decode(from data: Data) -> ChannelKeyVerifyResponse? {
try? JSONDecoder().decode(ChannelKeyVerifyResponse.self, from: data)
}
// MARK: - Binary Encoding
func toBinaryData() -> Data {
var data = Data()
data.appendString(channel)
// ResponderID as 8-byte hex string
var responderData = Data()
var tempID = responderID
while tempID.count >= 2 && responderData.count < 8 {
let hexByte = String(tempID.prefix(2))
if let byte = UInt8(hexByte, radix: 16) {
responderData.append(byte)
}
tempID = String(tempID.dropFirst(2))
}
while responderData.count < 8 {
responderData.append(0)
}
data.append(responderData)
data.appendUInt8(verified ? 1 : 0)
data.appendDate(timestamp)
return data
}
static func fromBinaryData(_ data: Data) -> ChannelKeyVerifyResponse? {
// Create defensive copy
let dataCopy = Data(data)
var offset = 0
guard let channel = dataCopy.readString(at: &offset) else { return nil }
guard let responderIDData = dataCopy.readFixedBytes(at: &offset, count: 8) else { return nil }
let responderID = responderIDData.hexEncodedString()
guard let verifiedByte = dataCopy.readUInt8(at: &offset),
let timestamp = dataCopy.readDate(at: &offset) else { return nil }
let verified = verifiedByte != 0
return ChannelKeyVerifyResponse(channel: channel,
responderID: responderID,
verified: verified,
timestamp: timestamp)
}
}
// Channel password update (sent by owner to members)
struct ChannelPasswordUpdate: Codable {
let channel: String
let ownerID: String // Deprecated, kept for backward compatibility
let ownerFingerprint: String // Noise protocol fingerprint of owner
let encryptedPassword: Data // New password encrypted with recipient's Noise session
let newKeyCommitment: String // SHA256 of new key for verification
let timestamp: Date
init(channel: String, ownerID: String, ownerFingerprint: String, encryptedPassword: Data, newKeyCommitment: String) {
self.channel = channel
self.ownerID = ownerID
self.ownerFingerprint = ownerFingerprint
self.encryptedPassword = encryptedPassword
self.newKeyCommitment = newKeyCommitment
self.timestamp = Date()
}
// For binary decoding
private init(channel: String, ownerID: String, ownerFingerprint: String, encryptedPassword: Data, newKeyCommitment: String, timestamp: Date) {
self.channel = channel
self.ownerID = ownerID
self.ownerFingerprint = ownerFingerprint
self.encryptedPassword = encryptedPassword
self.newKeyCommitment = newKeyCommitment
self.timestamp = timestamp
}
func encode() -> Data? {
return try? JSONEncoder().encode(self)
}
static func decode(from data: Data) -> ChannelPasswordUpdate? {
try? JSONDecoder().decode(ChannelPasswordUpdate.self, from: data)
}
// MARK: - Binary Encoding
func toBinaryData() -> Data {
var data = Data()
data.appendString(channel)
// OwnerID as 8-byte hex string
var ownerData = Data()
var tempID = ownerID
while tempID.count >= 2 && ownerData.count < 8 {
let hexByte = String(tempID.prefix(2))
if let byte = UInt8(hexByte, radix: 16) {
ownerData.append(byte)
}
tempID = String(tempID.dropFirst(2))
}
while ownerData.count < 8 {
ownerData.append(0)
}
data.append(ownerData)
data.appendString(ownerFingerprint)
data.appendData(encryptedPassword)
data.appendString(newKeyCommitment)
data.appendDate(timestamp)
return data
}
static func fromBinaryData(_ data: Data) -> ChannelPasswordUpdate? {
// Create defensive copy
let dataCopy = Data(data)
var offset = 0
guard let channel = dataCopy.readString(at: &offset) else { return nil }
guard let ownerIDData = dataCopy.readFixedBytes(at: &offset, count: 8) else { return nil }
let ownerID = ownerIDData.hexEncodedString()
guard let ownerFingerprint = dataCopy.readString(at: &offset),
let encryptedPassword = dataCopy.readData(at: &offset),
let newKeyCommitment = dataCopy.readString(at: &offset),
let timestamp = dataCopy.readDate(at: &offset) else { return nil }
return ChannelPasswordUpdate(channel: channel,
ownerID: ownerID,
ownerFingerprint: ownerFingerprint,
encryptedPassword: encryptedPassword,
newKeyCommitment: newKeyCommitment,
timestamp: timestamp)
}
}
// Channel metadata announcement
struct ChannelMetadata: Codable {
let channel: String
let creatorID: String
let creatorFingerprint: String // Noise protocol fingerprint
let createdAt: Date
let isPasswordProtected: Bool
let keyCommitment: String? // SHA256 of channel key if password-protected
init(channel: String, creatorID: String, creatorFingerprint: String, isPasswordProtected: Bool, keyCommitment: String?) {
self.channel = channel
self.creatorID = creatorID
self.creatorFingerprint = creatorFingerprint
self.createdAt = Date()
self.isPasswordProtected = isPasswordProtected
self.keyCommitment = keyCommitment
}
// For binary decoding
private init(channel: String, creatorID: String, creatorFingerprint: String, createdAt: Date, isPasswordProtected: Bool, keyCommitment: String?) {
self.channel = channel
self.creatorID = creatorID
self.creatorFingerprint = creatorFingerprint
self.createdAt = createdAt
self.isPasswordProtected = isPasswordProtected
self.keyCommitment = keyCommitment
}
func encode() -> Data? {
return try? JSONEncoder().encode(self)
}
static func decode(from data: Data) -> ChannelMetadata? {
try? JSONDecoder().decode(ChannelMetadata.self, from: data)
}
// MARK: - Binary Encoding
func toBinaryData() -> Data {
var data = Data()
// Flags byte: bit 0 = hasKeyCommitment
var flags: UInt8 = 0
if keyCommitment != nil { flags |= 0x01 }
data.appendUInt8(flags)
data.appendString(channel)
// CreatorID as 8-byte hex string
var creatorData = Data()
var tempID = creatorID
while tempID.count >= 2 && creatorData.count < 8 {
let hexByte = String(tempID.prefix(2))
if let byte = UInt8(hexByte, radix: 16) {
creatorData.append(byte)
}
tempID = String(tempID.dropFirst(2))
}
while creatorData.count < 8 {
creatorData.append(0)
}
data.append(creatorData)
data.appendString(creatorFingerprint)
data.appendDate(createdAt)
data.appendUInt8(isPasswordProtected ? 1 : 0)
if let keyCommitment = keyCommitment {
data.appendString(keyCommitment)
}
return data
}
static func fromBinaryData(_ data: Data) -> ChannelMetadata? {
// Create defensive copy
let dataCopy = Data(data)
var offset = 0
guard let flags = dataCopy.readUInt8(at: &offset) else { return nil }
let hasKeyCommitment = (flags & 0x01) != 0
guard let channel = dataCopy.readString(at: &offset) else { return nil }
guard let creatorIDData = dataCopy.readFixedBytes(at: &offset, count: 8) else { return nil }
let creatorID = creatorIDData.hexEncodedString()
guard let creatorFingerprint = dataCopy.readString(at: &offset),
let createdAt = dataCopy.readDate(at: &offset),
let isPasswordProtectedByte = dataCopy.readUInt8(at: &offset) else { return nil }
let isPasswordProtected = isPasswordProtectedByte != 0
var keyCommitment: String? = nil
if hasKeyCommitment {
keyCommitment = dataCopy.readString(at: &offset)
}
return ChannelMetadata(channel: channel,
creatorID: creatorID,
creatorFingerprint: creatorFingerprint,
createdAt: createdAt,
isPasswordProtected: isPasswordProtected,
keyCommitment: keyCommitment)
}
}
// MARK: - Peer Identity Rotation
@ -1120,12 +766,9 @@ struct BitchatMessage: Codable, Equatable {
let recipientNickname: String?
let senderPeerID: String?
let mentions: [String]? // Array of mentioned nicknames
let channel: String? // Channel hashtag (e.g., "#general")
let encryptedContent: Data? // For password-protected rooms
let isEncrypted: Bool // Flag to indicate if content is encrypted
var deliveryStatus: DeliveryStatus? // Delivery tracking
init(id: String? = nil, sender: String, content: String, timestamp: Date, isRelay: Bool, originalSender: String? = nil, isPrivate: Bool = false, recipientNickname: String? = nil, senderPeerID: String? = nil, mentions: [String]? = nil, channel: String? = nil, encryptedContent: Data? = nil, isEncrypted: Bool = false, deliveryStatus: DeliveryStatus? = nil) {
init(id: String? = nil, sender: String, content: String, timestamp: Date, isRelay: Bool, originalSender: String? = nil, isPrivate: Bool = false, recipientNickname: String? = nil, senderPeerID: String? = nil, mentions: [String]? = nil, deliveryStatus: DeliveryStatus? = nil) {
self.id = id ?? UUID().uuidString
self.sender = sender
self.content = content
@ -1136,9 +779,6 @@ struct BitchatMessage: Codable, Equatable {
self.recipientNickname = recipientNickname
self.senderPeerID = senderPeerID
self.mentions = mentions
self.channel = channel
self.encryptedContent = encryptedContent
self.isEncrypted = isEncrypted
self.deliveryStatus = deliveryStatus ?? (isPrivate ? .sending : nil)
}
}
@ -1148,10 +788,6 @@ protocol BitchatDelegate: AnyObject {
func didConnectToPeer(_ peerID: String)
func didDisconnectFromPeer(_ peerID: String)
func didUpdatePeerList(_ peers: [String])
func didReceiveChannelLeave(_ channel: String, from peerID: String)
func didReceivePasswordProtectedChannelAnnouncement(_ channel: String, isProtected: Bool, creatorID: String?, keyCommitment: String?)
func didReceiveChannelRetentionAnnouncement(_ channel: String, enabled: Bool, creatorID: String?)
func decryptChannelMessage(_ encryptedContent: Data, channel: String) -> String?
// Optional method to check if a fingerprint belongs to a favorite peer
func isFavorite(fingerprint: String) -> Bool
@ -1160,14 +796,6 @@ protocol BitchatDelegate: AnyObject {
func didReceiveDeliveryAck(_ ack: DeliveryAck)
func didReceiveReadReceipt(_ receipt: ReadReceipt)
func didUpdateMessageDeliveryStatus(_ messageID: String, status: DeliveryStatus)
// Channel key verification methods
func didReceiveChannelKeyVerifyRequest(_ request: ChannelKeyVerifyRequest, from peerID: String)
func didReceiveChannelKeyVerifyResponse(_ response: ChannelKeyVerifyResponse, from peerID: String)
func didReceiveChannelPasswordUpdate(_ update: ChannelPasswordUpdate, from peerID: String)
// Channel metadata methods
func didReceiveChannelMetadata(_ metadata: ChannelMetadata, from peerID: String)
}
// Provide default implementation to make it effectively optional
@ -1176,23 +804,6 @@ extension BitchatDelegate {
return false
}
func didReceiveChannelLeave(_ channel: String, from peerID: String) {
// Default empty implementation
}
func didReceivePasswordProtectedChannelAnnouncement(_ channel: String, isProtected: Bool, creatorID: String?, keyCommitment: String?) {
// Default empty implementation
}
func didReceiveChannelRetentionAnnouncement(_ channel: String, enabled: Bool, creatorID: String?) {
// Default empty implementation
}
func decryptChannelMessage(_ encryptedContent: Data, channel: String) -> String? {
// Default returns nil (unable to decrypt)
return nil
}
func didReceiveDeliveryAck(_ ack: DeliveryAck) {
// Default empty implementation
}
@ -1204,20 +815,4 @@ extension BitchatDelegate {
func didUpdateMessageDeliveryStatus(_ messageID: String, status: DeliveryStatus) {
// Default empty implementation
}
func didReceiveChannelKeyVerifyRequest(_ request: ChannelKeyVerifyRequest, from peerID: String) {
// Default empty implementation
}
func didReceiveChannelKeyVerifyResponse(_ response: ChannelKeyVerifyResponse, from peerID: String) {
// Default empty implementation
}
func didReceiveChannelPasswordUpdate(_ update: ChannelPasswordUpdate, from peerID: String) {
// Default empty implementation
}
func didReceiveChannelMetadata(_ metadata: ChannelMetadata, from peerID: String) {
// Default empty implementation
}
}
}

View File

@ -753,7 +753,7 @@ class BluetoothMeshService: NSObject {
self.characteristic = characteristic
}
func sendMessage(_ content: String, mentions: [String] = [], channel: String? = nil, to recipientID: String? = nil, messageID: String? = nil, timestamp: Date? = nil) {
func sendMessage(_ content: String, mentions: [String] = [], to recipientID: String? = nil, messageID: String? = nil, timestamp: Date? = nil) {
// Defensive check for empty content
guard !content.isEmpty else { return }
messageQueue.async { [weak self] in
@ -772,8 +772,7 @@ class BluetoothMeshService: NSObject {
isPrivate: false,
recipientNickname: nil,
senderPeerID: self.myPeerID,
mentions: mentions.isEmpty ? nil : mentions,
channel: channel
mentions: mentions.isEmpty ? nil : mentions
)
if let messageData = message.toBinaryPayload() {
@ -889,24 +888,6 @@ class BluetoothMeshService: NSObject {
}
}
func sendChannelLeaveNotification(_ channel: String) {
messageQueue.async { [weak self] in
guard let self = self else { return }
// Create a leave packet with channel hashtag as payload
let packet = BitchatPacket(
type: MessageType.leave.rawValue,
senderID: Data(hexString: self.myPeerID) ?? Data(),
recipientID: SpecialRecipients.broadcast, // Broadcast to all
timestamp: UInt64(Date().timeIntervalSince1970 * 1000),
payload: Data(channel.utf8), // Channel hashtag as payload
signature: nil,
ttl: 3 // Short TTL for leave notifications
)
self.broadcastPacket(packet)
}
}
func sendDeliveryAck(_ ack: DeliveryAck, to recipientID: String) {
messageQueue.async { [weak self] in
@ -1040,105 +1021,9 @@ class BluetoothMeshService: NSObject {
}
}
func announcePasswordProtectedChannel(_ channel: String, isProtected: Bool = true, creatorID: String? = nil, keyCommitment: String? = nil) {
messageQueue.async { [weak self] in
guard let self = self else { return }
// Payload format: channel|isProtected|creatorID|keyCommitment
let protectedFlag = isProtected ? "1" : "0"
let creator = creatorID ?? self.myPeerID
let commitment = keyCommitment ?? ""
let payload = "\(channel)|\(protectedFlag)|\(creator)|\(commitment)"
let packet = BitchatPacket(
type: MessageType.channelAnnounce.rawValue,
senderID: Data(hexString: self.myPeerID) ?? Data(),
recipientID: SpecialRecipients.broadcast,
timestamp: UInt64(Date().timeIntervalSince1970 * 1000),
payload: Data(payload.utf8),
signature: nil,
ttl: 5 // Allow wider propagation for channel announcements
)
self.broadcastPacket(packet)
}
}
func sendChannelMetadata(_ metadata: ChannelMetadata) {
messageQueue.async { [weak self] in
guard let self = self else { return }
let metadataData = metadata.toBinaryData()
let packet = BitchatPacket(
type: MessageType.channelMetadata.rawValue,
senderID: Data(hexString: self.myPeerID) ?? Data(),
recipientID: SpecialRecipients.broadcast,
timestamp: UInt64(Date().timeIntervalSince1970 * 1000),
payload: metadataData,
signature: nil,
ttl: 5 // Allow wider propagation for channel metadata
)
self.broadcastPacket(packet)
}
}
func sendEncryptedChannelMessage(_ content: String, mentions: [String], channel: String, channelKey: SymmetricKey, messageID: String? = nil, timestamp: Date? = nil) {
messageQueue.async { [weak self] in
guard let self = self else { return }
let nickname = self.delegate as? ChatViewModel
let senderNick = nickname?.nickname ?? self.myPeerID
// Encrypt the content
guard let contentData = content.data(using: .utf8) else { return }
// Debug logging removed
do {
let sealedBox = try AES.GCM.seal(contentData, using: channelKey)
guard let encryptedData = sealedBox.combined else {
// Encryption failed to produce combined data
return
}
// Create message with encrypted content
let message = BitchatMessage(
id: messageID,
sender: senderNick,
content: "", // Empty placeholder since actual content is encrypted
timestamp: timestamp ?? Date(),
isRelay: false,
originalSender: nil,
isPrivate: false,
recipientNickname: nil,
senderPeerID: self.myPeerID,
mentions: mentions.isEmpty ? nil : mentions,
channel: channel,
encryptedContent: encryptedData,
isEncrypted: true
)
if let messageData = message.toBinaryPayload() {
let packet = BitchatPacket(
type: MessageType.message.rawValue,
senderID: Data(hexString: self.myPeerID) ?? Data(),
recipientID: SpecialRecipients.broadcast,
timestamp: UInt64(Date().timeIntervalSince1970 * 1000),
payload: messageData,
signature: nil,
ttl: self.adaptiveTTL
)
self.broadcastPacket(packet)
}
} catch {
}
}
}
private func sendAnnouncementToPeer(_ peerID: String) {
guard let vm = delegate as? ChatViewModel else { return }
@ -1209,7 +1094,7 @@ class BluetoothMeshService: NSObject {
// Emergency disconnect for panic situations
func emergencyDisconnectAll() {
SecureLogger.logSecurityEvent(.invalidKey(reason: "Emergency disconnect triggered"), level: .warning)
SecureLogger.log("Emergency disconnect triggered", category: SecureLogger.security, level: .warning)
// Stop advertising immediately
if peripheralManager?.isAdvertising == true {
@ -1582,11 +1467,9 @@ class BluetoothMeshService: NSObject {
MessageRetryService.shared.addMessageForRetry(
content: message.content,
mentions: message.mentions,
channel: message.channel,
isPrivate: message.isPrivate,
recipientPeerID: nil,
recipientNickname: message.recipientNickname,
channelKey: nil,
originalMessageID: message.id,
originalTimestamp: message.timestamp
)
@ -1646,24 +1529,12 @@ class BluetoothMeshService: NSObject {
// This is our own message that failed to send
if packet.type == MessageType.message.rawValue,
let message = BitchatMessage.fromBinaryPayload(packet.payload) {
// For encrypted channel messages, we need to preserve the channel key
var channelKeyData: Data? = nil
if let channel = message.channel, message.isEncrypted {
// This is an encrypted channel message
if let viewModel = delegate as? ChatViewModel,
let channelKey = viewModel.channelKeys[channel] {
channelKeyData = channelKey.withUnsafeBytes { Data($0) }
}
}
MessageRetryService.shared.addMessageForRetry(
content: message.content,
mentions: message.mentions,
channel: message.channel,
isPrivate: message.isPrivate,
recipientPeerID: nil,
recipientNickname: message.recipientNickname,
channelKey: channelKeyData,
originalMessageID: message.id,
originalTimestamp: message.timestamp
)
@ -1698,7 +1569,7 @@ class BluetoothMeshService: NSObject {
let currentTime = UInt64(Date().timeIntervalSince1970 * 1000) // milliseconds
let timeDiff = abs(Int64(currentTime) - Int64(packet.timestamp))
if timeDiff > 300000 { // 5 minutes in milliseconds
SecureLogger.logSecurityEvent(.replayAttackDetected(channel: "timestamp-\(senderID)"), level: .warning)
SecureLogger.log("Replay attack detected - timestamp from \(senderID)", category: SecureLogger.security, level: .warning)
SecureLogger.log("Dropped message with stale timestamp. Age: \(timeDiff/1000)s from \(senderID)", category: SecureLogger.security, level: .warning)
return
}
@ -1772,17 +1643,7 @@ class BluetoothMeshService: NSObject {
self.peerNicknames[senderID] = message.sender
}
// Handle encrypted channel messages
var finalContent = message.content
if message.isEncrypted, let channel = message.channel, let encryptedData = message.encryptedContent {
// Try to decrypt the content
if let decryptedContent = self.delegate?.decryptChannelMessage(encryptedData, channel: channel) {
finalContent = decryptedContent
} else {
// Unable to decrypt - show placeholder
finalContent = "[Encrypted message - password required]"
}
}
let finalContent = message.content
let messageWithPeerID = BitchatMessage(
id: message.id, // Preserve the original message ID
@ -1794,10 +1655,7 @@ class BluetoothMeshService: NSObject {
isPrivate: false,
recipientNickname: nil,
senderPeerID: senderID,
mentions: message.mentions,
channel: message.channel,
encryptedContent: message.encryptedContent,
isEncrypted: message.isEncrypted
mentions: message.mentions
)
// Track last message time from this peer
@ -1808,22 +1666,6 @@ class BluetoothMeshService: NSObject {
self.delegate?.didReceiveMessage(messageWithPeerID)
}
// Generate and send ACK for channel messages if we're mentioned or it's a small channel
let viewModel = self.delegate as? ChatViewModel
let myNickname = viewModel?.nickname ?? self.myPeerID
if let _ = message.channel,
let mentions = message.mentions,
(mentions.contains(myNickname) || self.activePeers.count < 10) {
if let ack = DeliveryTracker.shared.generateAck(
for: messageWithPeerID,
myPeerID: self.myPeerID,
myNickname: myNickname,
hopCount: UInt8(self.maxTTL - packet.ttl)
) {
SecureLogger.log("Generating delivery ACK for channel mention message \(messageWithPeerID.id)", category: SecureLogger.session, level: .debug)
self.sendDeliveryAck(ack, to: senderID)
}
}
}
// Relay broadcast messages
@ -1897,7 +1739,6 @@ class BluetoothMeshService: NSObject {
recipientNickname: message.recipientNickname,
senderPeerID: senderID,
mentions: message.mentions,
channel: message.channel,
deliveryStatus: nil // Will be set to .delivered in ChatViewModel
)
@ -2180,39 +2021,23 @@ class BluetoothMeshService: NSObject {
case .leave:
let senderID = packet.senderID.hexEncodedString()
// Check if payload contains a channel hashtag
if let channel = String(data: packet.payload, encoding: .utf8),
channel.hasPrefix("#") {
// Channel leave notification
DispatchQueue.main.async {
self.delegate?.didReceiveChannelLeave(channel, from: senderID)
}
// Relay if TTL > 0
if packet.ttl > 1 {
var relayPacket = packet
relayPacket.ttl -= 1
self.broadcastPacket(relayPacket)
}
} else {
// Legacy peer disconnect (keeping for backwards compatibility)
if String(data: packet.payload, encoding: .utf8) != nil {
// Remove from active peers with proper locking
collectionsQueue.sync(flags: .barrier) {
_ = self.activePeers.remove(senderID)
_ = self.peerNicknames.removeValue(forKey: senderID)
}
announcedPeers.remove(senderID)
// Show leave message
DispatchQueue.main.async {
self.delegate?.didDisconnectFromPeer(senderID)
}
self.notifyPeerListUpdate()
}
// Legacy peer disconnect (keeping for backwards compatibility)
if String(data: packet.payload, encoding: .utf8) != nil {
// Remove from active peers with proper locking
collectionsQueue.sync(flags: .barrier) {
_ = self.activePeers.remove(senderID)
_ = self.peerNicknames.removeValue(forKey: senderID)
}
announcedPeers.remove(senderID)
// Show leave message
DispatchQueue.main.async {
self.delegate?.didDisconnectFromPeer(senderID)
}
self.notifyPeerListUpdate()
}
case .fragmentStart, .fragmentContinue, .fragmentEnd:
// let fragmentTypeStr = packet.type == MessageType.fragmentStart.rawValue ? "START" :
@ -2232,29 +2057,6 @@ class BluetoothMeshService: NSObject {
self.broadcastPacket(relayPacket)
}
case .channelAnnounce:
if let payloadStr = String(data: packet.payload, encoding: .utf8) {
// Parse payload: channel|isProtected|creatorID|keyCommitment
let components = payloadStr.split(separator: "|").map(String.init)
if components.count >= 3 {
let channel = components[0]
let isProtected = components[1] == "1"
let creatorID = components[2]
let keyCommitment = components.count >= 4 ? components[3] : nil
DispatchQueue.main.async {
self.delegate?.didReceivePasswordProtectedChannelAnnouncement(channel, isProtected: isProtected, creatorID: creatorID, keyCommitment: keyCommitment)
}
// Relay announcement
if packet.ttl > 1 {
var relayPacket = packet
relayPacket.ttl -= 1
self.broadcastPacket(relayPacket)
}
}
}
case .deliveryAck:
// Handle delivery acknowledgment
@ -2514,34 +2316,6 @@ class BluetoothMeshService: NSObject {
handleNoiseEncryptedMessage(from: senderID, encryptedData: packet.payload, originalPacket: packet)
}
case .channelKeyVerifyRequest:
// Handle channel key verification request
let senderID = packet.senderID.hexEncodedString()
if !isPeerIDOurs(senderID) {
handleChannelKeyVerifyRequest(from: senderID, data: packet.payload)
}
case .channelKeyVerifyResponse:
// Handle channel key verification response
let senderID = packet.senderID.hexEncodedString()
if !isPeerIDOurs(senderID) {
handleChannelKeyVerifyResponse(from: senderID, data: packet.payload)
}
case .channelPasswordUpdate:
// Handle channel password update from owner
let senderID = packet.senderID.hexEncodedString()
if !isPeerIDOurs(senderID) {
handleChannelPasswordUpdate(from: senderID, data: packet.payload)
}
case .channelMetadata:
// Handle channel metadata announcement
let senderID = packet.senderID.hexEncodedString()
if !isPeerIDOurs(senderID) {
handleChannelMetadata(from: senderID, data: packet.payload)
}
case .versionHello:
// Handle version negotiation hello
let senderID = packet.senderID.hexEncodedString()
@ -3694,52 +3468,6 @@ extension BluetoothMeshService: CBPeripheralManagerDelegate {
}
}
private func handleChannelKeyVerifyRequest(from peerID: String, data: Data) {
guard let request = ChannelKeyVerifyRequest.fromBinaryData(data) ?? ChannelKeyVerifyRequest.decode(from: data) else { return }
// Forward to delegate (ChatViewModel) to handle
DispatchQueue.main.async { [weak self] in
self?.delegate?.didReceiveChannelKeyVerifyRequest(request, from: peerID)
}
}
private func handleChannelKeyVerifyResponse(from peerID: String, data: Data) {
guard let response = ChannelKeyVerifyResponse.fromBinaryData(data) ?? ChannelKeyVerifyResponse.decode(from: data) else { return }
// Forward to delegate (ChatViewModel) to handle
DispatchQueue.main.async { [weak self] in
self?.delegate?.didReceiveChannelKeyVerifyResponse(response, from: peerID)
}
}
private func handleChannelPasswordUpdate(from peerID: String, data: Data) {
// First decrypt the data using Noise session
// Use noiseService directly
do {
// Decrypt the outer message
let decryptedData = try noiseService.decrypt(data, from: peerID)
// Parse the password update
guard let update = ChannelPasswordUpdate.fromBinaryData(decryptedData) ?? ChannelPasswordUpdate.decode(from: decryptedData) else { return }
// Forward to delegate (ChatViewModel) to handle
DispatchQueue.main.async { [weak self] in
self?.delegate?.didReceiveChannelPasswordUpdate(update, from: peerID)
}
} catch {
}
}
private func handleChannelMetadata(from peerID: String, data: Data) {
// Channel metadata is broadcast unencrypted (like channel announcements)
guard let metadata = ChannelMetadata.fromBinaryData(data) ?? ChannelMetadata.decode(from: data) else { return }
// Forward to delegate (ChatViewModel) to handle
DispatchQueue.main.async { [weak self] in
self?.delegate?.didReceiveChannelMetadata(metadata, from: peerID)
}
}
// MARK: - Protocol Version Negotiation
@ -3927,81 +3655,8 @@ extension BluetoothMeshService: CBPeripheralManagerDelegate {
#endif
}
func sendChannelKeyVerifyRequest(_ request: ChannelKeyVerifyRequest, to peers: [String]) {
let requestData = request.toBinaryData()
// Send to each peer
for peerID in peers {
let packet = BitchatPacket(
type: MessageType.channelKeyVerifyRequest.rawValue,
senderID: Data(myPeerID.utf8),
recipientID: Data(peerID.utf8),
timestamp: UInt64(Date().timeIntervalSince1970 * 1000),
payload: requestData,
signature: nil,
ttl: 3 // Limited TTL for verification requests
)
broadcastPacket(packet)
}
}
func sendChannelKeyVerifyResponse(_ response: ChannelKeyVerifyResponse, to peerID: String) {
let responseData = response.toBinaryData()
let packet = BitchatPacket(
type: MessageType.channelKeyVerifyResponse.rawValue,
senderID: Data(myPeerID.utf8),
recipientID: Data(peerID.utf8),
timestamp: UInt64(Date().timeIntervalSince1970 * 1000),
payload: responseData,
signature: nil,
ttl: 3 // Limited TTL for responses
)
broadcastPacket(packet)
}
func sendChannelPasswordUpdate(_ password: String, channel: String, newCommitment: String, to peerID: String) {
// Use noiseService directly
// Check if we have a Noise session with this peer
if !noiseService.hasEstablishedSession(with: peerID) {
return
}
// Get our fingerprint
let myFingerprint = noiseService.getIdentityFingerprint()
// Create password update with encrypted password field
let update = ChannelPasswordUpdate(
channel: channel,
ownerID: myPeerID, // Keep for backward compatibility
ownerFingerprint: myFingerprint,
encryptedPassword: Data(password.utf8), // Will be encrypted as whole message
newKeyCommitment: newCommitment
)
let updateData = update.toBinaryData()
do {
// Encrypt the entire update message
let encryptedData = try noiseService.encrypt(updateData, for: peerID)
let packet = BitchatPacket(
type: MessageType.channelPasswordUpdate.rawValue,
senderID: Data(myPeerID.utf8),
recipientID: Data(peerID.utf8),
timestamp: UInt64(Date().timeIntervalSince1970 * 1000),
payload: encryptedData,
signature: nil,
ttl: 3 // Limited TTL for password updates
)
broadcastPacket(packet)
} catch {
}
}
private func sendNoiseIdentityAnnounce(to specificPeerID: String? = nil) {
// Rate limit identity announcements

View File

@ -41,19 +41,16 @@ class DeliveryTracker {
let recipientID: String
let recipientNickname: String
let retryCount: Int
let isChannelMessage: Bool
let isFavorite: Bool
var ackedBy: Set<String> = [] // For tracking partial channel delivery
let expectedRecipients: Int // For channel messages
var timeoutTimer: Timer?
var isTimedOut: Bool {
let timeout: TimeInterval = isFavorite ? 300 : (isChannelMessage ? 60 : 30)
let timeout: TimeInterval = isFavorite ? 300 : 30
return Date().timeIntervalSince(sentAt) > timeout
}
var shouldRetry: Bool {
return retryCount < 3 && isFavorite && !isChannelMessage
return retryCount < 3 && isFavorite
}
}
@ -68,10 +65,10 @@ class DeliveryTracker {
// MARK: - Public Methods
func trackMessage(_ message: BitchatMessage, recipientID: String, recipientNickname: String, isFavorite: Bool = false, expectedRecipients: Int = 1) {
// Don't track broadcasts or certain message types
guard message.isPrivate || message.channel != nil else { return }
// Only track private messages
guard message.isPrivate else { return }
SecureLogger.log("Tracking message \(message.id) - private: \(message.isPrivate), channel: \(message.channel ?? "none"), recipient: \(recipientNickname)", category: SecureLogger.session, level: .info)
SecureLogger.log("Tracking message \(message.id) - private: \(message.isPrivate), recipient: \(recipientNickname)", category: SecureLogger.session, level: .info)
let delivery = PendingDelivery(
@ -80,9 +77,7 @@ class DeliveryTracker {
recipientID: recipientID,
recipientNickname: recipientNickname,
retryCount: 0,
isChannelMessage: message.channel != nil,
isFavorite: isFavorite,
expectedRecipients: expectedRecipients,
timeoutTimer: nil
)
@ -126,7 +121,7 @@ class DeliveryTracker {
receivedAckIDs.insert(ack.ackID)
// Find the pending delivery
guard var delivery = pendingDeliveries[ack.originalMessageID] else {
guard let delivery = pendingDeliveries[ack.originalMessageID] else {
// Message might have already been delivered or timed out
SecureLogger.log("No pending delivery found for message \(ack.originalMessageID)", category: SecureLogger.session, level: .warning)
return
@ -135,28 +130,10 @@ class DeliveryTracker {
// Cancel timeout timer
delivery.timeoutTimer?.invalidate()
if delivery.isChannelMessage {
// Track partial delivery for channel messages
delivery.ackedBy.insert(ack.recipientID)
pendingDeliveries[ack.originalMessageID] = delivery
let deliveredCount = delivery.ackedBy.count
let totalExpected = delivery.expectedRecipients
if deliveredCount >= totalExpected || deliveredCount >= max(1, totalExpected / 2) {
// Consider delivered if we got ACKs from at least half the expected recipients
updateDeliveryStatus(ack.originalMessageID, status: .delivered(to: "\(deliveredCount) members", at: Date()))
pendingDeliveries.removeValue(forKey: ack.originalMessageID)
} else {
// Update partial delivery status
updateDeliveryStatus(ack.originalMessageID, status: .partiallyDelivered(reached: deliveredCount, total: totalExpected))
}
} else {
// Direct message - mark as delivered
SecureLogger.log("Marking private message \(ack.originalMessageID) as delivered to \(ack.recipientNickname)", category: SecureLogger.session, level: .info)
updateDeliveryStatus(ack.originalMessageID, status: .delivered(to: ack.recipientNickname, at: Date()))
pendingDeliveries.removeValue(forKey: ack.originalMessageID)
}
// Direct message - mark as delivered
SecureLogger.log("Marking private message \(ack.originalMessageID) as delivered to \(ack.recipientNickname)", category: SecureLogger.session, level: .info)
updateDeliveryStatus(ack.originalMessageID, status: .delivered(to: ack.recipientNickname, at: Date()))
pendingDeliveries.removeValue(forKey: ack.originalMessageID)
}
func generateAck(for message: BitchatMessage, myPeerID: String, myNickname: String, hopCount: UInt8) -> DeliveryAck? {
@ -165,8 +142,8 @@ class DeliveryTracker {
return nil
}
// Don't ACK broadcasts or system messages
guard message.isPrivate || message.channel != nil else {
// Only ACK private messages
guard message.isPrivate else {
return nil
}
@ -212,11 +189,9 @@ class DeliveryTracker {
return
}
let isFavorite = delivery.isFavorite
let isChannelMessage = delivery.isChannelMessage
pendingLock.unlock()
let timeout = isFavorite ? favoriteTimeout :
(isChannelMessage ? roomMessageTimeout : privateMessageTimeout)
let timeout = isFavorite ? favoriteTimeout : privateMessageTimeout
let timer = Timer.scheduledTimer(withTimeInterval: timeout, repeats: false) { [weak self] _ in
self?.handleTimeout(messageID: messageID)
@ -238,7 +213,6 @@ class DeliveryTracker {
}
let shouldRetry = delivery.shouldRetry
let isChannelMessage = delivery.isChannelMessage
if shouldRetry {
pendingLock.unlock()
@ -246,7 +220,7 @@ class DeliveryTracker {
retryDelivery(messageID: messageID)
} else {
// Mark as failed
let reason = isChannelMessage ? "No response from channel members" : "Message not delivered"
let reason = "Message not delivered"
pendingDeliveries.removeValue(forKey: messageID)
pendingLock.unlock()
updateDeliveryStatus(messageID, status: .failed(reason: reason))
@ -267,10 +241,7 @@ class DeliveryTracker {
recipientID: delivery.recipientID,
recipientNickname: delivery.recipientNickname,
retryCount: delivery.retryCount + 1,
isChannelMessage: delivery.isChannelMessage,
isFavorite: delivery.isFavorite,
ackedBy: delivery.ackedBy,
expectedRecipients: delivery.expectedRecipients,
timeoutTimer: nil
)

View File

@ -95,62 +95,6 @@ class KeychainManager {
#endif
}
// MARK: - Channel Passwords
func saveChannelPassword(_ password: String, for channel: String) -> Bool {
let key = "channel_\(channel)"
let result = save(password, forKey: key)
SecureLogger.logKeyOperation("save", keyType: "channel password for \(channel)", success: result)
return result
}
func getChannelPassword(for channel: String) -> String? {
let key = "channel_\(channel)"
return retrieve(forKey: key)
}
func deleteChannelPassword(for channel: String) -> Bool {
let key = "channel_\(channel)"
let result = delete(forKey: key)
SecureLogger.logKeyOperation("delete", keyType: "channel password for \(channel)", success: result)
return result
}
func getAllChannelPasswords() -> [String: String] {
var passwords: [String: String] = [:]
// Build query without kSecReturnData to avoid error -50
var query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: service,
kSecMatchLimit as String: kSecMatchLimitAll,
kSecReturnAttributes as String: true
]
// For sandboxed apps, use the app group
if isSandboxed() {
query[kSecAttrAccessGroup as String] = appGroup
}
var result: AnyObject?
let status = SecItemCopyMatching(query as CFDictionary, &result)
if status == errSecSuccess, let items = result as? [[String: Any]] {
for item in items {
if let account = item[kSecAttrAccount as String] as? String,
account.hasPrefix("channel_") {
// Now retrieve the actual password data for this specific item
let channel = String(account.dropFirst(8)) // Remove "channel_" prefix
if let password = getChannelPassword(for: channel) {
passwords[channel] = password
}
}
}
}
return passwords
}
// MARK: - Identity Keys
func saveIdentityKey(_ keyData: Data, forKey key: String) -> Bool {
@ -290,7 +234,7 @@ class KeychainManager {
// Delete ALL keychain data for panic mode
func deleteAllKeychainData() -> Bool {
SecureLogger.logSecurityEvent(.invalidKey(reason: "Panic mode - deleting all keychain data"), level: .warning)
SecureLogger.log("Panic mode - deleting all keychain data", category: SecureLogger.security, level: .warning)
var totalDeleted = 0

View File

@ -16,11 +16,9 @@ struct RetryableMessage {
let originalTimestamp: Date?
let content: String
let mentions: [String]?
let channel: String?
let isPrivate: Bool
let recipientPeerID: String?
let recipientNickname: String?
let channelKey: Data?
let retryCount: Int
let maxRetries: Int = 3
let nextRetryTime: Date
@ -53,11 +51,9 @@ class MessageRetryService {
func addMessageForRetry(
content: String,
mentions: [String]? = nil,
channel: String? = nil,
isPrivate: Bool = false,
recipientPeerID: String? = nil,
recipientNickname: String? = nil,
channelKey: Data? = nil,
originalMessageID: String? = nil,
originalTimestamp: Date? = nil
) {
@ -87,11 +83,9 @@ class MessageRetryService {
originalTimestamp: originalTimestamp,
content: content,
mentions: mentions,
channel: channel,
isPrivate: isPrivate,
recipientPeerID: recipientPeerID,
recipientNickname: recipientNickname,
channelKey: channelKey,
retryCount: 0,
nextRetryTime: Date().addingTimeInterval(retryInterval)
)
@ -168,43 +162,9 @@ class MessageRetryService {
originalTimestamp: message.originalTimestamp,
content: message.content,
mentions: message.mentions,
channel: message.channel,
isPrivate: message.isPrivate,
recipientPeerID: message.recipientPeerID,
recipientNickname: message.recipientNickname,
channelKey: message.channelKey,
retryCount: message.retryCount + 1,
nextRetryTime: Date().addingTimeInterval(self.retryInterval * Double(message.retryCount + 2))
)
self.retryQueue.append(updatedMessage)
}
} else if let channel = message.channel, let channelKeyData = message.channelKey {
// For channel messages, check if we have peers in the channel
if !connectedPeers.isEmpty {
// Recreate SymmetricKey from data
let channelKey = SymmetricKey(data: channelKeyData)
meshService.sendEncryptedChannelMessage(
message.content,
mentions: message.mentions ?? [],
channel: channel,
channelKey: channelKey,
messageID: message.originalMessageID,
timestamp: message.originalTimestamp
)
} else {
// No peers connected, keep in queue
var updatedMessage = message
updatedMessage = RetryableMessage(
id: message.id,
originalMessageID: message.originalMessageID,
originalTimestamp: message.originalTimestamp,
content: message.content,
mentions: message.mentions,
channel: message.channel,
isPrivate: message.isPrivate,
recipientPeerID: message.recipientPeerID,
recipientNickname: message.recipientNickname,
channelKey: message.channelKey,
retryCount: message.retryCount + 1,
nextRetryTime: Date().addingTimeInterval(self.retryInterval * Double(message.retryCount + 2))
)
@ -216,7 +176,6 @@ class MessageRetryService {
meshService.sendMessage(
message.content,
mentions: message.mentions ?? [],
channel: message.channel,
to: nil,
messageID: message.originalMessageID,
timestamp: message.originalTimestamp
@ -230,11 +189,9 @@ class MessageRetryService {
originalTimestamp: message.originalTimestamp,
content: message.content,
mentions: message.mentions,
channel: message.channel,
isPrivate: message.isPrivate,
recipientPeerID: message.recipientPeerID,
recipientNickname: message.recipientNickname,
channelKey: message.channelKey,
retryCount: message.retryCount + 1,
nextRetryTime: Date().addingTimeInterval(self.retryInterval * Double(message.retryCount + 2))
)

View File

@ -10,6 +10,41 @@ import Foundation
import CryptoKit
import os.log
// MARK: - Encryption Status
enum EncryptionStatus: Equatable {
case none
case noiseHandshaking
case noiseSecured
case noiseVerified
var icon: String {
switch self {
case .none:
return "lock.slash"
case .noiseHandshaking:
return "lock.rotation"
case .noiseSecured:
return "lock"
case .noiseVerified:
return "lock.shield"
}
}
var description: String {
switch self {
case .none:
return "Not encrypted"
case .noiseHandshaking:
return "Establishing encryption..."
case .noiseSecured:
return "Encrypted"
case .noiseVerified:
return "Encrypted & Verified"
}
}
}
// MARK: - Noise Encryption Service
class NoiseEncryptionService {
@ -24,9 +59,6 @@ class NoiseEncryptionService {
// Session manager
private let sessionManager: NoiseSessionManager
// Channel encryption
private let channelEncryption = NoiseChannelEncryption()
// Peer fingerprints (SHA256 hash of static public key)
private var peerFingerprints: [String: String] = [:] // peerID -> fingerprint
private var fingerprintToPeerID: [String: String] = [:] // fingerprint -> peerID
@ -133,7 +165,7 @@ class NoiseEncryptionService {
let deletedStatic = KeychainManager.shared.deleteIdentityKey(forKey: "noiseStaticKey")
let deletedSigning = KeychainManager.shared.deleteIdentityKey(forKey: "ed25519SigningKey")
SecureLogger.logKeyOperation("delete", keyType: "identity keys", success: deletedStatic && deletedSigning)
SecureLogger.logSecurityEvent(.invalidKey(reason: "Panic mode activated - identity cleared"), level: .warning)
SecureLogger.log("Panic mode activated - identity cleared", category: SecureLogger.security, level: .warning)
// Stop rekey timer
stopRekeyTimer()
}
@ -340,69 +372,7 @@ class NoiseEncryptionService {
let hash = SHA256.hash(data: publicKey.rawRepresentation)
return hash.map { String(format: "%02x", $0) }.joined()
}
// MARK: - Channel Encryption
/// Set password for a channel
func setChannelPassword(_ password: String, for channel: String) {
// Validate channel name
guard NoiseSecurityValidator.validateChannelName(channel) else {
SecureLogger.log("Invalid channel name for password", category: SecureLogger.security, level: .warning)
return
}
// Validate password is not empty
guard !password.isEmpty else {
SecureLogger.log("Empty password rejected for channel", category: SecureLogger.security, level: .warning)
return
}
channelEncryption.setChannelPassword(password, for: channel)
SecureLogger.logKeyOperation("set", keyType: "channel password", success: true)
}
/// Load channel password from keychain
func loadChannelPassword(for channel: String) -> Bool {
return channelEncryption.loadChannelPassword(for: channel)
}
/// Remove channel password
func removeChannelPassword(for channel: String) {
channelEncryption.removeChannelPassword(for: channel)
}
/// Encrypt message for a channel
func encryptChannelMessage(_ message: String, for channel: String) throws -> Data {
return try channelEncryption.encryptChannelMessage(message, for: channel)
}
/// Decrypt channel message
func decryptChannelMessage(_ encryptedData: Data, for channel: String) throws -> String {
return try channelEncryption.decryptChannelMessage(encryptedData, for: channel)
}
/// Share channel password with a peer securely via Noise
func shareChannelPassword(_ password: String, channel: String, with peerID: String) throws -> Data? {
// Create channel key packet
guard let keyPacket = channelEncryption.createChannelKeyPacket(password: password, channel: channel) else {
return nil
}
// Encrypt via Noise session
return try encrypt(keyPacket, for: peerID)
}
/// Process received channel key via Noise
func processReceivedChannelKey(_ encryptedData: Data, from peerID: String) throws {
// Decrypt via Noise session
let decryptedData = try decrypt(encryptedData, from: peerID)
// Process channel key packet
if let (channel, password) = channelEncryption.processChannelKeyPacket(decryptedData) {
setChannelPassword(password, for: channel)
}
}
// MARK: - Session Maintenance
private func startRekeyTimer() {
@ -424,7 +394,7 @@ class NoiseEncryptionService {
// Attempt to rekey the session
do {
try sessionManager.initiateRekey(for: peerID)
SecureLogger.logSecurityEvent(.keyRotation(channel: peerID))
SecureLogger.log("Key rotation initiated for peer: \(peerID)", category: SecureLogger.security, level: .info)
// Signal that handshake is needed
onHandshakeRequired?(peerID)
@ -515,4 +485,4 @@ struct NoiseMessage: Codable {
enum NoiseEncryptionError: Error {
case handshakeRequired
case sessionNotEstablished
}
}

View File

@ -1,202 +0,0 @@
//
// NoiseTestingHelper.swift
// bitchat
//
// This is free and unencumbered software released into the public domain.
// For more information, see <https://unlicense.org>
//
import Foundation
// MARK: - Encryption Status Enum
public enum EncryptionStatus {
case noiseVerified // Noise + fingerprint verified
case noiseSecured // Noise established
case noiseHandshaking // Noise in progress
case none // No encryption
var icon: String {
switch self {
case .noiseVerified:
return "checkmark.shield.fill" // Verified secure
case .noiseSecured:
return "lock.fill" // Secure
case .noiseHandshaking:
return "lock.rotation" // In progress
// Legacy case removed
case .none:
return "lock.slash" // Not secure
}
}
var description: String {
switch self {
case .noiseVerified:
return "Verified Secure"
case .noiseSecured:
return "Secure (Noise)"
case .noiseHandshaking:
return "Securing..."
// Legacy case removed
case .none:
return "Not Encrypted"
}
}
}
// MARK: - Testing Helper for Noise Protocol Migration
#if DEBUG
class NoiseTestingHelper {
static let shared = NoiseTestingHelper()
// Test Scenarios Checklist
struct TestScenario {
let name: String
let steps: [String]
var passed: Bool = false
}
private var testScenarios: [TestScenario] = [
TestScenario(
name: "Basic Handshake",
steps: [
"1. Connect two devices via Bluetooth",
"2. Verify Noise handshake completes (check logs)",
"3. Confirm lock icon appears next to peer name",
"4. Send a message and verify delivery"
]
),
TestScenario(
name: "Legacy Fallback",
steps: [
"1. Connect old app version to new version",
"2. Verify legacy encryption still works",
"3. Check for warning icon (not fully secure)",
"4. Messages should still deliver"
]
),
TestScenario(
name: "Fingerprint Verification",
steps: [
"1. Long-press on peer name to see fingerprint",
"2. Compare fingerprints on both devices",
"3. Mark as verified",
"4. Check for verified checkmark"
]
),
TestScenario(
name: "Channel Encryption",
steps: [
"1. Create password-protected channel",
"2. Join from another device",
"3. Send messages to channel",
"4. Verify only members can decrypt"
]
),
TestScenario(
name: "Session Recovery",
steps: [
"1. Establish Noise session",
"2. Force quit app",
"3. Reopen and reconnect",
"4. Verify session re-establishes automatically"
]
),
TestScenario(
name: "Rate Limiting",
steps: [
"1. Send many messages rapidly",
"2. Verify rate limit kicks in after 100 msgs/sec",
"3. Wait and verify messaging resumes",
"4. Check no messages lost"
]
),
TestScenario(
name: "Panic Mode",
steps: [
"1. Establish sessions with peers",
"2. Trigger panic mode (shake device)",
"3. Verify all keys cleared",
"4. Check new identity generated on restart"
]
)
]
// Debug logging for Noise events
func logNoiseEvent(_ event: String, details: Any? = nil) {
// Logging removed - keeping method signature for compatibility
}
// Get encryption status for peer
func getEncryptionStatus(for peerID: String, noiseService: NoiseEncryptionService) -> EncryptionStatus {
if noiseService.hasEstablishedSession(with: peerID) {
// Check if fingerprint is verified
if let fingerprint = noiseService.getPeerFingerprint(peerID),
isFingerprinted(peerID: peerID, fingerprint: fingerprint) {
return .noiseVerified
}
return .noiseSecured
} else {
// Always use Noise - no legacy encryption
return .noiseHandshaking
}
}
// Store verified fingerprints (in production, use Keychain)
private var verifiedFingerprints: [String: String] = [:]
func verifyFingerprint(peerID: String, fingerprint: String) {
verifiedFingerprints[peerID] = fingerprint
}
func isFingerprinted(peerID: String, fingerprint: String) -> Bool {
return verifiedFingerprints[peerID] == fingerprint
}
// Format fingerprint for display
func formatFingerprint(_ fingerprint: String) -> String {
// Convert to uppercase and format into 2 lines (8 groups of 4 on each line)
let uppercased = fingerprint.uppercased()
var formatted = ""
for (index, char) in uppercased.enumerated() {
// Add space every 4 characters (but not at the start)
if index > 0 && index % 4 == 0 {
// Add newline after 32 characters (8 groups of 4)
if index == 32 {
formatted += "\n"
} else {
formatted += " "
}
}
formatted += String(char)
}
return formatted
}
// Get test scenario checklist
func getTestChecklist() -> String {
var checklist = "NOISE PROTOCOL TEST CHECKLIST\n"
checklist += "=" .repeated(30) + "\n\n"
for scenario in testScenarios {
checklist += "□ \(scenario.name)\n"
for step in scenario.steps {
checklist += " \(step)\n"
}
checklist += "\n"
}
return checklist
}
}
// String extension for repeating
extension String {
func repeated(_ count: Int) -> String {
return String(repeating: self, count: count)
}
}
#endif

View File

@ -66,9 +66,6 @@ class SecureLogger {
case handshakeCompleted(peerID: String)
case handshakeFailed(peerID: String, error: String)
case sessionExpired(peerID: String)
case keyRotation(channel: String)
case invalidKey(reason: String)
case replayAttackDetected(channel: String)
case authenticationFailed(peerID: String)
var message: String {
@ -81,12 +78,6 @@ class SecureLogger {
return "Handshake failed with peer: \(sanitize(peerID)), error: \(error)"
case .sessionExpired(let peerID):
return "Session expired for peer: \(sanitize(peerID))"
case .keyRotation(let channel):
return "Key rotation performed for channel: \(sanitize(channel))"
case .invalidKey(let reason):
return "Invalid key detected: \(reason)"
case .replayAttackDetected(let channel):
return "Replay attack detected on channel: \(sanitize(channel))"
case .authenticationFailed(let peerID):
return "Authentication failed for peer: \(sanitize(peerID))"
}
@ -243,4 +234,4 @@ func secureLog(_ items: Any..., separator: String = " ", terminator: String = "\
let message = items.map { String(describing: $0) }.joined(separator: separator)
SecureLogger.log(message, level: .debug, file: file, line: line, function: function)
#endif
}
}

File diff suppressed because it is too large Load Diff

View File

@ -28,8 +28,6 @@ struct AppInfoView: View {
static let extendedRange = ("antenna.radiowaves.left.and.right", "extended range", "messages relay through peers, increasing the distance")
static let favorites = ("star.fill", "favorites", "store-and-forward messages for favorite people")
static let mentions = ("at", "mentions", "use @nickname to notify specific people")
static let channels = ("number", "channels", "create #channels for topic-based conversations")
static let privateChannels = ("lock.fill", "private channels", "secure channels with passwords and noise encryption")
}
enum Privacy {
@ -46,7 +44,6 @@ struct AppInfoView: View {
"• swipe left for sidebar",
"• tap a peer to start a private chat",
"• use @nickname to mention someone",
"• use #channelname to create/join channels",
"• triple-tap \"bitchat\" for panic mode",
"• triple-tap chat messages to clear current chat"
]
@ -55,10 +52,8 @@ struct AppInfoView: View {
enum Commands {
static let title = "COMMANDS"
static let list = [
"/j #channel - join or create a channel",
"/m @name - send private message",
"/w - see who's online",
"/channels - show all discovered channels",
"/block @name - block a peer",
"/block - list blocked peers",
"/unblock @name - unblock a peer",
@ -148,14 +143,6 @@ struct AppInfoView: View {
FeatureRow(icon: Strings.Features.mentions.0,
title: Strings.Features.mentions.1,
description: Strings.Features.mentions.2)
FeatureRow(icon: Strings.Features.channels.0,
title: Strings.Features.channels.1,
description: Strings.Features.channels.2)
FeatureRow(icon: Strings.Features.privateChannels.0,
title: Strings.Features.privateChannels.1,
description: Strings.Features.privateChannels.2)
}
// Privacy

View File

@ -18,18 +18,10 @@ struct ContentView: View {
@State private var showSidebar = false
@State private var sidebarDragOffset: CGFloat = 0
@State private var showAppInfo = false
@State private var showPasswordInput = false
@State private var passwordInputChannel: String? = nil
@State private var passwordInput = ""
@State private var showPasswordPrompt = false
@State private var passwordPromptInput = ""
@State private var showPasswordError = false
@State private var showCommandSuggestions = false
@State private var commandSuggestions: [String] = []
@State private var showLeaveChannelAlert = false
@State private var backSwipeOffset: CGFloat = 0
@State private var showPrivateChat = false
@State private var showChannel = false
private var backgroundColor: Color {
colorScheme == .dark ? Color.black : Color.white
@ -84,41 +76,6 @@ struct ContentView: View {
.animation(.spring(response: 0.3, dampingFraction: 0.8), value: showPrivateChat)
}
// Channel slide-over
if viewModel.currentChannel != nil {
channelView
.frame(width: geometry.size.width)
.background(backgroundColor)
.transition(.asymmetric(
insertion: .move(edge: .trailing),
removal: .move(edge: .trailing)
))
.offset(x: showChannel ? 0 : geometry.size.width)
.offset(x: backSwipeOffset)
.gesture(
DragGesture()
.onChanged { value in
if value.translation.width > 0 {
backSwipeOffset = min(value.translation.width, geometry.size.width)
}
}
.onEnded { value in
if value.translation.width > 50 || (value.translation.width > 30 && value.velocity.width > 300) {
withAnimation(.spring(response: 0.3, dampingFraction: 0.8)) {
showChannel = false
backSwipeOffset = 0
viewModel.switchToChannel(nil)
}
} else {
withAnimation(.spring(response: 0.3, dampingFraction: 0.8)) {
backSwipeOffset = 0
}
}
}
)
.animation(.spring(response: 0.3, dampingFraction: 0.8), value: showChannel)
}
// Sidebar overlay
HStack(spacing: 0) {
// Tap to dismiss area
@ -152,11 +109,6 @@ struct ContentView: View {
showPrivateChat = newValue != nil
}
}
.onChange(of: viewModel.currentChannel) { newValue in
withAnimation(.spring(response: 0.3, dampingFraction: 0.8)) {
showChannel = newValue != nil
}
}
.sheet(isPresented: $showAppInfo) {
AppInfoView()
}
@ -168,54 +120,9 @@ struct ContentView: View {
FingerprintView(viewModel: viewModel, peerID: peerID)
}
}
.alert("Set Channel Password", isPresented: $showPasswordInput) {
SecureField("Password", text: $passwordInput)
Button("Cancel", role: .cancel) {
passwordInput = ""
passwordInputChannel = nil
}
Button("Set Password") {
if let channel = passwordInputChannel, !passwordInput.isEmpty {
viewModel.setChannelPassword(passwordInput, for: channel)
passwordInput = ""
passwordInputChannel = nil
}
}
} message: {
Text("Enter a password to protect \(passwordInputChannel ?? "channel"). Others will need this password to read messages.")
}
.alert("Enter Channel Password", isPresented: Binding(
get: { viewModel.showPasswordPrompt },
set: { viewModel.showPasswordPrompt = $0 }
)) {
SecureField("Password", text: $passwordPromptInput)
Button("Cancel", role: .cancel) {
passwordPromptInput = ""
viewModel.passwordPromptChannel = nil
}
Button("Join") {
if let channel = viewModel.passwordPromptChannel, !passwordPromptInput.isEmpty {
let success = viewModel.joinChannel(channel, password: passwordPromptInput)
if success {
passwordPromptInput = ""
} else {
// Wrong password - show error
passwordPromptInput = ""
showPasswordError = true
}
}
}
} message: {
Text("Channel \(viewModel.passwordPromptChannel ?? "") is password protected. Enter the password to join.")
}
.alert("Wrong Password", isPresented: $showPasswordError) {
Button("OK", role: .cancel) { }
} message: {
Text("The password you entered is incorrect. Please try again.")
}
}
private func messagesView(for channel: String?, privatePeer: String?) -> some View {
private func messagesView(privatePeer: String?) -> some View {
ScrollViewReader { proxy in
ScrollView {
LazyVStack(alignment: .leading, spacing: 2) {
@ -223,9 +130,6 @@ struct ContentView: View {
if let privatePeer = privatePeer {
let msgs = viewModel.getPrivateChatMessages(for: privatePeer)
return msgs
} else if let channel = channel {
let msgs = viewModel.getChannelMessages(channel)
return msgs
} else {
return viewModel.messages
}
@ -294,7 +198,7 @@ struct ContentView: View {
viewModel.sendMessage("/clear")
}
.onChange(of: viewModel.messages.count) { _ in
if channel == nil && privatePeer == nil && !viewModel.messages.isEmpty {
if privatePeer == nil && !viewModel.messages.isEmpty {
withAnimation {
proxy.scrollTo(viewModel.messages.last?.id, anchor: .bottom)
}
@ -309,15 +213,6 @@ struct ContentView: View {
}
}
}
.onChange(of: viewModel.channelMessages) { _ in
if let channelName = channel,
let messages = viewModel.channelMessages[channelName],
!messages.isEmpty {
withAnimation {
proxy.scrollTo(messages.last?.id, anchor: .bottom)
}
}
}
.onAppear {
// Also check when view appears
if let peerID = privatePeer {
@ -376,23 +271,14 @@ struct ContentView: View {
(["/block"], "[nickname]", "block or list blocked peers"),
(["/clear"], nil, "clear chat messages"),
(["/hug"], "<nickname>", "send someone a warm hug"),
(["/j", "/join"], "<channel>", "join or create a channel"),
(["/m", "/msg"], "<nickname> [message]", "send private message"),
(["/channels"], nil, "show all discovered channels"),
(["/slap"], "<nickname>", "slap someone with a trout"),
(["/unblock"], "<nickname>", "unblock a peer"),
(["/w"], nil, "see who's online")
]
let channelCommandInfo: [(commands: [String], syntax: String?, description: String)] = [
(["/pass"], "[password]", "change channel password"),
(["/transfer"], "<nickname>", "transfer channel ownership")
]
// Build the display
let allCommands = viewModel.currentChannel != nil
? commandInfo + channelCommandInfo
: commandInfo
let allCommands = commandInfo
// Show matching commands
ForEach(commandSuggestions, id: \.self) { command in
@ -450,13 +336,6 @@ struct ContentView: View {
.lineLimit(1)
.fixedSize()
.padding(.leading, 12)
} else if let currentChannel = viewModel.currentChannel, viewModel.passwordProtectedChannels.contains(currentChannel) {
Text("<@\(viewModel.nickname)> →")
.font(.system(size: 12, weight: .medium, design: .monospaced))
.foregroundColor(Color.orange)
.lineLimit(1)
.fixedSize()
.padding(.leading, 12)
} else {
Text("<@\(viewModel.nickname)>")
.font(.system(size: 12, weight: .medium, design: .monospaced))
@ -479,24 +358,16 @@ struct ContentView: View {
// Check for command autocomplete
if newValue.hasPrefix("/") && newValue.count >= 1 {
// Build context-aware command list
var commandDescriptions = [
let commandDescriptions = [
("/block", "block or list blocked peers"),
("/channels", "show all discovered channels"),
("/clear", "clear chat messages"),
("/hug", "send someone a warm hug"),
("/j", "join or create a channel"),
("/m", "send private message"),
("/slap", "slap someone with a trout"),
("/unblock", "unblock a peer"),
("/w", "see who's online")
]
// Add channel-specific commands if in a channel
if viewModel.currentChannel != nil {
commandDescriptions.append(("/pass", "change channel password"))
commandDescriptions.append(("/transfer", "transfer channel ownership"))
}
let input = newValue.lowercased()
// Map of aliases to primary commands
@ -535,8 +406,7 @@ struct ContentView: View {
Image(systemName: "arrow.up.circle.fill")
.font(.system(size: 20))
.foregroundColor(messageText.isEmpty ? Color.gray :
(viewModel.selectedPrivateChatPeer != nil ||
(viewModel.currentChannel != nil && viewModel.passwordProtectedChannels.contains(viewModel.currentChannel ?? "")))
viewModel.selectedPrivateChatPeer != nil
? Color.orange : textColor)
}
.buttonStyle(.plain)
@ -557,133 +427,6 @@ struct ContentView: View {
messageText = ""
}
@ViewBuilder
private var channelsSection: some View {
if !viewModel.joinedChannels.isEmpty {
VStack(alignment: .leading, spacing: 6) {
HStack(spacing: 4) {
Image(systemName: "square.split.2x2")
.font(.system(size: 10))
.accessibilityHidden(true)
Text("CHANNELS")
.font(.system(size: 11, weight: .bold, design: .monospaced))
}
.foregroundColor(secondaryTextColor)
.padding(.horizontal, 12)
ForEach(Array(viewModel.joinedChannels).sorted(), id: \.self) { channel in
channelButton(for: channel)
}
}
}
}
@ViewBuilder
private func channelButton(for channel: String) -> some View {
Button(action: {
// Check if channel needs password and we don't have it
if viewModel.passwordProtectedChannels.contains(channel) && viewModel.channelKeys[channel] == nil {
// Need password
viewModel.passwordPromptChannel = channel
viewModel.showPasswordPrompt = true
} else {
// Can enter channel
viewModel.switchToChannel(channel)
withAnimation(.spring()) {
showSidebar = false
}
}
}) {
HStack {
// Lock icon for password protected channels
if viewModel.passwordProtectedChannels.contains(channel) {
Image(systemName: "lock.fill")
.font(.system(size: 10))
.foregroundColor(secondaryTextColor)
.accessibilityLabel("Password protected")
}
Text(channel)
.font(.system(size: 14, design: .monospaced))
.foregroundColor(viewModel.currentChannel == channel ? Color.blue : textColor)
Spacer()
// Unread count
if let unreadCount = viewModel.unreadChannelMessages[channel], unreadCount > 0 {
Text("\(unreadCount)")
.font(.system(size: 10, weight: .bold, design: .monospaced))
.foregroundColor(backgroundColor)
.padding(.horizontal, 6)
.padding(.vertical, 2)
.background(Color.orange)
.clipShape(Capsule())
}
// Channel controls
if viewModel.currentChannel == channel {
channelControls(for: channel)
}
}
}
.buttonStyle(.plain)
.padding(.horizontal, 12)
.padding(.vertical, 4)
.background(viewModel.currentChannel == channel ? backgroundColor.opacity(0.5) : Color.clear)
}
@ViewBuilder
private func channelControls(for channel: String) -> some View {
HStack(spacing: 4) {
// Password button for channel creator only
if viewModel.isChannelOwner(channel) {
Button(action: {
// Toggle password protection
if viewModel.passwordProtectedChannels.contains(channel) {
viewModel.removeChannelPassword(for: channel)
} else {
// Show password input
showPasswordInput = true
passwordInputChannel = channel
}
}) {
HStack(spacing: 2) {
Image(systemName: viewModel.passwordProtectedChannels.contains(channel) ? "lock.fill" : "lock")
.font(.system(size: 10))
}
.foregroundColor(viewModel.passwordProtectedChannels.contains(channel) ? backgroundColor : secondaryTextColor)
.padding(.horizontal, 8)
.padding(.vertical, 2)
.background(viewModel.passwordProtectedChannels.contains(channel) ? Color.orange : Color.clear)
.overlay(
RoundedRectangle(cornerRadius: 4)
.stroke(viewModel.passwordProtectedChannels.contains(channel) ? Color.orange : secondaryTextColor.opacity(0.5), lineWidth: 1)
)
}
.buttonStyle(.plain)
.accessibilityLabel(viewModel.passwordProtectedChannels.contains(channel) ? "Remove password" : "Set password")
}
// Leave button
Button(action: {
showLeaveChannelAlert = true
}) {
Image(systemName: "xmark.circle.fill")
.font(.system(size: 14))
.foregroundColor(Color.red.opacity(0.6))
}
.buttonStyle(.plain)
.alert("leave channel", isPresented: $showLeaveChannelAlert) {
Button("cancel", role: .cancel) { }
Button("leave", role: .destructive) {
viewModel.leaveChannel(channel)
}
} message: {
Text("sure you want to leave \(channel)?")
}
}
}
private var sidebarView: some View {
HStack(spacing: 0) {
// Grey vertical bar for visual continuity
@ -708,23 +451,10 @@ struct ContentView: View {
// Rooms and People list
ScrollView {
VStack(alignment: .leading, spacing: 12) {
// Channels section
channelsSection
if !viewModel.joinedChannels.isEmpty {
Divider()
.padding(.vertical, 4)
}
// People section
VStack(alignment: .leading, spacing: 8) {
// Show appropriate header based on context
if let currentChannel = viewModel.currentChannel {
Text("IN \(currentChannel.uppercased())")
.font(.system(size: 11, weight: .semibold, design: .monospaced))
.foregroundColor(secondaryTextColor)
.padding(.horizontal, 12)
} else if !viewModel.connectedPeers.isEmpty {
if !viewModel.connectedPeers.isEmpty {
HStack(spacing: 4) {
Image(systemName: "person.2.fill")
.font(.system(size: 10))
@ -741,38 +471,13 @@ struct ContentView: View {
.font(.system(size: 14, design: .monospaced))
.foregroundColor(secondaryTextColor)
.padding(.horizontal)
} else if let currentChannel = viewModel.currentChannel,
let channelMemberIDs = viewModel.channelMembers[currentChannel],
channelMemberIDs.isEmpty {
Text("no one in this channel yet...")
.font(.system(size: 14, design: .monospaced))
.foregroundColor(secondaryTextColor)
.padding(.horizontal)
} else {
let peerNicknames = viewModel.meshService.getPeerNicknames()
let peerRSSI = viewModel.meshService.getPeerRSSI()
let myPeerID = viewModel.meshService.myPeerID
// Filter peers based on current channel
let peersToShow: [String] = {
if let currentChannel = viewModel.currentChannel,
let channelMemberIDs = viewModel.channelMembers[currentChannel] {
// Show only peers who have sent messages to this channel (including self)
// Start with channel members who are also connected
var memberPeers = viewModel.connectedPeers.filter { channelMemberIDs.contains($0) }
// Always include ourselves if we're a channel member
if channelMemberIDs.contains(myPeerID) && !memberPeers.contains(myPeerID) {
memberPeers.append(myPeerID)
}
return memberPeers
} else {
// Show all connected peers in main chat
return viewModel.connectedPeers
}
}()
// Show all connected peers
let peersToShow: [String] = viewModel.connectedPeers
// Sort peers: favorites first, then alphabetically by nickname
let sortedPeers = peersToShow.sorted { peer1, peer2 in
@ -888,7 +593,7 @@ struct ContentView: View {
VStack(spacing: 0) {
mainHeaderView
Divider()
messagesView(for: nil, privatePeer: nil)
messagesView(privatePeer: nil)
Divider()
inputView
}
@ -935,7 +640,7 @@ struct ContentView: View {
VStack(spacing: 0) {
privateHeaderView
Divider()
messagesView(for: nil, privatePeer: viewModel.selectedPrivateChatPeer)
messagesView(privatePeer: viewModel.selectedPrivateChatPeer)
Divider()
inputView
}
@ -944,24 +649,6 @@ struct ContentView: View {
}
}
private var channelView: some View {
HStack(spacing: 0) {
// Vertical separator bar
Rectangle()
.fill(Color.gray.opacity(0.3))
.frame(width: 1)
VStack(spacing: 0) {
channelHeaderView
Divider()
messagesView(for: viewModel.currentChannel, privatePeer: nil)
Divider()
inputView
}
.background(backgroundColor)
.foregroundColor(textColor)
}
}
private var mainHeaderView: some View {
HStack(spacing: 0) {
@ -999,16 +686,6 @@ struct ContentView: View {
// People counter with unread indicator
HStack(spacing: 4) {
// Check for any unread channel messages
let hasUnreadChannelMessages = viewModel.unreadChannelMessages.values.contains { $0 > 0 }
if hasUnreadChannelMessages {
Image(systemName: "number")
.font(.system(size: 12))
.foregroundColor(Color.blue)
.accessibilityLabel("Unread channel messages")
}
if !viewModel.unreadPrivateMessages.isEmpty {
Image(systemName: "envelope.fill")
.font(.system(size: 12))
@ -1017,7 +694,6 @@ struct ContentView: View {
}
let otherPeersCount = viewModel.connectedPeers.filter { $0 != viewModel.meshService.myPeerID }.count
let channelCount = viewModel.joinedChannels.count
HStack(spacing: 4) {
// People icon with count
@ -1027,18 +703,6 @@ struct ContentView: View {
Text("\(otherPeersCount)")
.font(.system(size: 12, design: .monospaced))
.accessibilityHidden(true)
// Channels icon with count (only if there are channels)
if channelCount > 0 {
Text("·")
.font(.system(size: 12, design: .monospaced))
Image(systemName: "square.split.2x2")
.font(.system(size: 11))
.accessibilityLabel("\(channelCount) active \(channelCount == 1 ? "channel" : "channels")")
Text("\(channelCount)")
.font(.system(size: 12, design: .monospaced))
.accessibilityHidden(true)
}
}
.foregroundColor(viewModel.isConnected ? textColor : Color.red)
}
@ -1123,125 +787,6 @@ struct ContentView: View {
}
}
private var channelHeaderView: some View {
Group {
if let currentChannel = viewModel.currentChannel {
HStack {
Button(action: {
withAnimation(.spring(response: 0.3, dampingFraction: 0.8)) {
showChannel = false
viewModel.switchToChannel(nil)
}
}) {
HStack(spacing: 4) {
Image(systemName: "chevron.left")
.font(.system(size: 12))
Text("back")
.font(.system(size: 14, design: .monospaced))
}
.foregroundColor(textColor)
}
.buttonStyle(.plain)
.accessibilityLabel("Back to main chat")
Spacer()
Button(action: {
withAnimation(.spring(response: 0.3, dampingFraction: 0.8)) {
showSidebar.toggle()
sidebarDragOffset = 0
}
}) {
HStack(spacing: 4) {
if viewModel.passwordProtectedChannels.contains(currentChannel) {
Image(systemName: "lock.fill")
.font(.system(size: 14))
.foregroundColor(Color.orange)
.accessibilityLabel("Password protected channel")
}
Text(currentChannel)
.font(.system(size: 16, weight: .medium, design: .monospaced))
.foregroundColor(viewModel.passwordProtectedChannels.contains(currentChannel) ? Color.orange : Color.blue)
// Verification status indicator after channel name
if viewModel.passwordProtectedChannels.contains(currentChannel),
let status = viewModel.channelVerificationStatus[currentChannel] {
switch status {
case .verifying:
ProgressView()
.scaleEffect(0.5)
.frame(width: 12, height: 12)
case .verified:
Image(systemName: "checkmark.circle.fill")
.font(.system(size: 12))
.foregroundColor(Color.green)
case .failed:
Image(systemName: "xmark.circle.fill")
.font(.system(size: 12))
.foregroundColor(Color.red)
case .unverified:
Image(systemName: "questionmark.circle")
.font(.system(size: 12))
.foregroundColor(Color.gray)
.help("Password verification pending")
}
}
}
}
.buttonStyle(.plain)
.frame(maxWidth: .infinity)
Spacer()
HStack(spacing: 8) {
// Password button for channel creator only
if viewModel.isChannelOwner(currentChannel) {
Button(action: {
// Toggle password protection
if viewModel.passwordProtectedChannels.contains(currentChannel) {
viewModel.removeChannelPassword(for: currentChannel)
} else {
// Show password input
showPasswordInput = true
passwordInputChannel = currentChannel
}
}) {
Image(systemName: viewModel.passwordProtectedChannels.contains(currentChannel) ? "lock.fill" : "lock")
.font(.system(size: 16))
.foregroundColor(viewModel.passwordProtectedChannels.contains(currentChannel) ? Color.yellow : textColor)
}
.buttonStyle(.plain)
.accessibilityLabel(viewModel.passwordProtectedChannels.contains(currentChannel) ? "Remove channel password" : "Set channel password")
}
// Leave channel button
Button(action: {
showLeaveChannelAlert = true
}) {
Image(systemName: "xmark.circle")
.font(.system(size: 16))
.foregroundColor(Color.red.opacity(0.8))
}
.buttonStyle(.plain)
.alert("leave channel?", isPresented: $showLeaveChannelAlert) {
Button("cancel", role: .cancel) { }
Button("leave", role: .destructive) {
viewModel.leaveChannel(currentChannel)
}
} message: {
Text("sure you want to leave \(currentChannel)?")
}
}
}
.frame(height: 44)
.padding(.horizontal, 12)
.background(backgroundColor.opacity(0.95))
} else {
EmptyView()
}
}
}
}
// Helper view for rendering message content with clickable hashtags

View File

@ -1,129 +0,0 @@
//
// NoiseTestingView.swift
// bitchat
//
// This is free and unencumbered software released into the public domain.
// For more information, see <https://unlicense.org>
//
import SwiftUI
#if DEBUG
struct NoiseTestingView: View {
@ObservedObject var viewModel: ChatViewModel
@Environment(\.colorScheme) var colorScheme
@State private var testChecklist = NoiseTestingHelper.shared.getTestChecklist()
private var textColor: Color {
colorScheme == .dark ? Color.green : Color(red: 0, green: 0.5, blue: 0)
}
private var backgroundColor: Color {
colorScheme == .dark ? Color.black : Color.white
}
var body: some View {
VStack(alignment: .leading, spacing: 16) {
// Header
Text("NOISE PROTOCOL TEST HELPER")
.font(.system(size: 16, weight: .bold, design: .monospaced))
.foregroundColor(textColor)
.padding(.bottom)
// Status Overview
VStack(alignment: .leading, spacing: 8) {
Text("CURRENT STATUS:")
.font(.system(size: 12, weight: .bold, design: .monospaced))
.foregroundColor(textColor.opacity(0.7))
ForEach(viewModel.connectedPeers, id: \.self) { peerID in
let nickname = viewModel.meshService.getPeerNicknames()[peerID] ?? "Unknown"
let status = viewModel.getEncryptionStatus(for: peerID)
HStack {
Image(systemName: status.icon)
.font(.system(size: 12))
.foregroundColor(status == .noiseVerified ? Color.green :
status == .noiseSecured ? textColor :
Color.red)
Text("\(nickname): \(status.description)")
.font(.system(size: 12, design: .monospaced))
.foregroundColor(textColor)
Spacer()
}
}
if viewModel.connectedPeers.isEmpty {
Text("No peers connected")
.font(.system(size: 12, design: .monospaced))
.foregroundColor(Color.gray)
}
}
.padding()
.background(Color.gray.opacity(0.1))
.cornerRadius(8)
// Test Checklist
ScrollView {
Text(testChecklist)
.font(.system(size: 11, design: .monospaced))
.foregroundColor(textColor)
.textSelection(.enabled)
}
.padding()
.background(Color.gray.opacity(0.1))
.cornerRadius(8)
// Debug Actions
HStack(spacing: 16) {
Button("Force Handshake") {
// Trigger handshake with all peers by sending a broadcast announce
// This will cause all peers to re-exchange keys
viewModel.meshService.sendBroadcastAnnounce()
}
.foregroundColor(textColor)
Button("Clear Sessions") {
// Clear all Noise sessions for testing
let noiseService = viewModel.meshService.getNoiseService()
for peerID in viewModel.connectedPeers {
noiseService.removePeer(peerID)
}
viewModel.peerEncryptionStatus.removeAll()
}
.foregroundColor(Color.orange)
Button("Copy Logs") {
// Copy test results to clipboard
var logs = "NOISE PROTOCOL TEST RESULTS\n"
logs += "===========================\n\n"
logs += "Timestamp: \(Date())\n"
logs += "Connected Peers: \(viewModel.connectedPeers.count)\n\n"
for peerID in viewModel.connectedPeers {
let nickname = viewModel.meshService.getPeerNicknames()[peerID] ?? "Unknown"
let status = viewModel.getEncryptionStatus(for: peerID)
logs += "\(nickname) (\(peerID)): \(status.description)\n"
}
#if os(iOS)
UIPasteboard.general.string = logs
#else
NSPasteboard.general.clearContents()
NSPasteboard.general.setString(logs, forType: .string)
#endif
}
.foregroundColor(textColor)
Spacer()
}
}
.padding()
.frame(width: 500, height: 600)
.background(backgroundColor)
}
}
#endif

View File

@ -1,110 +0,0 @@
//
// BinaryProtocolTests.swift
// bitchatTests
//
// This is free and unencumbered software released into the public domain.
// For more information, see <https://unlicense.org>
//
import XCTest
@testable import bitchat
class BinaryProtocolTests: XCTestCase {
func testPacketEncodingDecoding() {
// Test basic packet
let packet = BitchatPacket(
type: MessageType.message.rawValue,
senderID: Data("testuser".utf8),
recipientID: Data("recipient".utf8),
timestamp: UInt64(Date().timeIntervalSince1970 * 1000),
payload: Data("Hello, World!".utf8),
signature: nil,
ttl: 5
)
// Encode
guard let encoded = packet.toBinaryData() else {
XCTFail("Failed to encode packet")
return
}
// Decode
guard let decoded = BitchatPacket.from(encoded) else {
XCTFail("Failed to decode packet")
return
}
// Verify
XCTAssertEqual(decoded.version, packet.version)
XCTAssertEqual(decoded.type, packet.type)
XCTAssertEqual(decoded.ttl, packet.ttl)
XCTAssertEqual(decoded.timestamp, packet.timestamp)
XCTAssertEqual(decoded.payload, packet.payload)
}
func testBroadcastPacket() {
let packet = BitchatPacket(
type: MessageType.message.rawValue,
senderID: Data("sender".utf8),
recipientID: SpecialRecipients.broadcast,
timestamp: UInt64(Date().timeIntervalSince1970 * 1000),
payload: Data("Broadcast message".utf8),
signature: nil,
ttl: 3
)
guard let encoded = packet.toBinaryData() else {
XCTFail("Failed to encode broadcast packet")
return
}
guard let decoded = BitchatPacket.from(encoded) else {
XCTFail("Failed to decode broadcast packet")
return
}
// Verify broadcast recipient
XCTAssertEqual(decoded.recipientID, SpecialRecipients.broadcast)
}
func testPacketWithSignature() {
let signature = Data(repeating: 0xAB, count: 64)
let packet = BitchatPacket(
type: MessageType.message.rawValue,
senderID: Data("sender".utf8),
recipientID: Data("recipient".utf8),
timestamp: UInt64(Date().timeIntervalSince1970 * 1000),
payload: Data("Signed message".utf8),
signature: signature,
ttl: 5
)
guard let encoded = packet.toBinaryData() else {
XCTFail("Failed to encode signed packet")
return
}
guard let decoded = BitchatPacket.from(encoded) else {
XCTFail("Failed to decode signed packet")
return
}
XCTAssertNotNil(decoded.signature)
XCTAssertEqual(decoded.signature, signature)
}
func testInvalidPacketHandling() {
// Test empty data
XCTAssertNil(BitchatPacket.from(Data()))
// Test truncated data
let truncated = Data(repeating: 0, count: 10)
XCTAssertNil(BitchatPacket.from(truncated))
// Test invalid version
var invalidVersion = Data(repeating: 0, count: 100)
invalidVersion[0] = 99 // Invalid version
XCTAssertNil(BitchatPacket.from(invalidVersion))
}
}

View File

@ -1,230 +0,0 @@
//
// BinaryProtocolVersionTests.swift
// bitchatTests
//
// This is free and unencumbered software released into the public domain.
// For more information, see <https://unlicense.org>
//
import XCTest
@testable import bitchat
class BinaryProtocolVersionTests: XCTestCase {
// MARK: - Version Support Tests
func testCurrentVersionIsSupported() {
// Current version should always be supported
XCTAssertTrue(ProtocolVersion.isSupported(ProtocolVersion.current))
}
func testVersion1IsSupported() {
// Version 1 must be supported for backward compatibility
XCTAssertTrue(ProtocolVersion.isSupported(1))
}
func testUnsupportedVersionsRejected() {
// Test various unsupported versions
XCTAssertFalse(ProtocolVersion.isSupported(0))
XCTAssertFalse(ProtocolVersion.isSupported(2))
XCTAssertFalse(ProtocolVersion.isSupported(99))
XCTAssertFalse(ProtocolVersion.isSupported(255))
}
// MARK: - Binary Protocol Version Handling
func testBinaryProtocolRejectsUnsupportedVersion() {
// Create a packet with unsupported version
var data = Data()
// Header
data.append(99) // Unsupported version
data.append(MessageType.message.rawValue)
data.append(5) // TTL
// Timestamp (8 bytes)
let timestamp = UInt64(Date().timeIntervalSince1970 * 1000)
for i in (0..<8).reversed() {
data.append(UInt8((timestamp >> (i * 8)) & 0xFF))
}
// Flags (no recipient, no signature)
data.append(0)
// Payload length (2 bytes)
let payload = Data("test".utf8)
let payloadLength = UInt16(payload.count)
data.append(UInt8((payloadLength >> 8) & 0xFF))
data.append(UInt8(payloadLength & 0xFF))
// SenderID (8 bytes)
data.append(Data(repeating: 0x01, count: 8))
// Payload
data.append(payload)
// Try to decode - should fail due to unsupported version
let decoded = BinaryProtocol.decode(data)
XCTAssertNil(decoded, "Should reject packet with unsupported version")
}
func testBinaryProtocolAcceptsVersion1() {
// Create a valid version 1 packet
let packet = BitchatPacket(
type: MessageType.message.rawValue,
senderID: Data("sender12".utf8),
recipientID: nil,
timestamp: UInt64(Date().timeIntervalSince1970 * 1000),
payload: Data("Hello".utf8),
signature: nil,
ttl: 3
)
// Encode
guard let encoded = packet.toBinaryData() else {
XCTFail("Failed to encode version 1 packet")
return
}
// Decode
guard let decoded = BitchatPacket.from(encoded) else {
XCTFail("Failed to decode version 1 packet")
return
}
XCTAssertEqual(decoded.version, 1)
XCTAssertEqual(decoded.payload, Data("Hello".utf8))
}
// MARK: - Version Message Type Tests
func testVersionHelloMessageType() {
let hello = VersionHello(
clientVersion: "1.0.0",
platform: "iOS"
)
guard let helloData = hello.encode() else {
XCTFail("Failed to encode VersionHello")
return
}
let packet = BitchatPacket(
type: MessageType.versionHello.rawValue,
ttl: 1,
senderID: "testpeer",
payload: helloData
)
XCTAssertEqual(packet.type, MessageType.versionHello.rawValue)
XCTAssertEqual(MessageType.versionHello.description, "versionHello")
}
func testVersionAckMessageType() {
let ack = VersionAck(
agreedVersion: 1,
serverVersion: "1.0.0",
platform: "macOS"
)
guard let ackData = ack.encode() else {
XCTFail("Failed to encode VersionAck")
return
}
let packet = BitchatPacket(
type: MessageType.versionAck.rawValue,
ttl: 1,
senderID: "testpeer",
payload: ackData
)
XCTAssertEqual(packet.type, MessageType.versionAck.rawValue)
XCTAssertEqual(MessageType.versionAck.description, "versionAck")
}
// MARK: - Compression Compatibility Tests
func testCompressedPacketWithVersion() {
// Create a large payload that will trigger compression
let largeContent = String(repeating: "Hello World! ", count: 100)
let packet = BitchatPacket(
type: MessageType.message.rawValue,
senderID: Data("sender12".utf8),
recipientID: nil,
timestamp: UInt64(Date().timeIntervalSince1970 * 1000),
payload: Data(largeContent.utf8),
signature: nil,
ttl: 3
)
// Encode (should compress)
guard let encoded = packet.toBinaryData() else {
XCTFail("Failed to encode packet with compression")
return
}
// Decode
guard let decoded = BitchatPacket.from(encoded) else {
XCTFail("Failed to decode compressed packet")
return
}
// Verify version is preserved
XCTAssertEqual(decoded.version, 1)
XCTAssertEqual(decoded.payload, Data(largeContent.utf8))
}
// MARK: - Future Version Migration Tests
func testVersionSetConsistency() {
// Ensure version constants are consistent
XCTAssertTrue(ProtocolVersion.supportedVersions.contains(ProtocolVersion.current))
XCTAssertTrue(ProtocolVersion.supportedVersions.contains(ProtocolVersion.minimum))
XCTAssertGreaterThanOrEqual(ProtocolVersion.current, ProtocolVersion.minimum)
XCTAssertLessThanOrEqual(ProtocolVersion.current, ProtocolVersion.maximum)
}
func testVersionNegotiationAlwaysPicksHighest() {
// When multiple versions are supported, should pick highest
let clientVersions: [UInt8] = [1, 2, 3, 4, 5]
let serverVersions: [UInt8] = [3, 4, 5, 6, 7]
let agreed = ProtocolVersion.negotiateVersion(
clientVersions: clientVersions,
serverVersions: serverVersions
)
XCTAssertEqual(agreed, 5) // Highest common version
}
// MARK: - Packet Size Tests with Version Negotiation
func testVersionNegotiationPacketsAreSmall() {
// Version negotiation should use minimal bandwidth
let hello = VersionHello(
clientVersion: "1.0.0",
platform: "iOS"
)
guard let helloData = hello.encode() else {
XCTFail("Failed to encode hello")
return
}
let packet = BitchatPacket(
type: MessageType.versionHello.rawValue,
ttl: 1,
senderID: "12345678",
payload: helloData
)
guard let encoded = packet.toBinaryData() else {
XCTFail("Failed to encode packet")
return
}
// Version negotiation packets should be reasonably small
XCTAssertLessThan(encoded.count, 512, "Version negotiation packet too large")
}
}

View File

@ -1,242 +0,0 @@
//
// BitchatMessageTests.swift
// bitchatTests
//
// This is free and unencumbered software released into the public domain.
// For more information, see <https://unlicense.org>
//
import XCTest
@testable import bitchat
class BitchatMessageTests: XCTestCase {
func testMessageEncodingDecoding() {
let message = BitchatMessage(
sender: "testuser",
content: "Hello, World!",
timestamp: Date(),
isRelay: false,
originalSender: nil,
isPrivate: false,
recipientNickname: nil,
senderPeerID: "peer123",
mentions: ["alice", "bob"]
)
guard let encoded = message.toBinaryPayload() else {
XCTFail("Failed to encode message")
return
}
guard let decoded = BitchatMessage.fromBinaryPayload(encoded) else {
XCTFail("Failed to decode message")
return
}
XCTAssertEqual(decoded.sender, message.sender)
XCTAssertEqual(decoded.content, message.content)
XCTAssertEqual(decoded.isPrivate, message.isPrivate)
XCTAssertEqual(decoded.mentions?.count, 2)
XCTAssertTrue(decoded.mentions?.contains("alice") ?? false)
XCTAssertTrue(decoded.mentions?.contains("bob") ?? false)
}
func testRoomMessage() {
let channelMessage = BitchatMessage(
sender: "alice",
content: "Hello #general",
timestamp: Date(),
isRelay: false,
originalSender: nil,
isPrivate: false,
recipientNickname: nil,
senderPeerID: "alice123",
mentions: nil,
channel: "#general"
)
guard let encoded = channelMessage.toBinaryPayload() else {
XCTFail("Failed to encode channel message")
return
}
guard let decoded = BitchatMessage.fromBinaryPayload(encoded) else {
XCTFail("Failed to decode channel message")
return
}
XCTAssertEqual(decoded.channel, "#general")
XCTAssertEqual(decoded.content, channelMessage.content)
}
func testEncryptedRoomMessage() {
let encryptedData = Data([1, 2, 3, 4, 5, 6, 7, 8]) // Mock encrypted content
let encryptedMessage = BitchatMessage(
sender: "bob",
content: "", // Empty for encrypted messages
timestamp: Date(),
isRelay: false,
originalSender: nil,
isPrivate: false,
recipientNickname: nil,
senderPeerID: "bob456",
mentions: nil,
channel: "#secret",
encryptedContent: encryptedData,
isEncrypted: true
)
guard let encoded = encryptedMessage.toBinaryPayload() else {
XCTFail("Failed to encode encrypted message")
return
}
guard let decoded = BitchatMessage.fromBinaryPayload(encoded) else {
XCTFail("Failed to decode encrypted message")
return
}
XCTAssertTrue(decoded.isEncrypted)
XCTAssertEqual(decoded.encryptedContent, encryptedData)
XCTAssertEqual(decoded.channel, "#secret")
XCTAssertEqual(decoded.content, "") // Content should be empty for encrypted messages
}
func testPrivateMessage() {
let privateMessage = BitchatMessage(
sender: "alice",
content: "This is private",
timestamp: Date(),
isRelay: false,
originalSender: nil,
isPrivate: true,
recipientNickname: "bob",
senderPeerID: "alicePeer"
)
guard let encoded = privateMessage.toBinaryPayload() else {
XCTFail("Failed to encode private message")
return
}
guard let decoded = BitchatMessage.fromBinaryPayload(encoded) else {
XCTFail("Failed to decode private message")
return
}
XCTAssertTrue(decoded.isPrivate)
XCTAssertEqual(decoded.recipientNickname, "bob")
}
func testRelayMessage() {
let relayMessage = BitchatMessage(
sender: "charlie",
content: "Relayed message",
timestamp: Date(),
isRelay: true,
originalSender: "alice",
isPrivate: false
)
guard let encoded = relayMessage.toBinaryPayload() else {
XCTFail("Failed to encode relay message")
return
}
guard let decoded = BitchatMessage.fromBinaryPayload(encoded) else {
XCTFail("Failed to decode relay message")
return
}
XCTAssertTrue(decoded.isRelay)
XCTAssertEqual(decoded.originalSender, "alice")
}
func testEmptyContent() {
let emptyMessage = BitchatMessage(
sender: "user",
content: "",
timestamp: Date(),
isRelay: false,
originalSender: nil
)
guard let encoded = emptyMessage.toBinaryPayload() else {
XCTFail("Failed to encode empty message")
return
}
guard let decoded = BitchatMessage.fromBinaryPayload(encoded) else {
XCTFail("Failed to decode empty message")
return
}
XCTAssertEqual(decoded.content, "")
}
func testLongContent() {
let longContent = String(repeating: "A", count: 1000)
let longMessage = BitchatMessage(
sender: "user",
content: longContent,
timestamp: Date(),
isRelay: false,
originalSender: nil
)
guard let encoded = longMessage.toBinaryPayload() else {
XCTFail("Failed to encode long message")
return
}
guard let decoded = BitchatMessage.fromBinaryPayload(encoded) else {
XCTFail("Failed to decode long message")
return
}
XCTAssertEqual(decoded.content, longContent)
}
func testPrivateMessageWithAllFieldsForNoise() {
// Test that private messages with ID field (used by Noise) are encoded/decoded correctly
let messageID = UUID().uuidString
let privateMessage = BitchatMessage(
id: messageID,
sender: "alice",
content: "Hello Bob, this is a private message via Noise",
timestamp: Date(),
isRelay: false,
originalSender: nil,
isPrivate: true,
recipientNickname: "bob",
senderPeerID: "alice-peer-id-123",
mentions: nil,
channel: nil
)
// Encode to binary payload (as used by Noise encryption)
guard let encoded = privateMessage.toBinaryPayload() else {
XCTFail("Failed to encode private message with ID to binary payload")
return
}
// Decode from binary payload (as received from Noise decryption)
guard let decoded = BitchatMessage.fromBinaryPayload(encoded) else {
XCTFail("Failed to decode private message with ID from binary payload")
return
}
// Verify all fields match
XCTAssertEqual(decoded.id, messageID)
XCTAssertEqual(decoded.sender, "alice")
XCTAssertEqual(decoded.content, "Hello Bob, this is a private message via Noise")
XCTAssertEqual(decoded.isPrivate, true)
XCTAssertEqual(decoded.recipientNickname, "bob")
XCTAssertEqual(decoded.senderPeerID, "alice-peer-id-123")
XCTAssertNil(decoded.channel)
XCTAssertFalse(decoded.isRelay)
XCTAssertNil(decoded.originalSender)
}
}

View File

@ -1,101 +0,0 @@
//
// BloomFilterTests.swift
// bitchatTests
//
// This is free and unencumbered software released into the public domain.
// For more information, see <https://unlicense.org>
//
import XCTest
@testable import bitchat
class BloomFilterTests: XCTestCase {
func testBasicBloomFilter() {
var filter = OptimizedBloomFilter(expectedItems: 100, falsePositiveRate: 0.01)
// Test insertion and lookup
let testStrings = ["message1", "message2", "message3", "test123"]
for str in testStrings {
XCTAssertFalse(filter.contains(str))
filter.insert(str)
XCTAssertTrue(filter.contains(str))
}
}
func testFalsePositiveRate() {
var filter = OptimizedBloomFilter(expectedItems: 100, falsePositiveRate: 0.01)
let itemCount = 100
// Insert items
for i in 0..<itemCount {
filter.insert("item\(i)")
}
// Check false positive rate
var falsePositives = 0
let testCount = 1000
for i in itemCount..<(itemCount + testCount) {
if filter.contains("item\(i)") {
falsePositives += 1
}
}
let falsePositiveRate = Double(falsePositives) / Double(testCount)
// With optimized bloom filter targeting 1% false positive rate
XCTAssertLessThan(falsePositiveRate, 0.02) // Allow some margin
}
func testReset() {
var filter = OptimizedBloomFilter(expectedItems: 100, falsePositiveRate: 0.01)
// Insert some items
filter.insert("test1")
filter.insert("test2")
filter.insert("test3")
XCTAssertTrue(filter.contains("test1"))
XCTAssertTrue(filter.contains("test2"))
XCTAssertTrue(filter.contains("test3"))
// Reset
filter.reset()
// Should no longer contain items
XCTAssertFalse(filter.contains("test1"))
XCTAssertFalse(filter.contains("test2"))
XCTAssertFalse(filter.contains("test3"))
}
func testHashDistribution() {
var filter = OptimizedBloomFilter(expectedItems: 1000, falsePositiveRate: 0.01)
// Insert many items
for i in 0..<500 {
filter.insert("message-\(i)")
}
// Check false positive rate
let estimatedRate = filter.estimatedFalsePositiveRate
// Should be well below target since we're at 50% capacity
XCTAssertLessThan(estimatedRate, 0.01)
// Test memory efficiency
let memoryBytes = filter.memorySizeBytes
XCTAssertLessThan(memoryBytes, 2048) // Should be under 2KB for this size
}
func testAdaptiveBloomFilter() {
// Test small network
let smallFilter = OptimizedBloomFilter.adaptive(for: 20)
XCTAssertLessThan(smallFilter.memorySizeBytes, 1024)
// Test large network
let largeFilter = OptimizedBloomFilter.adaptive(for: 1000)
XCTAssertGreaterThan(largeFilter.memorySizeBytes, 2048)
}
}

View File

@ -1,207 +0,0 @@
//
// ChannelVerificationTests.swift
// bitchatTests
//
// This is free and unencumbered software released into the public domain.
// For more information, see <https://unlicense.org>
//
import XCTest
import CryptoKit
@testable import bitchat
class ChannelVerificationTests: XCTestCase {
var viewModel: ChatViewModel!
var mockMeshService: MockBluetoothMeshService!
override func setUp() {
super.setUp()
viewModel = ChatViewModel()
mockMeshService = MockBluetoothMeshService()
viewModel.meshService = mockMeshService
}
override func tearDown() {
viewModel = nil
mockMeshService = nil
super.tearDown()
}
// MARK: - Key Derivation Tests
func testChannelKeyDerivation() {
let password = "testPassword123"
let channel = "#testchannel"
// Derive key twice with same inputs
let key1 = viewModel.deriveChannelKey(from: password, channelName: channel)
let key2 = viewModel.deriveChannelKey(from: password, channelName: channel)
// Keys should be identical for same password/channel
XCTAssertEqual(key1.withUnsafeBytes { Data($0) },
key2.withUnsafeBytes { Data($0) })
}
func testDifferentPasswordsProduceDifferentKeys() {
let channel = "#testchannel"
let password1 = "password123"
let password2 = "password456"
let key1 = viewModel.deriveChannelKey(from: password1, channelName: channel)
let key2 = viewModel.deriveChannelKey(from: password2, channelName: channel)
// Different passwords should produce different keys
XCTAssertNotEqual(key1.withUnsafeBytes { Data($0) },
key2.withUnsafeBytes { Data($0) })
}
func testKeyCommitmentComputation() {
let password = "testPassword"
let channel = "#test"
let key = viewModel.deriveChannelKey(from: password, channelName: channel)
let commitment1 = viewModel.computeKeyCommitment(for: key)
let commitment2 = viewModel.computeKeyCommitment(for: key)
// Same key should produce same commitment
XCTAssertEqual(commitment1, commitment2)
// Commitment should be 64 characters (SHA256 hex)
XCTAssertEqual(commitment1.count, 64)
}
// MARK: - Verification Request/Response Tests
func testChannelKeyVerifyRequestHandling() {
// Setup
let channel = "#test"
let password = "secret123"
let peerID = "peer123"
// Join channel with password
_ = viewModel.joinChannel(channel, password: password)
// Create verification request with matching key
let key = viewModel.deriveChannelKey(from: password, channelName: channel)
let commitment = viewModel.computeKeyCommitment(for: key)
let request = ChannelKeyVerifyRequest(
channel: channel,
requesterID: peerID,
keyCommitment: commitment
)
// Handle request
viewModel.didReceiveChannelKeyVerifyRequest(request, from: peerID)
// Should have sent a positive response
XCTAssertTrue(mockMeshService.sentVerifyResponse)
XCTAssertTrue(mockMeshService.lastVerifyResponse?.verified ?? false)
}
func testChannelKeyVerifyResponseHandling() {
// Setup
let channel = "#test"
let peerID = "peer123"
// Set initial verification status
viewModel.channelVerificationStatus[channel] = .verifying
viewModel.joinedChannels.insert(channel)
// Create positive response
let response = ChannelKeyVerifyResponse(
channel: channel,
responderID: peerID,
verified: true
)
// Handle response
viewModel.didReceiveChannelKeyVerifyResponse(response, from: peerID)
// Status should be verified
XCTAssertEqual(viewModel.channelVerificationStatus[channel], .verified)
}
func testFailedVerificationResponse() {
// Setup
let channel = "#test"
let peerID = "peer123"
viewModel.channelVerificationStatus[channel] = .verifying
viewModel.joinedChannels.insert(channel)
// Create negative response
let response = ChannelKeyVerifyResponse(
channel: channel,
responderID: peerID,
verified: false
)
// Handle response
viewModel.didReceiveChannelKeyVerifyResponse(response, from: peerID)
// Status should be failed
XCTAssertEqual(viewModel.channelVerificationStatus[channel], .failed)
}
// MARK: - Password Update Tests
func testChannelPasswordUpdateHandling() {
// Setup
let channel = "#test"
let ownerID = "owner123"
let newPassword = "newSecret456"
// Join channel first
viewModel.joinedChannels.insert(channel)
viewModel.channelCreators[channel] = ownerID
// Simulate having a Noise session
mockMeshService.mockNoiseSessionEstablished = true
// Create password update
let newKey = viewModel.deriveChannelKey(from: newPassword, channelName: channel)
let newCommitment = viewModel.computeKeyCommitment(for: newKey)
let update = ChannelPasswordUpdate(
channel: channel,
ownerID: ownerID,
ownerFingerprint: "test-fingerprint", // Mock fingerprint
encryptedPassword: Data(), // Would be encrypted in real scenario
newKeyCommitment: newCommitment
)
// Mock decryption to return new password
mockMeshService.mockDecryptedPassword = newPassword
// Handle update
viewModel.didReceiveChannelPasswordUpdate(update, from: ownerID)
// Should have updated local key
XCTAssertNotNil(viewModel.channelKeys[channel])
XCTAssertEqual(viewModel.channelKeyCommitments[channel], newCommitment)
}
}
// MARK: - Mock Mesh Service
class MockBluetoothMeshService: BluetoothMeshService {
var sentVerifyResponse = false
var lastVerifyResponse: ChannelKeyVerifyResponse?
var mockNoiseSessionEstablished = false
var mockDecryptedPassword: String?
// Mock the method without override since it's not overrideable
func mockSendChannelKeyVerifyResponse(_ response: ChannelKeyVerifyResponse, to peerID: String) {
sentVerifyResponse = true
lastVerifyResponse = response
// Call the real method if needed
super.sendChannelKeyVerifyResponse(response, to: peerID)
}
override func getNoiseService() -> NoiseEncryptionService {
// Return actual noise service - tests should use real crypto
return super.getNoiseService()
}
}

View File

@ -1,183 +0,0 @@
//
// KeychainIntegrationTests.swift
// bitchatTests
//
// Integration tests for keychain functionality
//
import XCTest
@testable import bitchat
class KeychainIntegrationTests: XCTestCase {
override func setUp() {
super.setUp()
// Start with clean state
_ = KeychainManager.shared.deleteAllKeychainData()
}
override func tearDown() {
// Clean up test data
_ = KeychainManager.shared.deleteAllKeychainData()
super.tearDown()
}
// MARK: - App Lifecycle Simulation Tests
func testCompleteAppLifecycle() {
print("\n🧪 Testing Complete App Lifecycle")
// 1. First app launch - create identity
print("1️⃣ First launch...")
let service1 = NoiseEncryptionService()
let fingerprint1 = service1.getIdentityFingerprint()
print(" Initial fingerprint: \(fingerprint1)")
// Verify stored in keychain
let keychainData1 = KeychainManager.shared.getIdentityKey(forKey: "noiseStaticKey")
XCTAssertNotNil(keychainData1, "Identity should be in keychain after first launch")
// 2. App goes to background and comes back
print("2️⃣ Background/foreground cycle...")
let service2 = NoiseEncryptionService()
let fingerprint2 = service2.getIdentityFingerprint()
XCTAssertEqual(fingerprint1, fingerprint2, "Identity should persist through background")
// 3. App terminates and relaunches
print("3️⃣ Terminate and relaunch...")
// In real app this would be a new process
let service3 = NoiseEncryptionService()
let fingerprint3 = service3.getIdentityFingerprint()
XCTAssertEqual(fingerprint1, fingerprint3, "Identity should persist through termination")
// 4. User triggers panic mode
print("4️⃣ Panic mode triggered...")
service3.clearPersistentIdentity()
// 5. App creates new identity
print("5️⃣ New identity after panic...")
let service4 = NoiseEncryptionService()
let fingerprint4 = service4.getIdentityFingerprint()
XCTAssertNotEqual(fingerprint1, fingerprint4, "New identity should be created after panic")
print(" New fingerprint: \(fingerprint4)")
print("✅ Lifecycle test complete\n")
}
// MARK: - Channel Password Tests
func testChannelPasswordPersistence() {
let channel1 = "#testchannel1"
let channel2 = "#testchannel2"
let password1 = "password123"
let password2 = "differentpass456"
// Save passwords
XCTAssertTrue(KeychainManager.shared.saveChannelPassword(password1, for: channel1))
XCTAssertTrue(KeychainManager.shared.saveChannelPassword(password2, for: channel2))
// Retrieve passwords
XCTAssertEqual(KeychainManager.shared.getChannelPassword(for: channel1), password1)
XCTAssertEqual(KeychainManager.shared.getChannelPassword(for: channel2), password2)
// Test getAllChannelPasswords
let allPasswords = KeychainManager.shared.getAllChannelPasswords()
XCTAssertEqual(allPasswords.count, 2)
XCTAssertEqual(allPasswords[channel1], password1)
XCTAssertEqual(allPasswords[channel2], password2)
// Delete one password
XCTAssertTrue(KeychainManager.shared.deleteChannelPassword(for: channel1))
XCTAssertNil(KeychainManager.shared.getChannelPassword(for: channel1))
XCTAssertEqual(KeychainManager.shared.getChannelPassword(for: channel2), password2)
}
// MARK: - Security Tests
func testNoPlaintextInUserDefaults() {
// Create services to generate keys
_ = NoiseEncryptionService()
// Check UserDefaults for any sensitive data
let keysToCheck = [
"bitchat.noiseIdentityKey",
"bitchat.channelPasswords",
"bitchat.identityKey",
"bitchat.staticKey"
]
for key in keysToCheck {
let data = UserDefaults.standard.object(forKey: key)
XCTAssertNil(data, "UserDefaults should not contain: \(key)")
}
}
// MARK: - Error Handling Tests
func testKeychainErrorRecovery() {
// Test that the app can recover from keychain errors
// This is difficult to test without mocking, but we can verify
// that multiple save attempts don't crash
let testData = "test".data(using: .utf8)!
// Rapid saves
for i in 0..<10 {
let saved = KeychainManager.shared.saveIdentityKey(testData, forKey: "rapidTest\(i)")
XCTAssertTrue(saved, "Save \(i) should succeed")
}
// Rapid deletes
for i in 0..<10 {
_ = KeychainManager.shared.deleteIdentityKey(forKey: "rapidTest\(i)")
}
}
// MARK: - Cleanup Tests
func testAggressiveCleanupOnlyDeletesBitchatItems() {
// This test verifies we don't delete other apps' keychain items
// Add a non-bitchat item (simulating another app)
let otherAppQuery: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: "com.otherapp.service",
kSecAttrAccount as String: "other_app_account",
kSecValueData as String: "other app data".data(using: .utf8)!
]
// Clean first
SecItemDelete(otherAppQuery as CFDictionary)
// Add the item
let addStatus = SecItemAdd(otherAppQuery as CFDictionary, nil)
XCTAssertEqual(addStatus, errSecSuccess, "Should add other app item")
// Add a bitchat legacy item
let bitchatQuery: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: "com.bitchat.legacy",
kSecAttrAccount as String: "test_account",
kSecValueData as String: "bitchat data".data(using: .utf8)!
]
SecItemDelete(bitchatQuery as CFDictionary)
let bitchatStatus = SecItemAdd(bitchatQuery as CFDictionary, nil)
XCTAssertEqual(bitchatStatus, errSecSuccess, "Should add bitchat item")
// Run aggressive cleanup
_ = KeychainManager.shared.aggressiveCleanupLegacyItems()
// Verify other app item still exists
var result: AnyObject?
let checkStatus = SecItemCopyMatching(otherAppQuery as CFDictionary, &result)
XCTAssertEqual(checkStatus, errSecSuccess, "Other app item should still exist")
// Verify bitchat item was deleted
var bitchatResult: AnyObject?
let bitchatCheck = SecItemCopyMatching(bitchatQuery as CFDictionary, &bitchatResult)
XCTAssertEqual(bitchatCheck, errSecItemNotFound, "Bitchat legacy item should be deleted")
// Clean up
SecItemDelete(otherAppQuery as CFDictionary)
}
}

View File

@ -1,247 +0,0 @@
//
// MessagePaddingTests.swift
// bitchatTests
//
// This is free and unencumbered software released into the public domain.
// For more information, see <https://unlicense.org>
//
import XCTest
@testable import bitchat
class MessagePaddingTests: XCTestCase {
func testBasicPadding() {
let originalData = Data("Hello".utf8)
let targetSize = 256
let padded = MessagePadding.pad(originalData, toSize: targetSize)
XCTAssertEqual(padded.count, targetSize)
let unpadded = MessagePadding.unpad(padded)
XCTAssertEqual(unpadded, originalData)
}
func testMultipleBlockSizes() {
let testMessages = [
"Hi",
"This is a longer message",
"This is an even longer message that should require a larger block size",
String(repeating: "A", count: 500)
]
for message in testMessages {
let data = Data(message.utf8)
let blockSize = MessagePadding.optimalBlockSize(for: data.count)
// Block size should be reasonable
XCTAssertGreaterThan(blockSize, data.count)
XCTAssertTrue(MessagePadding.blockSizes.contains(blockSize) || blockSize == data.count)
let padded = MessagePadding.pad(data, toSize: blockSize)
// Check if padding was applied (only if needed padding <= 255)
let paddingNeeded = blockSize - data.count
if paddingNeeded <= 255 {
XCTAssertEqual(padded.count, blockSize)
let unpadded = MessagePadding.unpad(padded)
XCTAssertEqual(unpadded, data)
} else {
// No padding applied if more than 255 bytes needed
XCTAssertEqual(padded, data)
}
}
}
func testPaddingWithLargeData() {
let largeData = Data(repeating: 0xFF, count: 1500)
let blockSize = MessagePadding.optimalBlockSize(for: largeData.count)
// Should use 2048 block
XCTAssertEqual(blockSize, 2048)
let padded = MessagePadding.pad(largeData, toSize: blockSize)
// Since padding needed (548 bytes) > 255, no padding is applied
XCTAssertEqual(padded.count, largeData.count)
XCTAssertEqual(padded, largeData)
// Test with data that fits within PKCS#7 limits
let smallerData = Data(repeating: 0xAA, count: 1800)
let paddedSmaller = MessagePadding.pad(smallerData, toSize: 2048)
// Padding needed is 248 bytes, which is < 255, so padding should work
XCTAssertEqual(paddedSmaller.count, 2048)
let unpaddedSmaller = MessagePadding.unpad(paddedSmaller)
XCTAssertEqual(unpaddedSmaller, smallerData)
}
func testInvalidPadding() {
// Test empty data
let empty = Data()
let unpaddedEmpty = MessagePadding.unpad(empty)
XCTAssertEqual(unpaddedEmpty, empty)
// Test data with invalid padding length
var invalidPadding = Data(repeating: 0x00, count: 100)
invalidPadding[99] = 255 // Invalid padding length
let result = MessagePadding.unpad(invalidPadding)
XCTAssertEqual(result, invalidPadding) // Should return original if invalid
}
func testPaddingRandomness() {
// Ensure padding bytes are random (not predictable)
let data = Data("Test".utf8)
let padded1 = MessagePadding.pad(data, toSize: 256)
let padded2 = MessagePadding.pad(data, toSize: 256)
// Same size
XCTAssertEqual(padded1.count, padded2.count)
// But different padding bytes (with very high probability)
XCTAssertNotEqual(padded1, padded2)
// Both should unpad to same data
XCTAssertEqual(MessagePadding.unpad(padded1), data)
XCTAssertEqual(MessagePadding.unpad(padded2), data)
}
// MARK: - Edge Case Tests
func testExactBlockSizeData() {
// Test data that exactly matches block sizes
for blockSize in MessagePadding.blockSizes {
// Account for 16 bytes encryption overhead
let dataSize = blockSize - 16
let data = Data(repeating: 0x42, count: dataSize)
let optimalSize = MessagePadding.optimalBlockSize(for: data.count)
XCTAssertEqual(optimalSize, blockSize)
// Should fit exactly, no padding needed
let padded = MessagePadding.pad(data, toSize: blockSize)
XCTAssertEqual(padded.count, blockSize)
}
}
func testOneByteOverBlockSize() {
// Test data that's one byte over block size threshold
let blockSizes = [256, 512, 1024]
for blockSize in blockSizes {
// Create data that's 1 byte too large for current block
let dataSize = blockSize - 16 + 1
let data = Data(repeating: 0x42, count: dataSize)
let optimalSize = MessagePadding.optimalBlockSize(for: data.count)
// Should jump to next block size
if blockSize < 2048 {
XCTAssertGreaterThan(optimalSize, blockSize)
}
}
}
func testVerySmallData() {
// Test tiny messages
let tinyMessages = [
Data([0x01]),
Data([0x01, 0x02]),
Data("a".utf8),
Data()
]
for data in tinyMessages {
let blockSize = MessagePadding.optimalBlockSize(for: data.count)
XCTAssertEqual(blockSize, 256) // Should use minimum block size
if !data.isEmpty {
let padded = MessagePadding.pad(data, toSize: blockSize)
XCTAssertEqual(padded.count, blockSize)
let unpadded = MessagePadding.unpad(padded)
XCTAssertEqual(unpadded, data)
}
}
}
func testPaddingBoundaryConditions() {
// Test PKCS#7 padding limit (255 bytes)
let testCases = [
(dataSize: 1, targetSize: 256), // Need 255 bytes padding - exactly at limit
(dataSize: 2, targetSize: 256), // Need 254 bytes padding - just under limit
(dataSize: 256, targetSize: 512), // Need 256 bytes padding - just over limit
]
for testCase in testCases {
let data = Data(repeating: 0x42, count: testCase.dataSize)
let padded = MessagePadding.pad(data, toSize: testCase.targetSize)
let paddingNeeded = testCase.targetSize - testCase.dataSize
if paddingNeeded <= 255 {
// Padding should be applied
XCTAssertEqual(padded.count, testCase.targetSize)
// Verify correct padding byte value
let paddingByte = padded[padded.count - 1]
XCTAssertEqual(Int(paddingByte), paddingNeeded)
// Should unpad correctly
let unpadded = MessagePadding.unpad(padded)
XCTAssertEqual(unpadded, data)
} else {
// No padding applied
XCTAssertEqual(padded, data)
}
}
}
func testCorruptedPadding() {
let data = Data("Test message".utf8)
let padded = MessagePadding.pad(data, toSize: 256)
// Corrupt the padding length byte
var corrupted = padded
corrupted[corrupted.count - 1] = 0
let result = MessagePadding.unpad(corrupted)
XCTAssertEqual(result, corrupted) // Should return original when padding is invalid
// Test with padding length > data size
var corruptedTooLarge = padded
corruptedTooLarge[corruptedTooLarge.count - 1] = 255
let result2 = MessagePadding.unpad(corruptedTooLarge)
XCTAssertEqual(result2, corruptedTooLarge)
}
func testDataAlreadyLargerThanTarget() {
let data = Data(repeating: 0x42, count: 1000)
let tooSmallTarget = 256
// Should return original data when it's already larger than target
let result = MessagePadding.pad(data, toSize: tooSmallTarget)
XCTAssertEqual(result, data)
XCTAssertEqual(result.count, data.count)
}
func testOptimalBlockSizeForLargeData() {
// Test data larger than largest block size
let hugeData = Data(repeating: 0x42, count: 5000)
let blockSize = MessagePadding.optimalBlockSize(for: hugeData.count)
// Should return data size when larger than all blocks
XCTAssertEqual(blockSize, hugeData.count)
}
func testPaddingPerformance() {
let data = Data(repeating: 0x42, count: 1000)
measure {
for _ in 0..<1000 {
let blockSize = MessagePadding.optimalBlockSize(for: data.count)
let padded = MessagePadding.pad(data, toSize: blockSize)
_ = MessagePadding.unpad(padded)
}
}
}
}

View File

@ -1,222 +0,0 @@
//
// NoiseChannelEncryptionTests.swift
// bitchatTests
//
// This is free and unencumbered software released into the public domain.
// For more information, see <https://unlicense.org>
//
import XCTest
import CryptoKit
@testable import bitchat
class NoiseChannelEncryptionTests: XCTestCase {
// MARK: - Channel Key Derivation with Fingerprint Tests
func testChannelEncryptionWithFingerprint() {
let encryption = NoiseChannelEncryption()
let password = "test-password-123"
let channel = "#secure-channel"
let fingerprint = "e36f7993abc123def456789012345678901234567890abcdef1234567890abcd"
// Set channel password with fingerprint
encryption.setChannelPasswordForCreator(password, channel: channel, creatorFingerprint: fingerprint)
// Test encryption
let message = "This is a secret message"
do {
let encrypted = try encryption.encryptChannelMessage(message, for: channel)
// Ensure it's actually encrypted
XCTAssertNotEqual(encrypted, Data(message.utf8))
XCTAssertGreaterThan(encrypted.count, message.count) // Should have IV + tag
// Test decryption
let decrypted = try encryption.decryptChannelMessage(encrypted, for: channel)
XCTAssertEqual(decrypted, message)
} catch {
XCTFail("Encryption/decryption failed: \(error)")
}
}
func testBackwardsCompatibilityWithoutFingerprint() {
let encryption = NoiseChannelEncryption()
let password = "test-password-123"
let channel = "#legacy-channel"
// Set password without fingerprint (legacy mode)
encryption.setChannelPassword(password, for: channel)
// Encrypt message
let message = "Legacy message"
do {
let encrypted = try encryption.encryptChannelMessage(message, for: channel)
// Should still work
let decrypted = try encryption.decryptChannelMessage(encrypted, for: channel)
XCTAssertEqual(decrypted, message)
} catch {
XCTFail("Legacy encryption failed: \(error)")
}
}
func testDifferentFingerprintsProduceDifferentEncryption() throws {
let encryption1 = NoiseChannelEncryption()
let encryption2 = NoiseChannelEncryption()
let password = "same-password"
let channel = "#test-channel"
let message = "Test message"
let fingerprint1 = "1111111111111111111111111111111111111111111111111111111111111111"
let fingerprint2 = "2222222222222222222222222222222222222222222222222222222222222222"
// Set same password with different fingerprints
encryption1.setChannelPasswordForCreator(password, channel: channel, creatorFingerprint: fingerprint1)
encryption2.setChannelPasswordForCreator(password, channel: channel, creatorFingerprint: fingerprint2)
// Encrypt same message
let encrypted1 = try encryption1.encryptChannelMessage(message, for: channel)
let encrypted2 = try encryption2.encryptChannelMessage(message, for: channel)
// Encrypted data should be different (different keys due to different salts)
// Note: We can't directly compare ciphertexts due to random IVs, but we can verify they don't decrypt with wrong key
// Try to decrypt with wrong fingerprint - should fail
encryption1.removeChannelPassword(for: channel)
encryption1.setChannelPasswordForCreator(password, channel: channel, creatorFingerprint: fingerprint2)
XCTAssertThrowsError(try encryption1.decryptChannelMessage(encrypted1, for: channel)) { error in
// Should fail to decrypt because key is different
}
}
// MARK: - Key Management Tests
func testChannelKeyPersistence() {
let encryption = NoiseChannelEncryption()
let password = "persistent-password"
let channel = "#persistent-channel"
// Set and save password
encryption.setChannelPassword(password, for: channel)
// Verify it's saved in keychain
XCTAssertTrue(encryption.loadChannelPassword(for: channel))
// Create new instance and load
let encryption2 = NoiseChannelEncryption()
XCTAssertTrue(encryption2.loadChannelPassword(for: channel))
// Should be able to decrypt messages from first instance
do {
let message = "Cross-instance message"
let encrypted = try encryption.encryptChannelMessage(message, for: channel)
let decrypted = try encryption2.decryptChannelMessage(encrypted, for: channel)
XCTAssertEqual(decrypted, message)
} catch {
XCTFail("Cross-instance encryption failed: \(error)")
}
// Clean up
encryption.removeChannelPassword(for: channel)
}
func testChannelKeyPacketCreation() {
let encryption = NoiseChannelEncryption()
let password = "shared-password"
let channel = "#shared-channel"
// Create key packet
guard let packet = encryption.createChannelKeyPacket(password: password, channel: channel) else {
XCTFail("Failed to create key packet")
return
}
// Verify packet structure
XCTAssertGreaterThan(packet.count, 32) // Should have channel name + password + metadata
// Process packet in another instance
let encryption2 = NoiseChannelEncryption()
guard let (extractedChannel, extractedPassword) = encryption2.processChannelKeyPacket(packet) else {
XCTFail("Failed to process key packet")
return
}
XCTAssertEqual(extractedChannel, channel)
XCTAssertEqual(extractedPassword, password)
}
// MARK: - Error Handling Tests
func testDecryptionWithWrongPassword() {
let encryption = NoiseChannelEncryption()
let channel = "#error-test"
// Encrypt with one password
encryption.setChannelPassword("correct-password", for: channel)
let message = "Secret message"
do {
let encrypted = try encryption.encryptChannelMessage(message, for: channel)
// Change to wrong password
encryption.setChannelPassword("wrong-password", for: channel)
// Should fail to decrypt
XCTAssertThrowsError(try encryption.decryptChannelMessage(encrypted, for: channel))
} catch {
XCTFail("Encryption failed: \(error)")
}
}
func testEncryptionWithoutPassword() {
let encryption = NoiseChannelEncryption()
let channel = "#no-password"
// Try to encrypt without setting password
XCTAssertThrowsError(try encryption.encryptChannelMessage("Test", for: channel)) { error in
// Should throw channelKeyMissing error
if let encryptionError = error as? NoiseChannelEncryptionError {
XCTAssertEqual(encryptionError, NoiseChannelEncryptionError.channelKeyMissing)
} else {
XCTFail("Wrong error type")
}
}
}
func testInvalidChannelName() {
let encryption = NoiseChannelEncryption()
// Empty channel
XCTAssertThrowsError(try encryption.encryptChannelMessage("Test", for: ""))
// Channel without # prefix
XCTAssertThrowsError(try encryption.encryptChannelMessage("Test", for: "invalid"))
}
// MARK: - Performance Tests
func testEncryptionPerformance() {
let encryption = NoiseChannelEncryption()
let channel = "#perf-test"
let fingerprint = "e36f7993abc123def456789012345678901234567890abcdef1234567890abcd"
encryption.setChannelPasswordForCreator("test-password", channel: channel, creatorFingerprint: fingerprint)
let message = String(repeating: "Hello World! ", count: 100) // ~1.3KB message
measure {
do {
let encrypted = try encryption.encryptChannelMessage(message, for: channel)
_ = try encryption.decryptChannelMessage(encrypted, for: channel)
} catch {
XCTFail("Performance test failed: \(error)")
}
}
}
}

View File

@ -1,202 +0,0 @@
//
// NoiseIdentityPersistenceTests.swift
// bitchatTests
//
// Tests for Noise Protocol identity key persistence
//
import XCTest
@testable import bitchat
class NoiseIdentityPersistenceTests: XCTestCase {
override func setUp() {
super.setUp()
// Clean up any existing test data
cleanupTestData()
}
override func tearDown() {
// Clean up after tests
cleanupTestData()
super.tearDown()
}
private func cleanupTestData() {
// Clear any existing identity keys
_ = KeychainManager.shared.deleteIdentityKey(forKey: "noiseStaticKey")
_ = KeychainManager.shared.deleteIdentityKey(forKey: "messageRetentionKey")
// Clear any UserDefaults that might interfere
UserDefaults.standard.removeObject(forKey: "bitchat.noiseIdentityKey")
UserDefaults.standard.removeObject(forKey: "bitchat.messageRetentionKey")
UserDefaults.standard.synchronize()
}
// MARK: - Identity Persistence Tests
func testIdentityPersistsAcrossInstances() {
// Create first instance
let service1 = NoiseEncryptionService()
let fingerprint1 = service1.getIdentityFingerprint()
let publicKey1 = service1.getStaticPublicKeyData()
XCTAssertFalse(fingerprint1.isEmpty, "Fingerprint should not be empty")
XCTAssertEqual(publicKey1.count, 32, "Public key should be 32 bytes")
// Create second instance
let service2 = NoiseEncryptionService()
let fingerprint2 = service2.getIdentityFingerprint()
let publicKey2 = service2.getStaticPublicKeyData()
// Verify same identity
XCTAssertEqual(fingerprint1, fingerprint2, "Fingerprint should persist across instances")
XCTAssertEqual(publicKey1, publicKey2, "Public key should persist across instances")
}
func testIdentityNotStoredInUserDefaults() {
// Create service to generate identity
_ = NoiseEncryptionService()
// Verify identity is NOT in UserDefaults
let userDefaultsData = UserDefaults.standard.data(forKey: "bitchat.noiseIdentityKey")
XCTAssertNil(userDefaultsData, "Identity key should NOT be stored in UserDefaults")
}
func testIdentityStoredInKeychain() {
// Create service to generate identity
_ = NoiseEncryptionService()
// Verify identity IS in Keychain
let keychainData = KeychainManager.shared.getIdentityKey(forKey: "noiseStaticKey")
XCTAssertNotNil(keychainData, "Identity key should be stored in Keychain")
XCTAssertEqual(keychainData?.count, 32, "Identity key should be 32 bytes")
}
func testPanicModeClearsIdentity() {
// Create service and get initial fingerprint
let service1 = NoiseEncryptionService()
let fingerprint1 = service1.getIdentityFingerprint()
// Clear identity (panic mode)
service1.clearPersistentIdentity()
// Create new service and verify new identity
let service2 = NoiseEncryptionService()
let fingerprint2 = service2.getIdentityFingerprint()
XCTAssertNotEqual(fingerprint1, fingerprint2, "New identity should be created after panic mode")
}
func testMultipleRapidInstantiations() {
// Create multiple services rapidly
var fingerprints: [String] = []
for _ in 0..<10 {
let service = NoiseEncryptionService()
fingerprints.append(service.getIdentityFingerprint())
}
// Verify all fingerprints are the same
let firstFingerprint = fingerprints[0]
for fingerprint in fingerprints {
XCTAssertEqual(fingerprint, firstFingerprint, "All instances should have same identity")
}
}
func testKeychainAccessFailureHandling() {
// This test would require mocking KeychainManager, but we can at least
// verify the service initializes even if keychain is problematic
let service = NoiseEncryptionService()
XCTAssertFalse(service.getIdentityFingerprint().isEmpty, "Service should initialize with valid identity")
}
// MARK: - Message Retention Key Tests
// Message retention feature has been removed
// MARK: - Keychain Service Name Tests
func testKeychainServiceName() {
// Verify we're using the correct service name
let expectedService = "chat.bitchat"
// Save a test item
let testKey = "test_service_verification"
let testData = "test".data(using: .utf8)!
_ = KeychainManager.shared.saveIdentityKey(testData, forKey: testKey)
// Query directly to verify service name
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: expectedService,
kSecAttrAccount as String: "identity_\(testKey)",
kSecMatchLimit as String: kSecMatchLimitOne,
kSecReturnData as String: true
]
var result: AnyObject?
let status = SecItemCopyMatching(query as CFDictionary, &result)
XCTAssertEqual(status, errSecSuccess, "Should find item with expected service name")
XCTAssertNotNil(result as? Data, "Should retrieve data")
// Clean up
_ = KeychainManager.shared.deleteIdentityKey(forKey: testKey)
}
// MARK: - Legacy Cleanup Tests
func testLegacyKeychainCleanup() {
// Create some legacy items with old service names
let legacyServices = [
"com.bitchat.passwords",
"com.bitchat.noise.identity",
"bitchat.keychain"
]
// Add test items with legacy service names
for service in legacyServices {
let addQuery: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: service,
kSecAttrAccount as String: "test_legacy_item",
kSecValueData as String: "test".data(using: .utf8)!
]
// Add item (ignore if already exists)
_ = SecItemAdd(addQuery as CFDictionary, nil)
}
// Run aggressive cleanup
let deletedCount = KeychainManager.shared.aggressiveCleanupLegacyItems()
// Verify items were deleted
XCTAssertGreaterThan(deletedCount, 0, "Should delete at least some legacy items")
// Verify legacy items are gone
for service in legacyServices {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: service,
kSecMatchLimit as String: kSecMatchLimitOne
]
var result: AnyObject?
let status = SecItemCopyMatching(query as CFDictionary, &result)
XCTAssertEqual(status, errSecItemNotFound, "Legacy service '\(service)' should be deleted")
}
}
// MARK: - Performance Tests
func testIdentityLoadPerformance() {
// Ensure identity exists
_ = NoiseEncryptionService()
measure {
// Measure how long it takes to load identity
_ = NoiseEncryptionService()
}
}
}

View File

@ -1,310 +0,0 @@
//
// NoiseKeyRotationTests.swift
// bitchatTests
//
// This is free and unencumbered software released into the public domain.
// For more information, see <https://unlicense.org>
//
import XCTest
import CryptoKit
@testable import bitchat
class NoiseKeyRotationTests: XCTestCase {
var keyRotation: NoiseChannelKeyRotation!
override func setUp() {
super.setUp()
keyRotation = NoiseChannelKeyRotation()
}
override func tearDown() {
// Clean up test data
keyRotation.clearEpochs(for: "#test-channel")
super.tearDown()
}
// MARK: - Basic Key Rotation Tests
func testInitialKeyGeneration() {
let channel = "#test-channel"
let password = "test-password"
let fingerprint = "abc123def456"
// Get initial key
guard let rotatedKey = keyRotation.getCurrentKey(
for: channel,
basePassword: password,
creatorFingerprint: fingerprint
) else {
XCTFail("Failed to get initial key")
return
}
XCTAssertEqual(rotatedKey.epoch.epochNumber, 1)
XCTAssertTrue(rotatedKey.isActive)
XCTAssertNotNil(rotatedKey.key)
}
func testKeyRotation() {
let channel = "#test-channel"
let password = "test-password"
let fingerprint = "abc123def456"
// Get initial key
let initialKey = keyRotation.getCurrentKey(
for: channel,
basePassword: password,
creatorFingerprint: fingerprint
)
// Rotate key
let newEpoch = keyRotation.rotateChannelKey(
for: channel,
basePassword: password,
creatorFingerprint: fingerprint
)
XCTAssertEqual(newEpoch.epochNumber, 2)
XCTAssertNotNil(newEpoch.previousEpochCommitment)
// Get new current key
let rotatedKey = keyRotation.getCurrentKey(
for: channel,
basePassword: password,
creatorFingerprint: fingerprint
)
XCTAssertEqual(rotatedKey?.epoch.epochNumber, 2)
// Keys should be different
if let initial = initialKey, let rotated = rotatedKey {
XCTAssertNotEqual(
initial.key.withUnsafeBytes { Data($0) },
rotated.key.withUnsafeBytes { Data($0) }
)
}
}
func testKeyRotationNeeded() {
let channel = "#test-channel"
let password = "test-password"
let fingerprint = "abc123def456"
// Initially needs rotation (no epochs)
XCTAssertTrue(keyRotation.needsKeyRotation(for: channel))
// After getting initial key, shouldn't need rotation
_ = keyRotation.getCurrentKey(
for: channel,
basePassword: password,
creatorFingerprint: fingerprint
)
XCTAssertFalse(keyRotation.needsKeyRotation(for: channel))
// Note: We can't easily test time-based rotation need without
// modifying internal state or waiting 22+ hours
}
// MARK: - Multiple Epoch Tests
func testMultipleEpochsForDecryption() {
let channel = "#test-channel"
let password = "test-password"
let fingerprint = "abc123def456"
// Create initial epoch
_ = keyRotation.getCurrentKey(
for: channel,
basePassword: password,
creatorFingerprint: fingerprint
)
// Rotate multiple times
for _ in 0..<3 {
_ = keyRotation.rotateChannelKey(
for: channel,
basePassword: password,
creatorFingerprint: fingerprint
)
}
// Get valid keys for decryption
let validKeys = keyRotation.getValidKeysForDecryption(
channel: channel,
basePassword: password,
creatorFingerprint: fingerprint
)
// Should have at least the current epoch
XCTAssertGreaterThanOrEqual(validKeys.count, 1)
// Check that we have the latest epoch
XCTAssertTrue(validKeys.contains { $0.epoch.epochNumber == 4 })
}
func testEpochKeyDerivationConsistency() {
let channel = "#test-channel"
let password = "test-password"
let fingerprint = "abc123def456"
// Get key for epoch 1
let key1a = keyRotation.getCurrentKey(
for: channel,
basePassword: password,
creatorFingerprint: fingerprint
)
// Get the same key again
let key1b = keyRotation.getCurrentKey(
for: channel,
basePassword: password,
creatorFingerprint: fingerprint
)
// Keys should be identical for same epoch
if let a = key1a, let b = key1b {
XCTAssertEqual(
a.key.withUnsafeBytes { Data($0) },
b.key.withUnsafeBytes { Data($0) }
)
XCTAssertEqual(a.epoch.epochNumber, b.epoch.epochNumber)
}
}
// MARK: - Edge Cases
func testMaxEpochLimit() {
let channel = "#test-channel"
let password = "test-password"
let fingerprint = "abc123def456"
// Create initial epoch
_ = keyRotation.getCurrentKey(
for: channel,
basePassword: password,
creatorFingerprint: fingerprint
)
// Rotate many times (more than max stored epochs)
for _ in 0..<10 {
_ = keyRotation.rotateChannelKey(
for: channel,
basePassword: password,
creatorFingerprint: fingerprint
)
}
// Get all valid epochs
let validKeys = keyRotation.getValidKeysForDecryption(
channel: channel,
basePassword: password,
creatorFingerprint: fingerprint
)
// Should not exceed reasonable limit
XCTAssertLessThanOrEqual(validKeys.count, 7) // maxStoredEpochs
}
func testDifferentChannelsDifferentEpochs() {
let password = "test-password"
let fingerprint = "abc123def456"
let channel1 = "#channel-1"
let channel2 = "#channel-2"
// Get keys for both channels
let key1 = keyRotation.getCurrentKey(
for: channel1,
basePassword: password,
creatorFingerprint: fingerprint
)
let key2 = keyRotation.getCurrentKey(
for: channel2,
basePassword: password,
creatorFingerprint: fingerprint
)
// Keys should be different even with same password
if let k1 = key1, let k2 = key2 {
XCTAssertNotEqual(
k1.key.withUnsafeBytes { Data($0) },
k2.key.withUnsafeBytes { Data($0) }
)
}
}
// MARK: - Integration Tests
func testKeyRotationWithEncryption() throws {
let channel = "#test-channel"
let password = "test-password"
let fingerprint = "abc123def456"
// Get initial key
guard let initialRotatedKey = keyRotation.getCurrentKey(
for: channel,
basePassword: password,
creatorFingerprint: fingerprint
) else {
XCTFail("Failed to get initial key")
return
}
// Encrypt a message with initial key
let message = "Test message before rotation"
let nonce = ChaChaPoly.Nonce()
let sealed1 = try ChaChaPoly.seal(
Data(message.utf8),
using: initialRotatedKey.key,
nonce: nonce
)
// Rotate key
_ = keyRotation.rotateChannelKey(
for: channel,
basePassword: password,
creatorFingerprint: fingerprint
)
// Get new key
guard let newRotatedKey = keyRotation.getCurrentKey(
for: channel,
basePassword: password,
creatorFingerprint: fingerprint
) else {
XCTFail("Failed to get rotated key")
return
}
// New key should not decrypt old message
XCTAssertThrowsError(
try ChaChaPoly.open(sealed1, using: newRotatedKey.key)
)
// But we should still be able to decrypt with old epoch key
let validKeys = keyRotation.getValidKeysForDecryption(
channel: channel,
basePassword: password,
creatorFingerprint: fingerprint
)
// Try each valid key until one works
var decrypted = false
for rotatedKey in validKeys {
do {
let plaintext = try ChaChaPoly.open(sealed1, using: rotatedKey.key)
XCTAssertEqual(String(data: plaintext, encoding: .utf8), message)
decrypted = true
break
} catch {
continue
}
}
XCTAssertTrue(decrypted, "Failed to decrypt with any valid key")
}
}

View File

@ -1,369 +0,0 @@
//
// NoiseProtocolTests.swift
// bitchatTests
//
// This is free and unencumbered software released into the public domain.
// For more information, see <https://unlicense.org>
//
import XCTest
import CryptoKit
@testable import bitchat
class NoiseProtocolTests: XCTestCase {
// MARK: - Cipher State Tests
func testCipherStateEncryptDecrypt() throws {
let key = SymmetricKey(size: .bits256)
let cipher = NoiseCipherState(key: key)
let plaintext = "Hello, Noise Protocol!".data(using: .utf8)!
let associatedData = "metadata".data(using: .utf8)!
// Encrypt
let ciphertext = try cipher.encrypt(plaintext: plaintext, associatedData: associatedData)
// Create new cipher with same key for decryption
let decryptCipher = NoiseCipherState(key: key)
let decrypted = try decryptCipher.decrypt(ciphertext: ciphertext, associatedData: associatedData)
XCTAssertEqual(plaintext, decrypted)
}
func testCipherStateNonceIncrement() throws {
let key = SymmetricKey(size: .bits256)
let cipher = NoiseCipherState(key: key)
let plaintext = "Test".data(using: .utf8)!
// Encrypt multiple messages
let ct1 = try cipher.encrypt(plaintext: plaintext)
let ct2 = try cipher.encrypt(plaintext: plaintext)
let ct3 = try cipher.encrypt(plaintext: plaintext)
// All ciphertexts should be different due to nonce increment
XCTAssertNotEqual(ct1, ct2)
XCTAssertNotEqual(ct2, ct3)
XCTAssertNotEqual(ct1, ct3)
}
// MARK: - Symmetric State Tests
func testSymmetricStateInitialization() {
let protocolName = "Noise_XX_25519_ChaChaPoly_SHA256"
let state = NoiseSymmetricState(protocolName: protocolName)
// Hash should be initialized with protocol name
let hash = state.getHandshakeHash()
XCTAssertEqual(hash.count, 32) // SHA256 output
}
func testSymmetricStateMixKey() throws {
let state = NoiseSymmetricState(protocolName: "Noise_XX_25519_ChaChaPoly_SHA256")
let keyMaterial = Data(repeating: 0x42, count: 32)
state.mixKey(keyMaterial)
// After mixKey, cipher should be initialized
let plaintext = "Test".data(using: .utf8)!
let encrypted = try state.encryptAndHash(plaintext)
XCTAssertNotEqual(plaintext, encrypted)
XCTAssertEqual(encrypted.count, plaintext.count + 16) // ChaCha20Poly1305 adds 16-byte tag
}
// MARK: - Handshake State Tests
func testNoiseXXHandshakeComplete() throws {
// Create initiator and responder
let initiatorStatic = Curve25519.KeyAgreement.PrivateKey()
let responderStatic = Curve25519.KeyAgreement.PrivateKey()
var initiator = NoiseHandshakeState(role: .initiator, pattern: .XX, localStaticKey: initiatorStatic)
var responder = NoiseHandshakeState(role: .responder, pattern: .XX, localStaticKey: responderStatic)
// Message 1: initiator -> responder (e)
let msg1 = try initiator.writeMessage()
_ = try responder.readMessage(msg1)
// Message 2: responder -> initiator (e, ee, s, es)
let msg2 = try responder.writeMessage()
_ = try initiator.readMessage(msg2)
// Message 3: initiator -> responder (s, se)
let msg3 = try initiator.writeMessage()
_ = try responder.readMessage(msg3)
// Both should have completed handshake
XCTAssertTrue(initiator.isHandshakeComplete())
XCTAssertTrue(responder.isHandshakeComplete())
// Get transport ciphers
let (initSend, initRecv) = try initiator.getTransportCiphers()
let (respSend, respRecv) = try responder.getTransportCiphers()
// Test transport encryption
let testMessage = "Secret message".data(using: .utf8)!
let encrypted = try initSend.encrypt(plaintext: testMessage)
let decrypted = try respRecv.decrypt(ciphertext: encrypted)
XCTAssertEqual(testMessage, decrypted)
// Test reverse direction
let encrypted2 = try respSend.encrypt(plaintext: testMessage)
let decrypted2 = try initRecv.decrypt(ciphertext: encrypted2)
XCTAssertEqual(testMessage, decrypted2)
}
func testNoiseXXWithPayloads() throws {
let initiatorStatic = Curve25519.KeyAgreement.PrivateKey()
let responderStatic = Curve25519.KeyAgreement.PrivateKey()
var initiator = NoiseHandshakeState(role: .initiator, pattern: .XX, localStaticKey: initiatorStatic)
var responder = NoiseHandshakeState(role: .responder, pattern: .XX, localStaticKey: responderStatic)
// Message 1 with payload
let payload1 = "Hello from initiator".data(using: .utf8)!
let msg1 = try initiator.writeMessage(payload: payload1)
let received1 = try responder.readMessage(msg1)
XCTAssertEqual(payload1, received1)
// Message 2 with payload
let payload2 = "Hello from responder".data(using: .utf8)!
let msg2 = try responder.writeMessage(payload: payload2)
let received2 = try initiator.readMessage(msg2)
XCTAssertEqual(payload2, received2)
// Message 3 with payload
let payload3 = "Final message".data(using: .utf8)!
let msg3 = try initiator.writeMessage(payload: payload3)
let received3 = try responder.readMessage(msg3)
XCTAssertEqual(payload3, received3)
}
// MARK: - Session Tests
func testNoiseSessionLifecycle() throws {
let aliceKey = Curve25519.KeyAgreement.PrivateKey()
let bobKey = Curve25519.KeyAgreement.PrivateKey()
let aliceSession = NoiseSession(peerID: "bob", role: .initiator, localStaticKey: aliceKey)
let bobSession = NoiseSession(peerID: "alice", role: .responder, localStaticKey: bobKey)
// Start handshake - only initiator calls startHandshake
let msg1 = try aliceSession.startHandshake()
XCTAssertFalse(msg1.isEmpty, "Initiator should send first message")
// Process messages - responder will auto-initialize on first message
let msg2 = try bobSession.processHandshakeMessage(msg1)!
XCTAssertFalse(msg2.isEmpty, "Responder should send second message")
let msg3 = try aliceSession.processHandshakeMessage(msg2)!
XCTAssertFalse(msg3.isEmpty, "Initiator should send third message")
let finalMsg = try bobSession.processHandshakeMessage(msg3)
XCTAssertNil(finalMsg, "No more messages after handshake complete")
// Both sessions should be established
XCTAssertTrue(aliceSession.isEstablished(), "Alice session should be established")
XCTAssertTrue(bobSession.isEstablished(), "Bob session should be established")
// Test encryption
let plaintext = "Test message".data(using: .utf8)!
let encrypted = try aliceSession.encrypt(plaintext)
let decrypted = try bobSession.decrypt(encrypted)
XCTAssertEqual(plaintext, decrypted)
}
// MARK: - Integration Tests
func testNoiseEncryptionServiceIntegration() throws {
// Clean up any existing keys
_ = KeychainManager.shared.deleteIdentityKey(forKey: "noiseStaticKey")
let service1 = NoiseEncryptionService()
let service2 = NoiseEncryptionService()
let peer1ID = "peer1"
let peer2ID = "peer2"
// Initiate handshake from peer1 to peer2
let handshake1 = try service1.initiateHandshake(with: peer2ID)
// Process on peer2 and get response
let handshake2 = try service2.processHandshakeMessage(from: peer1ID, message: handshake1)!
// Process response on peer1
let handshake3 = try service1.processHandshakeMessage(from: peer2ID, message: handshake2)!
// Final message on peer2
let final = try service2.processHandshakeMessage(from: peer1ID, message: handshake3)
XCTAssertNil(final)
// Both should have established sessions
XCTAssertTrue(service1.hasEstablishedSession(with: peer2ID))
XCTAssertTrue(service2.hasEstablishedSession(with: peer1ID))
// Test message encryption
let message = "Secret message".data(using: .utf8)!
let encrypted = try service1.encrypt(message, for: peer2ID)
let decrypted = try service2.decrypt(encrypted, from: peer1ID)
XCTAssertEqual(message, decrypted)
}
func testBidirectionalNoiseSession() throws {
// This test verifies that messages can be sent in both directions after handshake
let aliceKey = Curve25519.KeyAgreement.PrivateKey()
let bobKey = Curve25519.KeyAgreement.PrivateKey()
// Create session managers
let aliceManager = NoiseSessionManager(localStaticKey: aliceKey)
let bobManager = NoiseSessionManager(localStaticKey: bobKey)
// Alice initiates handshake (msg1: -> e)
let msg1 = try aliceManager.initiateHandshake(with: "bob")
XCTAssertFalse(msg1.isEmpty)
// Bob processes and responds (msg2: <- e, ee, s, es)
let msg2 = try bobManager.handleIncomingHandshake(from: "alice", message: msg1)
XCTAssertNotNil(msg2)
XCTAssertFalse(msg2!.isEmpty)
// Alice processes and sends final message (msg3: -> s, se)
let msg3 = try aliceManager.handleIncomingHandshake(from: "bob", message: msg2!)
XCTAssertNotNil(msg3)
XCTAssertFalse(msg3!.isEmpty)
// Bob processes final message
let msg4 = try bobManager.handleIncomingHandshake(from: "alice", message: msg3!)
XCTAssertNil(msg4) // Now handshake is complete
// Verify both sessions are established
XCTAssertTrue(aliceManager.getSession(for: "bob")?.isEstablished() ?? false)
XCTAssertTrue(bobManager.getSession(for: "alice")?.isEstablished() ?? false)
// Test Alice -> Bob
let aliceMessage = "Hello Bob!".data(using: .utf8)!
let encrypted1 = try aliceManager.encrypt(aliceMessage, for: "bob")
let decrypted1 = try bobManager.decrypt(encrypted1, from: "alice")
XCTAssertEqual(decrypted1, aliceMessage)
// Test Bob -> Alice
let bobMessage = "Hello Alice!".data(using: .utf8)!
let encrypted2 = try bobManager.encrypt(bobMessage, for: "alice")
let decrypted2 = try aliceManager.decrypt(encrypted2, from: "bob")
XCTAssertEqual(decrypted2, bobMessage)
// Test multiple messages in both directions
for i in 1...5 {
// Alice -> Bob
let msg = "Message \(i) from Alice".data(using: .utf8)!
let enc = try aliceManager.encrypt(msg, for: "bob")
let dec = try bobManager.decrypt(enc, from: "alice")
XCTAssertEqual(dec, msg)
// Bob -> Alice
let msg2 = "Message \(i) from Bob".data(using: .utf8)!
let enc2 = try bobManager.encrypt(msg2, for: "alice")
let dec2 = try aliceManager.decrypt(enc2, from: "bob")
XCTAssertEqual(dec2, msg2)
}
}
// MARK: - Channel Encryption Tests
func testChannelEncryption() throws {
let channelEnc = NoiseChannelEncryption()
let channel = "#test-channel"
let password = "super-secret-password"
// Set channel password
channelEnc.setChannelPassword(password, for: channel)
// Encrypt message
let message = "Hello channel!"
let encrypted = try channelEnc.encryptChannelMessage(message, for: channel)
// Decrypt message
let decrypted = try channelEnc.decryptChannelMessage(encrypted, for: channel)
XCTAssertEqual(message, decrypted)
}
func testChannelKeyDerivation() {
let channelEnc = NoiseChannelEncryption()
let password = "test-password"
// Same password and channel should produce same key
let key1 = channelEnc.deriveChannelKey(from: password, channel: "#channel1")
let key2 = channelEnc.deriveChannelKey(from: password, channel: "#channel1")
// Different channels should produce different keys
let key3 = channelEnc.deriveChannelKey(from: password, channel: "#channel2")
// Can't directly compare SymmetricKey, but we can test encryption
let testData = "test".data(using: .utf8)!
let nonce = ChaChaPoly.Nonce()
let sealed1 = try! ChaChaPoly.seal(testData, using: key1, nonce: nonce)
let sealed2 = try! ChaChaPoly.seal(testData, using: key2, nonce: nonce)
XCTAssertEqual(sealed1.ciphertext, sealed2.ciphertext)
// Different key should produce different ciphertext
let sealed3 = try! ChaChaPoly.seal(testData, using: key3, nonce: nonce)
XCTAssertNotEqual(sealed1.ciphertext, sealed3.ciphertext)
}
// MARK: - Security Tests
func testHandshakeAuthentication() throws {
let aliceKey = Curve25519.KeyAgreement.PrivateKey()
let bobKey = Curve25519.KeyAgreement.PrivateKey()
let eveKey = Curve25519.KeyAgreement.PrivateKey() // Attacker
var alice = NoiseHandshakeState(role: .initiator, pattern: .XX, localStaticKey: aliceKey)
var eve = NoiseHandshakeState(role: .responder, pattern: .XX, localStaticKey: eveKey)
// Alice initiates handshake thinking she's talking to Bob
let msg1 = try alice.writeMessage()
_ = try eve.readMessage(msg1)
// Eve responds with her keys
let msg2 = try eve.writeMessage()
_ = try alice.readMessage(msg2)
// Alice completes handshake
let msg3 = try alice.writeMessage()
_ = try eve.readMessage(msg3)
// Both complete handshake, but Alice has Eve's public key, not Bob's
let aliceRemoteKey = alice.getRemoteStaticPublicKey()
XCTAssertEqual(aliceRemoteKey?.rawRepresentation, eveKey.publicKey.rawRepresentation)
XCTAssertNotEqual(aliceRemoteKey?.rawRepresentation, bobKey.publicKey.rawRepresentation)
// This demonstrates that authentication requires out-of-band verification
// or pre-shared knowledge of public keys
}
func testReplayProtection() throws {
let key = SymmetricKey(size: .bits256)
let cipher1 = NoiseCipherState(key: key)
let cipher2 = NoiseCipherState(key: key)
let plaintext = "Test".data(using: .utf8)!
// Encrypt a message
let ciphertext = try cipher1.encrypt(plaintext: plaintext)
// Decrypt normally works
_ = try cipher2.decrypt(ciphertext: ciphertext)
// Replaying the same ciphertext should fail due to nonce mismatch
XCTAssertThrowsError(try cipher2.decrypt(ciphertext: ciphertext))
}
}

View File

@ -1,206 +0,0 @@
//
// NoiseRateLimiterTests.swift
// bitchatTests
//
// This is free and unencumbered software released into the public domain.
// For more information, see <https://unlicense.org>
//
import XCTest
@testable import bitchat
class NoiseRateLimiterTests: XCTestCase {
// MARK: - Basic Rate Limiting Tests
func testHandshakeRateLimiting() {
let rateLimiter = NoiseRateLimiter()
let peerID = "test-peer"
// First few handshakes should be allowed
XCTAssertTrue(rateLimiter.allowHandshake(from: peerID))
XCTAssertTrue(rateLimiter.allowHandshake(from: peerID))
XCTAssertTrue(rateLimiter.allowHandshake(from: peerID))
// After hitting limit, should be rate limited
// Default is 3 handshakes per minute
XCTAssertFalse(rateLimiter.allowHandshake(from: peerID))
XCTAssertFalse(rateLimiter.allowHandshake(from: peerID))
}
func testMessageRateLimiting() {
let rateLimiter = NoiseRateLimiter()
let peerID = "test-peer"
// Messages have higher limit (100 per minute default)
for _ in 0..<100 {
XCTAssertTrue(rateLimiter.allowMessage(from: peerID))
}
// 101st message should be rate limited
XCTAssertFalse(rateLimiter.allowMessage(from: peerID))
}
func testPerPeerRateLimiting() {
let rateLimiter = NoiseRateLimiter()
let peer1 = "alice"
let peer2 = "bob"
// Rate limit peer1
XCTAssertTrue(rateLimiter.allowHandshake(from: peer1))
XCTAssertTrue(rateLimiter.allowHandshake(from: peer1))
XCTAssertTrue(rateLimiter.allowHandshake(from: peer1))
XCTAssertFalse(rateLimiter.allowHandshake(from: peer1))
// Peer2 should still be allowed
XCTAssertTrue(rateLimiter.allowHandshake(from: peer2))
XCTAssertTrue(rateLimiter.allowHandshake(from: peer2))
}
// MARK: - Time Window Tests
func testRateLimitResetsAfterWindow() {
let rateLimiter = NoiseRateLimiter()
let peerID = "test-peer"
// Use up the limit
for _ in 0..<3 {
XCTAssertTrue(rateLimiter.allowHandshake(from: peerID))
}
XCTAssertFalse(rateLimiter.allowHandshake(from: peerID))
// Simulate time passing by clearing the window
rateLimiter.clearExpiredEntries()
// Should be allowed again after window expires
// Note: In real implementation, this would require actual time to pass
// For testing, we might need to inject a clock or expose internal state
}
// MARK: - Global Rate Limiting Tests
func testGlobalHandshakeLimit() {
let rateLimiter = NoiseRateLimiter()
// Global limit prevents too many handshakes across all peers
var allowedCount = 0
// Try many handshakes from different peers
for i in 0..<50 {
let peerID = "peer-\(i)"
if rateLimiter.allowHandshake(from: peerID) {
allowedCount += 1
}
}
// Should hit global limit before allowing all 50
XCTAssertLessThan(allowedCount, 50)
XCTAssertGreaterThan(allowedCount, 10) // But should allow reasonable amount
}
// MARK: - Attack Mitigation Tests
func testRapidHandshakeAttackMitigation() {
let rateLimiter = NoiseRateLimiter()
let attackerID = "attacker"
var blockedCount = 0
// Simulate rapid handshake attempts
for _ in 0..<20 {
if !rateLimiter.allowHandshake(from: attackerID) {
blockedCount += 1
}
}
// Most attempts should be blocked
XCTAssertGreaterThan(blockedCount, 15)
}
func testDistributedAttackMitigation() {
let rateLimiter = NoiseRateLimiter()
var blockedCount = 0
// Simulate distributed attack from many IPs
for i in 0..<100 {
let attackerID = "192.168.1.\(i)"
// Each attacker tries multiple times
for _ in 0..<5 {
if !rateLimiter.allowHandshake(from: attackerID) {
blockedCount += 1
}
}
}
// Global rate limiting should kick in
XCTAssertGreaterThan(blockedCount, 0)
}
// MARK: - Memory Management Tests
func testMemoryBoundedTracking() {
let rateLimiter = NoiseRateLimiter()
// Add many different peers
for i in 0..<10000 {
let peerID = "peer-\(i)"
_ = rateLimiter.allowMessage(from: peerID)
}
// Rate limiter should have bounds on memory usage
// Implementation should clean up old entries
rateLimiter.clearExpiredEntries()
// Verify it still functions correctly
XCTAssertTrue(rateLimiter.allowMessage(from: "new-peer"))
}
// MARK: - Configuration Tests
func testCustomRateLimits() {
// Test with custom configuration
let config = NoiseRateLimiter.Configuration(
handshakesPerMinute: 5,
messagesPerMinute: 200,
globalHandshakesPerMinute: 30
)
let rateLimiter = NoiseRateLimiter(configuration: config)
let peerID = "test-peer"
// Should allow up to 5 handshakes
for i in 0..<5 {
XCTAssertTrue(rateLimiter.allowHandshake(from: peerID), "Handshake \(i+1) should be allowed")
}
// 6th should be blocked
XCTAssertFalse(rateLimiter.allowHandshake(from: peerID))
}
// MARK: - Thread Safety Tests
func testConcurrentAccess() {
let rateLimiter = NoiseRateLimiter()
let expectation = self.expectation(description: "Concurrent access")
expectation.expectedFulfillmentCount = 10
// Multiple threads accessing rate limiter
for i in 0..<10 {
DispatchQueue.global().async {
let peerID = "peer-\(i)"
for _ in 0..<100 {
_ = rateLimiter.allowMessage(from: peerID)
}
expectation.fulfill()
}
}
waitForExpectations(timeout: 5) { error in
XCTAssertNil(error)
}
// Verify rate limiter still works
XCTAssertTrue(rateLimiter.allowMessage(from: "final-test"))
}
}

View File

@ -1,507 +0,0 @@
//
// NoiseSecurityTests.swift
// bitchatTests
//
// This is free and unencumbered software released into the public domain.
// For more information, see <https://unlicense.org>
//
import XCTest
import CryptoKit
@testable import bitchat
class NoiseSecurityTests: XCTestCase {
// MARK: - Channel Password Salt Tests
func testChannelPasswordSaltIncludesFingerprint() {
let encryption = NoiseChannelEncryption()
let password = "test-password-123"
let channel = "#secure-channel"
// Derive key without fingerprint
let key1 = encryption.deriveChannelKey(from: password, channel: channel, creatorFingerprint: nil)
// Derive key with fingerprint
let fingerprint = "e36f7993abc123def456789012345678901234567890abcdef1234567890abcd"
let key2 = encryption.deriveChannelKey(from: password, channel: channel, creatorFingerprint: fingerprint)
// Keys should be different due to different salts
XCTAssertNotEqual(key1.withUnsafeBytes { Data($0) }, key2.withUnsafeBytes { Data($0) })
}
func testChannelPasswordDerivationPerformance() {
let encryption = NoiseChannelEncryption()
let password = "test-password-123"
let channel = "#performance-test"
let fingerprint = "e36f7993abc123def456789012345678901234567890abcdef1234567890abcd"
// Measure time for PBKDF2 with 210,000 iterations
measure {
_ = encryption.deriveChannelKey(from: password, channel: channel, creatorFingerprint: fingerprint)
}
// Should complete within reasonable time (< 1 second on modern hardware)
}
func testDifferentChannelsProduceDifferentKeys() {
let encryption = NoiseChannelEncryption()
let password = "same-password"
let fingerprint = "e36f7993abc123def456789012345678901234567890abcdef1234567890abcd"
let key1 = encryption.deriveChannelKey(from: password, channel: "#channel1", creatorFingerprint: fingerprint)
let key2 = encryption.deriveChannelKey(from: password, channel: "#channel2", creatorFingerprint: fingerprint)
// Same password but different channels should produce different keys
XCTAssertNotEqual(key1.withUnsafeBytes { Data($0) }, key2.withUnsafeBytes { Data($0) })
}
// MARK: - Message Padding Tests
func testMessagePaddingAppliedToAllPackets() throws {
// Create a small packet
let packet = BitchatPacket(
type: MessageType.message.rawValue,
senderID: Data("testuser".utf8),
recipientID: nil,
timestamp: UInt64(Date().timeIntervalSince1970 * 1000),
payload: Data("Hello".utf8),
signature: nil,
ttl: 3
)
// Encode packet
guard let encodedData = packet.toBinaryData() else {
XCTFail("Failed to encode packet")
return
}
// Check that size matches one of the standard block sizes
let blockSizes = [256, 512, 1024, 2048]
XCTAssertTrue(blockSizes.contains(encodedData.count) || encodedData.count > 2048,
"Encoded data size \(encodedData.count) doesn't match expected block sizes")
// Decode should work correctly
guard let decodedPacket = BitchatPacket.from(encodedData) else {
XCTFail("Failed to decode packet")
return
}
// Verify decoded content matches original
XCTAssertEqual(decodedPacket.type, packet.type)
XCTAssertEqual(String(data: decodedPacket.payload, encoding: .utf8),
String(data: packet.payload, encoding: .utf8))
}
func testPaddingConsistentAcrossMessages() {
// Create multiple packets with same size payload
let packets: [BitchatPacket] = (0..<5).map { i in
BitchatPacket(
type: MessageType.message.rawValue,
senderID: Data("user\(i)".utf8),
recipientID: nil,
timestamp: UInt64(Date().timeIntervalSince1970 * 1000),
payload: Data("Same size message content here".utf8),
signature: nil,
ttl: 3
)
}
// Encode all packets
let encodedSizes = packets.compactMap { $0.toBinaryData()?.count }
// All should have same padded size
XCTAssertEqual(encodedSizes.count, packets.count)
let firstSize = encodedSizes[0]
XCTAssertTrue(encodedSizes.allSatisfy { $0 == firstSize },
"All packets with similar content should pad to same size")
}
// MARK: - Public Key Validation Tests
func testValidPublicKeyAccepted() throws {
// Generate a valid key
let validKey = Curve25519.KeyAgreement.PrivateKey()
let publicKeyData = validKey.publicKey.rawRepresentation
// Should validate successfully
let validated = try NoiseHandshakeState.validatePublicKey(publicKeyData)
XCTAssertEqual(validated.rawRepresentation, publicKeyData)
}
func testAllZeroKeyRejected() {
let zeroKey = Data(repeating: 0x00, count: 32)
XCTAssertThrowsError(try NoiseHandshakeState.validatePublicKey(zeroKey)) { error in
XCTAssertEqual(error as? NoiseError, NoiseError.invalidPublicKey)
}
}
func testAllOneKeyRejected() {
let oneKey = Data(repeating: 0xFF, count: 32)
XCTAssertThrowsError(try NoiseHandshakeState.validatePublicKey(oneKey)) { error in
XCTAssertEqual(error as? NoiseError, NoiseError.invalidPublicKey)
}
}
func testInvalidKeySizeRejected() {
// Too short
let shortKey = Data(repeating: 0x42, count: 16)
XCTAssertThrowsError(try NoiseHandshakeState.validatePublicKey(shortKey)) { error in
XCTAssertEqual(error as? NoiseError, NoiseError.invalidPublicKey)
}
// Too long
let longKey = Data(repeating: 0x42, count: 64)
XCTAssertThrowsError(try NoiseHandshakeState.validatePublicKey(longKey)) { error in
XCTAssertEqual(error as? NoiseError, NoiseError.invalidPublicKey)
}
}
func testWeakKeyRejected() {
// Known weak Curve25519 key patterns
// Low order points that would result in weak DH
let weakKeys = [
Data([0x01] + Array(repeating: 0x00, count: 31)), // Near zero
Data(Array(repeating: 0x00, count: 31) + [0x01]), // Different pattern
]
for weakKey in weakKeys {
// CryptoKit should reject these during DH operation
if (try? NoiseHandshakeState.validatePublicKey(weakKey)) != nil {
// If key creation succeeds, DH should fail in validation
print("Note: Weak key pattern was not rejected by CryptoKit directly")
}
}
}
// MARK: - Integration Tests
func testSecureHandshakeWithValidation() throws {
// Create two parties
let aliceStatic = Curve25519.KeyAgreement.PrivateKey()
let bobStatic = Curve25519.KeyAgreement.PrivateKey()
var alice = NoiseHandshakeState(role: .initiator, pattern: .XX, localStaticKey: aliceStatic)
var bob = NoiseHandshakeState(role: .responder, pattern: .XX, localStaticKey: bobStatic)
// Perform handshake - validation happens automatically
let msg1 = try alice.writeMessage()
_ = try bob.readMessage(msg1)
let msg2 = try bob.writeMessage()
_ = try alice.readMessage(msg2)
let msg3 = try alice.writeMessage()
_ = try bob.readMessage(msg3)
// Both should complete successfully
XCTAssertTrue(alice.isHandshakeComplete())
XCTAssertTrue(bob.isHandshakeComplete())
}
func testPaddedMessageTransmission() throws {
// Create a packet and encode it
let originalMessage = "Test message for padding"
let packet = BitchatPacket(
type: MessageType.message.rawValue,
senderID: Data("sender123".utf8),
recipientID: Data("recipient".utf8),
timestamp: UInt64(Date().timeIntervalSince1970 * 1000),
payload: Data(originalMessage.utf8),
signature: nil,
ttl: 5
)
// Encode (with padding)
guard let encoded = packet.toBinaryData() else {
XCTFail("Failed to encode")
return
}
// Verify padded size
XCTAssertTrue(encoded.count >= originalMessage.count + 21) // Header + sender + payload
// Decode (removes padding)
guard let decoded = BitchatPacket.from(encoded) else {
XCTFail("Failed to decode")
return
}
// Verify message integrity
XCTAssertEqual(String(data: decoded.payload, encoding: .utf8), originalMessage)
}
// MARK: - Session Rekeying Tests
func testSessionRekeyingTriggered() {
// Create session manager
let localKey = Curve25519.KeyAgreement.PrivateKey()
let sessionManager = NoiseSessionManager(localStaticKey: localKey)
// Create a session
let session = sessionManager.createSession(for: "testPeer", role: .initiator)
// Complete handshake
let remoteKey = Curve25519.KeyAgreement.PrivateKey()
var remoteHandshake = NoiseHandshakeState(role: .responder, pattern: .XX, localStaticKey: remoteKey)
do {
let msg1 = try session.startHandshake()
_ = try remoteHandshake.readMessage(msg1)
let msg2 = try remoteHandshake.writeMessage()
_ = try session.processHandshakeMessage(msg2)
let msg3 = try session.writeMessage()
_ = try remoteHandshake.readMessage(msg3)
XCTAssertTrue(session.isEstablished())
// Get sessions needing rekey (should be empty)
var needsRekey = sessionManager.getSessionsNeedingRekey()
XCTAssertTrue(needsRekey.isEmpty)
// Force the session to need rekeying by manipulating its state
if let secureSession = session as? SecureNoiseSession {
// Set old activity time
let oldTime = Date().addingTimeInterval(-35 * 60)
secureSession.setLastActivityTimeForTesting(oldTime)
// Now check again
needsRekey = sessionManager.getSessionsNeedingRekey()
XCTAssertFalse(needsRekey.isEmpty)
XCTAssertTrue(needsRekey.contains(where: { $0.peerID == "testPeer" && $0.needsRekey }))
}
} catch {
XCTFail("Test failed: \(error)")
}
}
func testRekeyInitiation() {
// Create session manager
let localKey = Curve25519.KeyAgreement.PrivateKey()
let sessionManager = NoiseSessionManager(localStaticKey: localKey)
// Create and establish a session
let session = sessionManager.createSession(for: "testPeer", role: .initiator)
// Complete handshake
let remoteKey = Curve25519.KeyAgreement.PrivateKey()
var remoteHandshake = NoiseHandshakeState(role: .responder, pattern: .XX, localStaticKey: remoteKey)
do {
let msg1 = try session.startHandshake()
_ = try remoteHandshake.readMessage(msg1)
let msg2 = try remoteHandshake.writeMessage()
_ = try session.processHandshakeMessage(msg2)
let msg3 = try session.writeMessage()
_ = try remoteHandshake.readMessage(msg3)
XCTAssertTrue(session.isEstablished())
// Store the old session's remote key
let oldRemoteKey = session.getRemoteStaticPublicKey()
XCTAssertNotNil(oldRemoteKey)
// Initiate rekey
try sessionManager.initiateRekey(for: "testPeer")
// The old session should be removed
let currentSession = sessionManager.getSession(for: "testPeer")
XCTAssertNil(currentSession) // Session removed, waiting for new handshake
} catch {
XCTFail("Test failed: \(error)")
}
}
// MARK: - Integration Tests
func testFullRekeyHandshake() {
// Create encryption service
let alice = NoiseEncryptionService()
let bob = NoiseEncryptionService()
let aliceID = "alice"
let bobID = "bob"
do {
// Initial handshake
let msg1 = try alice.initiateHandshake(with: bobID)
let msg2 = try bob.processHandshakeMessage(from: aliceID, message: msg1)!
_ = try alice.processHandshakeMessage(from: bobID, message: msg2)
// Verify sessions established
XCTAssertTrue(alice.hasEstablishedSession(with: bobID))
XCTAssertTrue(bob.hasEstablishedSession(with: aliceID))
// Exchange some messages
let plaintext1 = "Hello Bob"
let encrypted1 = try alice.encrypt(Data(plaintext1.utf8), for: bobID)
let decrypted1 = try bob.decrypt(encrypted1, from: aliceID)
XCTAssertEqual(String(data: decrypted1, encoding: .utf8), plaintext1)
// Force session to expire by manipulating internal state
// (In real scenario, this would happen after 30 minutes or 1M messages)
// Trigger rekey from Alice's side
var rekeyHandshakeCompleted = false
alice.onHandshakeRequired = { peerID in
XCTAssertEqual(peerID, bobID)
rekeyHandshakeCompleted = true
}
// After rekey, should be able to continue messaging
// Note: In real implementation, the rekey would be triggered automatically
} catch {
XCTFail("Integration test failed: \(error)")
}
}
func testErrorHandlingDuringHandshake() {
let service = NoiseEncryptionService()
// Test invalid peer ID
XCTAssertThrowsError(try service.initiateHandshake(with: "")) { error in
if let securityError = error as? NoiseSecurityError {
XCTAssertEqual(securityError, NoiseSecurityError.invalidPeerID)
}
}
// Test invalid handshake message
XCTAssertThrowsError(try service.processHandshakeMessage(from: "peer", message: Data())) { error in
// Should fail to parse empty data as handshake
}
// Test oversized handshake message
let oversizedMessage = Data(repeating: 0x42, count: 100_000)
XCTAssertThrowsError(try service.processHandshakeMessage(from: "peer", message: oversizedMessage)) { error in
if let securityError = error as? NoiseSecurityError {
XCTAssertEqual(securityError, NoiseSecurityError.messageTooLarge)
}
}
}
func testRateLimitingIntegration() {
let service = NoiseEncryptionService()
let peerID = "rate-limited-peer"
var handshakeAttempts = 0
var rateLimitHit = false
// Try many rapid handshakes
for _ in 0..<10 {
do {
_ = try service.initiateHandshake(with: peerID)
handshakeAttempts += 1
} catch {
if let securityError = error as? NoiseSecurityError,
securityError == NoiseSecurityError.rateLimitExceeded {
rateLimitHit = true
break
}
}
}
// Should hit rate limit before all 10 attempts
XCTAssertTrue(rateLimitHit)
XCTAssertLessThan(handshakeAttempts, 10)
}
func testChannelEncryptionIntegration() {
let service = NoiseEncryptionService()
let channel = "#integration-test"
let password = "test-password"
let fingerprint = service.getIdentityFingerprint()
// Set channel password
service.setChannelPassword(password, for: channel)
// Encrypt channel message
do {
let message = "Channel message test"
let encrypted = try service.encryptChannelMessage(message, for: channel)
// Verify it's encrypted
XCTAssertNotEqual(encrypted, Data(message.utf8))
// Decrypt
let decrypted = try service.decryptChannelMessage(encrypted, for: channel)
XCTAssertEqual(decrypted, message)
// Clean up
service.removeChannelPassword(for: channel)
} catch {
XCTFail("Channel encryption failed: \(error)")
}
}
func testSecureSessionConcurrency() {
let aliceKey = Curve25519.KeyAgreement.PrivateKey()
let bobKey = Curve25519.KeyAgreement.PrivateKey()
let alice = SecureNoiseSession(peerID: "bob", role: .initiator, localStaticKey: aliceKey)
let bob = SecureNoiseSession(peerID: "alice", role: .responder, localStaticKey: bobKey)
// Complete handshake
do {
let msg1 = try alice.startHandshake()
_ = try bob.processHandshakeMessage(msg1)
let msg2 = try bob.writeMessage()
_ = try alice.processHandshakeMessage(msg2)
let msg3 = try alice.writeMessage()
_ = try bob.processHandshakeMessage(msg3)
XCTAssertTrue(alice.isEstablished())
XCTAssertTrue(bob.isEstablished())
// Concurrent encryption/decryption
let expectation = self.expectation(description: "Concurrent operations")
expectation.expectedFulfillmentCount = 20
let queue = DispatchQueue(label: "test.concurrent", attributes: .concurrent)
for i in 0..<10 {
// Encrypt from Alice
queue.async {
do {
let message = "Message \(i) from Alice"
let encrypted = try alice.encrypt(Data(message.utf8))
let decrypted = try bob.decrypt(encrypted)
XCTAssertEqual(String(data: decrypted, encoding: .utf8), message)
expectation.fulfill()
} catch {
XCTFail("Concurrent encrypt failed: \(error)")
}
}
// Encrypt from Bob
queue.async {
do {
let message = "Message \(i) from Bob"
let encrypted = try bob.encrypt(Data(message.utf8))
let decrypted = try alice.decrypt(encrypted)
XCTAssertEqual(String(data: decrypted, encoding: .utf8), message)
expectation.fulfill()
} catch {
XCTFail("Concurrent decrypt failed: \(error)")
}
}
}
waitForExpectations(timeout: 5)
} catch {
XCTFail("Handshake failed: \(error)")
}
}
}

View File

@ -1,191 +0,0 @@
//
// NoiseSecurityValidatorTests.swift
// bitchatTests
//
// This is free and unencumbered software released into the public domain.
// For more information, see <https://unlicense.org>
//
import XCTest
@testable import bitchat
class NoiseSecurityValidatorTests: XCTestCase {
// MARK: - Peer ID Validation Tests
func testValidPeerIDAccepted() {
// Valid peer IDs
XCTAssertTrue(NoiseSecurityValidator.validatePeerID("user123"))
XCTAssertTrue(NoiseSecurityValidator.validatePeerID("alice"))
XCTAssertTrue(NoiseSecurityValidator.validatePeerID("bob_2024"))
XCTAssertTrue(NoiseSecurityValidator.validatePeerID("test-user"))
XCTAssertTrue(NoiseSecurityValidator.validatePeerID("192.168.1.1:8080")) // IP:port format
}
func testInvalidPeerIDRejected() {
// Empty
XCTAssertFalse(NoiseSecurityValidator.validatePeerID(""))
// Too long (over 255 chars)
let longID = String(repeating: "a", count: 256)
XCTAssertFalse(NoiseSecurityValidator.validatePeerID(longID))
// Control characters
XCTAssertFalse(NoiseSecurityValidator.validatePeerID("user\0null"))
XCTAssertFalse(NoiseSecurityValidator.validatePeerID("user\nline"))
XCTAssertFalse(NoiseSecurityValidator.validatePeerID("user\ttab"))
// Path traversal attempts
XCTAssertFalse(NoiseSecurityValidator.validatePeerID("../../../etc/passwd"))
XCTAssertFalse(NoiseSecurityValidator.validatePeerID("user/../admin"))
}
// MARK: - Message Size Validation Tests
func testValidMessageSizeAccepted() {
// Small message
let smallData = Data(repeating: 0x42, count: 100)
XCTAssertTrue(NoiseSecurityValidator.validateMessageSize(smallData))
// Medium message (1MB)
let mediumData = Data(repeating: 0x42, count: 1024 * 1024)
XCTAssertTrue(NoiseSecurityValidator.validateMessageSize(mediumData))
// Just under limit (10MB - 1 byte)
let nearLimitData = Data(repeating: 0x42, count: 10 * 1024 * 1024 - 1)
XCTAssertTrue(NoiseSecurityValidator.validateMessageSize(nearLimitData))
}
func testOversizedMessageRejected() {
// Exactly at limit (10MB)
let limitData = Data(repeating: 0x42, count: 10 * 1024 * 1024)
XCTAssertFalse(NoiseSecurityValidator.validateMessageSize(limitData))
// Over limit
let overData = Data(repeating: 0x42, count: 11 * 1024 * 1024)
XCTAssertFalse(NoiseSecurityValidator.validateMessageSize(overData))
}
func testHandshakeMessageSizeValidation() {
// Valid handshake size
let validHandshake = Data(repeating: 0x42, count: 500)
XCTAssertTrue(NoiseSecurityValidator.validateHandshakeMessageSize(validHandshake))
// Too large for handshake (over 64KB)
let largeHandshake = Data(repeating: 0x42, count: 65 * 1024)
XCTAssertFalse(NoiseSecurityValidator.validateHandshakeMessageSize(largeHandshake))
}
// MARK: - Channel Name Validation Tests
func testValidChannelNameAccepted() {
XCTAssertTrue(NoiseSecurityValidator.validateChannelName("#general"))
XCTAssertTrue(NoiseSecurityValidator.validateChannelName("#test-channel"))
XCTAssertTrue(NoiseSecurityValidator.validateChannelName("#channel_123"))
XCTAssertTrue(NoiseSecurityValidator.validateChannelName("#🎉party"))
XCTAssertTrue(NoiseSecurityValidator.validateChannelName("#2024"))
}
func testInvalidChannelNameRejected() {
// Missing # prefix
XCTAssertFalse(NoiseSecurityValidator.validateChannelName("general"))
// Empty or just #
XCTAssertFalse(NoiseSecurityValidator.validateChannelName(""))
XCTAssertFalse(NoiseSecurityValidator.validateChannelName("#"))
// Too long (over 50 chars)
let longName = "#" + String(repeating: "a", count: 51)
XCTAssertFalse(NoiseSecurityValidator.validateChannelName(longName))
// Invalid characters
XCTAssertFalse(NoiseSecurityValidator.validateChannelName("#channel\nwith\nnewlines"))
XCTAssertFalse(NoiseSecurityValidator.validateChannelName("#../../etc"))
XCTAssertFalse(NoiseSecurityValidator.validateChannelName("#channel<script>"))
}
// MARK: - Encryption Parameters Validation
func testValidateEncryptionNonce() {
// Valid 12-byte nonce for ChaCha20
let validNonce = Data(repeating: 0x42, count: 12)
XCTAssertTrue(NoiseSecurityValidator.validateNonce(validNonce))
// Invalid sizes
let shortNonce = Data(repeating: 0x42, count: 8)
XCTAssertFalse(NoiseSecurityValidator.validateNonce(shortNonce))
let longNonce = Data(repeating: 0x42, count: 16)
XCTAssertFalse(NoiseSecurityValidator.validateNonce(longNonce))
// Empty
XCTAssertFalse(NoiseSecurityValidator.validateNonce(Data()))
}
func testValidateKeyMaterial() {
// Valid 32-byte key
let validKey = Data(repeating: 0x42, count: 32)
XCTAssertTrue(NoiseSecurityValidator.validateKeyMaterial(validKey))
// Invalid sizes
XCTAssertFalse(NoiseSecurityValidator.validateKeyMaterial(Data(repeating: 0x42, count: 16)))
XCTAssertFalse(NoiseSecurityValidator.validateKeyMaterial(Data(repeating: 0x42, count: 64)))
XCTAssertFalse(NoiseSecurityValidator.validateKeyMaterial(Data()))
}
// MARK: - Input Sanitization Tests
func testSanitizePeerID() {
// Normal case
XCTAssertEqual(NoiseSecurityValidator.sanitizePeerID("alice123"), "alice123")
// Remove control characters
XCTAssertEqual(NoiseSecurityValidator.sanitizePeerID("alice\0bob"), "alicebob")
XCTAssertEqual(NoiseSecurityValidator.sanitizePeerID("user\n\r\t"), "user")
// Truncate long IDs
let longID = String(repeating: "a", count: 300)
let sanitized = NoiseSecurityValidator.sanitizePeerID(longID)
XCTAssertEqual(sanitized.count, 255)
// Empty becomes placeholder
XCTAssertEqual(NoiseSecurityValidator.sanitizePeerID(""), "unknown")
}
func testSanitizeChannelName() {
// Normal case
XCTAssertEqual(NoiseSecurityValidator.sanitizeChannelName("#general"), "#general")
// Add # prefix if missing
XCTAssertEqual(NoiseSecurityValidator.sanitizeChannelName("general"), "#general")
// Remove invalid characters
XCTAssertEqual(NoiseSecurityValidator.sanitizeChannelName("#test\nchannel"), "#testchannel")
// Truncate long names
let longName = String(repeating: "a", count: 100)
let sanitized = NoiseSecurityValidator.sanitizeChannelName(longName)
XCTAssertTrue(sanitized.hasPrefix("#"))
XCTAssertLessThanOrEqual(sanitized.count, 50)
}
// MARK: - Security Pattern Detection Tests
func testDetectSuspiciousPatterns() {
// Path traversal
XCTAssertTrue(NoiseSecurityValidator.containsSuspiciousPattern("../../../etc/passwd"))
XCTAssertTrue(NoiseSecurityValidator.containsSuspiciousPattern("..\\..\\windows\\system32"))
// Script injection
XCTAssertTrue(NoiseSecurityValidator.containsSuspiciousPattern("<script>alert('xss')</script>"))
XCTAssertTrue(NoiseSecurityValidator.containsSuspiciousPattern("javascript:void(0)"))
// SQL injection patterns
XCTAssertTrue(NoiseSecurityValidator.containsSuspiciousPattern("'; DROP TABLE users; --"))
XCTAssertTrue(NoiseSecurityValidator.containsSuspiciousPattern("1' OR '1'='1"))
// Normal text should pass
XCTAssertFalse(NoiseSecurityValidator.containsSuspiciousPattern("Hello, this is a normal message!"))
XCTAssertFalse(NoiseSecurityValidator.containsSuspiciousPattern("Meeting at 3:00 PM"))
}
}

View File

@ -1,437 +0,0 @@
//
// PasswordProtectedChannelTests.swift
// bitchatTests
//
// This is free and unencumbered software released into the public domain.
// For more information, see <https://unlicense.org>
//
import XCTest
import CryptoKit
import CommonCrypto
@testable import bitchat
class PasswordProtectedChannelTests: XCTestCase {
var viewModel: ChatViewModel!
override func setUp() {
super.setUp()
// Clear UserDefaults to ensure test isolation
clearAllUserDefaults()
// Create a fresh view model for each test
viewModel = ChatViewModel()
// Ensure clean state
viewModel.passwordProtectedChannels.removeAll()
viewModel.channelCreators.removeAll()
viewModel.channelPasswords.removeAll()
viewModel.channelKeys.removeAll()
viewModel.joinedChannels.removeAll()
viewModel.channelMembers.removeAll()
viewModel.channelMessages.removeAll()
}
private func clearAllUserDefaults() {
let defaults = UserDefaults.standard
defaults.removeObject(forKey: "bitchat_nickname")
defaults.removeObject(forKey: "bitchat_joined_channels")
defaults.removeObject(forKey: "bitchat_password_protected_channels")
defaults.removeObject(forKey: "bitchat_channel_creators")
defaults.removeObject(forKey: "bitchat_channel_passwords")
defaults.removeObject(forKey: "bitchat_favorite_peers")
defaults.synchronize()
}
override func tearDown() {
// Clean up after tests
clearAllUserDefaults()
viewModel = nil
super.tearDown()
}
// MARK: - Password Key Derivation Tests
func testPasswordKeyDerivation() {
// Same password and channel should always produce same key
let password = "secretPassword123"
let channelName = "#testchannel"
let key1 = deriveChannelKey(from: password, channelName: channelName)
let key2 = deriveChannelKey(from: password, channelName: channelName)
// Keys should be identical
XCTAssertEqual(key1, key2, "Same password and channel should produce same key")
}
func testDifferentPasswordsProduceDifferentKeys() {
let channelName = "#testchannel"
let password1 = "password123"
let password2 = "different456"
let key1 = deriveChannelKey(from: password1, channelName: channelName)
let key2 = deriveChannelKey(from: password2, channelName: channelName)
XCTAssertNotEqual(key1, key2, "Different passwords should produce different keys")
}
func testDifferentChannelsProduceDifferentKeys() {
let password = "samePassword"
let channel1 = "#channel1"
let channel2 = "#channel2"
let key1 = deriveChannelKey(from: password, channelName: channel1)
let key2 = deriveChannelKey(from: password, channelName: channel2)
XCTAssertNotEqual(key1, key2, "Same password in different channels should produce different keys")
}
// MARK: - Channel Creation and Joining Tests
func testJoinUnprotectedChannel() {
let channelName = "#public"
let success = viewModel.joinChannel(channelName)
XCTAssertTrue(success, "Should be able to join unprotected channel")
XCTAssertTrue(viewModel.joinedChannels.contains(channelName))
XCTAssertEqual(viewModel.currentChannel, channelName)
XCTAssertTrue(viewModel.channelMembers[channelName]?.contains(viewModel.meshService.myPeerID) ?? false)
}
func testCreatePasswordProtectedChannel() {
let channelName = "#private"
let password = "secret123"
// Join channel first
let joinSuccess = viewModel.joinChannel(channelName)
XCTAssertTrue(joinSuccess)
// Set password
viewModel.setChannelPassword(password, for: channelName)
XCTAssertTrue(viewModel.passwordProtectedChannels.contains(channelName))
XCTAssertNotNil(viewModel.channelKeys[channelName])
XCTAssertEqual(viewModel.channelPasswords[channelName], password)
XCTAssertEqual(viewModel.channelCreators[channelName], viewModel.meshService.myPeerID)
}
func testJoinPasswordProtectedEmptyChannel() {
let channelName = "#protected"
let password = "test123"
// Simulate channel being marked as password protected
viewModel.passwordProtectedChannels.insert(channelName)
// Try to join with password - should be accepted tentatively for empty channel
let success = viewModel.joinChannel(channelName, password: password)
XCTAssertTrue(success, "Should accept tentative access to empty password-protected channel")
XCTAssertNotNil(viewModel.channelKeys[channelName], "Should store key tentatively")
XCTAssertEqual(viewModel.channelPasswords[channelName], password, "Should store password tentatively")
// Should have a system message explaining tentative access
let hasSystemMessage = viewModel.messages.contains { $0.sender == "system" && $0.content.contains("waiting for encrypted messages to verify password") }
XCTAssertTrue(hasSystemMessage, "Should add system message explaining tentative access")
}
func testJoinPasswordProtectedChannelWithMessages() {
let channelName = "#secure"
let correctPassword = "correct123"
let wrongPassword = "wrong456"
let testMessage = "Test encrypted message"
// First, create the channel and set password as creator
let _ = viewModel.joinChannel(channelName)
viewModel.setChannelPassword(correctPassword, for: channelName)
// Simulate an encrypted message in the channel
let key = viewModel.channelKeys[channelName]!
guard let messageData = testMessage.data(using: .utf8) else {
XCTFail("Failed to convert message to data")
return
}
do {
let sealedBox = try AES.GCM.seal(messageData, using: key)
let encryptedData = sealedBox.combined!
let encryptedMsg = BitchatMessage(
sender: "alice",
content: "[Encrypted message - password required]",
timestamp: Date(),
isRelay: false,
originalSender: nil,
isPrivate: false,
recipientNickname: nil,
senderPeerID: "alice123",
mentions: nil,
channel: channelName,
encryptedContent: encryptedData,
isEncrypted: true
)
// Add to channel messages
viewModel.channelMessages[channelName] = [encryptedMsg]
// Clear keys to simulate another user
viewModel.channelKeys.removeValue(forKey: channelName)
viewModel.channelPasswords.removeValue(forKey: channelName)
// Try to join with wrong password
let wrongSuccess = viewModel.joinChannel(channelName, password: wrongPassword)
XCTAssertFalse(wrongSuccess, "Should reject wrong password")
// Try to join with correct password
let correctSuccess = viewModel.joinChannel(channelName, password: correctPassword)
XCTAssertTrue(correctSuccess, "Should accept correct password")
XCTAssertNotNil(viewModel.channelKeys[channelName], "Should store key for correct password")
} catch {
XCTFail("Encryption failed: \(error)")
}
}
// MARK: - Password Verification Tests
func testEncryptDecryptChannelMessage() {
let channelName = "#crypto"
let password = "cryptoKey"
let testMessage = "This is a secret message"
// Derive key
let key = deriveChannelKey(from: password, channelName: channelName)
// Encrypt
guard let messageData = testMessage.data(using: .utf8) else {
XCTFail("Failed to convert message to data")
return
}
do {
let sealedBox = try AES.GCM.seal(messageData, using: key)
let encryptedData = sealedBox.combined!
// Store key and decrypt
viewModel.channelKeys[channelName] = key
let decrypted = viewModel.decryptChannelMessage(encryptedData, channel: channelName)
XCTAssertEqual(decrypted, testMessage, "Decrypted message should match original")
} catch {
XCTFail("Encryption failed: \(error)")
}
}
func testWrongPasswordFailsDecryption() {
let channelName = "#secure"
let correctPassword = "correct"
let wrongPassword = "wrong"
let testMessage = "Secret content"
// Encrypt with correct password
let correctKey = deriveChannelKey(from: correctPassword, channelName: channelName)
guard let messageData = testMessage.data(using: .utf8) else {
XCTFail("Failed to convert message to data")
return
}
do {
let sealedBox = try AES.GCM.seal(messageData, using: correctKey)
let encryptedData = sealedBox.combined!
// Try to decrypt with wrong password
let wrongKey = deriveChannelKey(from: wrongPassword, channelName: channelName)
let decrypted = viewModel.decryptChannelMessage(encryptedData, channel: channelName, testKey: wrongKey)
XCTAssertNil(decrypted, "Wrong password should fail to decrypt")
} catch {
XCTFail("Encryption failed: \(error)")
}
}
// MARK: - Channel Creator Tests
func testOnlyCreatorCanSetPassword() {
let channelName = "#owned"
let password = "ownerOnly"
// Join channel (becomes creator)
let _ = viewModel.joinChannel(channelName)
// Set password as creator
viewModel.setChannelPassword(password, for: channelName)
XCTAssertTrue(viewModel.passwordProtectedChannels.contains(channelName))
// Simulate another user trying to set password
viewModel.channelCreators[channelName] = "otherUser123"
viewModel.setChannelPassword("hackerPassword", for: channelName)
// Password should not change
XCTAssertEqual(viewModel.channelPasswords[channelName], password, "Non-creator should not be able to change password")
}
func testCreatorCanRemovePassword() {
let channelName = "#changeable"
let password = "temporary"
// Create protected channel
let _ = viewModel.joinChannel(channelName)
viewModel.setChannelPassword(password, for: channelName)
XCTAssertTrue(viewModel.passwordProtectedChannels.contains(channelName))
// Remove password
viewModel.removeChannelPassword(for: channelName)
XCTAssertFalse(viewModel.passwordProtectedChannels.contains(channelName))
XCTAssertNil(viewModel.channelKeys[channelName])
XCTAssertNil(viewModel.channelPasswords[channelName])
}
// MARK: - Message Handling Tests
func testReceiveEncryptedMessageWithoutKey() {
let channelName = "#encrypted"
// Join channel without password
let _ = viewModel.joinChannel(channelName)
// Simulate receiving encrypted message
let encryptedMessage = BitchatMessage(
sender: "alice",
content: "[Encrypted message - password required]",
timestamp: Date(),
isRelay: false,
originalSender: nil,
isPrivate: false,
recipientNickname: nil,
senderPeerID: "alice123",
mentions: nil,
channel: channelName,
encryptedContent: Data([1, 2, 3, 4]), // dummy encrypted data
isEncrypted: true
)
viewModel.didReceiveMessage(encryptedMessage)
// Should mark channel as password protected
XCTAssertTrue(viewModel.passwordProtectedChannels.contains(channelName))
// Should add system message
let channelMessages = viewModel.channelMessages[channelName] ?? []
let hasSystemMessage = channelMessages.contains { $0.sender == "system" && $0.content.contains("password protected") }
XCTAssertTrue(hasSystemMessage, "Should add system message about password protection")
}
// MARK: - Command Tests
func testJoinCommand() {
let input = "/join #testchannel"
viewModel.sendMessage(input)
XCTAssertTrue(viewModel.joinedChannels.contains("#testchannel"))
XCTAssertEqual(viewModel.currentChannel, "#testchannel")
}
func testJoinCommandAlias() {
let input = "/j #quick"
viewModel.sendMessage(input)
XCTAssertTrue(viewModel.joinedChannels.contains("#quick"))
XCTAssertEqual(viewModel.currentChannel, "#quick")
}
func testInvalidChannelName() {
let input = "/j #invalid-channel!"
viewModel.sendMessage(input)
XCTAssertFalse(viewModel.joinedChannels.contains("#invalid-channel!"))
// Should have system message about invalid name
let hasErrorMessage = viewModel.messages.contains { $0.sender == "system" && $0.content.contains("invalid channel name") }
XCTAssertTrue(hasErrorMessage)
}
// MARK: - Key Commitment Tests
func testKeyCommitmentVerification() {
let channelName = "#commitment"
let password = "testpass123"
// Join and set password
let _ = viewModel.joinChannel(channelName)
viewModel.setChannelPassword(password, for: channelName)
// Verify key commitment was stored
XCTAssertNotNil(viewModel.channelKeyCommitments[channelName], "Should store key commitment")
// Simulate another user with the stored commitment
let commitment = viewModel.channelKeyCommitments[channelName]!
viewModel.channelKeys.removeValue(forKey: channelName)
viewModel.channelPasswords.removeValue(forKey: channelName)
// Manually set the commitment as if received from network
viewModel.channelKeyCommitments[channelName] = commitment
// Try with wrong password - should fail immediately
let wrongSuccess = viewModel.joinChannel(channelName, password: "wrongpass")
XCTAssertFalse(wrongSuccess, "Should reject wrong password via commitment check")
// Try with correct password - should succeed
let correctSuccess = viewModel.joinChannel(channelName, password: password)
XCTAssertTrue(correctSuccess, "Should accept correct password via commitment check")
}
func testOwnershipTransfer() {
let channelName = "#transfertest"
let password = "ownerpass"
// Create channel and set password
let _ = viewModel.joinChannel(channelName)
viewModel.setChannelPassword(password, for: channelName)
// Verify creator is set
XCTAssertEqual(viewModel.channelCreators[channelName], viewModel.meshService.myPeerID)
// Simulate transfer (in real app would use /transfer command)
let newOwnerID = "newowner123"
viewModel.channelCreators[channelName] = newOwnerID
// Verify ownership changed
XCTAssertEqual(viewModel.channelCreators[channelName], newOwnerID)
XCTAssertNotEqual(viewModel.channelCreators[channelName], viewModel.meshService.myPeerID)
}
}
// MARK: - Helper Extensions for Testing
extension PasswordProtectedChannelTests {
// Helper method to derive channel key for testing
// This duplicates the logic from ChatViewModel for testing purposes
func deriveChannelKey(from password: String, channelName: String) -> SymmetricKey {
let salt = channelName.data(using: .utf8)!
let iterations = 100000
let keyLength = 32
var derivedKey = Data(count: keyLength)
let passwordData = password.data(using: .utf8)!
_ = derivedKey.withUnsafeMutableBytes { derivedKeyBytes in
salt.withUnsafeBytes { saltBytes in
passwordData.withUnsafeBytes { passwordBytes in
CCKeyDerivationPBKDF(
CCPBKDFAlgorithm(kCCPBKDF2),
passwordBytes.baseAddress, passwordData.count,
saltBytes.baseAddress, salt.count,
CCPseudoRandomAlgorithm(kCCPRFHmacAlgSHA256),
UInt32(iterations),
derivedKeyBytes.baseAddress, keyLength
)
}
}
}
return SymmetricKey(data: derivedKey)
}
}

View File

@ -1,357 +0,0 @@
//
// ProtocolVersionNegotiationTests.swift
// bitchatTests
//
// This is free and unencumbered software released into the public domain.
// For more information, see <https://unlicense.org>
//
import XCTest
@testable import bitchat
class ProtocolVersionNegotiationTests: XCTestCase {
// MARK: - VersionHello Tests
func testVersionHelloEncodingDecoding() {
let hello = VersionHello(
supportedVersions: [1, 2, 3],
preferredVersion: 3,
clientVersion: "1.2.3",
platform: "iOS",
capabilities: ["noise", "compression"]
)
// Encode
guard let encoded = hello.encode() else {
XCTFail("Failed to encode VersionHello")
return
}
// Decode
guard let decoded = VersionHello.decode(from: encoded) else {
XCTFail("Failed to decode VersionHello")
return
}
// Verify
XCTAssertEqual(decoded.supportedVersions, hello.supportedVersions)
XCTAssertEqual(decoded.preferredVersion, hello.preferredVersion)
XCTAssertEqual(decoded.clientVersion, hello.clientVersion)
XCTAssertEqual(decoded.platform, hello.platform)
XCTAssertEqual(decoded.capabilities, hello.capabilities)
}
func testVersionHelloDefaults() {
let hello = VersionHello(
clientVersion: "1.0.0",
platform: "macOS"
)
XCTAssertEqual(hello.supportedVersions, Array(ProtocolVersion.supportedVersions))
XCTAssertEqual(hello.preferredVersion, ProtocolVersion.current)
XCTAssertNil(hello.capabilities)
}
// MARK: - VersionAck Tests
func testVersionAckEncodingDecoding() {
let ack = VersionAck(
agreedVersion: 2,
serverVersion: "1.1.0",
platform: "iOS",
capabilities: ["noise"],
rejected: false,
reason: nil
)
// Encode
guard let encoded = ack.encode() else {
XCTFail("Failed to encode VersionAck")
return
}
// Decode
guard let decoded = VersionAck.decode(from: encoded) else {
XCTFail("Failed to decode VersionAck")
return
}
// Verify
XCTAssertEqual(decoded.agreedVersion, ack.agreedVersion)
XCTAssertEqual(decoded.serverVersion, ack.serverVersion)
XCTAssertEqual(decoded.platform, ack.platform)
XCTAssertEqual(decoded.capabilities, ack.capabilities)
XCTAssertEqual(decoded.rejected, ack.rejected)
XCTAssertNil(decoded.reason)
}
func testVersionAckRejection() {
let ack = VersionAck(
agreedVersion: 0,
serverVersion: "2.0.0",
platform: "macOS",
rejected: true,
reason: "No compatible version found"
)
guard let encoded = ack.encode(),
let decoded = VersionAck.decode(from: encoded) else {
XCTFail("Failed to encode/decode rejection VersionAck")
return
}
XCTAssertTrue(decoded.rejected)
XCTAssertEqual(decoded.reason, "No compatible version found")
XCTAssertEqual(decoded.agreedVersion, 0)
}
// MARK: - ProtocolVersion Tests
func testIsSupported() {
XCTAssertTrue(ProtocolVersion.isSupported(1))
XCTAssertFalse(ProtocolVersion.isSupported(99))
XCTAssertFalse(ProtocolVersion.isSupported(0))
}
func testVersionNegotiation() {
// Test successful negotiation
let clientVersions: [UInt8] = [1, 2, 3]
let serverVersions: [UInt8] = [1, 3, 4]
let agreed = ProtocolVersion.negotiateVersion(
clientVersions: clientVersions,
serverVersions: serverVersions
)
XCTAssertEqual(agreed, 3) // Should pick highest common version
}
func testVersionNegotiationNoCommon() {
// Test no common version
let clientVersions: [UInt8] = [2, 3]
let serverVersions: [UInt8] = [4, 5]
let agreed = ProtocolVersion.negotiateVersion(
clientVersions: clientVersions,
serverVersions: serverVersions
)
XCTAssertNil(agreed)
}
func testVersionNegotiationSingleCommon() {
// Test single common version
let clientVersions: [UInt8] = [1]
let serverVersions: [UInt8] = [1, 2, 3]
let agreed = ProtocolVersion.negotiateVersion(
clientVersions: clientVersions,
serverVersions: serverVersions
)
XCTAssertEqual(agreed, 1)
}
func testVersionNegotiationEmpty() {
// Test empty version lists
let agreed1 = ProtocolVersion.negotiateVersion(
clientVersions: [],
serverVersions: [1, 2]
)
XCTAssertNil(agreed1)
let agreed2 = ProtocolVersion.negotiateVersion(
clientVersions: [1, 2],
serverVersions: []
)
XCTAssertNil(agreed2)
}
// MARK: - Binary Protocol Integration Tests
func testVersionHelloPacketEncoding() {
let hello = VersionHello(
clientVersion: "1.0.0",
platform: "iOS"
)
guard let helloData = hello.encode() else {
XCTFail("Failed to encode VersionHello")
return
}
let packet = BitchatPacket(
type: MessageType.versionHello.rawValue,
ttl: 1,
senderID: "testpeer",
payload: helloData
)
guard let encoded = packet.toBinaryData() else {
XCTFail("Failed to encode packet")
return
}
guard let decoded = BitchatPacket.from(encoded) else {
XCTFail("Failed to decode packet")
return
}
XCTAssertEqual(decoded.type, MessageType.versionHello.rawValue)
XCTAssertEqual(decoded.ttl, 1)
// Verify payload can be decoded back to VersionHello
guard let decodedHello = VersionHello.decode(from: decoded.payload) else {
XCTFail("Failed to decode VersionHello from packet payload")
return
}
XCTAssertEqual(decodedHello.clientVersion, "1.0.0")
XCTAssertEqual(decodedHello.platform, "iOS")
}
func testVersionAckPacketEncoding() {
let ack = VersionAck(
agreedVersion: 1,
serverVersion: "1.0.0",
platform: "macOS"
)
guard let ackData = ack.encode() else {
XCTFail("Failed to encode VersionAck")
return
}
let packet = BitchatPacket(
type: MessageType.versionAck.rawValue,
senderID: Data("sender".utf8),
recipientID: Data("recipient".utf8),
timestamp: UInt64(Date().timeIntervalSince1970 * 1000),
payload: ackData,
signature: nil,
ttl: 1
)
guard let encoded = packet.toBinaryData() else {
XCTFail("Failed to encode packet")
return
}
guard let decoded = BitchatPacket.from(encoded) else {
XCTFail("Failed to decode packet")
return
}
XCTAssertEqual(decoded.type, MessageType.versionAck.rawValue)
// Verify payload can be decoded back to VersionAck
guard let decodedAck = VersionAck.decode(from: decoded.payload) else {
XCTFail("Failed to decode VersionAck from packet payload")
return
}
XCTAssertEqual(decodedAck.agreedVersion, 1)
XCTAssertEqual(decodedAck.serverVersion, "1.0.0")
XCTAssertEqual(decodedAck.platform, "macOS")
}
// MARK: - Version State Management Tests
func testVersionNegotiationStateTransitions() {
var state = VersionNegotiationState.none
// Test transition to helloSent
state = .helloSent
if case .helloSent = state {
// Success
} else {
XCTFail("State should be helloSent")
}
// Test transition to ackReceived
state = .ackReceived(version: 2)
if case .ackReceived(let version) = state {
XCTAssertEqual(version, 2)
} else {
XCTFail("State should be ackReceived")
}
// Test transition to failed
state = .failed(reason: "Version mismatch")
if case .failed(let reason) = state {
XCTAssertEqual(reason, "Version mismatch")
} else {
XCTFail("State should be failed")
}
}
// MARK: - Edge Cases
func testLargeVersionNumbers() {
let hello = VersionHello(
supportedVersions: [1, 127, 255],
preferredVersion: 255,
clientVersion: "99.99.99",
platform: "iOS"
)
guard let encoded = hello.encode(),
let decoded = VersionHello.decode(from: encoded) else {
XCTFail("Failed to encode/decode with large version numbers")
return
}
XCTAssertEqual(decoded.supportedVersions, [1, 127, 255])
XCTAssertEqual(decoded.preferredVersion, 255)
}
func testEmptyCapabilities() {
let hello = VersionHello(
clientVersion: "1.0.0",
platform: "iOS",
capabilities: []
)
guard let encoded = hello.encode(),
let decoded = VersionHello.decode(from: encoded) else {
XCTFail("Failed to encode/decode with empty capabilities")
return
}
XCTAssertEqual(decoded.capabilities, [])
}
func testLongCapabilityStrings() {
let longCapability = String(repeating: "a", count: 1000)
let hello = VersionHello(
clientVersion: "1.0.0",
platform: "iOS",
capabilities: [longCapability, "normal"]
)
guard let encoded = hello.encode(),
let decoded = VersionHello.decode(from: encoded) else {
XCTFail("Failed to encode/decode with long capability strings")
return
}
XCTAssertEqual(decoded.capabilities?.count, 2)
XCTAssertEqual(decoded.capabilities?[0], longCapability)
XCTAssertEqual(decoded.capabilities?[1], "normal")
}
func testInvalidJSON() {
let invalidData = Data("not json".utf8)
XCTAssertNil(VersionHello.decode(from: invalidData))
XCTAssertNil(VersionAck.decode(from: invalidData))
}
func testEmptyData() {
let emptyData = Data()
XCTAssertNil(VersionHello.decode(from: emptyData))
XCTAssertNil(VersionAck.decode(from: emptyData))
}
}

View File

@ -1,287 +0,0 @@
//
// SecureNoiseSessionTests.swift
// bitchatTests
//
// This is free and unencumbered software released into the public domain.
// For more information, see <https://unlicense.org>
//
import XCTest
import CryptoKit
@testable import bitchat
class SecureNoiseSessionTests: XCTestCase {
// MARK: - Session Timeout Tests
func testSessionTimesOutAfter30Minutes() {
let aliceKey = Curve25519.KeyAgreement.PrivateKey()
let session = SecureNoiseSession(peerID: "bob", role: .initiator, localStaticKey: aliceKey)
// Complete handshake
let bobKey = Curve25519.KeyAgreement.PrivateKey()
var bob = NoiseHandshakeState(role: .responder, pattern: .XX, localStaticKey: bobKey)
// Perform handshake
do {
let msg1 = try session.startHandshake()
_ = try bob.readMessage(msg1)
let msg2 = try bob.writeMessage()
_ = try session.processHandshakeMessage(msg2)
let msg3 = try session.writeMessage()
_ = try bob.readMessage(msg3)
XCTAssertTrue(session.isEstablished())
// Check initial state
XCTAssertFalse(session.needsRenegotiation())
// Fast-forward time by setting lastActivity to 31 minutes ago
let thirtyOneMinutesAgo = Date().addingTimeInterval(-31 * 60)
session.setLastActivityTimeForTesting(thirtyOneMinutesAgo)
// Should now need renegotiation
XCTAssertTrue(session.needsRenegotiation())
} catch {
XCTFail("Handshake failed: \(error)")
}
}
func testSessionRemainsValidUnder30Minutes() {
let aliceKey = Curve25519.KeyAgreement.PrivateKey()
let session = SecureNoiseSession(peerID: "bob", role: .initiator, localStaticKey: aliceKey)
// Complete handshake
let bobKey = Curve25519.KeyAgreement.PrivateKey()
var bob = NoiseHandshakeState(role: .responder, pattern: .XX, localStaticKey: bobKey)
do {
let msg1 = try session.startHandshake()
_ = try bob.readMessage(msg1)
let msg2 = try bob.writeMessage()
_ = try session.processHandshakeMessage(msg2)
let msg3 = try session.writeMessage()
_ = try bob.readMessage(msg3)
XCTAssertTrue(session.isEstablished())
// Set lastActivity to 29 minutes ago
let twentyNineMinutesAgo = Date().addingTimeInterval(-29 * 60)
session.setLastActivityTimeForTesting(twentyNineMinutesAgo)
// Should NOT need renegotiation
XCTAssertFalse(session.needsRenegotiation())
} catch {
XCTFail("Handshake failed: \(error)")
}
}
// MARK: - Message Count Limit Tests
func testSessionNeedsRekeyAfterMessageLimit() {
let aliceKey = Curve25519.KeyAgreement.PrivateKey()
let bobKey = Curve25519.KeyAgreement.PrivateKey()
let alice = SecureNoiseSession(peerID: "bob", role: .initiator, localStaticKey: aliceKey)
let bob = SecureNoiseSession(peerID: "alice", role: .responder, localStaticKey: bobKey)
// Complete handshake
do {
let msg1 = try alice.startHandshake()
_ = try bob.processHandshakeMessage(msg1)
let msg2 = try bob.writeMessage()
_ = try alice.processHandshakeMessage(msg2)
let msg3 = try alice.writeMessage()
_ = try bob.processHandshakeMessage(msg3)
XCTAssertTrue(alice.isEstablished())
XCTAssertTrue(bob.isEstablished())
// Check initial state
XCTAssertFalse(alice.needsRenegotiation())
// Set message count to just under 90% threshold (900,000)
alice.setMessageCountForTesting(899_999)
XCTAssertFalse(alice.needsRenegotiation())
// Set message count to 90% threshold
alice.setMessageCountForTesting(900_000)
XCTAssertTrue(alice.needsRenegotiation())
} catch {
XCTFail("Handshake failed: \(error)")
}
}
// MARK: - Activity Tracking Tests
func testActivityUpdatesOnEncryption() {
let aliceKey = Curve25519.KeyAgreement.PrivateKey()
let bobKey = Curve25519.KeyAgreement.PrivateKey()
let alice = SecureNoiseSession(peerID: "bob", role: .initiator, localStaticKey: aliceKey)
let bob = SecureNoiseSession(peerID: "alice", role: .responder, localStaticKey: bobKey)
// Complete handshake
do {
let msg1 = try alice.startHandshake()
_ = try bob.processHandshakeMessage(msg1)
let msg2 = try bob.writeMessage()
_ = try alice.processHandshakeMessage(msg2)
let msg3 = try alice.writeMessage()
_ = try bob.processHandshakeMessage(msg3)
// Set lastActivity to 5 minutes ago
let fiveMinutesAgo = Date().addingTimeInterval(-5 * 60)
alice.setLastActivityTimeForTesting(fiveMinutesAgo)
// Encrypt a message
let plaintext = Data("Hello Bob".utf8)
_ = try alice.encrypt(plaintext)
// Activity should be updated to now
let timeSinceUpdate = Date().timeIntervalSince(alice.lastActivityTime)
XCTAssertLessThan(timeSinceUpdate, 1.0) // Should be within 1 second
} catch {
XCTFail("Test failed: \(error)")
}
}
func testActivityUpdatesOnDecryption() {
let aliceKey = Curve25519.KeyAgreement.PrivateKey()
let bobKey = Curve25519.KeyAgreement.PrivateKey()
let alice = SecureNoiseSession(peerID: "bob", role: .initiator, localStaticKey: aliceKey)
let bob = SecureNoiseSession(peerID: "alice", role: .responder, localStaticKey: bobKey)
// Complete handshake
do {
let msg1 = try alice.startHandshake()
_ = try bob.processHandshakeMessage(msg1)
let msg2 = try bob.writeMessage()
_ = try alice.processHandshakeMessage(msg2)
let msg3 = try alice.writeMessage()
_ = try bob.processHandshakeMessage(msg3)
// Encrypt a message from Alice
let plaintext = Data("Hello Bob".utf8)
let ciphertext = try alice.encrypt(plaintext)
// Set Bob's lastActivity to 5 minutes ago
let fiveMinutesAgo = Date().addingTimeInterval(-5 * 60)
bob.setLastActivityTimeForTesting(fiveMinutesAgo)
// Decrypt the message
_ = try bob.decrypt(ciphertext)
// Activity should be updated to now
let timeSinceUpdate = Date().timeIntervalSince(bob.lastActivityTime)
XCTAssertLessThan(timeSinceUpdate, 1.0) // Should be within 1 second
} catch {
XCTFail("Test failed: \(error)")
}
}
// MARK: - Message Count Tracking Tests
func testMessageCountIncrements() {
let aliceKey = Curve25519.KeyAgreement.PrivateKey()
let bobKey = Curve25519.KeyAgreement.PrivateKey()
let alice = SecureNoiseSession(peerID: "bob", role: .initiator, localStaticKey: aliceKey)
let bob = SecureNoiseSession(peerID: "alice", role: .responder, localStaticKey: bobKey)
// Complete handshake
do {
let msg1 = try alice.startHandshake()
_ = try bob.processHandshakeMessage(msg1)
let msg2 = try bob.writeMessage()
_ = try alice.processHandshakeMessage(msg2)
let msg3 = try alice.writeMessage()
_ = try bob.processHandshakeMessage(msg3)
// Check initial message count
XCTAssertEqual(alice.messageCount, 0)
// Send multiple messages
for i in 1...5 {
let plaintext = Data("Message \(i)".utf8)
let ciphertext = try alice.encrypt(plaintext)
_ = try bob.decrypt(ciphertext)
}
// Check message count incremented
XCTAssertEqual(alice.messageCount, 5) // Alice sent 5 messages
XCTAssertEqual(bob.messageCount, 0) // Bob received but didn't send
} catch {
XCTFail("Test failed: \(error)")
}
}
// MARK: - Integration Tests
func testFullSessionLifecycle() {
let aliceKey = Curve25519.KeyAgreement.PrivateKey()
let bobKey = Curve25519.KeyAgreement.PrivateKey()
let alice = SecureNoiseSession(peerID: "bob", role: .initiator, localStaticKey: aliceKey)
let bob = SecureNoiseSession(peerID: "alice", role: .responder, localStaticKey: bobKey)
do {
// 1. Perform handshake
let msg1 = try alice.startHandshake()
_ = try bob.processHandshakeMessage(msg1)
let msg2 = try bob.writeMessage()
_ = try alice.processHandshakeMessage(msg2)
let msg3 = try alice.writeMessage()
_ = try bob.processHandshakeMessage(msg3)
XCTAssertTrue(alice.isEstablished())
XCTAssertTrue(bob.isEstablished())
// 2. Exchange messages
let message1 = "Hello from Alice"
let ciphertext1 = try alice.encrypt(Data(message1.utf8))
let decrypted1 = try bob.decrypt(ciphertext1)
XCTAssertEqual(String(data: decrypted1, encoding: .utf8), message1)
let message2 = "Hello from Bob"
let ciphertext2 = try bob.encrypt(Data(message2.utf8))
let decrypted2 = try alice.decrypt(ciphertext2)
XCTAssertEqual(String(data: decrypted2, encoding: .utf8), message2)
// 3. Check session health
XCTAssertFalse(alice.needsRenegotiation())
XCTAssertFalse(bob.needsRenegotiation())
// 4. Simulate time passing
let oldTime = Date().addingTimeInterval(-35 * 60)
alice.setLastActivityTimeForTesting(oldTime)
// 5. Check renegotiation needed
XCTAssertTrue(alice.needsRenegotiation())
} catch {
XCTFail("Test failed: \(error)")
}
}
}

View File

@ -1,323 +0,0 @@
//
// VersionNegotiationIntegrationTests.swift
// bitchatTests
//
// This is free and unencumbered software released into the public domain.
// For more information, see <https://unlicense.org>
//
import XCTest
import CoreBluetooth
@testable import bitchat
class VersionNegotiationIntegrationTests: XCTestCase {
var meshService: BluetoothMeshService!
var mockDelegate: MockBitchatDelegate!
override func setUp() {
super.setUp()
meshService = BluetoothMeshService()
mockDelegate = MockBitchatDelegate()
meshService.delegate = mockDelegate
}
override func tearDown() {
meshService = nil
mockDelegate = nil
super.tearDown()
}
// MARK: - Version Negotiation Flow Tests
func testVersionNegotiationSuccessFlow() {
let peerID = "testpeer12345678"
// Simulate receiving version hello
let hello = VersionHello(
supportedVersions: [1],
preferredVersion: 1,
clientVersion: "1.0.0",
platform: "iOS"
)
guard let helloData = hello.encode() else {
XCTFail("Failed to encode hello")
return
}
let helloPacket = BitchatPacket(
type: MessageType.versionHello.rawValue,
senderID: Data(hexString: peerID) ?? Data(),
recipientID: nil,
timestamp: UInt64(Date().timeIntervalSince1970 * 1000),
payload: helloData,
signature: nil,
ttl: 1
)
// Process the hello packet
meshService.handleReceivedPacket(helloPacket, from: peerID, peripheral: nil)
// Verify that version was negotiated
// Note: We'd need to expose negotiatedVersions or add a getter to properly test this
// For now, we're testing that the packet is processed without errors
// The service should have sent a version ack
// In a real test, we'd mock the broadcast mechanism to verify this
}
func testVersionNegotiationRejectionFlow() {
let peerID = "incompatiblepeer"
// Simulate receiving version hello with incompatible version
let hello = VersionHello(
supportedVersions: [99, 100], // Unsupported versions
preferredVersion: 100,
clientVersion: "99.0.0",
platform: "Unknown"
)
guard let helloData = hello.encode() else {
XCTFail("Failed to encode hello")
return
}
let helloPacket = BitchatPacket(
type: MessageType.versionHello.rawValue,
senderID: Data(hexString: peerID) ?? Data(),
recipientID: nil,
timestamp: UInt64(Date().timeIntervalSince1970 * 1000),
payload: helloData,
signature: nil,
ttl: 1
)
// Process the hello packet
meshService.handleReceivedPacket(helloPacket, from: peerID, peripheral: nil)
// The service should send a rejection ack
// In a real implementation, we'd verify the rejection was sent
}
func testBackwardCompatibilityWithLegacyPeer() {
let peerID = "legacypeer123456"
// Simulate receiving a Noise handshake init without prior version negotiation
let handshakePacket = BitchatPacket(
type: MessageType.noiseHandshakeInit.rawValue,
senderID: Data(hexString: peerID) ?? Data(),
recipientID: nil,
timestamp: UInt64(Date().timeIntervalSince1970 * 1000),
payload: Data("handshake_data".utf8),
signature: nil,
ttl: 3
)
// Process the handshake packet
meshService.handleReceivedPacket(handshakePacket, from: peerID, peripheral: nil)
// Should assume version 1 for backward compatibility
// The handshake should proceed normally
}
func testVersionAckHandling() {
let peerID = "ackpeer12345678"
// Simulate receiving version ack
let ack = VersionAck(
agreedVersion: 1,
serverVersion: "1.0.0",
platform: "macOS"
)
guard let ackData = ack.encode() else {
XCTFail("Failed to encode ack")
return
}
let ackPacket = BitchatPacket(
type: MessageType.versionAck.rawValue,
senderID: Data(hexString: peerID) ?? Data(),
recipientID: meshService.getMyPeerID().data(using: .utf8),
timestamp: UInt64(Date().timeIntervalSince1970 * 1000),
payload: ackData,
signature: nil,
ttl: 1
)
// Process the ack packet
meshService.handleReceivedPacket(ackPacket, from: peerID, peripheral: nil)
// Should update negotiated version and proceed with handshake
}
func testVersionAckRejectionHandling() {
let peerID = "rejectpeer123456"
// Simulate receiving rejection ack
let ack = VersionAck(
agreedVersion: 0,
serverVersion: "2.0.0",
platform: "iOS",
rejected: true,
reason: "No compatible protocol version"
)
guard let ackData = ack.encode() else {
XCTFail("Failed to encode rejection ack")
return
}
let ackPacket = BitchatPacket(
type: MessageType.versionAck.rawValue,
senderID: Data(hexString: peerID) ?? Data(),
recipientID: meshService.getMyPeerID().data(using: .utf8),
timestamp: UInt64(Date().timeIntervalSince1970 * 1000),
payload: ackData,
signature: nil,
ttl: 1
)
// Process the rejection ack
meshService.handleReceivedPacket(ackPacket, from: peerID, peripheral: nil)
// Should mark negotiation as failed
}
// MARK: - State Management Tests
func testVersionStateCleanupOnDisconnect() {
let peerID = "disconnectpeer12"
// First establish some version negotiation state
let hello = VersionHello(
clientVersion: "1.0.0",
platform: "iOS"
)
guard let helloData = hello.encode() else {
XCTFail("Failed to encode hello")
return
}
let helloPacket = BitchatPacket(
type: MessageType.versionHello.rawValue,
senderID: Data(hexString: peerID) ?? Data(),
recipientID: nil,
timestamp: UInt64(Date().timeIntervalSince1970 * 1000),
payload: helloData,
signature: nil,
ttl: 1
)
// Process hello to establish state
meshService.handleReceivedPacket(helloPacket, from: peerID, peripheral: nil)
// Simulate disconnect
// In real implementation, we'd trigger the disconnect logic
// and verify state is cleaned up
}
// MARK: - Error Handling Tests
func testMalformedVersionHello() {
let peerID = "malformedpeer123"
// Send malformed data
let malformedPacket = BitchatPacket(
type: MessageType.versionHello.rawValue,
senderID: Data(hexString: peerID) ?? Data(),
recipientID: nil,
timestamp: UInt64(Date().timeIntervalSince1970 * 1000),
payload: Data("not valid json".utf8),
signature: nil,
ttl: 1
)
// Should handle gracefully without crashing
meshService.handleReceivedPacket(malformedPacket, from: peerID, peripheral: nil)
}
func testMalformedVersionAck() {
let peerID = "malformedackpeer"
// Send malformed ack data
let malformedPacket = BitchatPacket(
type: MessageType.versionAck.rawValue,
senderID: Data(hexString: peerID) ?? Data(),
recipientID: meshService.getMyPeerID().data(using: .utf8),
timestamp: UInt64(Date().timeIntervalSince1970 * 1000),
payload: Data("{invalid json}".utf8),
signature: nil,
ttl: 1
)
// Should handle gracefully without crashing
meshService.handleReceivedPacket(malformedPacket, from: peerID, peripheral: nil)
}
// MARK: - Performance Tests
func testVersionNegotiationPerformance() {
measure {
// Test encoding/decoding performance
for i in 0..<1000 {
let hello = VersionHello(
supportedVersions: [1, 2, 3],
preferredVersion: 3,
clientVersion: "1.\(i).0",
platform: "iOS",
capabilities: ["cap1", "cap2", "cap3"]
)
if let data = hello.encode(),
let _ = VersionHello.decode(from: data) {
// Success
} else {
XCTFail("Failed at iteration \(i)")
}
}
}
}
}
// MARK: - Mock Delegate
class MockBitchatDelegate: BitchatDelegate {
var receivedMessages: [BitchatMessage] = []
var connectedPeers: [String] = []
var disconnectedPeers: [String] = []
func didReceiveMessage(_ message: BitchatMessage) {
receivedMessages.append(message)
}
func didConnectToPeer(_ peerID: String) {
connectedPeers.append(peerID)
}
func didDisconnectFromPeer(_ peerID: String) {
disconnectedPeers.append(peerID)
}
func didUpdatePeerList(_ peers: [String]) {
// Not used in these tests
}
func didReceiveChannelLeave(_ channel: String, from peerID: String) {
// Not used in these tests
}
func didReceivePasswordProtectedChannelAnnouncement(_ channel: String, isProtected: Bool, creatorID: String?, keyCommitment: String?) {
// Not used in these tests
}
func didReceiveChannelRetentionAnnouncement(_ channel: String, enabled: Bool, creatorID: String?) {
// Not used in these tests
}
func decryptChannelMessage(_ encryptedContent: Data, channel: String) -> String? {
return nil
}
}

View File

@ -1,297 +0,0 @@
//
// VersionNegotiationScenarioTests.swift
// bitchatTests
//
// This is free and unencumbered software released into the public domain.
// For more information, see <https://unlicense.org>
//
import XCTest
@testable import bitchat
class VersionNegotiationScenarioTests: XCTestCase {
// MARK: - Real-World Scenarios
func testOldClientConnectsToNewServer() {
// Scenario: Old client (v1 only) connects to new server (v1, v2, v3)
let oldClientVersions: [UInt8] = [1]
let newServerVersions: [UInt8] = [1, 2, 3]
let agreed = ProtocolVersion.negotiateVersion(
clientVersions: oldClientVersions,
serverVersions: newServerVersions
)
XCTAssertEqual(agreed, 1, "Should agree on v1 for backward compatibility")
}
func testNewClientConnectsToOldServer() {
// Scenario: New client (v1, v2, v3) connects to old server (v1 only)
let newClientVersions: [UInt8] = [1, 2, 3]
let oldServerVersions: [UInt8] = [1]
let agreed = ProtocolVersion.negotiateVersion(
clientVersions: newClientVersions,
serverVersions: oldServerVersions
)
XCTAssertEqual(agreed, 1, "Should agree on v1 for backward compatibility")
}
func testMixedVersionNetwork() {
// Scenario: Network with mixed client versions
let clients: [[UInt8]] = [
[1], // Old client
[1, 2], // Mid-version client
[1, 2, 3] // New client
]
// All should be able to negotiate with each other
for (i, client1) in clients.enumerated() {
for (j, client2) in clients.enumerated() {
let agreed = ProtocolVersion.negotiateVersion(
clientVersions: client1,
serverVersions: client2
)
XCTAssertNotNil(agreed, "Clients \(i) and \(j) should negotiate successfully")
XCTAssertGreaterThanOrEqual(agreed ?? 0, 1, "Should at least agree on v1")
}
}
}
func testFutureClientWithUnsupportedVersion() {
// Scenario: Future client with only unsupported versions
let futureClientVersions: [UInt8] = [10, 11, 12]
let currentServerVersions: [UInt8] = Array(ProtocolVersion.supportedVersions)
let agreed = ProtocolVersion.negotiateVersion(
clientVersions: futureClientVersions,
serverVersions: currentServerVersions
)
XCTAssertNil(agreed, "Should fail to negotiate with incompatible future client")
}
// MARK: - Race Condition Tests
func testSimultaneousVersionHello() {
// Scenario: Both peers send version hello at the same time
// This tests that the protocol handles simultaneous negotiation
let hello1 = VersionHello(
supportedVersions: [1, 2],
preferredVersion: 2,
clientVersion: "1.1.0",
platform: "iOS"
)
let hello2 = VersionHello(
supportedVersions: [1, 2, 3],
preferredVersion: 3,
clientVersion: "1.2.0",
platform: "macOS"
)
// Both should be able to encode/decode regardless of order
XCTAssertNotNil(hello1.encode())
XCTAssertNotNil(hello2.encode())
// Version negotiation should be deterministic
let agreed1 = ProtocolVersion.negotiateVersion(
clientVersions: hello1.supportedVersions,
serverVersions: hello2.supportedVersions
)
let agreed2 = ProtocolVersion.negotiateVersion(
clientVersions: hello2.supportedVersions,
serverVersions: hello1.supportedVersions
)
XCTAssertEqual(agreed1, agreed2, "Negotiation should be symmetric")
XCTAssertEqual(agreed1, 2, "Should agree on highest common version")
}
// MARK: - Error Recovery Tests
func testRecoveryFromFailedNegotiation() {
// Test that a peer can retry after failed negotiation
var state = VersionNegotiationState.failed(reason: "Network error")
// Reset state for retry
state = .none
// Should be able to start new negotiation
state = .helloSent
if case .helloSent = state {
// Success - can retry after failure
} else {
XCTFail("Should be able to retry after failed negotiation")
}
}
func testPartialMessageHandling() {
// Test handling of truncated version messages
let truncatedData = Data([123, 34]) // Partial JSON
XCTAssertNil(VersionHello.decode(from: truncatedData))
XCTAssertNil(VersionAck.decode(from: truncatedData))
// Should not crash, just return nil
}
// MARK: - Platform Compatibility Tests
func testCrossPlatformNegotiation() {
let platforms = ["iOS", "macOS", "iPadOS", "Unknown"]
for platform1 in platforms {
for platform2 in platforms {
let hello1 = VersionHello(
clientVersion: "1.0.0",
platform: platform1
)
let hello2 = VersionHello(
clientVersion: "1.0.0",
platform: platform2
)
// All platforms should be able to negotiate
XCTAssertNotNil(hello1.encode())
XCTAssertNotNil(hello2.encode())
// Platform difference should not affect version negotiation
let agreed = ProtocolVersion.negotiateVersion(
clientVersions: hello1.supportedVersions,
serverVersions: hello2.supportedVersions
)
XCTAssertNotNil(agreed, "\(platform1) and \(platform2) should negotiate")
}
}
}
// MARK: - Capability Tests
func testCapabilityNegotiation() {
// Test future capability negotiation
let clientCapabilities = ["noise", "compression", "multipath"]
let serverCapabilities = ["noise", "compression", "federation"]
_ = VersionHello(
clientVersion: "1.0.0",
platform: "iOS",
capabilities: clientCapabilities
)
_ = VersionAck(
agreedVersion: 1,
serverVersion: "1.0.0",
platform: "macOS",
capabilities: serverCapabilities
)
// Find common capabilities (for future use)
let commonCapabilities = Set(clientCapabilities).intersection(Set(serverCapabilities))
XCTAssertEqual(commonCapabilities, ["noise", "compression"])
}
func testEmptyCapabilityHandling() {
// Test peers with no capabilities
let hello1 = VersionHello(
clientVersion: "1.0.0",
platform: "iOS",
capabilities: nil
)
let hello2 = VersionHello(
clientVersion: "1.0.0",
platform: "macOS",
capabilities: []
)
XCTAssertNotNil(hello1.encode())
XCTAssertNotNil(hello2.encode())
// Should still negotiate successfully
let agreed = ProtocolVersion.negotiateVersion(
clientVersions: hello1.supportedVersions,
serverVersions: hello2.supportedVersions
)
XCTAssertNotNil(agreed)
}
// MARK: - Stress Tests
func testManyVersionsNegotiation() {
// Test with many supported versions
let manyVersions = Array<UInt8>(1...100)
let someVersions = Array<UInt8>([1, 50, 75, 100])
let agreed = ProtocolVersion.negotiateVersion(
clientVersions: manyVersions,
serverVersions: someVersions
)
XCTAssertEqual(agreed, 100, "Should pick highest common version")
}
func testRapidConnectionDisconnection() {
// Test rapid connect/disconnect cycles
var states: [String: VersionNegotiationState] = [:]
for i in 0..<100 {
let peerID = "peer\(i)"
// Connect
states[peerID] = .helloSent
// Negotiate
states[peerID] = .ackReceived(version: 1)
// Disconnect
states.removeValue(forKey: peerID)
}
XCTAssertTrue(states.isEmpty, "All states should be cleaned up")
}
// MARK: - Security Tests
func testLargeVersionListDoS() {
// Test protection against DoS with huge version lists
let hugeVersionList = Array<UInt8>(0...255) // All possible versions
let hello = VersionHello(
supportedVersions: hugeVersionList,
preferredVersion: 255,
clientVersion: "1.0.0",
platform: "iOS"
)
// Should handle without performance issues
let startTime = Date()
_ = hello.encode()
let encodingTime = Date().timeIntervalSince(startTime)
XCTAssertLessThan(encodingTime, 0.1, "Encoding should be fast even with large version list")
}
func testVersionDowngradeAttack() {
// Test that negotiation always picks highest common version
// to prevent downgrade attacks
let clientVersions: [UInt8] = [1, 2, 3]
let serverVersions: [UInt8] = [1, 2, 3]
let agreed = ProtocolVersion.negotiateVersion(
clientVersions: clientVersions,
serverVersions: serverVersions
)
XCTAssertEqual(agreed, 3, "Should not allow downgrade to lower version")
}
}