diff --git a/bitchat.xcodeproj/project.pbxproj b/bitchat.xcodeproj/project.pbxproj index 94b921ed..a67dfbc5 100644 --- a/bitchat.xcodeproj/project.pbxproj +++ b/bitchat.xcodeproj/project.pbxproj @@ -19,6 +19,10 @@ 047502902E5417660083520F /* LocationChannelsSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0475028E2E5417660083520F /* LocationChannelsSheet.swift */; }; 047502922E547ACC0083520F /* LocationChannelsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 047502912E547ACC0083520F /* LocationChannelsTests.swift */; }; 047502932E547ACC0083520F /* LocationChannelsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 047502912E547ACC0083520F /* LocationChannelsTests.swift */; }; + 047502AC2E55E8360083520F /* BinaryProtocolPaddingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 047502AB2E55E8360083520F /* BinaryProtocolPaddingTests.swift */; }; + 047502AD2E55E8360083520F /* BinaryProtocolPaddingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 047502AB2E55E8360083520F /* BinaryProtocolPaddingTests.swift */; }; + 047502B02E55E8450083520F /* InputValidatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 047502AE2E55E8450083520F /* InputValidatorTests.swift */; }; + 047502B12E55E8450083520F /* InputValidatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 047502AE2E55E8450083520F /* InputValidatorTests.swift */; }; 049BD3902E4EC4F0001A566B /* PrivateChatManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 049BD38F2E4EC4F0001A566B /* PrivateChatManager.swift */; }; 049BD3912E4EC4F0001A566B /* AutocompleteService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 049BD38C2E4EC4F0001A566B /* AutocompleteService.swift */; }; 049BD3922E4EC4F0001A566B /* CommandProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 049BD38D2E4EC4F0001A566B /* CommandProcessor.swift */; }; @@ -174,6 +178,8 @@ 0475028B2E54171C0083520F /* LocationChannelManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationChannelManager.swift; sourceTree = ""; }; 0475028E2E5417660083520F /* LocationChannelsSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationChannelsSheet.swift; sourceTree = ""; }; 047502912E547ACC0083520F /* LocationChannelsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationChannelsTests.swift; sourceTree = ""; }; + 047502AB2E55E8360083520F /* BinaryProtocolPaddingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BinaryProtocolPaddingTests.swift; sourceTree = ""; }; + 047502AE2E55E8450083520F /* InputValidatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InputValidatorTests.swift; sourceTree = ""; }; 049BD38C2E4EC4F0001A566B /* AutocompleteService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutocompleteService.swift; sourceTree = ""; }; 049BD38D2E4EC4F0001A566B /* CommandProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommandProcessor.swift; sourceTree = ""; }; 049BD38F2E4EC4F0001A566B /* PrivateChatManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivateChatManager.swift; sourceTree = ""; }; @@ -197,7 +203,7 @@ 3448F84BF86A42A3CC4A9379 /* NotificationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationService.swift; sourceTree = ""; }; 3668EEBB42FD4A24D5D83B7B /* bitchatShareExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = bitchatShareExtension.entitlements; sourceTree = ""; }; 394E8A1AC76EFAE352075BE9 /* NoiseEncryptionService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoiseEncryptionService.swift; sourceTree = ""; }; - 3A556661F74B7D5AE2F0521B /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; + 3A556661F74B7D5AE2F0521B /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 3A69677D382F1C3D5ED03F7D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 419BFFF209EBA93F410E9E9F /* FavoritesPersistenceService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoritesPersistenceService.swift; sourceTree = ""; }; 43613045E63D21D429396805 /* NoiseProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoiseProtocol.swift; sourceTree = ""; }; @@ -267,6 +273,14 @@ path = Fragmentation; sourceTree = ""; }; + 047502AF2E55E8450083520F /* Utils */ = { + isa = PBXGroup; + children = ( + 047502AE2E55E8450083520F /* InputValidatorTests.swift */, + ); + path = Utils; + sourceTree = ""; + }; 0575DCBD15C7C719ADDCB67E /* Models */ = { isa = PBXGroup; children = ( @@ -346,6 +360,7 @@ 84933DAE9D7E5D0155BA7AEA /* Protocol */ = { isa = PBXGroup; children = ( + 047502AB2E55E8360083520F /* BinaryProtocolPaddingTests.swift */, 0B3CC6FA298729906109F61B /* BinaryProtocolTests.swift */, ); path = Protocol; @@ -443,6 +458,7 @@ D80E19E04513C0046D611574 /* Noise */, 84933DAE9D7E5D0155BA7AEA /* Protocol */, 204CC4C7704C7348D456E374 /* TestUtilities */, + 047502AF2E55E8450083520F /* Utils */, ); path = bitchatTests; sourceTree = ""; @@ -785,6 +801,7 @@ AA6E067DB034FC0FA23C28A9 /* BinaryProtocolTests.swift in Sources */, 047502802E53A0FC0083520F /* FragmentationTests.swift in Sources */, 8F282E9CCA5AE1ECC001D2E4 /* IntegrationTests.swift in Sources */, + 047502B12E55E8450083520F /* InputValidatorTests.swift in Sources */, D727EA273CB214FC32612469 /* MockBluetoothMeshService.swift in Sources */, 047502932E547ACC0083520F /* LocationChannelsTests.swift in Sources */, 6C803BF930E7E19BE6E99EAA /* MockBLEService.swift in Sources */, @@ -794,6 +811,7 @@ A0A1C26EFBFDD5B8EFEEDE57 /* PublicChatE2ETests.swift in Sources */, A2977428C1D9EF9944C4BFAF /* BLEServiceTests.swift in Sources */, 2EFCCAA297B16FA2B56747C7 /* TestConstants.swift in Sources */, + 047502AD2E55E8360083520F /* BinaryProtocolPaddingTests.swift in Sources */, B45AD5BF95220A0289216D32 /* TestHelpers.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -805,6 +823,7 @@ 0B6F25559A21F8C69C8357C6 /* BinaryProtocolTests.swift in Sources */, 047502812E53A0FC0083520F /* FragmentationTests.swift in Sources */, 686441ABC2AF83EE98E6ECF2 /* IntegrationTests.swift in Sources */, + 047502B02E55E8450083520F /* InputValidatorTests.swift in Sources */, 8851F08D88C5B1DE7B9F55C6 /* MockBluetoothMeshService.swift in Sources */, 047502922E547ACC0083520F /* LocationChannelsTests.swift in Sources */, 3849CA6D99B2D536636DF4A6 /* MockBLEService.swift in Sources */, @@ -814,6 +833,7 @@ 8CE446C9364F54DF89E7A364 /* PublicChatE2ETests.swift in Sources */, BE729E149C98F775D9622D9C /* BLEServiceTests.swift in Sources */, 8D0196EAEE56973679F6A655 /* TestConstants.swift in Sources */, + 047502AC2E55E8360083520F /* BinaryProtocolPaddingTests.swift in Sources */, 37DDF3D09E2BAB92A5A8A9C1 /* TestHelpers.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/bitchat/Noise/NoiseSecurityConsiderations.swift b/bitchat/Noise/NoiseSecurityConsiderations.swift index deda4756..929d6df9 100644 --- a/bitchat/Noise/NoiseSecurityConsiderations.swift +++ b/bitchat/Noise/NoiseSecurityConsiderations.swift @@ -53,13 +53,9 @@ struct NoiseSecurityValidator { return data.count <= NoiseSecurityConstants.maxHandshakeMessageSize } - /// Validate peer ID format + /// Validate peer ID format using unified validator static func validatePeerID(_ peerID: String) -> Bool { - // Peer ID should be reasonable length and contain valid characters - let validCharset = CharacterSet.alphanumerics.union(CharacterSet(charactersIn: "-_")) - return peerID.count > 0 && - peerID.count <= 64 && - peerID.rangeOfCharacter(from: validCharset.inverted) == nil + return InputValidator.validatePeerID(peerID) } } diff --git a/bitchat/Protocols/BinaryProtocol.swift b/bitchat/Protocols/BinaryProtocol.swift index 2b348c59..d1ef8959 100644 --- a/bitchat/Protocols/BinaryProtocol.swift +++ b/bitchat/Protocols/BinaryProtocol.swift @@ -45,7 +45,7 @@ /// /// ## Compression Strategy /// - Automatic compression for payloads > 256 bytes -/// - LZ4 algorithm for speed over ratio +/// - zlib compression for broad compatibility on Apple platforms /// - Original size stored for decompression /// - Flag bit indicates compressed payload /// @@ -117,7 +117,7 @@ struct BinaryProtocol { } // Encode BitchatPacket to binary format - static func encode(_ packet: BitchatPacket) -> Data? { + static func encode(_ packet: BitchatPacket, padding: Bool = true) -> Data? { var data = Data() @@ -200,11 +200,14 @@ struct BinaryProtocol { // Apply padding to standard block sizes for traffic analysis resistance - let optimalSize = MessagePadding.optimalBlockSize(for: data.count) - let paddedData = MessagePadding.pad(data, toSize: optimalSize) - - - return paddedData + if padding { + let optimalSize = MessagePadding.optimalBlockSize(for: data.count) + let paddedData = MessagePadding.pad(data, toSize: optimalSize) + return paddedData + } else { + // Caller explicitly requested no padding (e.g., BLE write path) + return data + } } // Decode binary data to BitchatPacket diff --git a/bitchat/Protocols/BitchatProtocol.swift b/bitchat/Protocols/BitchatProtocol.swift index 4f138c21..8f7e86c1 100644 --- a/bitchat/Protocols/BitchatProtocol.swift +++ b/bitchat/Protocols/BitchatProtocol.swift @@ -232,8 +232,13 @@ struct BitchatPacket: Codable { BinaryProtocol.encode(self) } + func toBinaryData(padding: Bool = true) -> Data? { + BinaryProtocol.encode(self, padding: padding) + } + + // Backward-compatible helper (defaults to padded encoding) func toBinaryData() -> Data? { - BinaryProtocol.encode(self) + toBinaryData(padding: true) } /// Create binary representation for signing (without signature and TTL fields) diff --git a/bitchat/Services/BLEService.swift b/bitchat/Services/BLEService.swift index af024902..bbb89a8d 100644 --- a/bitchat/Services/BLEService.swift +++ b/bitchat/Services/BLEService.swift @@ -106,7 +106,7 @@ final class BLEService: NSObject { // MARK: - Maintenance Timer - private weak var maintenanceTimer: Timer? // Single timer for all maintenance tasks + private var maintenanceTimer: DispatchSourceTimer? // Single timer for all maintenance tasks private var maintenanceCounter = 0 // Track maintenance cycles // MARK: - Peer snapshots publisher (non-UI convenience) @@ -206,10 +206,14 @@ final class BLEService: NSObject { centralManager = CBCentralManager(delegate: self, queue: bleQueue) peripheralManager = CBPeripheralManager(delegate: self, queue: bleQueue) - // Single maintenance timer for all periodic tasks - maintenanceTimer = Timer.scheduledTimer(withTimeInterval: 10.0, repeats: true) { [weak self] _ in + // Single maintenance timer for all periodic tasks (dispatch-based for determinism) + let timer = DispatchSource.makeTimerSource(queue: bleQueue) + timer.schedule(deadline: .now() + 10.0, repeating: 10.0, leeway: .seconds(1)) + timer.setEventHandler { [weak self] in self?.performMaintenance() } + timer.resume() + maintenanceTimer = timer // Publish initial empty state publishFullPeerData() @@ -224,7 +228,7 @@ final class BLEService: NSObject { // No advertising policy to set; we never include Local Name in adverts. deinit { - maintenanceTimer?.invalidate() + maintenanceTimer?.cancel() centralManager?.stopScan() peripheralManager?.stopAdvertising() #if os(iOS) @@ -308,7 +312,7 @@ final class BLEService: NSObject { ) // Send immediately to all connected peers - if let data = leavePacket.toBinaryData() { + if let data = leavePacket.toBinaryData(padding: false) { // Send to peripherals we're connected to as central for state in peripherals.values where state.isConnected { if let characteristic = state.characteristic { @@ -331,7 +335,7 @@ final class BLEService: NSObject { } // Stop timer - maintenanceTimer?.invalidate() + maintenanceTimer?.cancel() maintenanceTimer = nil centralManager?.stopScan() @@ -718,12 +722,24 @@ final class BLEService: NSObject { // MARK: - Packet Broadcasting private func broadcastPacket(_ packet: BitchatPacket) { - guard let rawData = packet.toBinaryData() else { + // Balanced privacy: pad sensitive payloads (Noise handshake/encrypted), + // keep public/announce/leave unpadded to reduce airtime. + let padForBLE: Bool = { + if let t = MessageType(rawValue: packet.type) { + switch t { + case .noiseEncrypted, .noiseHandshake: + return true + default: + return false + } + } + return false + }() + + guard let data = packet.toBinaryData(padding: padForBLE) else { SecureLogger.log("❌ Failed to convert packet to binary data", category: SecureLogger.session, level: .error) return } - // Avoid sending padded data over BLE to reduce truncation risk - let data = MessagePadding.unpad(rawData) // Only log broadcasts for non-announce packets // Log encrypted and relayed packets for debugging @@ -737,7 +753,7 @@ final class BLEService: NSObject { // Check if application-level fragmentation needed for large messages // (CoreBluetooth only handles ATT-level fragmentation for single writes) if data.count > 512 && packet.type != MessageType.fragment.rawValue { - sendFragmentedPacket(packet) + sendFragmentedPacket(packet, pad: padForBLE) return } @@ -832,8 +848,8 @@ final class BLEService: NSObject { // Cannot deliver any BitChat frame via notifications on this link; skip notify SecureLogger.log("⚠️ Skipping notify: central max update length (\(minAllowed)) < 21", category: SecureLogger.session, level: .debug) } else if data.count > minAllowed { - // Fragment via protocol - sendFragmentedPacket(packet) + // Fragment via protocol (preserve chosen padding for BLE) + sendFragmentedPacket(packet, pad: padForBLE) return } let success = peripheralManager?.updateValue(data, for: characteristic, onSubscribedCentrals: nil) ?? false @@ -886,10 +902,9 @@ final class BLEService: NSObject { // MARK: - Fragmentation (Required for messages > BLE MTU) - private func sendFragmentedPacket(_ packet: BitchatPacket) { - guard let encoded = packet.toBinaryData() else { return } - // Fragment the unpadded frame; each fragment will be encoded (and padded) independently - let fullData = MessagePadding.unpad(encoded) + private func sendFragmentedPacket(_ packet: BitchatPacket, pad: Bool) { + guard let fullData = packet.toBinaryData(padding: pad) else { return } + // Fragment the unpadded frame; each fragment will be encoded independently let fragmentID = Data((0..<8).map { _ in UInt8.random(in: 0...255) }) let fragments = stride(from: 0, to: fullData.count, by: maxFragmentSize).map { offset in diff --git a/bitchat/Services/NostrTransport.swift b/bitchat/Services/NostrTransport.swift index c313c50c..7e42886d 100644 --- a/bitchat/Services/NostrTransport.swift +++ b/bitchat/Services/NostrTransport.swift @@ -37,7 +37,11 @@ final class NostrTransport: Transport { func getFingerprint(for peerID: String) -> String? { nil } func getNoiseSessionState(for peerID: String) -> LazyHandshakeState { .none } func triggerHandshake(with peerID: String) { /* no-op */ } - func getNoiseService() -> NoiseEncryptionService { NoiseEncryptionService() } + // Nostr does not use Noise sessions here; return a cached placeholder to avoid reallocation + private static var cachedNoiseService: NoiseEncryptionService = { + NoiseEncryptionService() + }() + func getNoiseService() -> NoiseEncryptionService { Self.cachedNoiseService } // Public broadcast not supported over Nostr here func sendMessage(_ content: String, mentions: [String]) { /* no-op */ } @@ -193,7 +197,7 @@ final class NostrTransport: Transport { guard let recipientNpub = recipientNostrPubkey else { return } guard let senderIdentity = try? NostrIdentityBridge.getCurrentNostrIdentity() else { return } SecureLogger.log("NostrTransport: preparing DELIVERED ack for id=\(messageID.prefix(8))… to \(recipientNpub.prefix(16))…", - category: SecureLogger.session, level: .info) + category: SecureLogger.session, level: .debug) let recipientHex: String do { let (hrp, data) = try Bech32.decode(recipientNpub) @@ -209,7 +213,7 @@ final class NostrTransport: Transport { return } SecureLogger.log("NostrTransport: sending DELIVERED ack giftWrap id=\(event.id.prefix(16))…", - category: SecureLogger.session, level: .info) + category: SecureLogger.session, level: .debug) NostrRelayManager.shared.sendEvent(event) } } diff --git a/bitchat/Utils/InputValidator.swift b/bitchat/Utils/InputValidator.swift index 82597242..0f5d110f 100644 --- a/bitchat/Utils/InputValidator.swift +++ b/bitchat/Utils/InputValidator.swift @@ -16,19 +16,17 @@ struct InputValidator { // MARK: - Peer ID Validation - /// Validates a peer ID from any source + /// Validates a peer ID from any source (short 16-hex, full 64-hex, or internal alnum/-/_ up to 64) static func validatePeerID(_ peerID: String) -> Bool { - // Handle both hex-encoded (from network) and alphanumeric (internal) formats - if peerID.count == Limits.hexPeerIDLength { - // Network format: 16 hex characters - return peerID.allSatisfy { $0.isHexDigit } - } else { - // Internal format: alphanumeric + dash/underscore - let validCharset = CharacterSet.alphanumerics.union(CharacterSet(charactersIn: "-_")) - return peerID.count > 0 && - peerID.count <= Limits.maxPeerIDLength && - peerID.rangeOfCharacter(from: validCharset.inverted) == nil - } + // Accept short routing IDs (16-hex) + if PeerIDResolver.isShortID(peerID) { return true } + // Accept full Noise key hex (64-hex) + if PeerIDResolver.isNoiseKeyHex(peerID) { return true } + // Internal format: alphanumeric + dash/underscore up to 64 + let validCharset = CharacterSet.alphanumerics.union(CharacterSet(charactersIn: "-_")) + return !peerID.isEmpty && + peerID.count <= Limits.maxPeerIDLength && + peerID.rangeOfCharacter(from: validCharset.inverted) == nil } // MARK: - String Content Validation diff --git a/bitchat/Views/ContentView.swift b/bitchat/Views/ContentView.swift index 88b60e1f..ac78fd8b 100644 --- a/bitchat/Views/ContentView.swift +++ b/bitchat/Views/ContentView.swift @@ -938,7 +938,9 @@ struct ContentView: View { switch locationManager.selectedChannel { case .location: let n = viewModel.geohashPeople.count - return (n, n > 0 ? Color.green : Color.secondary) + // Use standard green (dark: system green; light: custom darker green) + let standardGreen = (colorScheme == .dark) ? Color.green : Color(red: 0, green: 0.5, blue: 0) + return (n, n > 0 ? standardGreen : Color.secondary) case .mesh: let counts = viewModel.allPeers.reduce(into: (others: 0, mesh: 0)) { counts, peer in guard peer.id != viewModel.meshService.myPeerID else { return } @@ -1037,7 +1039,8 @@ struct ContentView: View { case .mesh: return Color.blue case .location: - return Color.green + // Standard green to avoid overly bright appearance in light mode + return (colorScheme == .dark) ? Color.green : Color(red: 0, green: 0.5, blue: 0) } }() Text(badgeText) diff --git a/bitchat/Views/LocationChannelsSheet.swift b/bitchat/Views/LocationChannelsSheet.swift index 3429566e..90b35e7e 100644 --- a/bitchat/Views/LocationChannelsSheet.swift +++ b/bitchat/Views/LocationChannelsSheet.swift @@ -6,6 +6,7 @@ struct LocationChannelsSheet: View { @Binding var isPresented: Bool @ObservedObject private var manager = LocationChannelManager.shared @EnvironmentObject var viewModel: ChatViewModel + @Environment(\.colorScheme) var colorScheme @State private var customGeohash: String = "" @State private var customError: String? = nil @@ -24,10 +25,10 @@ struct LocationChannelsSheet: View { Button(action: { manager.enableLocationChannels() }) { Text("get location and my geohashes") .font(.system(size: 12, design: .monospaced)) - .foregroundColor(Color.green) + .foregroundColor(standardGreen) .frame(maxWidth: .infinity) .padding(.vertical, 6) - .background(Color.green.opacity(0.12)) + .background(standardGreen.opacity(0.12)) .cornerRadius(6) } .buttonStyle(.plain) @@ -220,7 +221,7 @@ struct LocationChannelsSheet: View { if isSelected { Text("✔︎") .font(.system(size: 16, design: .monospaced)) - .foregroundColor(.green) + .foregroundColor(standardGreen) } } .frame(maxWidth: .infinity, alignment: .leading) @@ -274,4 +275,11 @@ struct LocationChannelsSheet: View { } } +// MARK: - Standardized Colors +extension LocationChannelsSheet { + private var standardGreen: Color { + (colorScheme == .dark) ? Color.green : Color(red: 0, green: 0.5, blue: 0) + } +} + #endif diff --git a/bitchatTests/Protocol/BinaryProtocolPaddingTests.swift b/bitchatTests/Protocol/BinaryProtocolPaddingTests.swift new file mode 100644 index 00000000..0edabf5d --- /dev/null +++ b/bitchatTests/Protocol/BinaryProtocolPaddingTests.swift @@ -0,0 +1,34 @@ +// +// BinaryProtocolPaddingTests.swift +// bitchatTests +// +// This is free and unencumbered software released into the public domain. +// + +import XCTest +@testable import bitchat + +final class BinaryProtocolPaddingTests: XCTestCase { + func test_padded_vs_unpadded_length() throws { + // Use helper to create a small test packet + let packet = TestHelpers.createTestPacket() + guard let padded = BinaryProtocol.encode(packet, padding: true) else { return XCTFail("encode padded") } + guard let unpadded = BinaryProtocol.encode(packet, padding: false) else { return XCTFail("encode unpadded") } + XCTAssertGreaterThanOrEqual(padded.count, unpadded.count, "Padded frame should be >= unpadded") + } + + func test_decode_padded_and_unpadded_round_trip() throws { + let packet = TestHelpers.createTestPacket() + // Padded + guard let padded = BinaryProtocol.encode(packet, padding: true) else { return XCTFail("encode padded") } + guard let dec1 = BinaryProtocol.decode(padded) else { return XCTFail("decode padded") } + XCTAssertEqual(dec1.type, packet.type) + XCTAssertEqual(dec1.payload, packet.payload) + // Unpadded + guard let unpadded = BinaryProtocol.encode(packet, padding: false) else { return XCTFail("encode unpadded") } + guard let dec2 = BinaryProtocol.decode(unpadded) else { return XCTFail("decode unpadded") } + XCTAssertEqual(dec2.type, packet.type) + XCTAssertEqual(dec2.payload, packet.payload) + } +} + diff --git a/bitchatTests/Utils/InputValidatorTests.swift b/bitchatTests/Utils/InputValidatorTests.swift new file mode 100644 index 00000000..f6b2617f --- /dev/null +++ b/bitchatTests/Utils/InputValidatorTests.swift @@ -0,0 +1,37 @@ +// +// InputValidatorTests.swift +// bitchatTests +// +// This is free and unencumbered software released into the public domain. +// + +import XCTest +@testable import bitchat + +final class InputValidatorTests: XCTestCase { + func test_accepts_short_hex_peer_id() { + XCTAssertTrue(InputValidator.validatePeerID("0011223344556677")) + XCTAssertTrue(InputValidator.validatePeerID("aabbccddeeff0011")) + } + + func test_accepts_full_noise_key_hex() { + let hex64 = String(repeating: "ab", count: 32) // 64 hex chars + XCTAssertTrue(InputValidator.validatePeerID(hex64)) + } + + func test_accepts_internal_alnum_dash_underscore() { + XCTAssertTrue(InputValidator.validatePeerID("peer_123-ABC")) + XCTAssertTrue(InputValidator.validatePeerID("nostr_user_01")) + } + + func test_rejects_invalid_characters() { + XCTAssertFalse(InputValidator.validatePeerID("peer!@#")) + XCTAssertFalse(InputValidator.validatePeerID("gggggggggggggggg")) // not hex for short form + } + + func test_rejects_too_long() { + let tooLong = String(repeating: "a", count: 65) + XCTAssertFalse(InputValidator.validatePeerID(tooLong)) + } +} +