mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-24 22:45:19 +00:00
feat: enhance password-protected rooms with key commitments and management
- Add key commitment scheme for immediate password verification - Implement ownership transfer with /transfer command - Add password change functionality with /pass command - Include room metadata in initialization messages - Remove /help command and rename /changepass to /pass - Make room names tappable to show sidebar - Remove spaces in person/room counter display - Update UI to show lock icons and orange colors for protected rooms - Fix password verification to work with empty rooms - Add comprehensive tests for new functionality This update significantly improves the security and usability of password-protected rooms by allowing immediate verification without waiting for encrypted messages.
This commit is contained in:
@@ -7,9 +7,13 @@
|
|||||||
objects = {
|
objects = {
|
||||||
|
|
||||||
/* Begin PBXBuildFile section */
|
/* Begin PBXBuildFile section */
|
||||||
|
0DAFF1DDE9BA83FF648D5AB3 /* BitchatMessageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3FA8FF26ABDC1C642A8C7AE5 /* BitchatMessageTests.swift */; };
|
||||||
10E68BB889356219189E38EC /* BitchatApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = EF625BB3AD919322C01A46B2 /* BitchatApp.swift */; };
|
10E68BB889356219189E38EC /* BitchatApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = EF625BB3AD919322C01A46B2 /* BitchatApp.swift */; };
|
||||||
1D9674FA5F998503831DC281 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A08E03AA0C63E97C91749AEC /* ContentView.swift */; };
|
1D9674FA5F998503831DC281 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A08E03AA0C63E97C91749AEC /* ContentView.swift */; };
|
||||||
|
230B11C5BF035D35638B21C8 /* PasswordProtectedRoomTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FB12482E4AD78B861C538449 /* PasswordProtectedRoomTests.swift */; };
|
||||||
|
2E71E320EA921498C57E023B /* BitchatMessageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3FA8FF26ABDC1C642A8C7AE5 /* BitchatMessageTests.swift */; };
|
||||||
4B747085D07A1BCE0F5BA612 /* BinaryProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2136C3E22D02D4A8DBE7EAB /* BinaryProtocol.swift */; };
|
4B747085D07A1BCE0F5BA612 /* BinaryProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2136C3E22D02D4A8DBE7EAB /* BinaryProtocol.swift */; };
|
||||||
|
56F0892E13CC0D1490A3691E /* BloomFilterTests.swift.disabled in Resources */ = {isa = PBXBuildFile; fileRef = 822AA698BDB7BB939B62A010 /* BloomFilterTests.swift.disabled */; };
|
||||||
61C81ED5F679D5E973EE0C07 /* NotificationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3448F84BF86A42A3CC4A9379 /* NotificationService.swift */; };
|
61C81ED5F679D5E973EE0C07 /* NotificationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3448F84BF86A42A3CC4A9379 /* NotificationService.swift */; };
|
||||||
6A624D9359BCCAAE58CE0D64 /* LoggingService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96CDF6D0AF0F2052A6C7E634 /* LoggingService.swift */; };
|
6A624D9359BCCAAE58CE0D64 /* LoggingService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96CDF6D0AF0F2052A6C7E634 /* LoggingService.swift */; };
|
||||||
6DE056E1EE9850E9FBF50157 /* BitchatProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 229F17B68CFF7AB1BC91C847 /* BitchatProtocol.swift */; };
|
6DE056E1EE9850E9FBF50157 /* BitchatProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 229F17B68CFF7AB1BC91C847 /* BitchatProtocol.swift */; };
|
||||||
@@ -20,32 +24,63 @@
|
|||||||
7576A357B278E5733E9D9F33 /* ChatViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6B8F7B7D55092C2540A7996 /* ChatViewModel.swift */; };
|
7576A357B278E5733E9D9F33 /* ChatViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6B8F7B7D55092C2540A7996 /* ChatViewModel.swift */; };
|
||||||
7A50E2F04A3515A7E90EEAE4 /* BluetoothMeshService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5C3D880FF8AE1673B20E1E3 /* BluetoothMeshService.swift */; };
|
7A50E2F04A3515A7E90EEAE4 /* BluetoothMeshService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5C3D880FF8AE1673B20E1E3 /* BluetoothMeshService.swift */; };
|
||||||
7DD72D928FF9DD3CA81B46B0 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 3A69677D382F1C3D5ED03F7D /* Assets.xcassets */; };
|
7DD72D928FF9DD3CA81B46B0 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 3A69677D382F1C3D5ED03F7D /* Assets.xcassets */; };
|
||||||
|
8F0BFC2D2B2A5E7B70C3B485 /* BinaryProtocolTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53D535D9CE0B875F47402290 /* BinaryProtocolTests.swift */; };
|
||||||
923027D6F2F417AFA2488127 /* BitchatProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 229F17B68CFF7AB1BC91C847 /* BitchatProtocol.swift */; };
|
923027D6F2F417AFA2488127 /* BitchatProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 229F17B68CFF7AB1BC91C847 /* BitchatProtocol.swift */; };
|
||||||
92D34E7A07C990C8A815B0CE /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A08E03AA0C63E97C91749AEC /* ContentView.swift */; };
|
92D34E7A07C990C8A815B0CE /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A08E03AA0C63E97C91749AEC /* ContentView.swift */; };
|
||||||
|
A0691E21A3A358AB9715AC20 /* BloomFilterTests.swift.disabled in Resources */ = {isa = PBXBuildFile; fileRef = 822AA698BDB7BB939B62A010 /* BloomFilterTests.swift.disabled */; };
|
||||||
ABAF130D88561F4A646F0430 /* AppInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 763E0DBA9492A654FC0CDCB9 /* AppInfoView.swift */; };
|
ABAF130D88561F4A646F0430 /* AppInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 763E0DBA9492A654FC0CDCB9 /* AppInfoView.swift */; };
|
||||||
AD11E46940D742AEAF547EB2 /* 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 */; };
|
||||||
BCCFEDC1EBE59323C3C470BF /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 3A69677D382F1C3D5ED03F7D /* Assets.xcassets */; };
|
BCCFEDC1EBE59323C3C470BF /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 3A69677D382F1C3D5ED03F7D /* Assets.xcassets */; };
|
||||||
|
CDAD6629EB69916B95C80DAF /* BinaryProtocolTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53D535D9CE0B875F47402290 /* BinaryProtocolTests.swift */; };
|
||||||
D450CF41F207BDE1A1AAA56E /* ChatViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6B8F7B7D55092C2540A7996 /* ChatViewModel.swift */; };
|
D450CF41F207BDE1A1AAA56E /* ChatViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6B8F7B7D55092C2540A7996 /* ChatViewModel.swift */; };
|
||||||
D948085736ED8E736C1DE3B0 /* BluetoothMeshService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5C3D880FF8AE1673B20E1E3 /* BluetoothMeshService.swift */; };
|
D948085736ED8E736C1DE3B0 /* BluetoothMeshService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5C3D880FF8AE1673B20E1E3 /* BluetoothMeshService.swift */; };
|
||||||
DDA1DFAB1FF7AADE52DC0F53 /* EncryptionService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6DC1563390A15C042D059CF9 /* EncryptionService.swift */; };
|
DDA1DFAB1FF7AADE52DC0F53 /* EncryptionService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6DC1563390A15C042D059CF9 /* EncryptionService.swift */; };
|
||||||
|
F00B713D5053617FB5F3F1BE /* MessagePaddingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8DE9CDF66D4E52D268851048 /* MessagePaddingTests.swift */; };
|
||||||
F455F011B3B648ADA233F998 /* BinaryProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2136C3E22D02D4A8DBE7EAB /* BinaryProtocol.swift */; };
|
F455F011B3B648ADA233F998 /* BinaryProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2136C3E22D02D4A8DBE7EAB /* BinaryProtocol.swift */; };
|
||||||
|
F4A689F5F34125AE1BFD5599 /* PasswordProtectedRoomTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FB12482E4AD78B861C538449 /* PasswordProtectedRoomTests.swift */; };
|
||||||
/* End PBXBuildFile section */
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
|
/* Begin PBXContainerItemProxy section */
|
||||||
|
96415D4F989854F908EAD303 /* PBXContainerItemProxy */ = {
|
||||||
|
isa = PBXContainerItemProxy;
|
||||||
|
containerPortal = 475D96681D0EA0AE57A4E06E /* Project object */;
|
||||||
|
proxyType = 1;
|
||||||
|
remoteGlobalIDString = AF077EA0474EDEDE2C72716C;
|
||||||
|
remoteInfo = bitchat_iOS;
|
||||||
|
};
|
||||||
|
FF470234EF8C6BB8865B80B5 /* PBXContainerItemProxy */ = {
|
||||||
|
isa = PBXContainerItemProxy;
|
||||||
|
containerPortal = 475D96681D0EA0AE57A4E06E /* Project object */;
|
||||||
|
proxyType = 1;
|
||||||
|
remoteGlobalIDString = 0576A29205865664C0937536;
|
||||||
|
remoteInfo = bitchat_macOS;
|
||||||
|
};
|
||||||
|
/* End PBXContainerItemProxy section */
|
||||||
|
|
||||||
/* Begin PBXFileReference section */
|
/* Begin PBXFileReference section */
|
||||||
|
03C57F452B55FD0FD8F51421 /* bitchatTests_macOS.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = bitchatTests_macOS.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
229F17B68CFF7AB1BC91C847 /* BitchatProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BitchatProtocol.swift; sourceTree = "<group>"; };
|
229F17B68CFF7AB1BC91C847 /* BitchatProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BitchatProtocol.swift; sourceTree = "<group>"; };
|
||||||
3448F84BF86A42A3CC4A9379 /* NotificationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationService.swift; sourceTree = "<group>"; };
|
3448F84BF86A42A3CC4A9379 /* NotificationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationService.swift; sourceTree = "<group>"; };
|
||||||
3A69677D382F1C3D5ED03F7D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; 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>"; };
|
||||||
|
53D535D9CE0B875F47402290 /* BinaryProtocolTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BinaryProtocolTests.swift; sourceTree = "<group>"; };
|
||||||
6DC1563390A15C042D059CF9 /* EncryptionService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EncryptionService.swift; sourceTree = "<group>"; };
|
6DC1563390A15C042D059CF9 /* EncryptionService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EncryptionService.swift; sourceTree = "<group>"; };
|
||||||
763E0DBA9492A654FC0CDCB9 /* AppInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppInfoView.swift; sourceTree = "<group>"; };
|
763E0DBA9492A654FC0CDCB9 /* AppInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppInfoView.swift; sourceTree = "<group>"; };
|
||||||
7EEBDA723E1CFD88758DA4AC /* bitchat.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = bitchat.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
7EEBDA723E1CFD88758DA4AC /* bitchat.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = bitchat.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
|
822AA698BDB7BB939B62A010 /* BloomFilterTests.swift.disabled */ = {isa = PBXFileReference; lastKnownFileType = text; path = BloomFilterTests.swift.disabled; sourceTree = "<group>"; };
|
||||||
|
8DE9CDF66D4E52D268851048 /* MessagePaddingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessagePaddingTests.swift; sourceTree = "<group>"; };
|
||||||
96CDF6D0AF0F2052A6C7E634 /* LoggingService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoggingService.swift; sourceTree = "<group>"; };
|
96CDF6D0AF0F2052A6C7E634 /* LoggingService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoggingService.swift; sourceTree = "<group>"; };
|
||||||
997D512074C64904D75DDD40 /* bitchat.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = bitchat.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
997D512074C64904D75DDD40 /* bitchat.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = bitchat.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
A08E03AA0C63E97C91749AEC /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
|
A08E03AA0C63E97C91749AEC /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
|
||||||
A2136C3E22D02D4A8DBE7EAB /* BinaryProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BinaryProtocol.swift; sourceTree = "<group>"; };
|
A2136C3E22D02D4A8DBE7EAB /* BinaryProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BinaryProtocol.swift; sourceTree = "<group>"; };
|
||||||
|
C0DB1DE27F0AAB5092663E8E /* bitchatTests_iOS.xctest */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = wrapper.cfbundle; path = bitchatTests_iOS.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
D5C3D880FF8AE1673B20E1E3 /* BluetoothMeshService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BluetoothMeshService.swift; sourceTree = "<group>"; };
|
D5C3D880FF8AE1673B20E1E3 /* BluetoothMeshService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BluetoothMeshService.swift; sourceTree = "<group>"; };
|
||||||
|
D69A18D27F9A565FD6041E12 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; };
|
||||||
E6B8F7B7D55092C2540A7996 /* ChatViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatViewModel.swift; sourceTree = "<group>"; };
|
E6B8F7B7D55092C2540A7996 /* ChatViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatViewModel.swift; sourceTree = "<group>"; };
|
||||||
EA706D8E5097785414646A8E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; };
|
EA706D8E5097785414646A8E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; };
|
||||||
EF625BB3AD919322C01A46B2 /* BitchatApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BitchatApp.swift; sourceTree = "<group>"; };
|
EF625BB3AD919322C01A46B2 /* BitchatApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BitchatApp.swift; sourceTree = "<group>"; };
|
||||||
|
FB12482E4AD78B861C538449 /* PasswordProtectedRoomTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PasswordProtectedRoomTests.swift; sourceTree = "<group>"; };
|
||||||
/* End PBXFileReference section */
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
/* Begin PBXGroup section */
|
/* Begin PBXGroup section */
|
||||||
@@ -53,6 +88,7 @@
|
|||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
2F82C5FC8433F4064F079D1F /* bitchat */,
|
2F82C5FC8433F4064F079D1F /* bitchat */,
|
||||||
|
C3D98EB3E1B455E321F519F4 /* bitchatTests */,
|
||||||
9F37F9F2C353B58AC809E93B /* Products */,
|
9F37F9F2C353B58AC809E93B /* Products */,
|
||||||
);
|
);
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -84,6 +120,8 @@
|
|||||||
children = (
|
children = (
|
||||||
997D512074C64904D75DDD40 /* bitchat.app */,
|
997D512074C64904D75DDD40 /* bitchat.app */,
|
||||||
7EEBDA723E1CFD88758DA4AC /* bitchat.app */,
|
7EEBDA723E1CFD88758DA4AC /* bitchat.app */,
|
||||||
|
C0DB1DE27F0AAB5092663E8E /* bitchatTests_iOS.xctest */,
|
||||||
|
03C57F452B55FD0FD8F51421 /* bitchatTests_macOS.xctest */,
|
||||||
);
|
);
|
||||||
name = Products;
|
name = Products;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -106,6 +144,19 @@
|
|||||||
path = Protocols;
|
path = Protocols;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
C3D98EB3E1B455E321F519F4 /* bitchatTests */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
53D535D9CE0B875F47402290 /* BinaryProtocolTests.swift */,
|
||||||
|
3FA8FF26ABDC1C642A8C7AE5 /* BitchatMessageTests.swift */,
|
||||||
|
822AA698BDB7BB939B62A010 /* BloomFilterTests.swift.disabled */,
|
||||||
|
D69A18D27F9A565FD6041E12 /* Info.plist */,
|
||||||
|
8DE9CDF66D4E52D268851048 /* MessagePaddingTests.swift */,
|
||||||
|
FB12482E4AD78B861C538449 /* PasswordProtectedRoomTests.swift */,
|
||||||
|
);
|
||||||
|
path = bitchatTests;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
D98A3186D7E4C72E35BDF7FE /* Services */ = {
|
D98A3186D7E4C72E35BDF7FE /* Services */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
@@ -138,6 +189,44 @@
|
|||||||
productReference = 7EEBDA723E1CFD88758DA4AC /* bitchat.app */;
|
productReference = 7EEBDA723E1CFD88758DA4AC /* bitchat.app */;
|
||||||
productType = "com.apple.product-type.application";
|
productType = "com.apple.product-type.application";
|
||||||
};
|
};
|
||||||
|
47FF23248747DD7CB666CB91 /* bitchatTests_macOS */ = {
|
||||||
|
isa = PBXNativeTarget;
|
||||||
|
buildConfigurationList = 1C27B5BA3DB46DDF0DBFEF62 /* Build configuration list for PBXNativeTarget "bitchatTests_macOS" */;
|
||||||
|
buildPhases = (
|
||||||
|
5C22AA7B9ACC5A861445C769 /* Sources */,
|
||||||
|
7F7221A3304F43E9CF496036 /* Resources */,
|
||||||
|
);
|
||||||
|
buildRules = (
|
||||||
|
);
|
||||||
|
dependencies = (
|
||||||
|
4AA8605DCAA64A45657EF0CA /* PBXTargetDependency */,
|
||||||
|
);
|
||||||
|
name = bitchatTests_macOS;
|
||||||
|
packageProductDependencies = (
|
||||||
|
);
|
||||||
|
productName = bitchatTests_macOS;
|
||||||
|
productReference = 03C57F452B55FD0FD8F51421 /* bitchatTests_macOS.xctest */;
|
||||||
|
productType = "com.apple.product-type.bundle.unit-test";
|
||||||
|
};
|
||||||
|
6CB97DF2EA57234CB3E563B8 /* bitchatTests_iOS */ = {
|
||||||
|
isa = PBXNativeTarget;
|
||||||
|
buildConfigurationList = 38C4AF6313E5037F25CEF30B /* Build configuration list for PBXNativeTarget "bitchatTests_iOS" */;
|
||||||
|
buildPhases = (
|
||||||
|
865C8403EF02C089369A9FCB /* Sources */,
|
||||||
|
B42D1108ADC08C2AD4FD6DB5 /* Resources */,
|
||||||
|
);
|
||||||
|
buildRules = (
|
||||||
|
);
|
||||||
|
dependencies = (
|
||||||
|
D8C09F21DB7DC06E8E672C21 /* PBXTargetDependency */,
|
||||||
|
);
|
||||||
|
name = bitchatTests_iOS;
|
||||||
|
packageProductDependencies = (
|
||||||
|
);
|
||||||
|
productName = bitchatTests_iOS;
|
||||||
|
productReference = C0DB1DE27F0AAB5092663E8E /* bitchatTests_iOS.xctest */;
|
||||||
|
productType = "com.apple.product-type.bundle.unit-test";
|
||||||
|
};
|
||||||
AF077EA0474EDEDE2C72716C /* bitchat_iOS */ = {
|
AF077EA0474EDEDE2C72716C /* bitchat_iOS */ = {
|
||||||
isa = PBXNativeTarget;
|
isa = PBXNativeTarget;
|
||||||
buildConfigurationList = 53EADEF7546F94DDF82271B9 /* Build configuration list for PBXNativeTarget "bitchat_iOS" */;
|
buildConfigurationList = 53EADEF7546F94DDF82271B9 /* Build configuration list for PBXNativeTarget "bitchat_iOS" */;
|
||||||
@@ -186,6 +275,8 @@
|
|||||||
projectDirPath = "";
|
projectDirPath = "";
|
||||||
projectRoot = "";
|
projectRoot = "";
|
||||||
targets = (
|
targets = (
|
||||||
|
6CB97DF2EA57234CB3E563B8 /* bitchatTests_iOS */,
|
||||||
|
47FF23248747DD7CB666CB91 /* bitchatTests_macOS */,
|
||||||
AF077EA0474EDEDE2C72716C /* bitchat_iOS */,
|
AF077EA0474EDEDE2C72716C /* bitchat_iOS */,
|
||||||
0576A29205865664C0937536 /* bitchat_macOS */,
|
0576A29205865664C0937536 /* bitchat_macOS */,
|
||||||
);
|
);
|
||||||
@@ -201,6 +292,22 @@
|
|||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
|
7F7221A3304F43E9CF496036 /* Resources */ = {
|
||||||
|
isa = PBXResourcesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
56F0892E13CC0D1490A3691E /* BloomFilterTests.swift.disabled in Resources */,
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
|
B42D1108ADC08C2AD4FD6DB5 /* Resources */ = {
|
||||||
|
isa = PBXResourcesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
A0691E21A3A358AB9715AC20 /* BloomFilterTests.swift.disabled in Resources */,
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
CD6E8F32BC38357473954F97 /* Resources */ = {
|
CD6E8F32BC38357473954F97 /* Resources */ = {
|
||||||
isa = PBXResourcesBuildPhase;
|
isa = PBXResourcesBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
@@ -246,9 +353,101 @@
|
|||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
|
5C22AA7B9ACC5A861445C769 /* Sources */ = {
|
||||||
|
isa = PBXSourcesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
8F0BFC2D2B2A5E7B70C3B485 /* BinaryProtocolTests.swift in Sources */,
|
||||||
|
2E71E320EA921498C57E023B /* BitchatMessageTests.swift in Sources */,
|
||||||
|
ADC66F95FBD513B10411ADB3 /* MessagePaddingTests.swift in Sources */,
|
||||||
|
F4A689F5F34125AE1BFD5599 /* PasswordProtectedRoomTests.swift in Sources */,
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
|
865C8403EF02C089369A9FCB /* Sources */ = {
|
||||||
|
isa = PBXSourcesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
CDAD6629EB69916B95C80DAF /* BinaryProtocolTests.swift in Sources */,
|
||||||
|
0DAFF1DDE9BA83FF648D5AB3 /* BitchatMessageTests.swift in Sources */,
|
||||||
|
F00B713D5053617FB5F3F1BE /* MessagePaddingTests.swift in Sources */,
|
||||||
|
230B11C5BF035D35638B21C8 /* PasswordProtectedRoomTests.swift in Sources */,
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
/* End PBXSourcesBuildPhase section */
|
/* End PBXSourcesBuildPhase section */
|
||||||
|
|
||||||
|
/* Begin PBXTargetDependency section */
|
||||||
|
4AA8605DCAA64A45657EF0CA /* PBXTargetDependency */ = {
|
||||||
|
isa = PBXTargetDependency;
|
||||||
|
target = 0576A29205865664C0937536 /* bitchat_macOS */;
|
||||||
|
targetProxy = FF470234EF8C6BB8865B80B5 /* PBXContainerItemProxy */;
|
||||||
|
};
|
||||||
|
D8C09F21DB7DC06E8E672C21 /* PBXTargetDependency */ = {
|
||||||
|
isa = PBXTargetDependency;
|
||||||
|
target = AF077EA0474EDEDE2C72716C /* bitchat_iOS */;
|
||||||
|
targetProxy = 96415D4F989854F908EAD303 /* PBXContainerItemProxy */;
|
||||||
|
};
|
||||||
|
/* End PBXTargetDependency section */
|
||||||
|
|
||||||
/* Begin XCBuildConfiguration section */
|
/* Begin XCBuildConfiguration section */
|
||||||
|
077A5203074247CF8F766E2F /* Debug */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||||
|
INFOPLIST_FILE = bitchatTests/Info.plist;
|
||||||
|
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
|
||||||
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
|
"$(inherited)",
|
||||||
|
"@executable_path/Frameworks",
|
||||||
|
"@loader_path/Frameworks",
|
||||||
|
);
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = chat.bitchat.tests;
|
||||||
|
SDKROOT = iphoneos;
|
||||||
|
SWIFT_VERSION = 5.0;
|
||||||
|
TARGETED_DEVICE_FAMILY = "1,2";
|
||||||
|
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/bitchat.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/bitchat";
|
||||||
|
};
|
||||||
|
name = Debug;
|
||||||
|
};
|
||||||
|
0DACAA261446D178EDD30ECA /* Release */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||||
|
INFOPLIST_FILE = bitchatTests/Info.plist;
|
||||||
|
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
|
||||||
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
|
"$(inherited)",
|
||||||
|
"@executable_path/Frameworks",
|
||||||
|
"@loader_path/Frameworks",
|
||||||
|
);
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = chat.bitchat.tests;
|
||||||
|
SDKROOT = iphoneos;
|
||||||
|
SWIFT_VERSION = 5.0;
|
||||||
|
TARGETED_DEVICE_FAMILY = "1,2";
|
||||||
|
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/bitchat.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/bitchat";
|
||||||
|
};
|
||||||
|
name = Release;
|
||||||
|
};
|
||||||
|
147FDAE548082D5B921C6F0B /* Release */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||||
|
COMBINE_HIDPI_IMAGES = YES;
|
||||||
|
INFOPLIST_FILE = bitchatTests/Info.plist;
|
||||||
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
|
"$(inherited)",
|
||||||
|
"@executable_path/../Frameworks",
|
||||||
|
"@loader_path/../Frameworks",
|
||||||
|
);
|
||||||
|
MACOSX_DEPLOYMENT_TARGET = 13.0;
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = chat.bitchat.tests;
|
||||||
|
SDKROOT = macosx;
|
||||||
|
SWIFT_VERSION = 5.0;
|
||||||
|
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/bitchat.app/Contents/MacOS/bitchat";
|
||||||
|
};
|
||||||
|
name = Release;
|
||||||
|
};
|
||||||
702E7395723CADA4B830F4A9 /* Debug */ = {
|
702E7395723CADA4B830F4A9 /* Debug */ = {
|
||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
@@ -270,9 +469,31 @@
|
|||||||
PRODUCT_BUNDLE_IDENTIFIER = chat.bitchat;
|
PRODUCT_BUNDLE_IDENTIFIER = chat.bitchat;
|
||||||
PRODUCT_NAME = bitchat;
|
PRODUCT_NAME = bitchat;
|
||||||
SDKROOT = iphoneos;
|
SDKROOT = iphoneos;
|
||||||
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES;
|
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
|
||||||
|
SUPPORTS_MACCATALYST = NO;
|
||||||
|
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
|
||||||
|
SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO;
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
TARGETED_DEVICE_FAMILY = "1,2";
|
TARGETED_DEVICE_FAMILY = 1;
|
||||||
|
};
|
||||||
|
name = Debug;
|
||||||
|
};
|
||||||
|
7FA2BADBF3B325125030CAB1 /* Debug */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||||
|
COMBINE_HIDPI_IMAGES = YES;
|
||||||
|
INFOPLIST_FILE = bitchatTests/Info.plist;
|
||||||
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
|
"$(inherited)",
|
||||||
|
"@executable_path/../Frameworks",
|
||||||
|
"@loader_path/../Frameworks",
|
||||||
|
);
|
||||||
|
MACOSX_DEPLOYMENT_TARGET = 13.0;
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = chat.bitchat.tests;
|
||||||
|
SDKROOT = macosx;
|
||||||
|
SWIFT_VERSION = 5.0;
|
||||||
|
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/bitchat.app/Contents/MacOS/bitchat";
|
||||||
};
|
};
|
||||||
name = Debug;
|
name = Debug;
|
||||||
};
|
};
|
||||||
@@ -297,9 +518,12 @@
|
|||||||
PRODUCT_BUNDLE_IDENTIFIER = chat.bitchat;
|
PRODUCT_BUNDLE_IDENTIFIER = chat.bitchat;
|
||||||
PRODUCT_NAME = bitchat;
|
PRODUCT_NAME = bitchat;
|
||||||
SDKROOT = iphoneos;
|
SDKROOT = iphoneos;
|
||||||
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES;
|
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
|
||||||
|
SUPPORTS_MACCATALYST = NO;
|
||||||
|
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
|
||||||
|
SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO;
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
TARGETED_DEVICE_FAMILY = "1,2";
|
TARGETED_DEVICE_FAMILY = 1;
|
||||||
};
|
};
|
||||||
name = Release;
|
name = Release;
|
||||||
};
|
};
|
||||||
@@ -479,6 +703,24 @@
|
|||||||
/* End XCBuildConfiguration section */
|
/* End XCBuildConfiguration section */
|
||||||
|
|
||||||
/* Begin XCConfigurationList section */
|
/* Begin XCConfigurationList section */
|
||||||
|
1C27B5BA3DB46DDF0DBFEF62 /* Build configuration list for PBXNativeTarget "bitchatTests_macOS" */ = {
|
||||||
|
isa = XCConfigurationList;
|
||||||
|
buildConfigurations = (
|
||||||
|
7FA2BADBF3B325125030CAB1 /* Debug */,
|
||||||
|
147FDAE548082D5B921C6F0B /* Release */,
|
||||||
|
);
|
||||||
|
defaultConfigurationIsVisible = 0;
|
||||||
|
defaultConfigurationName = Debug;
|
||||||
|
};
|
||||||
|
38C4AF6313E5037F25CEF30B /* Build configuration list for PBXNativeTarget "bitchatTests_iOS" */ = {
|
||||||
|
isa = XCConfigurationList;
|
||||||
|
buildConfigurations = (
|
||||||
|
077A5203074247CF8F766E2F /* Debug */,
|
||||||
|
0DACAA261446D178EDD30ECA /* Release */,
|
||||||
|
);
|
||||||
|
defaultConfigurationIsVisible = 0;
|
||||||
|
defaultConfigurationName = Debug;
|
||||||
|
};
|
||||||
3EA424CBD51200895D361189 /* Build configuration list for PBXProject "bitchat" */ = {
|
3EA424CBD51200895D361189 /* Build configuration list for PBXProject "bitchat" */ = {
|
||||||
isa = XCConfigurationList;
|
isa = XCConfigurationList;
|
||||||
buildConfigurations = (
|
buildConfigurations = (
|
||||||
|
|||||||
@@ -107,7 +107,9 @@ struct BinaryProtocol {
|
|||||||
var offset = 0
|
var offset = 0
|
||||||
|
|
||||||
// Header
|
// Header
|
||||||
_ = data[offset]; offset += 1 // version
|
let version = data[offset]; offset += 1
|
||||||
|
// Only support version 1
|
||||||
|
guard version == 1 else { return nil }
|
||||||
let type = data[offset]; offset += 1
|
let type = data[offset]; offset += 1
|
||||||
let ttl = data[offset]; offset += 1
|
let ttl = data[offset]; offset += 1
|
||||||
|
|
||||||
|
|||||||
@@ -18,15 +18,18 @@ struct MessagePadding {
|
|||||||
static func pad(_ data: Data, toSize targetSize: Int) -> Data {
|
static func pad(_ data: Data, toSize targetSize: Int) -> Data {
|
||||||
guard data.count < targetSize else { return data }
|
guard data.count < targetSize else { return data }
|
||||||
|
|
||||||
var padded = data
|
|
||||||
let paddingNeeded = targetSize - data.count
|
let paddingNeeded = targetSize - data.count
|
||||||
|
|
||||||
// Add random padding bytes (more secure than zeros)
|
// PKCS#7 only supports padding up to 255 bytes
|
||||||
|
// If we need more padding than that, don't pad - return original data
|
||||||
|
guard paddingNeeded <= 255 else { return data }
|
||||||
|
|
||||||
|
var padded = data
|
||||||
|
|
||||||
|
// Standard PKCS#7 padding
|
||||||
var randomBytes = [UInt8](repeating: 0, count: paddingNeeded - 1)
|
var randomBytes = [UInt8](repeating: 0, count: paddingNeeded - 1)
|
||||||
_ = SecRandomCopyBytes(kSecRandomDefault, paddingNeeded - 1, &randomBytes)
|
_ = SecRandomCopyBytes(kSecRandomDefault, paddingNeeded - 1, &randomBytes)
|
||||||
padded.append(contentsOf: randomBytes)
|
padded.append(contentsOf: randomBytes)
|
||||||
|
|
||||||
// Last byte indicates padding length (PKCS#7 style)
|
|
||||||
padded.append(UInt8(paddingNeeded))
|
padded.append(UInt8(paddingNeeded))
|
||||||
|
|
||||||
return padded
|
return padded
|
||||||
@@ -161,7 +164,7 @@ protocol BitchatDelegate: AnyObject {
|
|||||||
func didDisconnectFromPeer(_ peerID: String)
|
func didDisconnectFromPeer(_ peerID: String)
|
||||||
func didUpdatePeerList(_ peers: [String])
|
func didUpdatePeerList(_ peers: [String])
|
||||||
func didReceiveRoomLeave(_ room: String, from peerID: String)
|
func didReceiveRoomLeave(_ room: String, from peerID: String)
|
||||||
func didReceivePasswordProtectedRoomAnnouncement(_ room: String, isProtected: Bool, creatorID: String?)
|
func didReceivePasswordProtectedRoomAnnouncement(_ room: String, isProtected: Bool, creatorID: String?, keyCommitment: String?)
|
||||||
func decryptRoomMessage(_ encryptedContent: Data, room: String) -> String?
|
func decryptRoomMessage(_ encryptedContent: Data, room: String) -> String?
|
||||||
|
|
||||||
// Optional method to check if a fingerprint belongs to a favorite peer
|
// Optional method to check if a fingerprint belongs to a favorite peer
|
||||||
@@ -178,7 +181,7 @@ extension BitchatDelegate {
|
|||||||
// Default empty implementation
|
// Default empty implementation
|
||||||
}
|
}
|
||||||
|
|
||||||
func didReceivePasswordProtectedRoomAnnouncement(_ room: String, isProtected: Bool, creatorID: String?) {
|
func didReceivePasswordProtectedRoomAnnouncement(_ room: String, isProtected: Bool, creatorID: String?, keyCommitment: String?) {
|
||||||
// Default empty implementation
|
// Default empty implementation
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -685,14 +685,15 @@ class BluetoothMeshService: NSObject {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func announcePasswordProtectedRoom(_ room: String, isProtected: Bool = true, creatorID: String? = nil) {
|
func announcePasswordProtectedRoom(_ room: String, isProtected: Bool = true, creatorID: String? = nil, keyCommitment: String? = nil) {
|
||||||
messageQueue.async { [weak self] in
|
messageQueue.async { [weak self] in
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
|
|
||||||
// Payload format: room|isProtected|creatorID
|
// Payload format: room|isProtected|creatorID|keyCommitment
|
||||||
let protectedFlag = isProtected ? "1" : "0"
|
let protectedFlag = isProtected ? "1" : "0"
|
||||||
let creator = creatorID ?? self.myPeerID
|
let creator = creatorID ?? self.myPeerID
|
||||||
let payload = "\(room)|\(protectedFlag)|\(creator)"
|
let commitment = keyCommitment ?? ""
|
||||||
|
let payload = "\(room)|\(protectedFlag)|\(creator)|\(commitment)"
|
||||||
|
|
||||||
let packet = BitchatPacket(
|
let packet = BitchatPacket(
|
||||||
type: MessageType.roomAnnounce.rawValue,
|
type: MessageType.roomAnnounce.rawValue,
|
||||||
@@ -719,9 +720,14 @@ class BluetoothMeshService: NSObject {
|
|||||||
// Encrypt the content
|
// Encrypt the content
|
||||||
guard let contentData = content.data(using: .utf8) else { return }
|
guard let contentData = content.data(using: .utf8) else { return }
|
||||||
|
|
||||||
|
// Debug logging
|
||||||
|
let keyData = roomKey.withUnsafeBytes { Data($0) }
|
||||||
|
bitchatLog("Encrypting message for room \(room) with key hash: \(keyData.prefix(8).hexEncodedString())", category: "crypto")
|
||||||
|
|
||||||
do {
|
do {
|
||||||
let sealedBox = try AES.GCM.seal(contentData, using: roomKey)
|
let sealedBox = try AES.GCM.seal(contentData, using: roomKey)
|
||||||
let encryptedData = sealedBox.combined!
|
let encryptedData = sealedBox.combined!
|
||||||
|
bitchatLog("Successfully encrypted message, size: \(encryptedData.count) bytes", category: "crypto")
|
||||||
|
|
||||||
// Create message with encrypted content
|
// Create message with encrypted content
|
||||||
let message = BitchatMessage(
|
let message = BitchatMessage(
|
||||||
@@ -1309,12 +1315,15 @@ class BluetoothMeshService: NSObject {
|
|||||||
// Handle encrypted room messages
|
// Handle encrypted room messages
|
||||||
var finalContent = message.content
|
var finalContent = message.content
|
||||||
if message.isEncrypted, let room = message.room, let encryptedData = message.encryptedContent {
|
if message.isEncrypted, let room = message.room, let encryptedData = message.encryptedContent {
|
||||||
|
bitchatLog("Processing encrypted message in room \(room), encrypted data size: \(encryptedData.count)", category: "crypto")
|
||||||
// Try to decrypt the content
|
// Try to decrypt the content
|
||||||
if let decryptedContent = self.delegate?.decryptRoomMessage(encryptedData, room: room) {
|
if let decryptedContent = self.delegate?.decryptRoomMessage(encryptedData, room: room) {
|
||||||
finalContent = decryptedContent
|
finalContent = decryptedContent
|
||||||
|
bitchatLog("Successfully decrypted message in room \(room): \(decryptedContent.prefix(20))...", category: "crypto")
|
||||||
} else {
|
} else {
|
||||||
// Unable to decrypt - show placeholder
|
// Unable to decrypt - show placeholder
|
||||||
finalContent = "[Encrypted message - password required]"
|
finalContent = "[Encrypted message - password required]"
|
||||||
|
bitchatLog("Failed to decrypt message in room \(room) - will pass to ChatViewModel for re-attempt", category: "crypto")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1762,17 +1771,18 @@ class BluetoothMeshService: NSObject {
|
|||||||
|
|
||||||
case .roomAnnounce:
|
case .roomAnnounce:
|
||||||
if let payloadStr = String(data: packet.payload, encoding: .utf8) {
|
if let payloadStr = String(data: packet.payload, encoding: .utf8) {
|
||||||
// Parse payload: room|isProtected|creatorID
|
// Parse payload: room|isProtected|creatorID|keyCommitment
|
||||||
let components = payloadStr.split(separator: "|").map(String.init)
|
let components = payloadStr.split(separator: "|").map(String.init)
|
||||||
if components.count >= 3 {
|
if components.count >= 3 {
|
||||||
let room = components[0]
|
let room = components[0]
|
||||||
let isProtected = components[1] == "1"
|
let isProtected = components[1] == "1"
|
||||||
let creatorID = components[2]
|
let creatorID = components[2]
|
||||||
|
let keyCommitment = components.count >= 4 ? components[3] : nil
|
||||||
|
|
||||||
bitchatLog("Received room announcement: \(room) is \(isProtected ? "protected" : "public") by \(creatorID)", category: "room")
|
bitchatLog("Received room announcement: \(room) is \(isProtected ? "protected" : "public") by \(creatorID)" + (keyCommitment != nil ? " with commitment" : ""), category: "room")
|
||||||
|
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
self.delegate?.didReceivePasswordProtectedRoomAnnouncement(room, isProtected: isProtected, creatorID: creatorID)
|
self.delegate?.didReceivePasswordProtectedRoomAnnouncement(room, isProtected: isProtected, creatorID: creatorID, keyCommitment: keyCommitment)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Relay announcement
|
// Relay announcement
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
+175
-66
@@ -23,6 +23,9 @@ struct ContentView: View {
|
|||||||
@State private var passwordInput = ""
|
@State private var passwordInput = ""
|
||||||
@State private var showPasswordPrompt = false
|
@State private var showPasswordPrompt = false
|
||||||
@State private var passwordPromptInput = ""
|
@State private var passwordPromptInput = ""
|
||||||
|
@State private var showPasswordError = false
|
||||||
|
@State private var showCommandSuggestions = false
|
||||||
|
@State private var commandSuggestions: [String] = []
|
||||||
|
|
||||||
private var backgroundColor: Color {
|
private var backgroundColor: Color {
|
||||||
colorScheme == .dark ? Color.black : Color.white
|
colorScheme == .dark ? Color.black : Color.white
|
||||||
@@ -192,12 +195,23 @@ struct ContentView: View {
|
|||||||
}
|
}
|
||||||
Button("Join") {
|
Button("Join") {
|
||||||
if let room = viewModel.passwordPromptRoom, !passwordPromptInput.isEmpty {
|
if let room = viewModel.passwordPromptRoom, !passwordPromptInput.isEmpty {
|
||||||
viewModel.joinRoom(room, password: passwordPromptInput)
|
let success = viewModel.joinRoom(room, password: passwordPromptInput)
|
||||||
passwordPromptInput = ""
|
if success {
|
||||||
|
passwordPromptInput = ""
|
||||||
|
} else {
|
||||||
|
// Wrong password - show error
|
||||||
|
passwordPromptInput = ""
|
||||||
|
showPasswordError = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} message: {
|
} message: {
|
||||||
Text("This room is password protected. Enter the password to join.")
|
Text("Room \(viewModel.passwordPromptRoom ?? "") 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.")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -244,23 +258,46 @@ struct ContentView: View {
|
|||||||
.buttonStyle(.plain)
|
.buttonStyle(.plain)
|
||||||
} else if let currentRoom = viewModel.currentRoom {
|
} else if let currentRoom = viewModel.currentRoom {
|
||||||
// Room header
|
// Room header
|
||||||
HStack(spacing: 4) {
|
Button(action: {
|
||||||
Button(action: {
|
viewModel.switchToRoom(nil)
|
||||||
withAnimation(.spring(response: 0.3, dampingFraction: 0.8)) {
|
}) {
|
||||||
showSidebar.toggle()
|
HStack(spacing: 4) {
|
||||||
sidebarDragOffset = 0
|
Image(systemName: "chevron.left")
|
||||||
}
|
.font(.system(size: 12))
|
||||||
}) {
|
Text("back")
|
||||||
Text(currentRoom)
|
.font(.system(size: 14, design: .monospaced))
|
||||||
.font(.system(size: 18, weight: .medium, design: .monospaced))
|
|
||||||
.foregroundColor(Color.blue)
|
|
||||||
}
|
}
|
||||||
.buttonStyle(.plain)
|
.foregroundColor(textColor)
|
||||||
|
}
|
||||||
Spacer()
|
.buttonStyle(.plain)
|
||||||
|
|
||||||
// Password button for room creator
|
Spacer()
|
||||||
if viewModel.roomCreators[currentRoom] == viewModel.meshService.myPeerID || viewModel.roomCreators[currentRoom] == nil {
|
|
||||||
|
Button(action: {
|
||||||
|
withAnimation(.spring(response: 0.3, dampingFraction: 0.8)) {
|
||||||
|
showSidebar.toggle()
|
||||||
|
sidebarDragOffset = 0
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
HStack(spacing: 6) {
|
||||||
|
if viewModel.passwordProtectedRooms.contains(currentRoom) {
|
||||||
|
Image(systemName: "lock.fill")
|
||||||
|
.font(.system(size: 14))
|
||||||
|
.foregroundColor(Color.orange)
|
||||||
|
}
|
||||||
|
Text("room: \(currentRoom)")
|
||||||
|
.font(.system(size: 16, weight: .medium, design: .monospaced))
|
||||||
|
.foregroundColor(viewModel.passwordProtectedRooms.contains(currentRoom) ? Color.orange : Color.blue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
HStack(spacing: 8) {
|
||||||
|
// Password button for room creator only
|
||||||
|
if viewModel.roomCreators[currentRoom] == viewModel.meshService.myPeerID {
|
||||||
Button(action: {
|
Button(action: {
|
||||||
// Toggle password protection
|
// Toggle password protection
|
||||||
if viewModel.passwordProtectedRooms.contains(currentRoom) {
|
if viewModel.passwordProtectedRooms.contains(currentRoom) {
|
||||||
@@ -271,14 +308,9 @@ struct ContentView: View {
|
|||||||
passwordInputRoom = currentRoom
|
passwordInputRoom = currentRoom
|
||||||
}
|
}
|
||||||
}) {
|
}) {
|
||||||
Image(systemName: viewModel.passwordProtectedRooms.contains(currentRoom) ? "lock.open" : "lock")
|
Image(systemName: viewModel.passwordProtectedRooms.contains(currentRoom) ? "lock.fill" : "lock")
|
||||||
.font(.system(size: 14))
|
.font(.system(size: 16))
|
||||||
.foregroundColor(secondaryTextColor)
|
.foregroundColor(viewModel.passwordProtectedRooms.contains(currentRoom) ? Color.yellow : textColor)
|
||||||
.padding(6)
|
|
||||||
.overlay(
|
|
||||||
RoundedRectangle(cornerRadius: 4)
|
|
||||||
.stroke(secondaryTextColor.opacity(0.5), lineWidth: 1)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
.buttonStyle(.plain)
|
.buttonStyle(.plain)
|
||||||
}
|
}
|
||||||
@@ -287,31 +319,9 @@ struct ContentView: View {
|
|||||||
Button(action: {
|
Button(action: {
|
||||||
viewModel.leaveRoom(currentRoom)
|
viewModel.leaveRoom(currentRoom)
|
||||||
}) {
|
}) {
|
||||||
Text("leave room")
|
Text("leave")
|
||||||
.font(.system(size: 12, design: .monospaced))
|
.font(.system(size: 12, design: .monospaced))
|
||||||
.foregroundColor(Color.red)
|
.foregroundColor(Color.red)
|
||||||
.padding(.horizontal, 8)
|
|
||||||
.padding(.vertical, 2)
|
|
||||||
.overlay(
|
|
||||||
RoundedRectangle(cornerRadius: 4)
|
|
||||||
.stroke(Color.red.opacity(0.5), lineWidth: 1)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
.buttonStyle(.plain)
|
|
||||||
|
|
||||||
// Back to main button
|
|
||||||
Button(action: {
|
|
||||||
viewModel.switchToRoom(nil)
|
|
||||||
}) {
|
|
||||||
Text("main")
|
|
||||||
.font(.system(size: 12, design: .monospaced))
|
|
||||||
.foregroundColor(textColor)
|
|
||||||
.padding(.horizontal, 8)
|
|
||||||
.padding(.vertical, 2)
|
|
||||||
.overlay(
|
|
||||||
RoundedRectangle(cornerRadius: 4)
|
|
||||||
.stroke(textColor.opacity(0.5), lineWidth: 1)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
.buttonStyle(.plain)
|
.buttonStyle(.plain)
|
||||||
}
|
}
|
||||||
@@ -373,7 +383,7 @@ struct ContentView: View {
|
|||||||
let statusText = if !viewModel.isConnected {
|
let statusText = if !viewModel.isConnected {
|
||||||
"alone :/"
|
"alone :/"
|
||||||
} else if roomCount > 0 {
|
} else if roomCount > 0 {
|
||||||
"\(otherPeersCount) \(otherPeersCount == 1 ? "person" : "people") / \(roomCount) \(roomCount == 1 ? "room" : "rooms")"
|
"\(otherPeersCount) \(otherPeersCount == 1 ? "person" : "people")/\(roomCount) \(roomCount == 1 ? "room" : "rooms")"
|
||||||
} else {
|
} else {
|
||||||
"\(otherPeersCount) \(otherPeersCount == 1 ? "person" : "people")"
|
"\(otherPeersCount) \(otherPeersCount == 1 ? "person" : "people")"
|
||||||
}
|
}
|
||||||
@@ -419,6 +429,7 @@ struct ContentView: View {
|
|||||||
.font(.system(size: 14, design: .monospaced))
|
.font(.system(size: 14, design: .monospaced))
|
||||||
.fixedSize(horizontal: false, vertical: true)
|
.fixedSize(horizontal: false, vertical: true)
|
||||||
.frame(maxWidth: .infinity, alignment: .leading)
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
|
.textSelection(.enabled)
|
||||||
} else {
|
} else {
|
||||||
// Regular messages with tappable sender name
|
// Regular messages with tappable sender name
|
||||||
HStack(alignment: .top, spacing: 0) {
|
HStack(alignment: .top, spacing: 0) {
|
||||||
@@ -426,6 +437,7 @@ struct ContentView: View {
|
|||||||
Text("[\(viewModel.formatTimestamp(message.timestamp))] ")
|
Text("[\(viewModel.formatTimestamp(message.timestamp))] ")
|
||||||
.font(.system(size: 12, design: .monospaced))
|
.font(.system(size: 12, design: .monospaced))
|
||||||
.foregroundColor(secondaryTextColor)
|
.foregroundColor(secondaryTextColor)
|
||||||
|
.textSelection(.enabled)
|
||||||
|
|
||||||
// Tappable sender name
|
// Tappable sender name
|
||||||
if message.sender != viewModel.nickname {
|
if message.sender != viewModel.nickname {
|
||||||
@@ -445,6 +457,7 @@ struct ContentView: View {
|
|||||||
Text("<\(message.sender)>")
|
Text("<\(message.sender)>")
|
||||||
.font(.system(size: 12, weight: .medium, design: .monospaced))
|
.font(.system(size: 12, weight: .medium, design: .monospaced))
|
||||||
.foregroundColor(textColor)
|
.foregroundColor(textColor)
|
||||||
|
.textSelection(.enabled)
|
||||||
}
|
}
|
||||||
|
|
||||||
Text(" ")
|
Text(" ")
|
||||||
@@ -489,7 +502,57 @@ struct ContentView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private var inputView: some View {
|
private var inputView: some View {
|
||||||
HStack(alignment: .center, spacing: 4) {
|
VStack(spacing: 0) {
|
||||||
|
// Command suggestions
|
||||||
|
if showCommandSuggestions && !commandSuggestions.isEmpty {
|
||||||
|
VStack(alignment: .leading, spacing: 0) {
|
||||||
|
let commandDescriptions = [
|
||||||
|
"/j": "join or create a room",
|
||||||
|
"/list": "show your joined rooms",
|
||||||
|
"/w": "see who's online",
|
||||||
|
"/m": "send private message",
|
||||||
|
"/clear": "clear chat messages",
|
||||||
|
"/transfer": "transfer room ownership",
|
||||||
|
"/pass": "change room password"
|
||||||
|
]
|
||||||
|
|
||||||
|
ForEach(commandSuggestions, id: \.self) { command in
|
||||||
|
Button(action: {
|
||||||
|
// Replace current text with selected command
|
||||||
|
messageText = command + " "
|
||||||
|
showCommandSuggestions = false
|
||||||
|
commandSuggestions = []
|
||||||
|
}) {
|
||||||
|
HStack {
|
||||||
|
Text(command)
|
||||||
|
.font(.system(size: 11, design: .monospaced))
|
||||||
|
.foregroundColor(textColor)
|
||||||
|
.fontWeight(.medium)
|
||||||
|
Spacer()
|
||||||
|
if let description = commandDescriptions[command] {
|
||||||
|
Text(description)
|
||||||
|
.font(.system(size: 10, design: .monospaced))
|
||||||
|
.foregroundColor(secondaryTextColor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(.horizontal, 12)
|
||||||
|
.padding(.vertical, 3)
|
||||||
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
.background(Color.gray.opacity(0.1))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.background(backgroundColor)
|
||||||
|
.overlay(
|
||||||
|
RoundedRectangle(cornerRadius: 4)
|
||||||
|
.stroke(secondaryTextColor.opacity(0.3), lineWidth: 1)
|
||||||
|
)
|
||||||
|
.padding(.horizontal, 12)
|
||||||
|
.padding(.bottom, 4)
|
||||||
|
}
|
||||||
|
|
||||||
|
HStack(alignment: .center, spacing: 4) {
|
||||||
if viewModel.selectedPrivateChatPeer != nil {
|
if viewModel.selectedPrivateChatPeer != nil {
|
||||||
Text("<\(viewModel.nickname)> →")
|
Text("<\(viewModel.nickname)> →")
|
||||||
.font(.system(size: 12, weight: .medium, design: .monospaced))
|
.font(.system(size: 12, weight: .medium, design: .monospaced))
|
||||||
@@ -497,6 +560,13 @@ struct ContentView: View {
|
|||||||
.lineLimit(1)
|
.lineLimit(1)
|
||||||
.fixedSize()
|
.fixedSize()
|
||||||
.padding(.leading, 12)
|
.padding(.leading, 12)
|
||||||
|
} else if let currentRoom = viewModel.currentRoom, viewModel.passwordProtectedRooms.contains(currentRoom) {
|
||||||
|
Text("<\(viewModel.nickname)> →")
|
||||||
|
.font(.system(size: 12, weight: .medium, design: .monospaced))
|
||||||
|
.foregroundColor(Color.orange)
|
||||||
|
.lineLimit(1)
|
||||||
|
.fixedSize()
|
||||||
|
.padding(.leading, 12)
|
||||||
} else {
|
} else {
|
||||||
Text("<\(viewModel.nickname)>")
|
Text("<\(viewModel.nickname)>")
|
||||||
.font(.system(size: 12, weight: .medium, design: .monospaced))
|
.font(.system(size: 12, weight: .medium, design: .monospaced))
|
||||||
@@ -515,6 +585,27 @@ struct ContentView: View {
|
|||||||
// Get cursor position (approximate - end of text for now)
|
// Get cursor position (approximate - end of text for now)
|
||||||
let cursorPosition = newValue.count
|
let cursorPosition = newValue.count
|
||||||
viewModel.updateAutocomplete(for: newValue, cursorPosition: cursorPosition)
|
viewModel.updateAutocomplete(for: newValue, cursorPosition: cursorPosition)
|
||||||
|
|
||||||
|
// Check for command autocomplete
|
||||||
|
if newValue.hasPrefix("/") && newValue.count >= 1 {
|
||||||
|
let commandDescriptions = [
|
||||||
|
("/j", "join or create a room"),
|
||||||
|
("/list", "show your joined rooms"),
|
||||||
|
("/w", "see who's online"),
|
||||||
|
("/m", "send private message"),
|
||||||
|
("/clear", "clear chat messages"),
|
||||||
|
("/transfer", "transfer room ownership"),
|
||||||
|
("/pass", "change room password")
|
||||||
|
]
|
||||||
|
let input = newValue.lowercased()
|
||||||
|
commandSuggestions = commandDescriptions
|
||||||
|
.filter { $0.0.starts(with: input) }
|
||||||
|
.map { $0.0 }
|
||||||
|
showCommandSuggestions = !commandSuggestions.isEmpty
|
||||||
|
} else {
|
||||||
|
showCommandSuggestions = false
|
||||||
|
commandSuggestions = []
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.onSubmit {
|
.onSubmit {
|
||||||
sendMessage()
|
sendMessage()
|
||||||
@@ -523,13 +614,16 @@ struct ContentView: View {
|
|||||||
Button(action: sendMessage) {
|
Button(action: sendMessage) {
|
||||||
Image(systemName: "arrow.up.circle.fill")
|
Image(systemName: "arrow.up.circle.fill")
|
||||||
.font(.system(size: 20))
|
.font(.system(size: 20))
|
||||||
.foregroundColor(viewModel.selectedPrivateChatPeer != nil ? Color.orange : textColor)
|
.foregroundColor((viewModel.selectedPrivateChatPeer != nil ||
|
||||||
|
(viewModel.currentRoom != nil && viewModel.passwordProtectedRooms.contains(viewModel.currentRoom ?? "")))
|
||||||
|
? Color.orange : textColor)
|
||||||
}
|
}
|
||||||
.buttonStyle(.plain)
|
.buttonStyle(.plain)
|
||||||
.padding(.trailing, 12)
|
.padding(.trailing, 12)
|
||||||
}
|
}
|
||||||
.padding(.vertical, 8)
|
.padding(.vertical, 8)
|
||||||
.background(backgroundColor.opacity(0.95))
|
.background(backgroundColor.opacity(0.95))
|
||||||
|
}
|
||||||
.onAppear {
|
.onAppear {
|
||||||
isTextFieldFocused = true
|
isTextFieldFocused = true
|
||||||
}
|
}
|
||||||
@@ -574,9 +668,17 @@ struct ContentView: View {
|
|||||||
|
|
||||||
ForEach(Array(viewModel.joinedRooms).sorted(), id: \.self) { room in
|
ForEach(Array(viewModel.joinedRooms).sorted(), id: \.self) { room in
|
||||||
Button(action: {
|
Button(action: {
|
||||||
viewModel.switchToRoom(room)
|
// Check if room needs password and we don't have it
|
||||||
withAnimation(.spring()) {
|
if viewModel.passwordProtectedRooms.contains(room) && viewModel.roomKeys[room] == nil {
|
||||||
showSidebar = false
|
// Need password
|
||||||
|
viewModel.passwordPromptRoom = room
|
||||||
|
viewModel.showPasswordPrompt = true
|
||||||
|
} else {
|
||||||
|
// Can enter room
|
||||||
|
viewModel.switchToRoom(room)
|
||||||
|
withAnimation(.spring()) {
|
||||||
|
showSidebar = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}) {
|
}) {
|
||||||
HStack {
|
HStack {
|
||||||
@@ -607,7 +709,7 @@ struct ContentView: View {
|
|||||||
// Room controls
|
// Room controls
|
||||||
if viewModel.currentRoom == room {
|
if viewModel.currentRoom == room {
|
||||||
HStack(spacing: 4) {
|
HStack(spacing: 4) {
|
||||||
// Password button for room creator
|
// Password button for room creator only
|
||||||
if viewModel.roomCreators[room] == viewModel.meshService.myPeerID {
|
if viewModel.roomCreators[room] == viewModel.meshService.myPeerID {
|
||||||
Button(action: {
|
Button(action: {
|
||||||
// Toggle password protection
|
// Toggle password protection
|
||||||
@@ -619,14 +721,18 @@ struct ContentView: View {
|
|||||||
passwordInputRoom = room
|
passwordInputRoom = room
|
||||||
}
|
}
|
||||||
}) {
|
}) {
|
||||||
Image(systemName: viewModel.passwordProtectedRooms.contains(room) ? "lock.open" : "lock")
|
HStack(spacing: 2) {
|
||||||
.font(.system(size: 10))
|
Image(systemName: viewModel.passwordProtectedRooms.contains(room) ? "lock.fill" : "lock")
|
||||||
.foregroundColor(secondaryTextColor)
|
.font(.system(size: 10))
|
||||||
.padding(4)
|
}
|
||||||
.overlay(
|
.foregroundColor(viewModel.passwordProtectedRooms.contains(room) ? backgroundColor : secondaryTextColor)
|
||||||
RoundedRectangle(cornerRadius: 4)
|
.padding(.horizontal, 8)
|
||||||
.stroke(secondaryTextColor.opacity(0.5), lineWidth: 1)
|
.padding(.vertical, 2)
|
||||||
)
|
.background(viewModel.passwordProtectedRooms.contains(room) ? Color.orange : Color.clear)
|
||||||
|
.overlay(
|
||||||
|
RoundedRectangle(cornerRadius: 4)
|
||||||
|
.stroke(viewModel.passwordProtectedRooms.contains(room) ? Color.orange : secondaryTextColor.opacity(0.5), lineWidth: 1)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
.buttonStyle(.plain)
|
.buttonStyle(.plain)
|
||||||
}
|
}
|
||||||
@@ -846,22 +952,25 @@ struct MessageContentView: View {
|
|||||||
ForEach(Array(buildTextSegments().enumerated()), id: \.offset) { _, segment in
|
ForEach(Array(buildTextSegments().enumerated()), id: \.offset) { _, segment in
|
||||||
if segment.type == "hashtag" {
|
if segment.type == "hashtag" {
|
||||||
Button(action: {
|
Button(action: {
|
||||||
viewModel.joinRoom(segment.text)
|
_ = viewModel.joinRoom(segment.text)
|
||||||
}) {
|
}) {
|
||||||
Text(segment.text)
|
Text(segment.text)
|
||||||
.font(.system(size: 14, weight: .semibold, design: .monospaced))
|
.font(.system(size: 14, weight: .semibold, design: .monospaced))
|
||||||
.foregroundColor(Color.blue)
|
.foregroundColor(Color.blue)
|
||||||
.underline()
|
.underline()
|
||||||
|
.textSelection(.enabled)
|
||||||
}
|
}
|
||||||
.buttonStyle(.plain)
|
.buttonStyle(.plain)
|
||||||
} else if segment.type == "mention" {
|
} else if segment.type == "mention" {
|
||||||
Text(segment.text)
|
Text(segment.text)
|
||||||
.font(.system(size: 14, weight: .semibold, design: .monospaced))
|
.font(.system(size: 14, weight: .semibold, design: .monospaced))
|
||||||
.foregroundColor(Color.orange)
|
.foregroundColor(Color.orange)
|
||||||
|
.textSelection(.enabled)
|
||||||
} else {
|
} else {
|
||||||
Text(segment.text)
|
Text(segment.text)
|
||||||
.font(.system(size: 14, design: .monospaced))
|
.font(.system(size: 14, design: .monospaced))
|
||||||
.fontWeight(isMentioned ? .bold : .regular)
|
.fontWeight(isMentioned ? .bold : .regular)
|
||||||
|
.textSelection(.enabled)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ class BinaryProtocolTests: XCTestCase {
|
|||||||
func testPacketEncodingDecoding() {
|
func testPacketEncodingDecoding() {
|
||||||
// Test basic packet
|
// Test basic packet
|
||||||
let packet = BitchatPacket(
|
let packet = BitchatPacket(
|
||||||
version: 1,
|
|
||||||
type: MessageType.message.rawValue,
|
type: MessageType.message.rawValue,
|
||||||
senderID: Data("testuser".utf8),
|
senderID: Data("testuser".utf8),
|
||||||
recipientID: Data("recipient".utf8),
|
recipientID: Data("recipient".utf8),
|
||||||
|
|||||||
@@ -42,6 +42,68 @@ class BitchatMessageTests: XCTestCase {
|
|||||||
XCTAssertTrue(decoded.mentions?.contains("bob") ?? false)
|
XCTAssertTrue(decoded.mentions?.contains("bob") ?? false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func testRoomMessage() {
|
||||||
|
let roomMessage = BitchatMessage(
|
||||||
|
sender: "alice",
|
||||||
|
content: "Hello #general",
|
||||||
|
timestamp: Date(),
|
||||||
|
isRelay: false,
|
||||||
|
originalSender: nil,
|
||||||
|
isPrivate: false,
|
||||||
|
recipientNickname: nil,
|
||||||
|
senderPeerID: "alice123",
|
||||||
|
mentions: nil,
|
||||||
|
room: "#general"
|
||||||
|
)
|
||||||
|
|
||||||
|
guard let encoded = roomMessage.toBinaryPayload() else {
|
||||||
|
XCTFail("Failed to encode room message")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let decoded = BitchatMessage.fromBinaryPayload(encoded) else {
|
||||||
|
XCTFail("Failed to decode room message")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
XCTAssertEqual(decoded.room, "#general")
|
||||||
|
XCTAssertEqual(decoded.content, roomMessage.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,
|
||||||
|
room: "#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.room, "#secret")
|
||||||
|
XCTAssertEqual(decoded.content, "") // Content should be empty for encrypted messages
|
||||||
|
}
|
||||||
|
|
||||||
func testPrivateMessage() {
|
func testPrivateMessage() {
|
||||||
let privateMessage = BitchatMessage(
|
let privateMessage = BitchatMessage(
|
||||||
sender: "alice",
|
sender: "alice",
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleIdentifier</key>
|
||||||
|
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||||
|
<key>CFBundleInfoDictionaryVersion</key>
|
||||||
|
<string>6.0</string>
|
||||||
|
<key>CFBundleName</key>
|
||||||
|
<string>$(PRODUCT_NAME)</string>
|
||||||
|
<key>CFBundlePackageType</key>
|
||||||
|
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
|
||||||
|
<key>CFBundleShortVersionString</key>
|
||||||
|
<string>1.0</string>
|
||||||
|
<key>CFBundleVersion</key>
|
||||||
|
<string>1</string>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
@@ -39,9 +39,17 @@ class MessagePaddingTests: XCTestCase {
|
|||||||
XCTAssertTrue(MessagePadding.blockSizes.contains(blockSize) || blockSize == data.count)
|
XCTAssertTrue(MessagePadding.blockSizes.contains(blockSize) || blockSize == data.count)
|
||||||
|
|
||||||
let padded = MessagePadding.pad(data, toSize: blockSize)
|
let padded = MessagePadding.pad(data, toSize: blockSize)
|
||||||
let unpadded = MessagePadding.unpad(padded)
|
|
||||||
|
|
||||||
XCTAssertEqual(unpadded, data)
|
// 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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,10 +61,18 @@ class MessagePaddingTests: XCTestCase {
|
|||||||
XCTAssertEqual(blockSize, 2048)
|
XCTAssertEqual(blockSize, 2048)
|
||||||
|
|
||||||
let padded = MessagePadding.pad(largeData, toSize: blockSize)
|
let padded = MessagePadding.pad(largeData, toSize: blockSize)
|
||||||
XCTAssertEqual(padded.count, blockSize)
|
// Since padding needed (548 bytes) > 255, no padding is applied
|
||||||
|
XCTAssertEqual(padded.count, largeData.count)
|
||||||
|
XCTAssertEqual(padded, largeData)
|
||||||
|
|
||||||
let unpadded = MessagePadding.unpad(padded)
|
// Test with data that fits within PKCS#7 limits
|
||||||
XCTAssertEqual(unpadded, largeData)
|
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() {
|
func testInvalidPadding() {
|
||||||
|
|||||||
@@ -0,0 +1,437 @@
|
|||||||
|
//
|
||||||
|
// PasswordProtectedRoomTests.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 PasswordProtectedRoomTests: 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.passwordProtectedRooms.removeAll()
|
||||||
|
viewModel.roomCreators.removeAll()
|
||||||
|
viewModel.roomPasswords.removeAll()
|
||||||
|
viewModel.roomKeys.removeAll()
|
||||||
|
viewModel.joinedRooms.removeAll()
|
||||||
|
viewModel.roomMembers.removeAll()
|
||||||
|
viewModel.roomMessages.removeAll()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func clearAllUserDefaults() {
|
||||||
|
let defaults = UserDefaults.standard
|
||||||
|
defaults.removeObject(forKey: "bitchat_nickname")
|
||||||
|
defaults.removeObject(forKey: "bitchat_joined_rooms")
|
||||||
|
defaults.removeObject(forKey: "bitchat_password_protected_rooms")
|
||||||
|
defaults.removeObject(forKey: "bitchat_room_creators")
|
||||||
|
defaults.removeObject(forKey: "bitchat_room_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 room should always produce same key
|
||||||
|
let password = "secretPassword123"
|
||||||
|
let roomName = "#testroom"
|
||||||
|
|
||||||
|
let key1 = deriveRoomKey(from: password, roomName: roomName)
|
||||||
|
let key2 = deriveRoomKey(from: password, roomName: roomName)
|
||||||
|
|
||||||
|
// Keys should be identical
|
||||||
|
XCTAssertEqual(key1, key2, "Same password and room should produce same key")
|
||||||
|
}
|
||||||
|
|
||||||
|
func testDifferentPasswordsProduceDifferentKeys() {
|
||||||
|
let roomName = "#testroom"
|
||||||
|
let password1 = "password123"
|
||||||
|
let password2 = "different456"
|
||||||
|
|
||||||
|
let key1 = deriveRoomKey(from: password1, roomName: roomName)
|
||||||
|
let key2 = deriveRoomKey(from: password2, roomName: roomName)
|
||||||
|
|
||||||
|
XCTAssertNotEqual(key1, key2, "Different passwords should produce different keys")
|
||||||
|
}
|
||||||
|
|
||||||
|
func testDifferentRoomsProduceDifferentKeys() {
|
||||||
|
let password = "samePassword"
|
||||||
|
let room1 = "#room1"
|
||||||
|
let room2 = "#room2"
|
||||||
|
|
||||||
|
let key1 = deriveRoomKey(from: password, roomName: room1)
|
||||||
|
let key2 = deriveRoomKey(from: password, roomName: room2)
|
||||||
|
|
||||||
|
XCTAssertNotEqual(key1, key2, "Same password in different rooms should produce different keys")
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Room Creation and Joining Tests
|
||||||
|
|
||||||
|
func testJoinUnprotectedRoom() {
|
||||||
|
let roomName = "#public"
|
||||||
|
|
||||||
|
let success = viewModel.joinRoom(roomName)
|
||||||
|
|
||||||
|
XCTAssertTrue(success, "Should be able to join unprotected room")
|
||||||
|
XCTAssertTrue(viewModel.joinedRooms.contains(roomName))
|
||||||
|
XCTAssertEqual(viewModel.currentRoom, roomName)
|
||||||
|
XCTAssertTrue(viewModel.roomMembers[roomName]?.contains(viewModel.meshService.myPeerID) ?? false)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testCreatePasswordProtectedRoom() {
|
||||||
|
let roomName = "#private"
|
||||||
|
let password = "secret123"
|
||||||
|
|
||||||
|
// Join room first
|
||||||
|
let joinSuccess = viewModel.joinRoom(roomName)
|
||||||
|
XCTAssertTrue(joinSuccess)
|
||||||
|
|
||||||
|
// Set password
|
||||||
|
viewModel.setRoomPassword(password, for: roomName)
|
||||||
|
|
||||||
|
XCTAssertTrue(viewModel.passwordProtectedRooms.contains(roomName))
|
||||||
|
XCTAssertNotNil(viewModel.roomKeys[roomName])
|
||||||
|
XCTAssertEqual(viewModel.roomPasswords[roomName], password)
|
||||||
|
XCTAssertEqual(viewModel.roomCreators[roomName], viewModel.meshService.myPeerID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testJoinPasswordProtectedEmptyRoom() {
|
||||||
|
let roomName = "#protected"
|
||||||
|
let password = "test123"
|
||||||
|
|
||||||
|
// Simulate room being marked as password protected
|
||||||
|
viewModel.passwordProtectedRooms.insert(roomName)
|
||||||
|
|
||||||
|
// Try to join with password - should be accepted tentatively for empty room
|
||||||
|
let success = viewModel.joinRoom(roomName, password: password)
|
||||||
|
|
||||||
|
XCTAssertTrue(success, "Should accept tentative access to empty password-protected room")
|
||||||
|
XCTAssertNotNil(viewModel.roomKeys[roomName], "Should store key tentatively")
|
||||||
|
XCTAssertEqual(viewModel.roomPasswords[roomName], 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 testJoinPasswordProtectedRoomWithMessages() {
|
||||||
|
let roomName = "#secure"
|
||||||
|
let correctPassword = "correct123"
|
||||||
|
let wrongPassword = "wrong456"
|
||||||
|
let testMessage = "Test encrypted message"
|
||||||
|
|
||||||
|
// First, create the room and set password as creator
|
||||||
|
let _ = viewModel.joinRoom(roomName)
|
||||||
|
viewModel.setRoomPassword(correctPassword, for: roomName)
|
||||||
|
|
||||||
|
// Simulate an encrypted message in the room
|
||||||
|
let key = viewModel.roomKeys[roomName]!
|
||||||
|
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,
|
||||||
|
room: roomName,
|
||||||
|
encryptedContent: encryptedData,
|
||||||
|
isEncrypted: true
|
||||||
|
)
|
||||||
|
|
||||||
|
// Add to room messages
|
||||||
|
viewModel.roomMessages[roomName] = [encryptedMsg]
|
||||||
|
|
||||||
|
// Clear keys to simulate another user
|
||||||
|
viewModel.roomKeys.removeValue(forKey: roomName)
|
||||||
|
viewModel.roomPasswords.removeValue(forKey: roomName)
|
||||||
|
|
||||||
|
// Try to join with wrong password
|
||||||
|
let wrongSuccess = viewModel.joinRoom(roomName, password: wrongPassword)
|
||||||
|
XCTAssertFalse(wrongSuccess, "Should reject wrong password")
|
||||||
|
|
||||||
|
// Try to join with correct password
|
||||||
|
let correctSuccess = viewModel.joinRoom(roomName, password: correctPassword)
|
||||||
|
XCTAssertTrue(correctSuccess, "Should accept correct password")
|
||||||
|
XCTAssertNotNil(viewModel.roomKeys[roomName], "Should store key for correct password")
|
||||||
|
|
||||||
|
} catch {
|
||||||
|
XCTFail("Encryption failed: \(error)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Password Verification Tests
|
||||||
|
|
||||||
|
func testEncryptDecryptRoomMessage() {
|
||||||
|
let roomName = "#crypto"
|
||||||
|
let password = "cryptoKey"
|
||||||
|
let testMessage = "This is a secret message"
|
||||||
|
|
||||||
|
// Derive key
|
||||||
|
let key = deriveRoomKey(from: password, roomName: roomName)
|
||||||
|
|
||||||
|
// 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.roomKeys[roomName] = key
|
||||||
|
let decrypted = viewModel.decryptRoomMessage(encryptedData, room: roomName)
|
||||||
|
|
||||||
|
XCTAssertEqual(decrypted, testMessage, "Decrypted message should match original")
|
||||||
|
} catch {
|
||||||
|
XCTFail("Encryption failed: \(error)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testWrongPasswordFailsDecryption() {
|
||||||
|
let roomName = "#secure"
|
||||||
|
let correctPassword = "correct"
|
||||||
|
let wrongPassword = "wrong"
|
||||||
|
let testMessage = "Secret content"
|
||||||
|
|
||||||
|
// Encrypt with correct password
|
||||||
|
let correctKey = deriveRoomKey(from: correctPassword, roomName: roomName)
|
||||||
|
|
||||||
|
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 = deriveRoomKey(from: wrongPassword, roomName: roomName)
|
||||||
|
let decrypted = viewModel.decryptRoomMessage(encryptedData, room: roomName, testKey: wrongKey)
|
||||||
|
|
||||||
|
XCTAssertNil(decrypted, "Wrong password should fail to decrypt")
|
||||||
|
} catch {
|
||||||
|
XCTFail("Encryption failed: \(error)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Room Creator Tests
|
||||||
|
|
||||||
|
func testOnlyCreatorCanSetPassword() {
|
||||||
|
let roomName = "#owned"
|
||||||
|
let password = "ownerOnly"
|
||||||
|
|
||||||
|
// Join room (becomes creator)
|
||||||
|
let _ = viewModel.joinRoom(roomName)
|
||||||
|
|
||||||
|
// Set password as creator
|
||||||
|
viewModel.setRoomPassword(password, for: roomName)
|
||||||
|
XCTAssertTrue(viewModel.passwordProtectedRooms.contains(roomName))
|
||||||
|
|
||||||
|
// Simulate another user trying to set password
|
||||||
|
viewModel.roomCreators[roomName] = "otherUser123"
|
||||||
|
viewModel.setRoomPassword("hackerPassword", for: roomName)
|
||||||
|
|
||||||
|
// Password should not change
|
||||||
|
XCTAssertEqual(viewModel.roomPasswords[roomName], password, "Non-creator should not be able to change password")
|
||||||
|
}
|
||||||
|
|
||||||
|
func testCreatorCanRemovePassword() {
|
||||||
|
let roomName = "#changeable"
|
||||||
|
let password = "temporary"
|
||||||
|
|
||||||
|
// Create protected room
|
||||||
|
let _ = viewModel.joinRoom(roomName)
|
||||||
|
viewModel.setRoomPassword(password, for: roomName)
|
||||||
|
|
||||||
|
XCTAssertTrue(viewModel.passwordProtectedRooms.contains(roomName))
|
||||||
|
|
||||||
|
// Remove password
|
||||||
|
viewModel.removeRoomPassword(for: roomName)
|
||||||
|
|
||||||
|
XCTAssertFalse(viewModel.passwordProtectedRooms.contains(roomName))
|
||||||
|
XCTAssertNil(viewModel.roomKeys[roomName])
|
||||||
|
XCTAssertNil(viewModel.roomPasswords[roomName])
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Message Handling Tests
|
||||||
|
|
||||||
|
func testReceiveEncryptedMessageWithoutKey() {
|
||||||
|
let roomName = "#encrypted"
|
||||||
|
|
||||||
|
// Join room without password
|
||||||
|
let _ = viewModel.joinRoom(roomName)
|
||||||
|
|
||||||
|
// 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,
|
||||||
|
room: roomName,
|
||||||
|
encryptedContent: Data([1, 2, 3, 4]), // dummy encrypted data
|
||||||
|
isEncrypted: true
|
||||||
|
)
|
||||||
|
|
||||||
|
viewModel.didReceiveMessage(encryptedMessage)
|
||||||
|
|
||||||
|
// Should mark room as password protected
|
||||||
|
XCTAssertTrue(viewModel.passwordProtectedRooms.contains(roomName))
|
||||||
|
|
||||||
|
// Should add system message
|
||||||
|
let roomMessages = viewModel.roomMessages[roomName] ?? []
|
||||||
|
let hasSystemMessage = roomMessages.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 #testroom"
|
||||||
|
viewModel.sendMessage(input)
|
||||||
|
|
||||||
|
XCTAssertTrue(viewModel.joinedRooms.contains("#testroom"))
|
||||||
|
XCTAssertEqual(viewModel.currentRoom, "#testroom")
|
||||||
|
}
|
||||||
|
|
||||||
|
func testJoinCommandAlias() {
|
||||||
|
let input = "/j #quick"
|
||||||
|
viewModel.sendMessage(input)
|
||||||
|
|
||||||
|
XCTAssertTrue(viewModel.joinedRooms.contains("#quick"))
|
||||||
|
XCTAssertEqual(viewModel.currentRoom, "#quick")
|
||||||
|
}
|
||||||
|
|
||||||
|
func testInvalidRoomName() {
|
||||||
|
let input = "/j #invalid-room!"
|
||||||
|
viewModel.sendMessage(input)
|
||||||
|
|
||||||
|
XCTAssertFalse(viewModel.joinedRooms.contains("#invalid-room!"))
|
||||||
|
|
||||||
|
// Should have system message about invalid name
|
||||||
|
let hasErrorMessage = viewModel.messages.contains { $0.sender == "system" && $0.content.contains("Invalid room name") }
|
||||||
|
XCTAssertTrue(hasErrorMessage)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Key Commitment Tests
|
||||||
|
|
||||||
|
func testKeyCommitmentVerification() {
|
||||||
|
let roomName = "#commitment"
|
||||||
|
let password = "testpass123"
|
||||||
|
|
||||||
|
// Join and set password
|
||||||
|
let _ = viewModel.joinRoom(roomName)
|
||||||
|
viewModel.setRoomPassword(password, for: roomName)
|
||||||
|
|
||||||
|
// Verify key commitment was stored
|
||||||
|
XCTAssertNotNil(viewModel.roomKeyCommitments[roomName], "Should store key commitment")
|
||||||
|
|
||||||
|
// Simulate another user with the stored commitment
|
||||||
|
let commitment = viewModel.roomKeyCommitments[roomName]!
|
||||||
|
viewModel.roomKeys.removeValue(forKey: roomName)
|
||||||
|
viewModel.roomPasswords.removeValue(forKey: roomName)
|
||||||
|
|
||||||
|
// Manually set the commitment as if received from network
|
||||||
|
viewModel.roomKeyCommitments[roomName] = commitment
|
||||||
|
|
||||||
|
// Try with wrong password - should fail immediately
|
||||||
|
let wrongSuccess = viewModel.joinRoom(roomName, password: "wrongpass")
|
||||||
|
XCTAssertFalse(wrongSuccess, "Should reject wrong password via commitment check")
|
||||||
|
|
||||||
|
// Try with correct password - should succeed
|
||||||
|
let correctSuccess = viewModel.joinRoom(roomName, password: password)
|
||||||
|
XCTAssertTrue(correctSuccess, "Should accept correct password via commitment check")
|
||||||
|
}
|
||||||
|
|
||||||
|
func testOwnershipTransfer() {
|
||||||
|
let roomName = "#transfertest"
|
||||||
|
let password = "ownerpass"
|
||||||
|
|
||||||
|
// Create room and set password
|
||||||
|
let _ = viewModel.joinRoom(roomName)
|
||||||
|
viewModel.setRoomPassword(password, for: roomName)
|
||||||
|
|
||||||
|
// Verify creator is set
|
||||||
|
XCTAssertEqual(viewModel.roomCreators[roomName], viewModel.meshService.myPeerID)
|
||||||
|
|
||||||
|
// Simulate transfer (in real app would use /transfer command)
|
||||||
|
let newOwnerID = "newowner123"
|
||||||
|
viewModel.roomCreators[roomName] = newOwnerID
|
||||||
|
|
||||||
|
// Verify ownership changed
|
||||||
|
XCTAssertEqual(viewModel.roomCreators[roomName], newOwnerID)
|
||||||
|
XCTAssertNotEqual(viewModel.roomCreators[roomName], viewModel.meshService.myPeerID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Helper Extensions for Testing
|
||||||
|
|
||||||
|
extension PasswordProtectedRoomTests {
|
||||||
|
// Helper method to derive room key for testing
|
||||||
|
// This duplicates the logic from ChatViewModel for testing purposes
|
||||||
|
func deriveRoomKey(from password: String, roomName: String) -> SymmetricKey {
|
||||||
|
let salt = roomName.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)
|
||||||
|
}
|
||||||
|
}
|
||||||
+30
@@ -47,3 +47,33 @@ targets:
|
|||||||
CODE_SIGNING_ALLOWED: YES
|
CODE_SIGNING_ALLOWED: YES
|
||||||
ASSETCATALOG_COMPILER_APPICON_NAME: AppIcon
|
ASSETCATALOG_COMPILER_APPICON_NAME: AppIcon
|
||||||
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS: YES
|
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS: YES
|
||||||
|
|
||||||
|
bitchatTests_iOS:
|
||||||
|
type: bundle.unit-test
|
||||||
|
platform: iOS
|
||||||
|
sources:
|
||||||
|
- bitchatTests
|
||||||
|
dependencies:
|
||||||
|
- target: bitchat_iOS
|
||||||
|
settings:
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER: chat.bitchat.tests
|
||||||
|
INFOPLIST_FILE: bitchatTests/Info.plist
|
||||||
|
SWIFT_VERSION: 5.0
|
||||||
|
IPHONEOS_DEPLOYMENT_TARGET: 16.0
|
||||||
|
TEST_HOST: $(BUILT_PRODUCTS_DIR)/bitchat.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/bitchat
|
||||||
|
BUNDLE_LOADER: $(TEST_HOST)
|
||||||
|
|
||||||
|
bitchatTests_macOS:
|
||||||
|
type: bundle.unit-test
|
||||||
|
platform: macOS
|
||||||
|
sources:
|
||||||
|
- bitchatTests
|
||||||
|
dependencies:
|
||||||
|
- target: bitchat_macOS
|
||||||
|
settings:
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER: chat.bitchat.tests
|
||||||
|
INFOPLIST_FILE: bitchatTests/Info.plist
|
||||||
|
SWIFT_VERSION: 5.0
|
||||||
|
MACOSX_DEPLOYMENT_TARGET: 13.0
|
||||||
|
TEST_HOST: $(BUILT_PRODUCTS_DIR)/bitchat.app/Contents/MacOS/bitchat
|
||||||
|
BUNDLE_LOADER: $(TEST_HOST)
|
||||||
|
|||||||
Reference in New Issue
Block a user