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 = {
|
||||
|
||||
/* 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 */; };
|
||||
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 */; };
|
||||
56F0892E13CC0D1490A3691E /* BloomFilterTests.swift.disabled in Resources */ = {isa = PBXBuildFile; fileRef = 822AA698BDB7BB939B62A010 /* BloomFilterTests.swift.disabled */; };
|
||||
61C81ED5F679D5E973EE0C07 /* NotificationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3448F84BF86A42A3CC4A9379 /* NotificationService.swift */; };
|
||||
6A624D9359BCCAAE58CE0D64 /* LoggingService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96CDF6D0AF0F2052A6C7E634 /* LoggingService.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 */; };
|
||||
7A50E2F04A3515A7E90EEAE4 /* BluetoothMeshService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5C3D880FF8AE1673B20E1E3 /* BluetoothMeshService.swift */; };
|
||||
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 */; };
|
||||
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 */; };
|
||||
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 */; };
|
||||
CDAD6629EB69916B95C80DAF /* BinaryProtocolTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53D535D9CE0B875F47402290 /* BinaryProtocolTests.swift */; };
|
||||
D450CF41F207BDE1A1AAA56E /* ChatViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6B8F7B7D55092C2540A7996 /* ChatViewModel.swift */; };
|
||||
D948085736ED8E736C1DE3B0 /* BluetoothMeshService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5C3D880FF8AE1673B20E1E3 /* BluetoothMeshService.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 */; };
|
||||
F4A689F5F34125AE1BFD5599 /* PasswordProtectedRoomTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FB12482E4AD78B861C538449 /* PasswordProtectedRoomTests.swift */; };
|
||||
/* 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 */
|
||||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
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; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
FB12482E4AD78B861C538449 /* PasswordProtectedRoomTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PasswordProtectedRoomTests.swift; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXGroup section */
|
||||
@@ -53,6 +88,7 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
2F82C5FC8433F4064F079D1F /* bitchat */,
|
||||
C3D98EB3E1B455E321F519F4 /* bitchatTests */,
|
||||
9F37F9F2C353B58AC809E93B /* Products */,
|
||||
);
|
||||
sourceTree = "<group>";
|
||||
@@ -84,6 +120,8 @@
|
||||
children = (
|
||||
997D512074C64904D75DDD40 /* bitchat.app */,
|
||||
7EEBDA723E1CFD88758DA4AC /* bitchat.app */,
|
||||
C0DB1DE27F0AAB5092663E8E /* bitchatTests_iOS.xctest */,
|
||||
03C57F452B55FD0FD8F51421 /* bitchatTests_macOS.xctest */,
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
@@ -106,6 +144,19 @@
|
||||
path = Protocols;
|
||||
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 */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@@ -138,6 +189,44 @@
|
||||
productReference = 7EEBDA723E1CFD88758DA4AC /* bitchat.app */;
|
||||
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 */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 53EADEF7546F94DDF82271B9 /* Build configuration list for PBXNativeTarget "bitchat_iOS" */;
|
||||
@@ -186,6 +275,8 @@
|
||||
projectDirPath = "";
|
||||
projectRoot = "";
|
||||
targets = (
|
||||
6CB97DF2EA57234CB3E563B8 /* bitchatTests_iOS */,
|
||||
47FF23248747DD7CB666CB91 /* bitchatTests_macOS */,
|
||||
AF077EA0474EDEDE2C72716C /* bitchat_iOS */,
|
||||
0576A29205865664C0937536 /* bitchat_macOS */,
|
||||
);
|
||||
@@ -201,6 +292,22 @@
|
||||
);
|
||||
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 */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
@@ -246,9 +353,101 @@
|
||||
);
|
||||
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 */
|
||||
|
||||
/* 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 */
|
||||
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 */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
@@ -270,9 +469,31 @@
|
||||
PRODUCT_BUNDLE_IDENTIFIER = chat.bitchat;
|
||||
PRODUCT_NAME = bitchat;
|
||||
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;
|
||||
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;
|
||||
};
|
||||
@@ -297,9 +518,12 @@
|
||||
PRODUCT_BUNDLE_IDENTIFIER = chat.bitchat;
|
||||
PRODUCT_NAME = bitchat;
|
||||
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;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
TARGETED_DEVICE_FAMILY = 1;
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
@@ -479,6 +703,24 @@
|
||||
/* End XCBuildConfiguration 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" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
|
||||
@@ -107,7 +107,9 @@ struct BinaryProtocol {
|
||||
var offset = 0
|
||||
|
||||
// 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 ttl = data[offset]; offset += 1
|
||||
|
||||
|
||||
@@ -18,15 +18,18 @@ struct MessagePadding {
|
||||
static func pad(_ data: Data, toSize targetSize: Int) -> Data {
|
||||
guard data.count < targetSize else { return data }
|
||||
|
||||
var padded = data
|
||||
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)
|
||||
_ = SecRandomCopyBytes(kSecRandomDefault, paddingNeeded - 1, &randomBytes)
|
||||
padded.append(contentsOf: randomBytes)
|
||||
|
||||
// Last byte indicates padding length (PKCS#7 style)
|
||||
padded.append(UInt8(paddingNeeded))
|
||||
|
||||
return padded
|
||||
@@ -161,7 +164,7 @@ protocol BitchatDelegate: AnyObject {
|
||||
func didDisconnectFromPeer(_ peerID: String)
|
||||
func didUpdatePeerList(_ peers: [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?
|
||||
|
||||
// Optional method to check if a fingerprint belongs to a favorite peer
|
||||
@@ -178,7 +181,7 @@ extension BitchatDelegate {
|
||||
// Default empty implementation
|
||||
}
|
||||
|
||||
func didReceivePasswordProtectedRoomAnnouncement(_ room: String, isProtected: Bool, creatorID: String?) {
|
||||
func didReceivePasswordProtectedRoomAnnouncement(_ room: String, isProtected: Bool, creatorID: String?, keyCommitment: String?) {
|
||||
// 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
|
||||
guard let self = self else { return }
|
||||
|
||||
// Payload format: room|isProtected|creatorID
|
||||
// Payload format: room|isProtected|creatorID|keyCommitment
|
||||
let protectedFlag = isProtected ? "1" : "0"
|
||||
let creator = creatorID ?? self.myPeerID
|
||||
let payload = "\(room)|\(protectedFlag)|\(creator)"
|
||||
let commitment = keyCommitment ?? ""
|
||||
let payload = "\(room)|\(protectedFlag)|\(creator)|\(commitment)"
|
||||
|
||||
let packet = BitchatPacket(
|
||||
type: MessageType.roomAnnounce.rawValue,
|
||||
@@ -719,9 +720,14 @@ class BluetoothMeshService: NSObject {
|
||||
// Encrypt the content
|
||||
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 {
|
||||
let sealedBox = try AES.GCM.seal(contentData, using: roomKey)
|
||||
let encryptedData = sealedBox.combined!
|
||||
bitchatLog("Successfully encrypted message, size: \(encryptedData.count) bytes", category: "crypto")
|
||||
|
||||
// Create message with encrypted content
|
||||
let message = BitchatMessage(
|
||||
@@ -1309,12 +1315,15 @@ class BluetoothMeshService: NSObject {
|
||||
// Handle encrypted room messages
|
||||
var finalContent = message.content
|
||||
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
|
||||
if let decryptedContent = self.delegate?.decryptRoomMessage(encryptedData, room: room) {
|
||||
finalContent = decryptedContent
|
||||
bitchatLog("Successfully decrypted message in room \(room): \(decryptedContent.prefix(20))...", category: "crypto")
|
||||
} else {
|
||||
// Unable to decrypt - show placeholder
|
||||
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:
|
||||
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)
|
||||
if components.count >= 3 {
|
||||
let room = components[0]
|
||||
let isProtected = components[1] == "1"
|
||||
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 {
|
||||
self.delegate?.didReceivePasswordProtectedRoomAnnouncement(room, isProtected: isProtected, creatorID: creatorID)
|
||||
self.delegate?.didReceivePasswordProtectedRoomAnnouncement(room, isProtected: isProtected, creatorID: creatorID, keyCommitment: keyCommitment)
|
||||
}
|
||||
|
||||
// Relay announcement
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+154
-45
@@ -23,6 +23,9 @@ struct ContentView: View {
|
||||
@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] = []
|
||||
|
||||
private var backgroundColor: Color {
|
||||
colorScheme == .dark ? Color.black : Color.white
|
||||
@@ -192,12 +195,23 @@ struct ContentView: View {
|
||||
}
|
||||
Button("Join") {
|
||||
if let room = viewModel.passwordPromptRoom, !passwordPromptInput.isEmpty {
|
||||
viewModel.joinRoom(room, password: passwordPromptInput)
|
||||
let success = viewModel.joinRoom(room, password: passwordPromptInput)
|
||||
if success {
|
||||
passwordPromptInput = ""
|
||||
} else {
|
||||
// Wrong password - show error
|
||||
passwordPromptInput = ""
|
||||
showPasswordError = true
|
||||
}
|
||||
}
|
||||
}
|
||||
} 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)
|
||||
} else if let currentRoom = viewModel.currentRoom {
|
||||
// Room header
|
||||
Button(action: {
|
||||
viewModel.switchToRoom(nil)
|
||||
}) {
|
||||
HStack(spacing: 4) {
|
||||
Image(systemName: "chevron.left")
|
||||
.font(.system(size: 12))
|
||||
Text("back")
|
||||
.font(.system(size: 14, design: .monospaced))
|
||||
}
|
||||
.foregroundColor(textColor)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
|
||||
Spacer()
|
||||
|
||||
Button(action: {
|
||||
withAnimation(.spring(response: 0.3, dampingFraction: 0.8)) {
|
||||
showSidebar.toggle()
|
||||
sidebarDragOffset = 0
|
||||
}
|
||||
}) {
|
||||
Text(currentRoom)
|
||||
.font(.system(size: 18, weight: .medium, design: .monospaced))
|
||||
.foregroundColor(Color.blue)
|
||||
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()
|
||||
|
||||
// Password button for room creator
|
||||
if viewModel.roomCreators[currentRoom] == viewModel.meshService.myPeerID || viewModel.roomCreators[currentRoom] == nil {
|
||||
HStack(spacing: 8) {
|
||||
// Password button for room creator only
|
||||
if viewModel.roomCreators[currentRoom] == viewModel.meshService.myPeerID {
|
||||
Button(action: {
|
||||
// Toggle password protection
|
||||
if viewModel.passwordProtectedRooms.contains(currentRoom) {
|
||||
@@ -271,14 +308,9 @@ struct ContentView: View {
|
||||
passwordInputRoom = currentRoom
|
||||
}
|
||||
}) {
|
||||
Image(systemName: viewModel.passwordProtectedRooms.contains(currentRoom) ? "lock.open" : "lock")
|
||||
.font(.system(size: 14))
|
||||
.foregroundColor(secondaryTextColor)
|
||||
.padding(6)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 4)
|
||||
.stroke(secondaryTextColor.opacity(0.5), lineWidth: 1)
|
||||
)
|
||||
Image(systemName: viewModel.passwordProtectedRooms.contains(currentRoom) ? "lock.fill" : "lock")
|
||||
.font(.system(size: 16))
|
||||
.foregroundColor(viewModel.passwordProtectedRooms.contains(currentRoom) ? Color.yellow : textColor)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
@@ -287,31 +319,9 @@ struct ContentView: View {
|
||||
Button(action: {
|
||||
viewModel.leaveRoom(currentRoom)
|
||||
}) {
|
||||
Text("leave room")
|
||||
Text("leave")
|
||||
.font(.system(size: 12, design: .monospaced))
|
||||
.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)
|
||||
}
|
||||
@@ -419,6 +429,7 @@ struct ContentView: View {
|
||||
.font(.system(size: 14, design: .monospaced))
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.textSelection(.enabled)
|
||||
} else {
|
||||
// Regular messages with tappable sender name
|
||||
HStack(alignment: .top, spacing: 0) {
|
||||
@@ -426,6 +437,7 @@ struct ContentView: View {
|
||||
Text("[\(viewModel.formatTimestamp(message.timestamp))] ")
|
||||
.font(.system(size: 12, design: .monospaced))
|
||||
.foregroundColor(secondaryTextColor)
|
||||
.textSelection(.enabled)
|
||||
|
||||
// Tappable sender name
|
||||
if message.sender != viewModel.nickname {
|
||||
@@ -445,6 +457,7 @@ struct ContentView: View {
|
||||
Text("<\(message.sender)>")
|
||||
.font(.system(size: 12, weight: .medium, design: .monospaced))
|
||||
.foregroundColor(textColor)
|
||||
.textSelection(.enabled)
|
||||
}
|
||||
|
||||
Text(" ")
|
||||
@@ -489,6 +502,56 @@ struct ContentView: View {
|
||||
}
|
||||
|
||||
private var inputView: some View {
|
||||
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 {
|
||||
Text("<\(viewModel.nickname)> →")
|
||||
@@ -497,6 +560,13 @@ struct ContentView: View {
|
||||
.lineLimit(1)
|
||||
.fixedSize()
|
||||
.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 {
|
||||
Text("<\(viewModel.nickname)>")
|
||||
.font(.system(size: 12, weight: .medium, design: .monospaced))
|
||||
@@ -515,6 +585,27 @@ struct ContentView: View {
|
||||
// Get cursor position (approximate - end of text for now)
|
||||
let cursorPosition = newValue.count
|
||||
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 {
|
||||
sendMessage()
|
||||
@@ -523,13 +614,16 @@ struct ContentView: View {
|
||||
Button(action: sendMessage) {
|
||||
Image(systemName: "arrow.up.circle.fill")
|
||||
.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)
|
||||
.padding(.trailing, 12)
|
||||
}
|
||||
.padding(.vertical, 8)
|
||||
.background(backgroundColor.opacity(0.95))
|
||||
}
|
||||
.onAppear {
|
||||
isTextFieldFocused = true
|
||||
}
|
||||
@@ -574,10 +668,18 @@ struct ContentView: View {
|
||||
|
||||
ForEach(Array(viewModel.joinedRooms).sorted(), id: \.self) { room in
|
||||
Button(action: {
|
||||
// Check if room needs password and we don't have it
|
||||
if viewModel.passwordProtectedRooms.contains(room) && viewModel.roomKeys[room] == nil {
|
||||
// Need password
|
||||
viewModel.passwordPromptRoom = room
|
||||
viewModel.showPasswordPrompt = true
|
||||
} else {
|
||||
// Can enter room
|
||||
viewModel.switchToRoom(room)
|
||||
withAnimation(.spring()) {
|
||||
showSidebar = false
|
||||
}
|
||||
}
|
||||
}) {
|
||||
HStack {
|
||||
// Lock icon for password protected rooms
|
||||
@@ -607,7 +709,7 @@ struct ContentView: View {
|
||||
// Room controls
|
||||
if viewModel.currentRoom == room {
|
||||
HStack(spacing: 4) {
|
||||
// Password button for room creator
|
||||
// Password button for room creator only
|
||||
if viewModel.roomCreators[room] == viewModel.meshService.myPeerID {
|
||||
Button(action: {
|
||||
// Toggle password protection
|
||||
@@ -619,13 +721,17 @@ struct ContentView: View {
|
||||
passwordInputRoom = room
|
||||
}
|
||||
}) {
|
||||
Image(systemName: viewModel.passwordProtectedRooms.contains(room) ? "lock.open" : "lock")
|
||||
HStack(spacing: 2) {
|
||||
Image(systemName: viewModel.passwordProtectedRooms.contains(room) ? "lock.fill" : "lock")
|
||||
.font(.system(size: 10))
|
||||
.foregroundColor(secondaryTextColor)
|
||||
.padding(4)
|
||||
}
|
||||
.foregroundColor(viewModel.passwordProtectedRooms.contains(room) ? backgroundColor : secondaryTextColor)
|
||||
.padding(.horizontal, 8)
|
||||
.padding(.vertical, 2)
|
||||
.background(viewModel.passwordProtectedRooms.contains(room) ? Color.orange : Color.clear)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 4)
|
||||
.stroke(secondaryTextColor.opacity(0.5), lineWidth: 1)
|
||||
.stroke(viewModel.passwordProtectedRooms.contains(room) ? Color.orange : secondaryTextColor.opacity(0.5), lineWidth: 1)
|
||||
)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
@@ -846,22 +952,25 @@ struct MessageContentView: View {
|
||||
ForEach(Array(buildTextSegments().enumerated()), id: \.offset) { _, segment in
|
||||
if segment.type == "hashtag" {
|
||||
Button(action: {
|
||||
viewModel.joinRoom(segment.text)
|
||||
_ = viewModel.joinRoom(segment.text)
|
||||
}) {
|
||||
Text(segment.text)
|
||||
.font(.system(size: 14, weight: .semibold, design: .monospaced))
|
||||
.foregroundColor(Color.blue)
|
||||
.underline()
|
||||
.textSelection(.enabled)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
} else if segment.type == "mention" {
|
||||
Text(segment.text)
|
||||
.font(.system(size: 14, weight: .semibold, design: .monospaced))
|
||||
.foregroundColor(Color.orange)
|
||||
.textSelection(.enabled)
|
||||
} else {
|
||||
Text(segment.text)
|
||||
.font(.system(size: 14, design: .monospaced))
|
||||
.fontWeight(isMentioned ? .bold : .regular)
|
||||
.textSelection(.enabled)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,6 @@ class BinaryProtocolTests: XCTestCase {
|
||||
func testPacketEncodingDecoding() {
|
||||
// Test basic packet
|
||||
let packet = BitchatPacket(
|
||||
version: 1,
|
||||
type: MessageType.message.rawValue,
|
||||
senderID: Data("testuser".utf8),
|
||||
recipientID: Data("recipient".utf8),
|
||||
|
||||
@@ -42,6 +42,68 @@ class BitchatMessageTests: XCTestCase {
|
||||
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() {
|
||||
let privateMessage = BitchatMessage(
|
||||
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)
|
||||
|
||||
let padded = MessagePadding.pad(data, toSize: blockSize)
|
||||
let unpadded = MessagePadding.unpad(padded)
|
||||
|
||||
// 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)
|
||||
|
||||
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)
|
||||
XCTAssertEqual(unpadded, 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() {
|
||||
|
||||
@@ -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
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME: AppIcon
|
||||
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