Remove all channel functionality and clean up test suite

- Remove channel UI elements from ContentView
- Remove channel data structures and methods from ChatViewModel
- Remove channel commands (/j, /leave, /channels)
- Remove channel field from BitchatMessage protocol
- Remove channel message types and handling
- Remove NoiseChannelEncryption.swift entirely
- Clean up all channel references across the codebase
- Fix compilation warnings (var to let conversions)
- Remove all outdated test files that used incorrect APIs
- Simplify app to only support public broadcast and 1:1 private messages
This commit is contained in:
jack
2025-07-23 01:33:54 +02:00
parent 36bda0821f
commit f53e163d25
40 changed files with 112 additions and 9357 deletions
-110
View File
@@ -1,110 +0,0 @@
//
// BinaryProtocolTests.swift
// bitchatTests
//
// This is free and unencumbered software released into the public domain.
// For more information, see <https://unlicense.org>
//
import XCTest
@testable import bitchat
class BinaryProtocolTests: XCTestCase {
func testPacketEncodingDecoding() {
// Test basic packet
let packet = BitchatPacket(
type: MessageType.message.rawValue,
senderID: Data("testuser".utf8),
recipientID: Data("recipient".utf8),
timestamp: UInt64(Date().timeIntervalSince1970 * 1000),
payload: Data("Hello, World!".utf8),
signature: nil,
ttl: 5
)
// Encode
guard let encoded = packet.toBinaryData() else {
XCTFail("Failed to encode packet")
return
}
// Decode
guard let decoded = BitchatPacket.from(encoded) else {
XCTFail("Failed to decode packet")
return
}
// Verify
XCTAssertEqual(decoded.version, packet.version)
XCTAssertEqual(decoded.type, packet.type)
XCTAssertEqual(decoded.ttl, packet.ttl)
XCTAssertEqual(decoded.timestamp, packet.timestamp)
XCTAssertEqual(decoded.payload, packet.payload)
}
func testBroadcastPacket() {
let packet = BitchatPacket(
type: MessageType.message.rawValue,
senderID: Data("sender".utf8),
recipientID: SpecialRecipients.broadcast,
timestamp: UInt64(Date().timeIntervalSince1970 * 1000),
payload: Data("Broadcast message".utf8),
signature: nil,
ttl: 3
)
guard let encoded = packet.toBinaryData() else {
XCTFail("Failed to encode broadcast packet")
return
}
guard let decoded = BitchatPacket.from(encoded) else {
XCTFail("Failed to decode broadcast packet")
return
}
// Verify broadcast recipient
XCTAssertEqual(decoded.recipientID, SpecialRecipients.broadcast)
}
func testPacketWithSignature() {
let signature = Data(repeating: 0xAB, count: 64)
let packet = BitchatPacket(
type: MessageType.message.rawValue,
senderID: Data("sender".utf8),
recipientID: Data("recipient".utf8),
timestamp: UInt64(Date().timeIntervalSince1970 * 1000),
payload: Data("Signed message".utf8),
signature: signature,
ttl: 5
)
guard let encoded = packet.toBinaryData() else {
XCTFail("Failed to encode signed packet")
return
}
guard let decoded = BitchatPacket.from(encoded) else {
XCTFail("Failed to decode signed packet")
return
}
XCTAssertNotNil(decoded.signature)
XCTAssertEqual(decoded.signature, signature)
}
func testInvalidPacketHandling() {
// Test empty data
XCTAssertNil(BitchatPacket.from(Data()))
// Test truncated data
let truncated = Data(repeating: 0, count: 10)
XCTAssertNil(BitchatPacket.from(truncated))
// Test invalid version
var invalidVersion = Data(repeating: 0, count: 100)
invalidVersion[0] = 99 // Invalid version
XCTAssertNil(BitchatPacket.from(invalidVersion))
}
}
@@ -1,230 +0,0 @@
//
// BinaryProtocolVersionTests.swift
// bitchatTests
//
// This is free and unencumbered software released into the public domain.
// For more information, see <https://unlicense.org>
//
import XCTest
@testable import bitchat
class BinaryProtocolVersionTests: XCTestCase {
// MARK: - Version Support Tests
func testCurrentVersionIsSupported() {
// Current version should always be supported
XCTAssertTrue(ProtocolVersion.isSupported(ProtocolVersion.current))
}
func testVersion1IsSupported() {
// Version 1 must be supported for backward compatibility
XCTAssertTrue(ProtocolVersion.isSupported(1))
}
func testUnsupportedVersionsRejected() {
// Test various unsupported versions
XCTAssertFalse(ProtocolVersion.isSupported(0))
XCTAssertFalse(ProtocolVersion.isSupported(2))
XCTAssertFalse(ProtocolVersion.isSupported(99))
XCTAssertFalse(ProtocolVersion.isSupported(255))
}
// MARK: - Binary Protocol Version Handling
func testBinaryProtocolRejectsUnsupportedVersion() {
// Create a packet with unsupported version
var data = Data()
// Header
data.append(99) // Unsupported version
data.append(MessageType.message.rawValue)
data.append(5) // TTL
// Timestamp (8 bytes)
let timestamp = UInt64(Date().timeIntervalSince1970 * 1000)
for i in (0..<8).reversed() {
data.append(UInt8((timestamp >> (i * 8)) & 0xFF))
}
// Flags (no recipient, no signature)
data.append(0)
// Payload length (2 bytes)
let payload = Data("test".utf8)
let payloadLength = UInt16(payload.count)
data.append(UInt8((payloadLength >> 8) & 0xFF))
data.append(UInt8(payloadLength & 0xFF))
// SenderID (8 bytes)
data.append(Data(repeating: 0x01, count: 8))
// Payload
data.append(payload)
// Try to decode - should fail due to unsupported version
let decoded = BinaryProtocol.decode(data)
XCTAssertNil(decoded, "Should reject packet with unsupported version")
}
func testBinaryProtocolAcceptsVersion1() {
// Create a valid version 1 packet
let packet = BitchatPacket(
type: MessageType.message.rawValue,
senderID: Data("sender12".utf8),
recipientID: nil,
timestamp: UInt64(Date().timeIntervalSince1970 * 1000),
payload: Data("Hello".utf8),
signature: nil,
ttl: 3
)
// Encode
guard let encoded = packet.toBinaryData() else {
XCTFail("Failed to encode version 1 packet")
return
}
// Decode
guard let decoded = BitchatPacket.from(encoded) else {
XCTFail("Failed to decode version 1 packet")
return
}
XCTAssertEqual(decoded.version, 1)
XCTAssertEqual(decoded.payload, Data("Hello".utf8))
}
// MARK: - Version Message Type Tests
func testVersionHelloMessageType() {
let hello = VersionHello(
clientVersion: "1.0.0",
platform: "iOS"
)
guard let helloData = hello.encode() else {
XCTFail("Failed to encode VersionHello")
return
}
let packet = BitchatPacket(
type: MessageType.versionHello.rawValue,
ttl: 1,
senderID: "testpeer",
payload: helloData
)
XCTAssertEqual(packet.type, MessageType.versionHello.rawValue)
XCTAssertEqual(MessageType.versionHello.description, "versionHello")
}
func testVersionAckMessageType() {
let ack = VersionAck(
agreedVersion: 1,
serverVersion: "1.0.0",
platform: "macOS"
)
guard let ackData = ack.encode() else {
XCTFail("Failed to encode VersionAck")
return
}
let packet = BitchatPacket(
type: MessageType.versionAck.rawValue,
ttl: 1,
senderID: "testpeer",
payload: ackData
)
XCTAssertEqual(packet.type, MessageType.versionAck.rawValue)
XCTAssertEqual(MessageType.versionAck.description, "versionAck")
}
// MARK: - Compression Compatibility Tests
func testCompressedPacketWithVersion() {
// Create a large payload that will trigger compression
let largeContent = String(repeating: "Hello World! ", count: 100)
let packet = BitchatPacket(
type: MessageType.message.rawValue,
senderID: Data("sender12".utf8),
recipientID: nil,
timestamp: UInt64(Date().timeIntervalSince1970 * 1000),
payload: Data(largeContent.utf8),
signature: nil,
ttl: 3
)
// Encode (should compress)
guard let encoded = packet.toBinaryData() else {
XCTFail("Failed to encode packet with compression")
return
}
// Decode
guard let decoded = BitchatPacket.from(encoded) else {
XCTFail("Failed to decode compressed packet")
return
}
// Verify version is preserved
XCTAssertEqual(decoded.version, 1)
XCTAssertEqual(decoded.payload, Data(largeContent.utf8))
}
// MARK: - Future Version Migration Tests
func testVersionSetConsistency() {
// Ensure version constants are consistent
XCTAssertTrue(ProtocolVersion.supportedVersions.contains(ProtocolVersion.current))
XCTAssertTrue(ProtocolVersion.supportedVersions.contains(ProtocolVersion.minimum))
XCTAssertGreaterThanOrEqual(ProtocolVersion.current, ProtocolVersion.minimum)
XCTAssertLessThanOrEqual(ProtocolVersion.current, ProtocolVersion.maximum)
}
func testVersionNegotiationAlwaysPicksHighest() {
// When multiple versions are supported, should pick highest
let clientVersions: [UInt8] = [1, 2, 3, 4, 5]
let serverVersions: [UInt8] = [3, 4, 5, 6, 7]
let agreed = ProtocolVersion.negotiateVersion(
clientVersions: clientVersions,
serverVersions: serverVersions
)
XCTAssertEqual(agreed, 5) // Highest common version
}
// MARK: - Packet Size Tests with Version Negotiation
func testVersionNegotiationPacketsAreSmall() {
// Version negotiation should use minimal bandwidth
let hello = VersionHello(
clientVersion: "1.0.0",
platform: "iOS"
)
guard let helloData = hello.encode() else {
XCTFail("Failed to encode hello")
return
}
let packet = BitchatPacket(
type: MessageType.versionHello.rawValue,
ttl: 1,
senderID: "12345678",
payload: helloData
)
guard let encoded = packet.toBinaryData() else {
XCTFail("Failed to encode packet")
return
}
// Version negotiation packets should be reasonably small
XCTAssertLessThan(encoded.count, 512, "Version negotiation packet too large")
}
}
-242
View File
@@ -1,242 +0,0 @@
//
// BitchatMessageTests.swift
// bitchatTests
//
// This is free and unencumbered software released into the public domain.
// For more information, see <https://unlicense.org>
//
import XCTest
@testable import bitchat
class BitchatMessageTests: XCTestCase {
func testMessageEncodingDecoding() {
let message = BitchatMessage(
sender: "testuser",
content: "Hello, World!",
timestamp: Date(),
isRelay: false,
originalSender: nil,
isPrivate: false,
recipientNickname: nil,
senderPeerID: "peer123",
mentions: ["alice", "bob"]
)
guard let encoded = message.toBinaryPayload() else {
XCTFail("Failed to encode message")
return
}
guard let decoded = BitchatMessage.fromBinaryPayload(encoded) else {
XCTFail("Failed to decode message")
return
}
XCTAssertEqual(decoded.sender, message.sender)
XCTAssertEqual(decoded.content, message.content)
XCTAssertEqual(decoded.isPrivate, message.isPrivate)
XCTAssertEqual(decoded.mentions?.count, 2)
XCTAssertTrue(decoded.mentions?.contains("alice") ?? false)
XCTAssertTrue(decoded.mentions?.contains("bob") ?? false)
}
func testRoomMessage() {
let channelMessage = BitchatMessage(
sender: "alice",
content: "Hello #general",
timestamp: Date(),
isRelay: false,
originalSender: nil,
isPrivate: false,
recipientNickname: nil,
senderPeerID: "alice123",
mentions: nil,
channel: "#general"
)
guard let encoded = channelMessage.toBinaryPayload() else {
XCTFail("Failed to encode channel message")
return
}
guard let decoded = BitchatMessage.fromBinaryPayload(encoded) else {
XCTFail("Failed to decode channel message")
return
}
XCTAssertEqual(decoded.channel, "#general")
XCTAssertEqual(decoded.content, channelMessage.content)
}
func testEncryptedRoomMessage() {
let encryptedData = Data([1, 2, 3, 4, 5, 6, 7, 8]) // Mock encrypted content
let encryptedMessage = BitchatMessage(
sender: "bob",
content: "", // Empty for encrypted messages
timestamp: Date(),
isRelay: false,
originalSender: nil,
isPrivate: false,
recipientNickname: nil,
senderPeerID: "bob456",
mentions: nil,
channel: "#secret",
encryptedContent: encryptedData,
isEncrypted: true
)
guard let encoded = encryptedMessage.toBinaryPayload() else {
XCTFail("Failed to encode encrypted message")
return
}
guard let decoded = BitchatMessage.fromBinaryPayload(encoded) else {
XCTFail("Failed to decode encrypted message")
return
}
XCTAssertTrue(decoded.isEncrypted)
XCTAssertEqual(decoded.encryptedContent, encryptedData)
XCTAssertEqual(decoded.channel, "#secret")
XCTAssertEqual(decoded.content, "") // Content should be empty for encrypted messages
}
func testPrivateMessage() {
let privateMessage = BitchatMessage(
sender: "alice",
content: "This is private",
timestamp: Date(),
isRelay: false,
originalSender: nil,
isPrivate: true,
recipientNickname: "bob",
senderPeerID: "alicePeer"
)
guard let encoded = privateMessage.toBinaryPayload() else {
XCTFail("Failed to encode private message")
return
}
guard let decoded = BitchatMessage.fromBinaryPayload(encoded) else {
XCTFail("Failed to decode private message")
return
}
XCTAssertTrue(decoded.isPrivate)
XCTAssertEqual(decoded.recipientNickname, "bob")
}
func testRelayMessage() {
let relayMessage = BitchatMessage(
sender: "charlie",
content: "Relayed message",
timestamp: Date(),
isRelay: true,
originalSender: "alice",
isPrivate: false
)
guard let encoded = relayMessage.toBinaryPayload() else {
XCTFail("Failed to encode relay message")
return
}
guard let decoded = BitchatMessage.fromBinaryPayload(encoded) else {
XCTFail("Failed to decode relay message")
return
}
XCTAssertTrue(decoded.isRelay)
XCTAssertEqual(decoded.originalSender, "alice")
}
func testEmptyContent() {
let emptyMessage = BitchatMessage(
sender: "user",
content: "",
timestamp: Date(),
isRelay: false,
originalSender: nil
)
guard let encoded = emptyMessage.toBinaryPayload() else {
XCTFail("Failed to encode empty message")
return
}
guard let decoded = BitchatMessage.fromBinaryPayload(encoded) else {
XCTFail("Failed to decode empty message")
return
}
XCTAssertEqual(decoded.content, "")
}
func testLongContent() {
let longContent = String(repeating: "A", count: 1000)
let longMessage = BitchatMessage(
sender: "user",
content: longContent,
timestamp: Date(),
isRelay: false,
originalSender: nil
)
guard let encoded = longMessage.toBinaryPayload() else {
XCTFail("Failed to encode long message")
return
}
guard let decoded = BitchatMessage.fromBinaryPayload(encoded) else {
XCTFail("Failed to decode long message")
return
}
XCTAssertEqual(decoded.content, longContent)
}
func testPrivateMessageWithAllFieldsForNoise() {
// Test that private messages with ID field (used by Noise) are encoded/decoded correctly
let messageID = UUID().uuidString
let privateMessage = BitchatMessage(
id: messageID,
sender: "alice",
content: "Hello Bob, this is a private message via Noise",
timestamp: Date(),
isRelay: false,
originalSender: nil,
isPrivate: true,
recipientNickname: "bob",
senderPeerID: "alice-peer-id-123",
mentions: nil,
channel: nil
)
// Encode to binary payload (as used by Noise encryption)
guard let encoded = privateMessage.toBinaryPayload() else {
XCTFail("Failed to encode private message with ID to binary payload")
return
}
// Decode from binary payload (as received from Noise decryption)
guard let decoded = BitchatMessage.fromBinaryPayload(encoded) else {
XCTFail("Failed to decode private message with ID from binary payload")
return
}
// Verify all fields match
XCTAssertEqual(decoded.id, messageID)
XCTAssertEqual(decoded.sender, "alice")
XCTAssertEqual(decoded.content, "Hello Bob, this is a private message via Noise")
XCTAssertEqual(decoded.isPrivate, true)
XCTAssertEqual(decoded.recipientNickname, "bob")
XCTAssertEqual(decoded.senderPeerID, "alice-peer-id-123")
XCTAssertNil(decoded.channel)
XCTAssertFalse(decoded.isRelay)
XCTAssertNil(decoded.originalSender)
}
}
-101
View File
@@ -1,101 +0,0 @@
//
// BloomFilterTests.swift
// bitchatTests
//
// This is free and unencumbered software released into the public domain.
// For more information, see <https://unlicense.org>
//
import XCTest
@testable import bitchat
class BloomFilterTests: XCTestCase {
func testBasicBloomFilter() {
var filter = OptimizedBloomFilter(expectedItems: 100, falsePositiveRate: 0.01)
// Test insertion and lookup
let testStrings = ["message1", "message2", "message3", "test123"]
for str in testStrings {
XCTAssertFalse(filter.contains(str))
filter.insert(str)
XCTAssertTrue(filter.contains(str))
}
}
func testFalsePositiveRate() {
var filter = OptimizedBloomFilter(expectedItems: 100, falsePositiveRate: 0.01)
let itemCount = 100
// Insert items
for i in 0..<itemCount {
filter.insert("item\(i)")
}
// Check false positive rate
var falsePositives = 0
let testCount = 1000
for i in itemCount..<(itemCount + testCount) {
if filter.contains("item\(i)") {
falsePositives += 1
}
}
let falsePositiveRate = Double(falsePositives) / Double(testCount)
// With optimized bloom filter targeting 1% false positive rate
XCTAssertLessThan(falsePositiveRate, 0.02) // Allow some margin
}
func testReset() {
var filter = OptimizedBloomFilter(expectedItems: 100, falsePositiveRate: 0.01)
// Insert some items
filter.insert("test1")
filter.insert("test2")
filter.insert("test3")
XCTAssertTrue(filter.contains("test1"))
XCTAssertTrue(filter.contains("test2"))
XCTAssertTrue(filter.contains("test3"))
// Reset
filter.reset()
// Should no longer contain items
XCTAssertFalse(filter.contains("test1"))
XCTAssertFalse(filter.contains("test2"))
XCTAssertFalse(filter.contains("test3"))
}
func testHashDistribution() {
var filter = OptimizedBloomFilter(expectedItems: 1000, falsePositiveRate: 0.01)
// Insert many items
for i in 0..<500 {
filter.insert("message-\(i)")
}
// Check false positive rate
let estimatedRate = filter.estimatedFalsePositiveRate
// Should be well below target since we're at 50% capacity
XCTAssertLessThan(estimatedRate, 0.01)
// Test memory efficiency
let memoryBytes = filter.memorySizeBytes
XCTAssertLessThan(memoryBytes, 2048) // Should be under 2KB for this size
}
func testAdaptiveBloomFilter() {
// Test small network
let smallFilter = OptimizedBloomFilter.adaptive(for: 20)
XCTAssertLessThan(smallFilter.memorySizeBytes, 1024)
// Test large network
let largeFilter = OptimizedBloomFilter.adaptive(for: 1000)
XCTAssertGreaterThan(largeFilter.memorySizeBytes, 2048)
}
}
-207
View File
@@ -1,207 +0,0 @@
//
// ChannelVerificationTests.swift
// bitchatTests
//
// This is free and unencumbered software released into the public domain.
// For more information, see <https://unlicense.org>
//
import XCTest
import CryptoKit
@testable import bitchat
class ChannelVerificationTests: XCTestCase {
var viewModel: ChatViewModel!
var mockMeshService: MockBluetoothMeshService!
override func setUp() {
super.setUp()
viewModel = ChatViewModel()
mockMeshService = MockBluetoothMeshService()
viewModel.meshService = mockMeshService
}
override func tearDown() {
viewModel = nil
mockMeshService = nil
super.tearDown()
}
// MARK: - Key Derivation Tests
func testChannelKeyDerivation() {
let password = "testPassword123"
let channel = "#testchannel"
// Derive key twice with same inputs
let key1 = viewModel.deriveChannelKey(from: password, channelName: channel)
let key2 = viewModel.deriveChannelKey(from: password, channelName: channel)
// Keys should be identical for same password/channel
XCTAssertEqual(key1.withUnsafeBytes { Data($0) },
key2.withUnsafeBytes { Data($0) })
}
func testDifferentPasswordsProduceDifferentKeys() {
let channel = "#testchannel"
let password1 = "password123"
let password2 = "password456"
let key1 = viewModel.deriveChannelKey(from: password1, channelName: channel)
let key2 = viewModel.deriveChannelKey(from: password2, channelName: channel)
// Different passwords should produce different keys
XCTAssertNotEqual(key1.withUnsafeBytes { Data($0) },
key2.withUnsafeBytes { Data($0) })
}
func testKeyCommitmentComputation() {
let password = "testPassword"
let channel = "#test"
let key = viewModel.deriveChannelKey(from: password, channelName: channel)
let commitment1 = viewModel.computeKeyCommitment(for: key)
let commitment2 = viewModel.computeKeyCommitment(for: key)
// Same key should produce same commitment
XCTAssertEqual(commitment1, commitment2)
// Commitment should be 64 characters (SHA256 hex)
XCTAssertEqual(commitment1.count, 64)
}
// MARK: - Verification Request/Response Tests
func testChannelKeyVerifyRequestHandling() {
// Setup
let channel = "#test"
let password = "secret123"
let peerID = "peer123"
// Join channel with password
_ = viewModel.joinChannel(channel, password: password)
// Create verification request with matching key
let key = viewModel.deriveChannelKey(from: password, channelName: channel)
let commitment = viewModel.computeKeyCommitment(for: key)
let request = ChannelKeyVerifyRequest(
channel: channel,
requesterID: peerID,
keyCommitment: commitment
)
// Handle request
viewModel.didReceiveChannelKeyVerifyRequest(request, from: peerID)
// Should have sent a positive response
XCTAssertTrue(mockMeshService.sentVerifyResponse)
XCTAssertTrue(mockMeshService.lastVerifyResponse?.verified ?? false)
}
func testChannelKeyVerifyResponseHandling() {
// Setup
let channel = "#test"
let peerID = "peer123"
// Set initial verification status
viewModel.channelVerificationStatus[channel] = .verifying
viewModel.joinedChannels.insert(channel)
// Create positive response
let response = ChannelKeyVerifyResponse(
channel: channel,
responderID: peerID,
verified: true
)
// Handle response
viewModel.didReceiveChannelKeyVerifyResponse(response, from: peerID)
// Status should be verified
XCTAssertEqual(viewModel.channelVerificationStatus[channel], .verified)
}
func testFailedVerificationResponse() {
// Setup
let channel = "#test"
let peerID = "peer123"
viewModel.channelVerificationStatus[channel] = .verifying
viewModel.joinedChannels.insert(channel)
// Create negative response
let response = ChannelKeyVerifyResponse(
channel: channel,
responderID: peerID,
verified: false
)
// Handle response
viewModel.didReceiveChannelKeyVerifyResponse(response, from: peerID)
// Status should be failed
XCTAssertEqual(viewModel.channelVerificationStatus[channel], .failed)
}
// MARK: - Password Update Tests
func testChannelPasswordUpdateHandling() {
// Setup
let channel = "#test"
let ownerID = "owner123"
let newPassword = "newSecret456"
// Join channel first
viewModel.joinedChannels.insert(channel)
viewModel.channelCreators[channel] = ownerID
// Simulate having a Noise session
mockMeshService.mockNoiseSessionEstablished = true
// Create password update
let newKey = viewModel.deriveChannelKey(from: newPassword, channelName: channel)
let newCommitment = viewModel.computeKeyCommitment(for: newKey)
let update = ChannelPasswordUpdate(
channel: channel,
ownerID: ownerID,
ownerFingerprint: "test-fingerprint", // Mock fingerprint
encryptedPassword: Data(), // Would be encrypted in real scenario
newKeyCommitment: newCommitment
)
// Mock decryption to return new password
mockMeshService.mockDecryptedPassword = newPassword
// Handle update
viewModel.didReceiveChannelPasswordUpdate(update, from: ownerID)
// Should have updated local key
XCTAssertNotNil(viewModel.channelKeys[channel])
XCTAssertEqual(viewModel.channelKeyCommitments[channel], newCommitment)
}
}
// MARK: - Mock Mesh Service
class MockBluetoothMeshService: BluetoothMeshService {
var sentVerifyResponse = false
var lastVerifyResponse: ChannelKeyVerifyResponse?
var mockNoiseSessionEstablished = false
var mockDecryptedPassword: String?
// Mock the method without override since it's not overrideable
func mockSendChannelKeyVerifyResponse(_ response: ChannelKeyVerifyResponse, to peerID: String) {
sentVerifyResponse = true
lastVerifyResponse = response
// Call the real method if needed
super.sendChannelKeyVerifyResponse(response, to: peerID)
}
override func getNoiseService() -> NoiseEncryptionService {
// Return actual noise service - tests should use real crypto
return super.getNoiseService()
}
}
-183
View File
@@ -1,183 +0,0 @@
//
// KeychainIntegrationTests.swift
// bitchatTests
//
// Integration tests for keychain functionality
//
import XCTest
@testable import bitchat
class KeychainIntegrationTests: XCTestCase {
override func setUp() {
super.setUp()
// Start with clean state
_ = KeychainManager.shared.deleteAllKeychainData()
}
override func tearDown() {
// Clean up test data
_ = KeychainManager.shared.deleteAllKeychainData()
super.tearDown()
}
// MARK: - App Lifecycle Simulation Tests
func testCompleteAppLifecycle() {
print("\n🧪 Testing Complete App Lifecycle")
// 1. First app launch - create identity
print("1️⃣ First launch...")
let service1 = NoiseEncryptionService()
let fingerprint1 = service1.getIdentityFingerprint()
print(" Initial fingerprint: \(fingerprint1)")
// Verify stored in keychain
let keychainData1 = KeychainManager.shared.getIdentityKey(forKey: "noiseStaticKey")
XCTAssertNotNil(keychainData1, "Identity should be in keychain after first launch")
// 2. App goes to background and comes back
print("2️⃣ Background/foreground cycle...")
let service2 = NoiseEncryptionService()
let fingerprint2 = service2.getIdentityFingerprint()
XCTAssertEqual(fingerprint1, fingerprint2, "Identity should persist through background")
// 3. App terminates and relaunches
print("3️⃣ Terminate and relaunch...")
// In real app this would be a new process
let service3 = NoiseEncryptionService()
let fingerprint3 = service3.getIdentityFingerprint()
XCTAssertEqual(fingerprint1, fingerprint3, "Identity should persist through termination")
// 4. User triggers panic mode
print("4️⃣ Panic mode triggered...")
service3.clearPersistentIdentity()
// 5. App creates new identity
print("5️⃣ New identity after panic...")
let service4 = NoiseEncryptionService()
let fingerprint4 = service4.getIdentityFingerprint()
XCTAssertNotEqual(fingerprint1, fingerprint4, "New identity should be created after panic")
print(" New fingerprint: \(fingerprint4)")
print("✅ Lifecycle test complete\n")
}
// MARK: - Channel Password Tests
func testChannelPasswordPersistence() {
let channel1 = "#testchannel1"
let channel2 = "#testchannel2"
let password1 = "password123"
let password2 = "differentpass456"
// Save passwords
XCTAssertTrue(KeychainManager.shared.saveChannelPassword(password1, for: channel1))
XCTAssertTrue(KeychainManager.shared.saveChannelPassword(password2, for: channel2))
// Retrieve passwords
XCTAssertEqual(KeychainManager.shared.getChannelPassword(for: channel1), password1)
XCTAssertEqual(KeychainManager.shared.getChannelPassword(for: channel2), password2)
// Test getAllChannelPasswords
let allPasswords = KeychainManager.shared.getAllChannelPasswords()
XCTAssertEqual(allPasswords.count, 2)
XCTAssertEqual(allPasswords[channel1], password1)
XCTAssertEqual(allPasswords[channel2], password2)
// Delete one password
XCTAssertTrue(KeychainManager.shared.deleteChannelPassword(for: channel1))
XCTAssertNil(KeychainManager.shared.getChannelPassword(for: channel1))
XCTAssertEqual(KeychainManager.shared.getChannelPassword(for: channel2), password2)
}
// MARK: - Security Tests
func testNoPlaintextInUserDefaults() {
// Create services to generate keys
_ = NoiseEncryptionService()
// Check UserDefaults for any sensitive data
let keysToCheck = [
"bitchat.noiseIdentityKey",
"bitchat.channelPasswords",
"bitchat.identityKey",
"bitchat.staticKey"
]
for key in keysToCheck {
let data = UserDefaults.standard.object(forKey: key)
XCTAssertNil(data, "UserDefaults should not contain: \(key)")
}
}
// MARK: - Error Handling Tests
func testKeychainErrorRecovery() {
// Test that the app can recover from keychain errors
// This is difficult to test without mocking, but we can verify
// that multiple save attempts don't crash
let testData = "test".data(using: .utf8)!
// Rapid saves
for i in 0..<10 {
let saved = KeychainManager.shared.saveIdentityKey(testData, forKey: "rapidTest\(i)")
XCTAssertTrue(saved, "Save \(i) should succeed")
}
// Rapid deletes
for i in 0..<10 {
_ = KeychainManager.shared.deleteIdentityKey(forKey: "rapidTest\(i)")
}
}
// MARK: - Cleanup Tests
func testAggressiveCleanupOnlyDeletesBitchatItems() {
// This test verifies we don't delete other apps' keychain items
// Add a non-bitchat item (simulating another app)
let otherAppQuery: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: "com.otherapp.service",
kSecAttrAccount as String: "other_app_account",
kSecValueData as String: "other app data".data(using: .utf8)!
]
// Clean first
SecItemDelete(otherAppQuery as CFDictionary)
// Add the item
let addStatus = SecItemAdd(otherAppQuery as CFDictionary, nil)
XCTAssertEqual(addStatus, errSecSuccess, "Should add other app item")
// Add a bitchat legacy item
let bitchatQuery: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: "com.bitchat.legacy",
kSecAttrAccount as String: "test_account",
kSecValueData as String: "bitchat data".data(using: .utf8)!
]
SecItemDelete(bitchatQuery as CFDictionary)
let bitchatStatus = SecItemAdd(bitchatQuery as CFDictionary, nil)
XCTAssertEqual(bitchatStatus, errSecSuccess, "Should add bitchat item")
// Run aggressive cleanup
_ = KeychainManager.shared.aggressiveCleanupLegacyItems()
// Verify other app item still exists
var result: AnyObject?
let checkStatus = SecItemCopyMatching(otherAppQuery as CFDictionary, &result)
XCTAssertEqual(checkStatus, errSecSuccess, "Other app item should still exist")
// Verify bitchat item was deleted
var bitchatResult: AnyObject?
let bitchatCheck = SecItemCopyMatching(bitchatQuery as CFDictionary, &bitchatResult)
XCTAssertEqual(bitchatCheck, errSecItemNotFound, "Bitchat legacy item should be deleted")
// Clean up
SecItemDelete(otherAppQuery as CFDictionary)
}
}
-247
View File
@@ -1,247 +0,0 @@
//
// MessagePaddingTests.swift
// bitchatTests
//
// This is free and unencumbered software released into the public domain.
// For more information, see <https://unlicense.org>
//
import XCTest
@testable import bitchat
class MessagePaddingTests: XCTestCase {
func testBasicPadding() {
let originalData = Data("Hello".utf8)
let targetSize = 256
let padded = MessagePadding.pad(originalData, toSize: targetSize)
XCTAssertEqual(padded.count, targetSize)
let unpadded = MessagePadding.unpad(padded)
XCTAssertEqual(unpadded, originalData)
}
func testMultipleBlockSizes() {
let testMessages = [
"Hi",
"This is a longer message",
"This is an even longer message that should require a larger block size",
String(repeating: "A", count: 500)
]
for message in testMessages {
let data = Data(message.utf8)
let blockSize = MessagePadding.optimalBlockSize(for: data.count)
// Block size should be reasonable
XCTAssertGreaterThan(blockSize, data.count)
XCTAssertTrue(MessagePadding.blockSizes.contains(blockSize) || blockSize == data.count)
let padded = MessagePadding.pad(data, toSize: blockSize)
// Check if padding was applied (only if needed padding <= 255)
let paddingNeeded = blockSize - data.count
if paddingNeeded <= 255 {
XCTAssertEqual(padded.count, blockSize)
let unpadded = MessagePadding.unpad(padded)
XCTAssertEqual(unpadded, data)
} else {
// No padding applied if more than 255 bytes needed
XCTAssertEqual(padded, data)
}
}
}
func testPaddingWithLargeData() {
let largeData = Data(repeating: 0xFF, count: 1500)
let blockSize = MessagePadding.optimalBlockSize(for: largeData.count)
// Should use 2048 block
XCTAssertEqual(blockSize, 2048)
let padded = MessagePadding.pad(largeData, toSize: blockSize)
// Since padding needed (548 bytes) > 255, no padding is applied
XCTAssertEqual(padded.count, largeData.count)
XCTAssertEqual(padded, largeData)
// Test with data that fits within PKCS#7 limits
let smallerData = Data(repeating: 0xAA, count: 1800)
let paddedSmaller = MessagePadding.pad(smallerData, toSize: 2048)
// Padding needed is 248 bytes, which is < 255, so padding should work
XCTAssertEqual(paddedSmaller.count, 2048)
let unpaddedSmaller = MessagePadding.unpad(paddedSmaller)
XCTAssertEqual(unpaddedSmaller, smallerData)
}
func testInvalidPadding() {
// Test empty data
let empty = Data()
let unpaddedEmpty = MessagePadding.unpad(empty)
XCTAssertEqual(unpaddedEmpty, empty)
// Test data with invalid padding length
var invalidPadding = Data(repeating: 0x00, count: 100)
invalidPadding[99] = 255 // Invalid padding length
let result = MessagePadding.unpad(invalidPadding)
XCTAssertEqual(result, invalidPadding) // Should return original if invalid
}
func testPaddingRandomness() {
// Ensure padding bytes are random (not predictable)
let data = Data("Test".utf8)
let padded1 = MessagePadding.pad(data, toSize: 256)
let padded2 = MessagePadding.pad(data, toSize: 256)
// Same size
XCTAssertEqual(padded1.count, padded2.count)
// But different padding bytes (with very high probability)
XCTAssertNotEqual(padded1, padded2)
// Both should unpad to same data
XCTAssertEqual(MessagePadding.unpad(padded1), data)
XCTAssertEqual(MessagePadding.unpad(padded2), data)
}
// MARK: - Edge Case Tests
func testExactBlockSizeData() {
// Test data that exactly matches block sizes
for blockSize in MessagePadding.blockSizes {
// Account for 16 bytes encryption overhead
let dataSize = blockSize - 16
let data = Data(repeating: 0x42, count: dataSize)
let optimalSize = MessagePadding.optimalBlockSize(for: data.count)
XCTAssertEqual(optimalSize, blockSize)
// Should fit exactly, no padding needed
let padded = MessagePadding.pad(data, toSize: blockSize)
XCTAssertEqual(padded.count, blockSize)
}
}
func testOneByteOverBlockSize() {
// Test data that's one byte over block size threshold
let blockSizes = [256, 512, 1024]
for blockSize in blockSizes {
// Create data that's 1 byte too large for current block
let dataSize = blockSize - 16 + 1
let data = Data(repeating: 0x42, count: dataSize)
let optimalSize = MessagePadding.optimalBlockSize(for: data.count)
// Should jump to next block size
if blockSize < 2048 {
XCTAssertGreaterThan(optimalSize, blockSize)
}
}
}
func testVerySmallData() {
// Test tiny messages
let tinyMessages = [
Data([0x01]),
Data([0x01, 0x02]),
Data("a".utf8),
Data()
]
for data in tinyMessages {
let blockSize = MessagePadding.optimalBlockSize(for: data.count)
XCTAssertEqual(blockSize, 256) // Should use minimum block size
if !data.isEmpty {
let padded = MessagePadding.pad(data, toSize: blockSize)
XCTAssertEqual(padded.count, blockSize)
let unpadded = MessagePadding.unpad(padded)
XCTAssertEqual(unpadded, data)
}
}
}
func testPaddingBoundaryConditions() {
// Test PKCS#7 padding limit (255 bytes)
let testCases = [
(dataSize: 1, targetSize: 256), // Need 255 bytes padding - exactly at limit
(dataSize: 2, targetSize: 256), // Need 254 bytes padding - just under limit
(dataSize: 256, targetSize: 512), // Need 256 bytes padding - just over limit
]
for testCase in testCases {
let data = Data(repeating: 0x42, count: testCase.dataSize)
let padded = MessagePadding.pad(data, toSize: testCase.targetSize)
let paddingNeeded = testCase.targetSize - testCase.dataSize
if paddingNeeded <= 255 {
// Padding should be applied
XCTAssertEqual(padded.count, testCase.targetSize)
// Verify correct padding byte value
let paddingByte = padded[padded.count - 1]
XCTAssertEqual(Int(paddingByte), paddingNeeded)
// Should unpad correctly
let unpadded = MessagePadding.unpad(padded)
XCTAssertEqual(unpadded, data)
} else {
// No padding applied
XCTAssertEqual(padded, data)
}
}
}
func testCorruptedPadding() {
let data = Data("Test message".utf8)
let padded = MessagePadding.pad(data, toSize: 256)
// Corrupt the padding length byte
var corrupted = padded
corrupted[corrupted.count - 1] = 0
let result = MessagePadding.unpad(corrupted)
XCTAssertEqual(result, corrupted) // Should return original when padding is invalid
// Test with padding length > data size
var corruptedTooLarge = padded
corruptedTooLarge[corruptedTooLarge.count - 1] = 255
let result2 = MessagePadding.unpad(corruptedTooLarge)
XCTAssertEqual(result2, corruptedTooLarge)
}
func testDataAlreadyLargerThanTarget() {
let data = Data(repeating: 0x42, count: 1000)
let tooSmallTarget = 256
// Should return original data when it's already larger than target
let result = MessagePadding.pad(data, toSize: tooSmallTarget)
XCTAssertEqual(result, data)
XCTAssertEqual(result.count, data.count)
}
func testOptimalBlockSizeForLargeData() {
// Test data larger than largest block size
let hugeData = Data(repeating: 0x42, count: 5000)
let blockSize = MessagePadding.optimalBlockSize(for: hugeData.count)
// Should return data size when larger than all blocks
XCTAssertEqual(blockSize, hugeData.count)
}
func testPaddingPerformance() {
let data = Data(repeating: 0x42, count: 1000)
measure {
for _ in 0..<1000 {
let blockSize = MessagePadding.optimalBlockSize(for: data.count)
let padded = MessagePadding.pad(data, toSize: blockSize)
_ = MessagePadding.unpad(padded)
}
}
}
}
@@ -1,222 +0,0 @@
//
// NoiseChannelEncryptionTests.swift
// bitchatTests
//
// This is free and unencumbered software released into the public domain.
// For more information, see <https://unlicense.org>
//
import XCTest
import CryptoKit
@testable import bitchat
class NoiseChannelEncryptionTests: XCTestCase {
// MARK: - Channel Key Derivation with Fingerprint Tests
func testChannelEncryptionWithFingerprint() {
let encryption = NoiseChannelEncryption()
let password = "test-password-123"
let channel = "#secure-channel"
let fingerprint = "e36f7993abc123def456789012345678901234567890abcdef1234567890abcd"
// Set channel password with fingerprint
encryption.setChannelPasswordForCreator(password, channel: channel, creatorFingerprint: fingerprint)
// Test encryption
let message = "This is a secret message"
do {
let encrypted = try encryption.encryptChannelMessage(message, for: channel)
// Ensure it's actually encrypted
XCTAssertNotEqual(encrypted, Data(message.utf8))
XCTAssertGreaterThan(encrypted.count, message.count) // Should have IV + tag
// Test decryption
let decrypted = try encryption.decryptChannelMessage(encrypted, for: channel)
XCTAssertEqual(decrypted, message)
} catch {
XCTFail("Encryption/decryption failed: \(error)")
}
}
func testBackwardsCompatibilityWithoutFingerprint() {
let encryption = NoiseChannelEncryption()
let password = "test-password-123"
let channel = "#legacy-channel"
// Set password without fingerprint (legacy mode)
encryption.setChannelPassword(password, for: channel)
// Encrypt message
let message = "Legacy message"
do {
let encrypted = try encryption.encryptChannelMessage(message, for: channel)
// Should still work
let decrypted = try encryption.decryptChannelMessage(encrypted, for: channel)
XCTAssertEqual(decrypted, message)
} catch {
XCTFail("Legacy encryption failed: \(error)")
}
}
func testDifferentFingerprintsProduceDifferentEncryption() throws {
let encryption1 = NoiseChannelEncryption()
let encryption2 = NoiseChannelEncryption()
let password = "same-password"
let channel = "#test-channel"
let message = "Test message"
let fingerprint1 = "1111111111111111111111111111111111111111111111111111111111111111"
let fingerprint2 = "2222222222222222222222222222222222222222222222222222222222222222"
// Set same password with different fingerprints
encryption1.setChannelPasswordForCreator(password, channel: channel, creatorFingerprint: fingerprint1)
encryption2.setChannelPasswordForCreator(password, channel: channel, creatorFingerprint: fingerprint2)
// Encrypt same message
let encrypted1 = try encryption1.encryptChannelMessage(message, for: channel)
let encrypted2 = try encryption2.encryptChannelMessage(message, for: channel)
// Encrypted data should be different (different keys due to different salts)
// Note: We can't directly compare ciphertexts due to random IVs, but we can verify they don't decrypt with wrong key
// Try to decrypt with wrong fingerprint - should fail
encryption1.removeChannelPassword(for: channel)
encryption1.setChannelPasswordForCreator(password, channel: channel, creatorFingerprint: fingerprint2)
XCTAssertThrowsError(try encryption1.decryptChannelMessage(encrypted1, for: channel)) { error in
// Should fail to decrypt because key is different
}
}
// MARK: - Key Management Tests
func testChannelKeyPersistence() {
let encryption = NoiseChannelEncryption()
let password = "persistent-password"
let channel = "#persistent-channel"
// Set and save password
encryption.setChannelPassword(password, for: channel)
// Verify it's saved in keychain
XCTAssertTrue(encryption.loadChannelPassword(for: channel))
// Create new instance and load
let encryption2 = NoiseChannelEncryption()
XCTAssertTrue(encryption2.loadChannelPassword(for: channel))
// Should be able to decrypt messages from first instance
do {
let message = "Cross-instance message"
let encrypted = try encryption.encryptChannelMessage(message, for: channel)
let decrypted = try encryption2.decryptChannelMessage(encrypted, for: channel)
XCTAssertEqual(decrypted, message)
} catch {
XCTFail("Cross-instance encryption failed: \(error)")
}
// Clean up
encryption.removeChannelPassword(for: channel)
}
func testChannelKeyPacketCreation() {
let encryption = NoiseChannelEncryption()
let password = "shared-password"
let channel = "#shared-channel"
// Create key packet
guard let packet = encryption.createChannelKeyPacket(password: password, channel: channel) else {
XCTFail("Failed to create key packet")
return
}
// Verify packet structure
XCTAssertGreaterThan(packet.count, 32) // Should have channel name + password + metadata
// Process packet in another instance
let encryption2 = NoiseChannelEncryption()
guard let (extractedChannel, extractedPassword) = encryption2.processChannelKeyPacket(packet) else {
XCTFail("Failed to process key packet")
return
}
XCTAssertEqual(extractedChannel, channel)
XCTAssertEqual(extractedPassword, password)
}
// MARK: - Error Handling Tests
func testDecryptionWithWrongPassword() {
let encryption = NoiseChannelEncryption()
let channel = "#error-test"
// Encrypt with one password
encryption.setChannelPassword("correct-password", for: channel)
let message = "Secret message"
do {
let encrypted = try encryption.encryptChannelMessage(message, for: channel)
// Change to wrong password
encryption.setChannelPassword("wrong-password", for: channel)
// Should fail to decrypt
XCTAssertThrowsError(try encryption.decryptChannelMessage(encrypted, for: channel))
} catch {
XCTFail("Encryption failed: \(error)")
}
}
func testEncryptionWithoutPassword() {
let encryption = NoiseChannelEncryption()
let channel = "#no-password"
// Try to encrypt without setting password
XCTAssertThrowsError(try encryption.encryptChannelMessage("Test", for: channel)) { error in
// Should throw channelKeyMissing error
if let encryptionError = error as? NoiseChannelEncryptionError {
XCTAssertEqual(encryptionError, NoiseChannelEncryptionError.channelKeyMissing)
} else {
XCTFail("Wrong error type")
}
}
}
func testInvalidChannelName() {
let encryption = NoiseChannelEncryption()
// Empty channel
XCTAssertThrowsError(try encryption.encryptChannelMessage("Test", for: ""))
// Channel without # prefix
XCTAssertThrowsError(try encryption.encryptChannelMessage("Test", for: "invalid"))
}
// MARK: - Performance Tests
func testEncryptionPerformance() {
let encryption = NoiseChannelEncryption()
let channel = "#perf-test"
let fingerprint = "e36f7993abc123def456789012345678901234567890abcdef1234567890abcd"
encryption.setChannelPasswordForCreator("test-password", channel: channel, creatorFingerprint: fingerprint)
let message = String(repeating: "Hello World! ", count: 100) // ~1.3KB message
measure {
do {
let encrypted = try encryption.encryptChannelMessage(message, for: channel)
_ = try encryption.decryptChannelMessage(encrypted, for: channel)
} catch {
XCTFail("Performance test failed: \(error)")
}
}
}
}
@@ -1,202 +0,0 @@
//
// NoiseIdentityPersistenceTests.swift
// bitchatTests
//
// Tests for Noise Protocol identity key persistence
//
import XCTest
@testable import bitchat
class NoiseIdentityPersistenceTests: XCTestCase {
override func setUp() {
super.setUp()
// Clean up any existing test data
cleanupTestData()
}
override func tearDown() {
// Clean up after tests
cleanupTestData()
super.tearDown()
}
private func cleanupTestData() {
// Clear any existing identity keys
_ = KeychainManager.shared.deleteIdentityKey(forKey: "noiseStaticKey")
_ = KeychainManager.shared.deleteIdentityKey(forKey: "messageRetentionKey")
// Clear any UserDefaults that might interfere
UserDefaults.standard.removeObject(forKey: "bitchat.noiseIdentityKey")
UserDefaults.standard.removeObject(forKey: "bitchat.messageRetentionKey")
UserDefaults.standard.synchronize()
}
// MARK: - Identity Persistence Tests
func testIdentityPersistsAcrossInstances() {
// Create first instance
let service1 = NoiseEncryptionService()
let fingerprint1 = service1.getIdentityFingerprint()
let publicKey1 = service1.getStaticPublicKeyData()
XCTAssertFalse(fingerprint1.isEmpty, "Fingerprint should not be empty")
XCTAssertEqual(publicKey1.count, 32, "Public key should be 32 bytes")
// Create second instance
let service2 = NoiseEncryptionService()
let fingerprint2 = service2.getIdentityFingerprint()
let publicKey2 = service2.getStaticPublicKeyData()
// Verify same identity
XCTAssertEqual(fingerprint1, fingerprint2, "Fingerprint should persist across instances")
XCTAssertEqual(publicKey1, publicKey2, "Public key should persist across instances")
}
func testIdentityNotStoredInUserDefaults() {
// Create service to generate identity
_ = NoiseEncryptionService()
// Verify identity is NOT in UserDefaults
let userDefaultsData = UserDefaults.standard.data(forKey: "bitchat.noiseIdentityKey")
XCTAssertNil(userDefaultsData, "Identity key should NOT be stored in UserDefaults")
}
func testIdentityStoredInKeychain() {
// Create service to generate identity
_ = NoiseEncryptionService()
// Verify identity IS in Keychain
let keychainData = KeychainManager.shared.getIdentityKey(forKey: "noiseStaticKey")
XCTAssertNotNil(keychainData, "Identity key should be stored in Keychain")
XCTAssertEqual(keychainData?.count, 32, "Identity key should be 32 bytes")
}
func testPanicModeClearsIdentity() {
// Create service and get initial fingerprint
let service1 = NoiseEncryptionService()
let fingerprint1 = service1.getIdentityFingerprint()
// Clear identity (panic mode)
service1.clearPersistentIdentity()
// Create new service and verify new identity
let service2 = NoiseEncryptionService()
let fingerprint2 = service2.getIdentityFingerprint()
XCTAssertNotEqual(fingerprint1, fingerprint2, "New identity should be created after panic mode")
}
func testMultipleRapidInstantiations() {
// Create multiple services rapidly
var fingerprints: [String] = []
for _ in 0..<10 {
let service = NoiseEncryptionService()
fingerprints.append(service.getIdentityFingerprint())
}
// Verify all fingerprints are the same
let firstFingerprint = fingerprints[0]
for fingerprint in fingerprints {
XCTAssertEqual(fingerprint, firstFingerprint, "All instances should have same identity")
}
}
func testKeychainAccessFailureHandling() {
// This test would require mocking KeychainManager, but we can at least
// verify the service initializes even if keychain is problematic
let service = NoiseEncryptionService()
XCTAssertFalse(service.getIdentityFingerprint().isEmpty, "Service should initialize with valid identity")
}
// MARK: - Message Retention Key Tests
// Message retention feature has been removed
// MARK: - Keychain Service Name Tests
func testKeychainServiceName() {
// Verify we're using the correct service name
let expectedService = "chat.bitchat"
// Save a test item
let testKey = "test_service_verification"
let testData = "test".data(using: .utf8)!
_ = KeychainManager.shared.saveIdentityKey(testData, forKey: testKey)
// Query directly to verify service name
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: expectedService,
kSecAttrAccount as String: "identity_\(testKey)",
kSecMatchLimit as String: kSecMatchLimitOne,
kSecReturnData as String: true
]
var result: AnyObject?
let status = SecItemCopyMatching(query as CFDictionary, &result)
XCTAssertEqual(status, errSecSuccess, "Should find item with expected service name")
XCTAssertNotNil(result as? Data, "Should retrieve data")
// Clean up
_ = KeychainManager.shared.deleteIdentityKey(forKey: testKey)
}
// MARK: - Legacy Cleanup Tests
func testLegacyKeychainCleanup() {
// Create some legacy items with old service names
let legacyServices = [
"com.bitchat.passwords",
"com.bitchat.noise.identity",
"bitchat.keychain"
]
// Add test items with legacy service names
for service in legacyServices {
let addQuery: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: service,
kSecAttrAccount as String: "test_legacy_item",
kSecValueData as String: "test".data(using: .utf8)!
]
// Add item (ignore if already exists)
_ = SecItemAdd(addQuery as CFDictionary, nil)
}
// Run aggressive cleanup
let deletedCount = KeychainManager.shared.aggressiveCleanupLegacyItems()
// Verify items were deleted
XCTAssertGreaterThan(deletedCount, 0, "Should delete at least some legacy items")
// Verify legacy items are gone
for service in legacyServices {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: service,
kSecMatchLimit as String: kSecMatchLimitOne
]
var result: AnyObject?
let status = SecItemCopyMatching(query as CFDictionary, &result)
XCTAssertEqual(status, errSecItemNotFound, "Legacy service '\(service)' should be deleted")
}
}
// MARK: - Performance Tests
func testIdentityLoadPerformance() {
// Ensure identity exists
_ = NoiseEncryptionService()
measure {
// Measure how long it takes to load identity
_ = NoiseEncryptionService()
}
}
}
-310
View File
@@ -1,310 +0,0 @@
//
// NoiseKeyRotationTests.swift
// bitchatTests
//
// This is free and unencumbered software released into the public domain.
// For more information, see <https://unlicense.org>
//
import XCTest
import CryptoKit
@testable import bitchat
class NoiseKeyRotationTests: XCTestCase {
var keyRotation: NoiseChannelKeyRotation!
override func setUp() {
super.setUp()
keyRotation = NoiseChannelKeyRotation()
}
override func tearDown() {
// Clean up test data
keyRotation.clearEpochs(for: "#test-channel")
super.tearDown()
}
// MARK: - Basic Key Rotation Tests
func testInitialKeyGeneration() {
let channel = "#test-channel"
let password = "test-password"
let fingerprint = "abc123def456"
// Get initial key
guard let rotatedKey = keyRotation.getCurrentKey(
for: channel,
basePassword: password,
creatorFingerprint: fingerprint
) else {
XCTFail("Failed to get initial key")
return
}
XCTAssertEqual(rotatedKey.epoch.epochNumber, 1)
XCTAssertTrue(rotatedKey.isActive)
XCTAssertNotNil(rotatedKey.key)
}
func testKeyRotation() {
let channel = "#test-channel"
let password = "test-password"
let fingerprint = "abc123def456"
// Get initial key
let initialKey = keyRotation.getCurrentKey(
for: channel,
basePassword: password,
creatorFingerprint: fingerprint
)
// Rotate key
let newEpoch = keyRotation.rotateChannelKey(
for: channel,
basePassword: password,
creatorFingerprint: fingerprint
)
XCTAssertEqual(newEpoch.epochNumber, 2)
XCTAssertNotNil(newEpoch.previousEpochCommitment)
// Get new current key
let rotatedKey = keyRotation.getCurrentKey(
for: channel,
basePassword: password,
creatorFingerprint: fingerprint
)
XCTAssertEqual(rotatedKey?.epoch.epochNumber, 2)
// Keys should be different
if let initial = initialKey, let rotated = rotatedKey {
XCTAssertNotEqual(
initial.key.withUnsafeBytes { Data($0) },
rotated.key.withUnsafeBytes { Data($0) }
)
}
}
func testKeyRotationNeeded() {
let channel = "#test-channel"
let password = "test-password"
let fingerprint = "abc123def456"
// Initially needs rotation (no epochs)
XCTAssertTrue(keyRotation.needsKeyRotation(for: channel))
// After getting initial key, shouldn't need rotation
_ = keyRotation.getCurrentKey(
for: channel,
basePassword: password,
creatorFingerprint: fingerprint
)
XCTAssertFalse(keyRotation.needsKeyRotation(for: channel))
// Note: We can't easily test time-based rotation need without
// modifying internal state or waiting 22+ hours
}
// MARK: - Multiple Epoch Tests
func testMultipleEpochsForDecryption() {
let channel = "#test-channel"
let password = "test-password"
let fingerprint = "abc123def456"
// Create initial epoch
_ = keyRotation.getCurrentKey(
for: channel,
basePassword: password,
creatorFingerprint: fingerprint
)
// Rotate multiple times
for _ in 0..<3 {
_ = keyRotation.rotateChannelKey(
for: channel,
basePassword: password,
creatorFingerprint: fingerprint
)
}
// Get valid keys for decryption
let validKeys = keyRotation.getValidKeysForDecryption(
channel: channel,
basePassword: password,
creatorFingerprint: fingerprint
)
// Should have at least the current epoch
XCTAssertGreaterThanOrEqual(validKeys.count, 1)
// Check that we have the latest epoch
XCTAssertTrue(validKeys.contains { $0.epoch.epochNumber == 4 })
}
func testEpochKeyDerivationConsistency() {
let channel = "#test-channel"
let password = "test-password"
let fingerprint = "abc123def456"
// Get key for epoch 1
let key1a = keyRotation.getCurrentKey(
for: channel,
basePassword: password,
creatorFingerprint: fingerprint
)
// Get the same key again
let key1b = keyRotation.getCurrentKey(
for: channel,
basePassword: password,
creatorFingerprint: fingerprint
)
// Keys should be identical for same epoch
if let a = key1a, let b = key1b {
XCTAssertEqual(
a.key.withUnsafeBytes { Data($0) },
b.key.withUnsafeBytes { Data($0) }
)
XCTAssertEqual(a.epoch.epochNumber, b.epoch.epochNumber)
}
}
// MARK: - Edge Cases
func testMaxEpochLimit() {
let channel = "#test-channel"
let password = "test-password"
let fingerprint = "abc123def456"
// Create initial epoch
_ = keyRotation.getCurrentKey(
for: channel,
basePassword: password,
creatorFingerprint: fingerprint
)
// Rotate many times (more than max stored epochs)
for _ in 0..<10 {
_ = keyRotation.rotateChannelKey(
for: channel,
basePassword: password,
creatorFingerprint: fingerprint
)
}
// Get all valid epochs
let validKeys = keyRotation.getValidKeysForDecryption(
channel: channel,
basePassword: password,
creatorFingerprint: fingerprint
)
// Should not exceed reasonable limit
XCTAssertLessThanOrEqual(validKeys.count, 7) // maxStoredEpochs
}
func testDifferentChannelsDifferentEpochs() {
let password = "test-password"
let fingerprint = "abc123def456"
let channel1 = "#channel-1"
let channel2 = "#channel-2"
// Get keys for both channels
let key1 = keyRotation.getCurrentKey(
for: channel1,
basePassword: password,
creatorFingerprint: fingerprint
)
let key2 = keyRotation.getCurrentKey(
for: channel2,
basePassword: password,
creatorFingerprint: fingerprint
)
// Keys should be different even with same password
if let k1 = key1, let k2 = key2 {
XCTAssertNotEqual(
k1.key.withUnsafeBytes { Data($0) },
k2.key.withUnsafeBytes { Data($0) }
)
}
}
// MARK: - Integration Tests
func testKeyRotationWithEncryption() throws {
let channel = "#test-channel"
let password = "test-password"
let fingerprint = "abc123def456"
// Get initial key
guard let initialRotatedKey = keyRotation.getCurrentKey(
for: channel,
basePassword: password,
creatorFingerprint: fingerprint
) else {
XCTFail("Failed to get initial key")
return
}
// Encrypt a message with initial key
let message = "Test message before rotation"
let nonce = ChaChaPoly.Nonce()
let sealed1 = try ChaChaPoly.seal(
Data(message.utf8),
using: initialRotatedKey.key,
nonce: nonce
)
// Rotate key
_ = keyRotation.rotateChannelKey(
for: channel,
basePassword: password,
creatorFingerprint: fingerprint
)
// Get new key
guard let newRotatedKey = keyRotation.getCurrentKey(
for: channel,
basePassword: password,
creatorFingerprint: fingerprint
) else {
XCTFail("Failed to get rotated key")
return
}
// New key should not decrypt old message
XCTAssertThrowsError(
try ChaChaPoly.open(sealed1, using: newRotatedKey.key)
)
// But we should still be able to decrypt with old epoch key
let validKeys = keyRotation.getValidKeysForDecryption(
channel: channel,
basePassword: password,
creatorFingerprint: fingerprint
)
// Try each valid key until one works
var decrypted = false
for rotatedKey in validKeys {
do {
let plaintext = try ChaChaPoly.open(sealed1, using: rotatedKey.key)
XCTAssertEqual(String(data: plaintext, encoding: .utf8), message)
decrypted = true
break
} catch {
continue
}
}
XCTAssertTrue(decrypted, "Failed to decrypt with any valid key")
}
}
-369
View File
@@ -1,369 +0,0 @@
//
// NoiseProtocolTests.swift
// bitchatTests
//
// This is free and unencumbered software released into the public domain.
// For more information, see <https://unlicense.org>
//
import XCTest
import CryptoKit
@testable import bitchat
class NoiseProtocolTests: XCTestCase {
// MARK: - Cipher State Tests
func testCipherStateEncryptDecrypt() throws {
let key = SymmetricKey(size: .bits256)
let cipher = NoiseCipherState(key: key)
let plaintext = "Hello, Noise Protocol!".data(using: .utf8)!
let associatedData = "metadata".data(using: .utf8)!
// Encrypt
let ciphertext = try cipher.encrypt(plaintext: plaintext, associatedData: associatedData)
// Create new cipher with same key for decryption
let decryptCipher = NoiseCipherState(key: key)
let decrypted = try decryptCipher.decrypt(ciphertext: ciphertext, associatedData: associatedData)
XCTAssertEqual(plaintext, decrypted)
}
func testCipherStateNonceIncrement() throws {
let key = SymmetricKey(size: .bits256)
let cipher = NoiseCipherState(key: key)
let plaintext = "Test".data(using: .utf8)!
// Encrypt multiple messages
let ct1 = try cipher.encrypt(plaintext: plaintext)
let ct2 = try cipher.encrypt(plaintext: plaintext)
let ct3 = try cipher.encrypt(plaintext: plaintext)
// All ciphertexts should be different due to nonce increment
XCTAssertNotEqual(ct1, ct2)
XCTAssertNotEqual(ct2, ct3)
XCTAssertNotEqual(ct1, ct3)
}
// MARK: - Symmetric State Tests
func testSymmetricStateInitialization() {
let protocolName = "Noise_XX_25519_ChaChaPoly_SHA256"
let state = NoiseSymmetricState(protocolName: protocolName)
// Hash should be initialized with protocol name
let hash = state.getHandshakeHash()
XCTAssertEqual(hash.count, 32) // SHA256 output
}
func testSymmetricStateMixKey() throws {
let state = NoiseSymmetricState(protocolName: "Noise_XX_25519_ChaChaPoly_SHA256")
let keyMaterial = Data(repeating: 0x42, count: 32)
state.mixKey(keyMaterial)
// After mixKey, cipher should be initialized
let plaintext = "Test".data(using: .utf8)!
let encrypted = try state.encryptAndHash(plaintext)
XCTAssertNotEqual(plaintext, encrypted)
XCTAssertEqual(encrypted.count, plaintext.count + 16) // ChaCha20Poly1305 adds 16-byte tag
}
// MARK: - Handshake State Tests
func testNoiseXXHandshakeComplete() throws {
// Create initiator and responder
let initiatorStatic = Curve25519.KeyAgreement.PrivateKey()
let responderStatic = Curve25519.KeyAgreement.PrivateKey()
var initiator = NoiseHandshakeState(role: .initiator, pattern: .XX, localStaticKey: initiatorStatic)
var responder = NoiseHandshakeState(role: .responder, pattern: .XX, localStaticKey: responderStatic)
// Message 1: initiator -> responder (e)
let msg1 = try initiator.writeMessage()
_ = try responder.readMessage(msg1)
// Message 2: responder -> initiator (e, ee, s, es)
let msg2 = try responder.writeMessage()
_ = try initiator.readMessage(msg2)
// Message 3: initiator -> responder (s, se)
let msg3 = try initiator.writeMessage()
_ = try responder.readMessage(msg3)
// Both should have completed handshake
XCTAssertTrue(initiator.isHandshakeComplete())
XCTAssertTrue(responder.isHandshakeComplete())
// Get transport ciphers
let (initSend, initRecv) = try initiator.getTransportCiphers()
let (respSend, respRecv) = try responder.getTransportCiphers()
// Test transport encryption
let testMessage = "Secret message".data(using: .utf8)!
let encrypted = try initSend.encrypt(plaintext: testMessage)
let decrypted = try respRecv.decrypt(ciphertext: encrypted)
XCTAssertEqual(testMessage, decrypted)
// Test reverse direction
let encrypted2 = try respSend.encrypt(plaintext: testMessage)
let decrypted2 = try initRecv.decrypt(ciphertext: encrypted2)
XCTAssertEqual(testMessage, decrypted2)
}
func testNoiseXXWithPayloads() throws {
let initiatorStatic = Curve25519.KeyAgreement.PrivateKey()
let responderStatic = Curve25519.KeyAgreement.PrivateKey()
var initiator = NoiseHandshakeState(role: .initiator, pattern: .XX, localStaticKey: initiatorStatic)
var responder = NoiseHandshakeState(role: .responder, pattern: .XX, localStaticKey: responderStatic)
// Message 1 with payload
let payload1 = "Hello from initiator".data(using: .utf8)!
let msg1 = try initiator.writeMessage(payload: payload1)
let received1 = try responder.readMessage(msg1)
XCTAssertEqual(payload1, received1)
// Message 2 with payload
let payload2 = "Hello from responder".data(using: .utf8)!
let msg2 = try responder.writeMessage(payload: payload2)
let received2 = try initiator.readMessage(msg2)
XCTAssertEqual(payload2, received2)
// Message 3 with payload
let payload3 = "Final message".data(using: .utf8)!
let msg3 = try initiator.writeMessage(payload: payload3)
let received3 = try responder.readMessage(msg3)
XCTAssertEqual(payload3, received3)
}
// MARK: - Session Tests
func testNoiseSessionLifecycle() throws {
let aliceKey = Curve25519.KeyAgreement.PrivateKey()
let bobKey = Curve25519.KeyAgreement.PrivateKey()
let aliceSession = NoiseSession(peerID: "bob", role: .initiator, localStaticKey: aliceKey)
let bobSession = NoiseSession(peerID: "alice", role: .responder, localStaticKey: bobKey)
// Start handshake - only initiator calls startHandshake
let msg1 = try aliceSession.startHandshake()
XCTAssertFalse(msg1.isEmpty, "Initiator should send first message")
// Process messages - responder will auto-initialize on first message
let msg2 = try bobSession.processHandshakeMessage(msg1)!
XCTAssertFalse(msg2.isEmpty, "Responder should send second message")
let msg3 = try aliceSession.processHandshakeMessage(msg2)!
XCTAssertFalse(msg3.isEmpty, "Initiator should send third message")
let finalMsg = try bobSession.processHandshakeMessage(msg3)
XCTAssertNil(finalMsg, "No more messages after handshake complete")
// Both sessions should be established
XCTAssertTrue(aliceSession.isEstablished(), "Alice session should be established")
XCTAssertTrue(bobSession.isEstablished(), "Bob session should be established")
// Test encryption
let plaintext = "Test message".data(using: .utf8)!
let encrypted = try aliceSession.encrypt(plaintext)
let decrypted = try bobSession.decrypt(encrypted)
XCTAssertEqual(plaintext, decrypted)
}
// MARK: - Integration Tests
func testNoiseEncryptionServiceIntegration() throws {
// Clean up any existing keys
_ = KeychainManager.shared.deleteIdentityKey(forKey: "noiseStaticKey")
let service1 = NoiseEncryptionService()
let service2 = NoiseEncryptionService()
let peer1ID = "peer1"
let peer2ID = "peer2"
// Initiate handshake from peer1 to peer2
let handshake1 = try service1.initiateHandshake(with: peer2ID)
// Process on peer2 and get response
let handshake2 = try service2.processHandshakeMessage(from: peer1ID, message: handshake1)!
// Process response on peer1
let handshake3 = try service1.processHandshakeMessage(from: peer2ID, message: handshake2)!
// Final message on peer2
let final = try service2.processHandshakeMessage(from: peer1ID, message: handshake3)
XCTAssertNil(final)
// Both should have established sessions
XCTAssertTrue(service1.hasEstablishedSession(with: peer2ID))
XCTAssertTrue(service2.hasEstablishedSession(with: peer1ID))
// Test message encryption
let message = "Secret message".data(using: .utf8)!
let encrypted = try service1.encrypt(message, for: peer2ID)
let decrypted = try service2.decrypt(encrypted, from: peer1ID)
XCTAssertEqual(message, decrypted)
}
func testBidirectionalNoiseSession() throws {
// This test verifies that messages can be sent in both directions after handshake
let aliceKey = Curve25519.KeyAgreement.PrivateKey()
let bobKey = Curve25519.KeyAgreement.PrivateKey()
// Create session managers
let aliceManager = NoiseSessionManager(localStaticKey: aliceKey)
let bobManager = NoiseSessionManager(localStaticKey: bobKey)
// Alice initiates handshake (msg1: -> e)
let msg1 = try aliceManager.initiateHandshake(with: "bob")
XCTAssertFalse(msg1.isEmpty)
// Bob processes and responds (msg2: <- e, ee, s, es)
let msg2 = try bobManager.handleIncomingHandshake(from: "alice", message: msg1)
XCTAssertNotNil(msg2)
XCTAssertFalse(msg2!.isEmpty)
// Alice processes and sends final message (msg3: -> s, se)
let msg3 = try aliceManager.handleIncomingHandshake(from: "bob", message: msg2!)
XCTAssertNotNil(msg3)
XCTAssertFalse(msg3!.isEmpty)
// Bob processes final message
let msg4 = try bobManager.handleIncomingHandshake(from: "alice", message: msg3!)
XCTAssertNil(msg4) // Now handshake is complete
// Verify both sessions are established
XCTAssertTrue(aliceManager.getSession(for: "bob")?.isEstablished() ?? false)
XCTAssertTrue(bobManager.getSession(for: "alice")?.isEstablished() ?? false)
// Test Alice -> Bob
let aliceMessage = "Hello Bob!".data(using: .utf8)!
let encrypted1 = try aliceManager.encrypt(aliceMessage, for: "bob")
let decrypted1 = try bobManager.decrypt(encrypted1, from: "alice")
XCTAssertEqual(decrypted1, aliceMessage)
// Test Bob -> Alice
let bobMessage = "Hello Alice!".data(using: .utf8)!
let encrypted2 = try bobManager.encrypt(bobMessage, for: "alice")
let decrypted2 = try aliceManager.decrypt(encrypted2, from: "bob")
XCTAssertEqual(decrypted2, bobMessage)
// Test multiple messages in both directions
for i in 1...5 {
// Alice -> Bob
let msg = "Message \(i) from Alice".data(using: .utf8)!
let enc = try aliceManager.encrypt(msg, for: "bob")
let dec = try bobManager.decrypt(enc, from: "alice")
XCTAssertEqual(dec, msg)
// Bob -> Alice
let msg2 = "Message \(i) from Bob".data(using: .utf8)!
let enc2 = try bobManager.encrypt(msg2, for: "alice")
let dec2 = try aliceManager.decrypt(enc2, from: "bob")
XCTAssertEqual(dec2, msg2)
}
}
// MARK: - Channel Encryption Tests
func testChannelEncryption() throws {
let channelEnc = NoiseChannelEncryption()
let channel = "#test-channel"
let password = "super-secret-password"
// Set channel password
channelEnc.setChannelPassword(password, for: channel)
// Encrypt message
let message = "Hello channel!"
let encrypted = try channelEnc.encryptChannelMessage(message, for: channel)
// Decrypt message
let decrypted = try channelEnc.decryptChannelMessage(encrypted, for: channel)
XCTAssertEqual(message, decrypted)
}
func testChannelKeyDerivation() {
let channelEnc = NoiseChannelEncryption()
let password = "test-password"
// Same password and channel should produce same key
let key1 = channelEnc.deriveChannelKey(from: password, channel: "#channel1")
let key2 = channelEnc.deriveChannelKey(from: password, channel: "#channel1")
// Different channels should produce different keys
let key3 = channelEnc.deriveChannelKey(from: password, channel: "#channel2")
// Can't directly compare SymmetricKey, but we can test encryption
let testData = "test".data(using: .utf8)!
let nonce = ChaChaPoly.Nonce()
let sealed1 = try! ChaChaPoly.seal(testData, using: key1, nonce: nonce)
let sealed2 = try! ChaChaPoly.seal(testData, using: key2, nonce: nonce)
XCTAssertEqual(sealed1.ciphertext, sealed2.ciphertext)
// Different key should produce different ciphertext
let sealed3 = try! ChaChaPoly.seal(testData, using: key3, nonce: nonce)
XCTAssertNotEqual(sealed1.ciphertext, sealed3.ciphertext)
}
// MARK: - Security Tests
func testHandshakeAuthentication() throws {
let aliceKey = Curve25519.KeyAgreement.PrivateKey()
let bobKey = Curve25519.KeyAgreement.PrivateKey()
let eveKey = Curve25519.KeyAgreement.PrivateKey() // Attacker
var alice = NoiseHandshakeState(role: .initiator, pattern: .XX, localStaticKey: aliceKey)
var eve = NoiseHandshakeState(role: .responder, pattern: .XX, localStaticKey: eveKey)
// Alice initiates handshake thinking she's talking to Bob
let msg1 = try alice.writeMessage()
_ = try eve.readMessage(msg1)
// Eve responds with her keys
let msg2 = try eve.writeMessage()
_ = try alice.readMessage(msg2)
// Alice completes handshake
let msg3 = try alice.writeMessage()
_ = try eve.readMessage(msg3)
// Both complete handshake, but Alice has Eve's public key, not Bob's
let aliceRemoteKey = alice.getRemoteStaticPublicKey()
XCTAssertEqual(aliceRemoteKey?.rawRepresentation, eveKey.publicKey.rawRepresentation)
XCTAssertNotEqual(aliceRemoteKey?.rawRepresentation, bobKey.publicKey.rawRepresentation)
// This demonstrates that authentication requires out-of-band verification
// or pre-shared knowledge of public keys
}
func testReplayProtection() throws {
let key = SymmetricKey(size: .bits256)
let cipher1 = NoiseCipherState(key: key)
let cipher2 = NoiseCipherState(key: key)
let plaintext = "Test".data(using: .utf8)!
// Encrypt a message
let ciphertext = try cipher1.encrypt(plaintext: plaintext)
// Decrypt normally works
_ = try cipher2.decrypt(ciphertext: ciphertext)
// Replaying the same ciphertext should fail due to nonce mismatch
XCTAssertThrowsError(try cipher2.decrypt(ciphertext: ciphertext))
}
}
-206
View File
@@ -1,206 +0,0 @@
//
// NoiseRateLimiterTests.swift
// bitchatTests
//
// This is free and unencumbered software released into the public domain.
// For more information, see <https://unlicense.org>
//
import XCTest
@testable import bitchat
class NoiseRateLimiterTests: XCTestCase {
// MARK: - Basic Rate Limiting Tests
func testHandshakeRateLimiting() {
let rateLimiter = NoiseRateLimiter()
let peerID = "test-peer"
// First few handshakes should be allowed
XCTAssertTrue(rateLimiter.allowHandshake(from: peerID))
XCTAssertTrue(rateLimiter.allowHandshake(from: peerID))
XCTAssertTrue(rateLimiter.allowHandshake(from: peerID))
// After hitting limit, should be rate limited
// Default is 3 handshakes per minute
XCTAssertFalse(rateLimiter.allowHandshake(from: peerID))
XCTAssertFalse(rateLimiter.allowHandshake(from: peerID))
}
func testMessageRateLimiting() {
let rateLimiter = NoiseRateLimiter()
let peerID = "test-peer"
// Messages have higher limit (100 per minute default)
for _ in 0..<100 {
XCTAssertTrue(rateLimiter.allowMessage(from: peerID))
}
// 101st message should be rate limited
XCTAssertFalse(rateLimiter.allowMessage(from: peerID))
}
func testPerPeerRateLimiting() {
let rateLimiter = NoiseRateLimiter()
let peer1 = "alice"
let peer2 = "bob"
// Rate limit peer1
XCTAssertTrue(rateLimiter.allowHandshake(from: peer1))
XCTAssertTrue(rateLimiter.allowHandshake(from: peer1))
XCTAssertTrue(rateLimiter.allowHandshake(from: peer1))
XCTAssertFalse(rateLimiter.allowHandshake(from: peer1))
// Peer2 should still be allowed
XCTAssertTrue(rateLimiter.allowHandshake(from: peer2))
XCTAssertTrue(rateLimiter.allowHandshake(from: peer2))
}
// MARK: - Time Window Tests
func testRateLimitResetsAfterWindow() {
let rateLimiter = NoiseRateLimiter()
let peerID = "test-peer"
// Use up the limit
for _ in 0..<3 {
XCTAssertTrue(rateLimiter.allowHandshake(from: peerID))
}
XCTAssertFalse(rateLimiter.allowHandshake(from: peerID))
// Simulate time passing by clearing the window
rateLimiter.clearExpiredEntries()
// Should be allowed again after window expires
// Note: In real implementation, this would require actual time to pass
// For testing, we might need to inject a clock or expose internal state
}
// MARK: - Global Rate Limiting Tests
func testGlobalHandshakeLimit() {
let rateLimiter = NoiseRateLimiter()
// Global limit prevents too many handshakes across all peers
var allowedCount = 0
// Try many handshakes from different peers
for i in 0..<50 {
let peerID = "peer-\(i)"
if rateLimiter.allowHandshake(from: peerID) {
allowedCount += 1
}
}
// Should hit global limit before allowing all 50
XCTAssertLessThan(allowedCount, 50)
XCTAssertGreaterThan(allowedCount, 10) // But should allow reasonable amount
}
// MARK: - Attack Mitigation Tests
func testRapidHandshakeAttackMitigation() {
let rateLimiter = NoiseRateLimiter()
let attackerID = "attacker"
var blockedCount = 0
// Simulate rapid handshake attempts
for _ in 0..<20 {
if !rateLimiter.allowHandshake(from: attackerID) {
blockedCount += 1
}
}
// Most attempts should be blocked
XCTAssertGreaterThan(blockedCount, 15)
}
func testDistributedAttackMitigation() {
let rateLimiter = NoiseRateLimiter()
var blockedCount = 0
// Simulate distributed attack from many IPs
for i in 0..<100 {
let attackerID = "192.168.1.\(i)"
// Each attacker tries multiple times
for _ in 0..<5 {
if !rateLimiter.allowHandshake(from: attackerID) {
blockedCount += 1
}
}
}
// Global rate limiting should kick in
XCTAssertGreaterThan(blockedCount, 0)
}
// MARK: - Memory Management Tests
func testMemoryBoundedTracking() {
let rateLimiter = NoiseRateLimiter()
// Add many different peers
for i in 0..<10000 {
let peerID = "peer-\(i)"
_ = rateLimiter.allowMessage(from: peerID)
}
// Rate limiter should have bounds on memory usage
// Implementation should clean up old entries
rateLimiter.clearExpiredEntries()
// Verify it still functions correctly
XCTAssertTrue(rateLimiter.allowMessage(from: "new-peer"))
}
// MARK: - Configuration Tests
func testCustomRateLimits() {
// Test with custom configuration
let config = NoiseRateLimiter.Configuration(
handshakesPerMinute: 5,
messagesPerMinute: 200,
globalHandshakesPerMinute: 30
)
let rateLimiter = NoiseRateLimiter(configuration: config)
let peerID = "test-peer"
// Should allow up to 5 handshakes
for i in 0..<5 {
XCTAssertTrue(rateLimiter.allowHandshake(from: peerID), "Handshake \(i+1) should be allowed")
}
// 6th should be blocked
XCTAssertFalse(rateLimiter.allowHandshake(from: peerID))
}
// MARK: - Thread Safety Tests
func testConcurrentAccess() {
let rateLimiter = NoiseRateLimiter()
let expectation = self.expectation(description: "Concurrent access")
expectation.expectedFulfillmentCount = 10
// Multiple threads accessing rate limiter
for i in 0..<10 {
DispatchQueue.global().async {
let peerID = "peer-\(i)"
for _ in 0..<100 {
_ = rateLimiter.allowMessage(from: peerID)
}
expectation.fulfill()
}
}
waitForExpectations(timeout: 5) { error in
XCTAssertNil(error)
}
// Verify rate limiter still works
XCTAssertTrue(rateLimiter.allowMessage(from: "final-test"))
}
}
-507
View File
@@ -1,507 +0,0 @@
//
// NoiseSecurityTests.swift
// bitchatTests
//
// This is free and unencumbered software released into the public domain.
// For more information, see <https://unlicense.org>
//
import XCTest
import CryptoKit
@testable import bitchat
class NoiseSecurityTests: XCTestCase {
// MARK: - Channel Password Salt Tests
func testChannelPasswordSaltIncludesFingerprint() {
let encryption = NoiseChannelEncryption()
let password = "test-password-123"
let channel = "#secure-channel"
// Derive key without fingerprint
let key1 = encryption.deriveChannelKey(from: password, channel: channel, creatorFingerprint: nil)
// Derive key with fingerprint
let fingerprint = "e36f7993abc123def456789012345678901234567890abcdef1234567890abcd"
let key2 = encryption.deriveChannelKey(from: password, channel: channel, creatorFingerprint: fingerprint)
// Keys should be different due to different salts
XCTAssertNotEqual(key1.withUnsafeBytes { Data($0) }, key2.withUnsafeBytes { Data($0) })
}
func testChannelPasswordDerivationPerformance() {
let encryption = NoiseChannelEncryption()
let password = "test-password-123"
let channel = "#performance-test"
let fingerprint = "e36f7993abc123def456789012345678901234567890abcdef1234567890abcd"
// Measure time for PBKDF2 with 210,000 iterations
measure {
_ = encryption.deriveChannelKey(from: password, channel: channel, creatorFingerprint: fingerprint)
}
// Should complete within reasonable time (< 1 second on modern hardware)
}
func testDifferentChannelsProduceDifferentKeys() {
let encryption = NoiseChannelEncryption()
let password = "same-password"
let fingerprint = "e36f7993abc123def456789012345678901234567890abcdef1234567890abcd"
let key1 = encryption.deriveChannelKey(from: password, channel: "#channel1", creatorFingerprint: fingerprint)
let key2 = encryption.deriveChannelKey(from: password, channel: "#channel2", creatorFingerprint: fingerprint)
// Same password but different channels should produce different keys
XCTAssertNotEqual(key1.withUnsafeBytes { Data($0) }, key2.withUnsafeBytes { Data($0) })
}
// MARK: - Message Padding Tests
func testMessagePaddingAppliedToAllPackets() throws {
// Create a small packet
let packet = BitchatPacket(
type: MessageType.message.rawValue,
senderID: Data("testuser".utf8),
recipientID: nil,
timestamp: UInt64(Date().timeIntervalSince1970 * 1000),
payload: Data("Hello".utf8),
signature: nil,
ttl: 3
)
// Encode packet
guard let encodedData = packet.toBinaryData() else {
XCTFail("Failed to encode packet")
return
}
// Check that size matches one of the standard block sizes
let blockSizes = [256, 512, 1024, 2048]
XCTAssertTrue(blockSizes.contains(encodedData.count) || encodedData.count > 2048,
"Encoded data size \(encodedData.count) doesn't match expected block sizes")
// Decode should work correctly
guard let decodedPacket = BitchatPacket.from(encodedData) else {
XCTFail("Failed to decode packet")
return
}
// Verify decoded content matches original
XCTAssertEqual(decodedPacket.type, packet.type)
XCTAssertEqual(String(data: decodedPacket.payload, encoding: .utf8),
String(data: packet.payload, encoding: .utf8))
}
func testPaddingConsistentAcrossMessages() {
// Create multiple packets with same size payload
let packets: [BitchatPacket] = (0..<5).map { i in
BitchatPacket(
type: MessageType.message.rawValue,
senderID: Data("user\(i)".utf8),
recipientID: nil,
timestamp: UInt64(Date().timeIntervalSince1970 * 1000),
payload: Data("Same size message content here".utf8),
signature: nil,
ttl: 3
)
}
// Encode all packets
let encodedSizes = packets.compactMap { $0.toBinaryData()?.count }
// All should have same padded size
XCTAssertEqual(encodedSizes.count, packets.count)
let firstSize = encodedSizes[0]
XCTAssertTrue(encodedSizes.allSatisfy { $0 == firstSize },
"All packets with similar content should pad to same size")
}
// MARK: - Public Key Validation Tests
func testValidPublicKeyAccepted() throws {
// Generate a valid key
let validKey = Curve25519.KeyAgreement.PrivateKey()
let publicKeyData = validKey.publicKey.rawRepresentation
// Should validate successfully
let validated = try NoiseHandshakeState.validatePublicKey(publicKeyData)
XCTAssertEqual(validated.rawRepresentation, publicKeyData)
}
func testAllZeroKeyRejected() {
let zeroKey = Data(repeating: 0x00, count: 32)
XCTAssertThrowsError(try NoiseHandshakeState.validatePublicKey(zeroKey)) { error in
XCTAssertEqual(error as? NoiseError, NoiseError.invalidPublicKey)
}
}
func testAllOneKeyRejected() {
let oneKey = Data(repeating: 0xFF, count: 32)
XCTAssertThrowsError(try NoiseHandshakeState.validatePublicKey(oneKey)) { error in
XCTAssertEqual(error as? NoiseError, NoiseError.invalidPublicKey)
}
}
func testInvalidKeySizeRejected() {
// Too short
let shortKey = Data(repeating: 0x42, count: 16)
XCTAssertThrowsError(try NoiseHandshakeState.validatePublicKey(shortKey)) { error in
XCTAssertEqual(error as? NoiseError, NoiseError.invalidPublicKey)
}
// Too long
let longKey = Data(repeating: 0x42, count: 64)
XCTAssertThrowsError(try NoiseHandshakeState.validatePublicKey(longKey)) { error in
XCTAssertEqual(error as? NoiseError, NoiseError.invalidPublicKey)
}
}
func testWeakKeyRejected() {
// Known weak Curve25519 key patterns
// Low order points that would result in weak DH
let weakKeys = [
Data([0x01] + Array(repeating: 0x00, count: 31)), // Near zero
Data(Array(repeating: 0x00, count: 31) + [0x01]), // Different pattern
]
for weakKey in weakKeys {
// CryptoKit should reject these during DH operation
if (try? NoiseHandshakeState.validatePublicKey(weakKey)) != nil {
// If key creation succeeds, DH should fail in validation
print("Note: Weak key pattern was not rejected by CryptoKit directly")
}
}
}
// MARK: - Integration Tests
func testSecureHandshakeWithValidation() throws {
// Create two parties
let aliceStatic = Curve25519.KeyAgreement.PrivateKey()
let bobStatic = Curve25519.KeyAgreement.PrivateKey()
var alice = NoiseHandshakeState(role: .initiator, pattern: .XX, localStaticKey: aliceStatic)
var bob = NoiseHandshakeState(role: .responder, pattern: .XX, localStaticKey: bobStatic)
// Perform handshake - validation happens automatically
let msg1 = try alice.writeMessage()
_ = try bob.readMessage(msg1)
let msg2 = try bob.writeMessage()
_ = try alice.readMessage(msg2)
let msg3 = try alice.writeMessage()
_ = try bob.readMessage(msg3)
// Both should complete successfully
XCTAssertTrue(alice.isHandshakeComplete())
XCTAssertTrue(bob.isHandshakeComplete())
}
func testPaddedMessageTransmission() throws {
// Create a packet and encode it
let originalMessage = "Test message for padding"
let packet = BitchatPacket(
type: MessageType.message.rawValue,
senderID: Data("sender123".utf8),
recipientID: Data("recipient".utf8),
timestamp: UInt64(Date().timeIntervalSince1970 * 1000),
payload: Data(originalMessage.utf8),
signature: nil,
ttl: 5
)
// Encode (with padding)
guard let encoded = packet.toBinaryData() else {
XCTFail("Failed to encode")
return
}
// Verify padded size
XCTAssertTrue(encoded.count >= originalMessage.count + 21) // Header + sender + payload
// Decode (removes padding)
guard let decoded = BitchatPacket.from(encoded) else {
XCTFail("Failed to decode")
return
}
// Verify message integrity
XCTAssertEqual(String(data: decoded.payload, encoding: .utf8), originalMessage)
}
// MARK: - Session Rekeying Tests
func testSessionRekeyingTriggered() {
// Create session manager
let localKey = Curve25519.KeyAgreement.PrivateKey()
let sessionManager = NoiseSessionManager(localStaticKey: localKey)
// Create a session
let session = sessionManager.createSession(for: "testPeer", role: .initiator)
// Complete handshake
let remoteKey = Curve25519.KeyAgreement.PrivateKey()
var remoteHandshake = NoiseHandshakeState(role: .responder, pattern: .XX, localStaticKey: remoteKey)
do {
let msg1 = try session.startHandshake()
_ = try remoteHandshake.readMessage(msg1)
let msg2 = try remoteHandshake.writeMessage()
_ = try session.processHandshakeMessage(msg2)
let msg3 = try session.writeMessage()
_ = try remoteHandshake.readMessage(msg3)
XCTAssertTrue(session.isEstablished())
// Get sessions needing rekey (should be empty)
var needsRekey = sessionManager.getSessionsNeedingRekey()
XCTAssertTrue(needsRekey.isEmpty)
// Force the session to need rekeying by manipulating its state
if let secureSession = session as? SecureNoiseSession {
// Set old activity time
let oldTime = Date().addingTimeInterval(-35 * 60)
secureSession.setLastActivityTimeForTesting(oldTime)
// Now check again
needsRekey = sessionManager.getSessionsNeedingRekey()
XCTAssertFalse(needsRekey.isEmpty)
XCTAssertTrue(needsRekey.contains(where: { $0.peerID == "testPeer" && $0.needsRekey }))
}
} catch {
XCTFail("Test failed: \(error)")
}
}
func testRekeyInitiation() {
// Create session manager
let localKey = Curve25519.KeyAgreement.PrivateKey()
let sessionManager = NoiseSessionManager(localStaticKey: localKey)
// Create and establish a session
let session = sessionManager.createSession(for: "testPeer", role: .initiator)
// Complete handshake
let remoteKey = Curve25519.KeyAgreement.PrivateKey()
var remoteHandshake = NoiseHandshakeState(role: .responder, pattern: .XX, localStaticKey: remoteKey)
do {
let msg1 = try session.startHandshake()
_ = try remoteHandshake.readMessage(msg1)
let msg2 = try remoteHandshake.writeMessage()
_ = try session.processHandshakeMessage(msg2)
let msg3 = try session.writeMessage()
_ = try remoteHandshake.readMessage(msg3)
XCTAssertTrue(session.isEstablished())
// Store the old session's remote key
let oldRemoteKey = session.getRemoteStaticPublicKey()
XCTAssertNotNil(oldRemoteKey)
// Initiate rekey
try sessionManager.initiateRekey(for: "testPeer")
// The old session should be removed
let currentSession = sessionManager.getSession(for: "testPeer")
XCTAssertNil(currentSession) // Session removed, waiting for new handshake
} catch {
XCTFail("Test failed: \(error)")
}
}
// MARK: - Integration Tests
func testFullRekeyHandshake() {
// Create encryption service
let alice = NoiseEncryptionService()
let bob = NoiseEncryptionService()
let aliceID = "alice"
let bobID = "bob"
do {
// Initial handshake
let msg1 = try alice.initiateHandshake(with: bobID)
let msg2 = try bob.processHandshakeMessage(from: aliceID, message: msg1)!
_ = try alice.processHandshakeMessage(from: bobID, message: msg2)
// Verify sessions established
XCTAssertTrue(alice.hasEstablishedSession(with: bobID))
XCTAssertTrue(bob.hasEstablishedSession(with: aliceID))
// Exchange some messages
let plaintext1 = "Hello Bob"
let encrypted1 = try alice.encrypt(Data(plaintext1.utf8), for: bobID)
let decrypted1 = try bob.decrypt(encrypted1, from: aliceID)
XCTAssertEqual(String(data: decrypted1, encoding: .utf8), plaintext1)
// Force session to expire by manipulating internal state
// (In real scenario, this would happen after 30 minutes or 1M messages)
// Trigger rekey from Alice's side
var rekeyHandshakeCompleted = false
alice.onHandshakeRequired = { peerID in
XCTAssertEqual(peerID, bobID)
rekeyHandshakeCompleted = true
}
// After rekey, should be able to continue messaging
// Note: In real implementation, the rekey would be triggered automatically
} catch {
XCTFail("Integration test failed: \(error)")
}
}
func testErrorHandlingDuringHandshake() {
let service = NoiseEncryptionService()
// Test invalid peer ID
XCTAssertThrowsError(try service.initiateHandshake(with: "")) { error in
if let securityError = error as? NoiseSecurityError {
XCTAssertEqual(securityError, NoiseSecurityError.invalidPeerID)
}
}
// Test invalid handshake message
XCTAssertThrowsError(try service.processHandshakeMessage(from: "peer", message: Data())) { error in
// Should fail to parse empty data as handshake
}
// Test oversized handshake message
let oversizedMessage = Data(repeating: 0x42, count: 100_000)
XCTAssertThrowsError(try service.processHandshakeMessage(from: "peer", message: oversizedMessage)) { error in
if let securityError = error as? NoiseSecurityError {
XCTAssertEqual(securityError, NoiseSecurityError.messageTooLarge)
}
}
}
func testRateLimitingIntegration() {
let service = NoiseEncryptionService()
let peerID = "rate-limited-peer"
var handshakeAttempts = 0
var rateLimitHit = false
// Try many rapid handshakes
for _ in 0..<10 {
do {
_ = try service.initiateHandshake(with: peerID)
handshakeAttempts += 1
} catch {
if let securityError = error as? NoiseSecurityError,
securityError == NoiseSecurityError.rateLimitExceeded {
rateLimitHit = true
break
}
}
}
// Should hit rate limit before all 10 attempts
XCTAssertTrue(rateLimitHit)
XCTAssertLessThan(handshakeAttempts, 10)
}
func testChannelEncryptionIntegration() {
let service = NoiseEncryptionService()
let channel = "#integration-test"
let password = "test-password"
let fingerprint = service.getIdentityFingerprint()
// Set channel password
service.setChannelPassword(password, for: channel)
// Encrypt channel message
do {
let message = "Channel message test"
let encrypted = try service.encryptChannelMessage(message, for: channel)
// Verify it's encrypted
XCTAssertNotEqual(encrypted, Data(message.utf8))
// Decrypt
let decrypted = try service.decryptChannelMessage(encrypted, for: channel)
XCTAssertEqual(decrypted, message)
// Clean up
service.removeChannelPassword(for: channel)
} catch {
XCTFail("Channel encryption failed: \(error)")
}
}
func testSecureSessionConcurrency() {
let aliceKey = Curve25519.KeyAgreement.PrivateKey()
let bobKey = Curve25519.KeyAgreement.PrivateKey()
let alice = SecureNoiseSession(peerID: "bob", role: .initiator, localStaticKey: aliceKey)
let bob = SecureNoiseSession(peerID: "alice", role: .responder, localStaticKey: bobKey)
// Complete handshake
do {
let msg1 = try alice.startHandshake()
_ = try bob.processHandshakeMessage(msg1)
let msg2 = try bob.writeMessage()
_ = try alice.processHandshakeMessage(msg2)
let msg3 = try alice.writeMessage()
_ = try bob.processHandshakeMessage(msg3)
XCTAssertTrue(alice.isEstablished())
XCTAssertTrue(bob.isEstablished())
// Concurrent encryption/decryption
let expectation = self.expectation(description: "Concurrent operations")
expectation.expectedFulfillmentCount = 20
let queue = DispatchQueue(label: "test.concurrent", attributes: .concurrent)
for i in 0..<10 {
// Encrypt from Alice
queue.async {
do {
let message = "Message \(i) from Alice"
let encrypted = try alice.encrypt(Data(message.utf8))
let decrypted = try bob.decrypt(encrypted)
XCTAssertEqual(String(data: decrypted, encoding: .utf8), message)
expectation.fulfill()
} catch {
XCTFail("Concurrent encrypt failed: \(error)")
}
}
// Encrypt from Bob
queue.async {
do {
let message = "Message \(i) from Bob"
let encrypted = try bob.encrypt(Data(message.utf8))
let decrypted = try alice.decrypt(encrypted)
XCTAssertEqual(String(data: decrypted, encoding: .utf8), message)
expectation.fulfill()
} catch {
XCTFail("Concurrent decrypt failed: \(error)")
}
}
}
waitForExpectations(timeout: 5)
} catch {
XCTFail("Handshake failed: \(error)")
}
}
}
@@ -1,191 +0,0 @@
//
// NoiseSecurityValidatorTests.swift
// bitchatTests
//
// This is free and unencumbered software released into the public domain.
// For more information, see <https://unlicense.org>
//
import XCTest
@testable import bitchat
class NoiseSecurityValidatorTests: XCTestCase {
// MARK: - Peer ID Validation Tests
func testValidPeerIDAccepted() {
// Valid peer IDs
XCTAssertTrue(NoiseSecurityValidator.validatePeerID("user123"))
XCTAssertTrue(NoiseSecurityValidator.validatePeerID("alice"))
XCTAssertTrue(NoiseSecurityValidator.validatePeerID("bob_2024"))
XCTAssertTrue(NoiseSecurityValidator.validatePeerID("test-user"))
XCTAssertTrue(NoiseSecurityValidator.validatePeerID("192.168.1.1:8080")) // IP:port format
}
func testInvalidPeerIDRejected() {
// Empty
XCTAssertFalse(NoiseSecurityValidator.validatePeerID(""))
// Too long (over 255 chars)
let longID = String(repeating: "a", count: 256)
XCTAssertFalse(NoiseSecurityValidator.validatePeerID(longID))
// Control characters
XCTAssertFalse(NoiseSecurityValidator.validatePeerID("user\0null"))
XCTAssertFalse(NoiseSecurityValidator.validatePeerID("user\nline"))
XCTAssertFalse(NoiseSecurityValidator.validatePeerID("user\ttab"))
// Path traversal attempts
XCTAssertFalse(NoiseSecurityValidator.validatePeerID("../../../etc/passwd"))
XCTAssertFalse(NoiseSecurityValidator.validatePeerID("user/../admin"))
}
// MARK: - Message Size Validation Tests
func testValidMessageSizeAccepted() {
// Small message
let smallData = Data(repeating: 0x42, count: 100)
XCTAssertTrue(NoiseSecurityValidator.validateMessageSize(smallData))
// Medium message (1MB)
let mediumData = Data(repeating: 0x42, count: 1024 * 1024)
XCTAssertTrue(NoiseSecurityValidator.validateMessageSize(mediumData))
// Just under limit (10MB - 1 byte)
let nearLimitData = Data(repeating: 0x42, count: 10 * 1024 * 1024 - 1)
XCTAssertTrue(NoiseSecurityValidator.validateMessageSize(nearLimitData))
}
func testOversizedMessageRejected() {
// Exactly at limit (10MB)
let limitData = Data(repeating: 0x42, count: 10 * 1024 * 1024)
XCTAssertFalse(NoiseSecurityValidator.validateMessageSize(limitData))
// Over limit
let overData = Data(repeating: 0x42, count: 11 * 1024 * 1024)
XCTAssertFalse(NoiseSecurityValidator.validateMessageSize(overData))
}
func testHandshakeMessageSizeValidation() {
// Valid handshake size
let validHandshake = Data(repeating: 0x42, count: 500)
XCTAssertTrue(NoiseSecurityValidator.validateHandshakeMessageSize(validHandshake))
// Too large for handshake (over 64KB)
let largeHandshake = Data(repeating: 0x42, count: 65 * 1024)
XCTAssertFalse(NoiseSecurityValidator.validateHandshakeMessageSize(largeHandshake))
}
// MARK: - Channel Name Validation Tests
func testValidChannelNameAccepted() {
XCTAssertTrue(NoiseSecurityValidator.validateChannelName("#general"))
XCTAssertTrue(NoiseSecurityValidator.validateChannelName("#test-channel"))
XCTAssertTrue(NoiseSecurityValidator.validateChannelName("#channel_123"))
XCTAssertTrue(NoiseSecurityValidator.validateChannelName("#🎉party"))
XCTAssertTrue(NoiseSecurityValidator.validateChannelName("#2024"))
}
func testInvalidChannelNameRejected() {
// Missing # prefix
XCTAssertFalse(NoiseSecurityValidator.validateChannelName("general"))
// Empty or just #
XCTAssertFalse(NoiseSecurityValidator.validateChannelName(""))
XCTAssertFalse(NoiseSecurityValidator.validateChannelName("#"))
// Too long (over 50 chars)
let longName = "#" + String(repeating: "a", count: 51)
XCTAssertFalse(NoiseSecurityValidator.validateChannelName(longName))
// Invalid characters
XCTAssertFalse(NoiseSecurityValidator.validateChannelName("#channel\nwith\nnewlines"))
XCTAssertFalse(NoiseSecurityValidator.validateChannelName("#../../etc"))
XCTAssertFalse(NoiseSecurityValidator.validateChannelName("#channel<script>"))
}
// MARK: - Encryption Parameters Validation
func testValidateEncryptionNonce() {
// Valid 12-byte nonce for ChaCha20
let validNonce = Data(repeating: 0x42, count: 12)
XCTAssertTrue(NoiseSecurityValidator.validateNonce(validNonce))
// Invalid sizes
let shortNonce = Data(repeating: 0x42, count: 8)
XCTAssertFalse(NoiseSecurityValidator.validateNonce(shortNonce))
let longNonce = Data(repeating: 0x42, count: 16)
XCTAssertFalse(NoiseSecurityValidator.validateNonce(longNonce))
// Empty
XCTAssertFalse(NoiseSecurityValidator.validateNonce(Data()))
}
func testValidateKeyMaterial() {
// Valid 32-byte key
let validKey = Data(repeating: 0x42, count: 32)
XCTAssertTrue(NoiseSecurityValidator.validateKeyMaterial(validKey))
// Invalid sizes
XCTAssertFalse(NoiseSecurityValidator.validateKeyMaterial(Data(repeating: 0x42, count: 16)))
XCTAssertFalse(NoiseSecurityValidator.validateKeyMaterial(Data(repeating: 0x42, count: 64)))
XCTAssertFalse(NoiseSecurityValidator.validateKeyMaterial(Data()))
}
// MARK: - Input Sanitization Tests
func testSanitizePeerID() {
// Normal case
XCTAssertEqual(NoiseSecurityValidator.sanitizePeerID("alice123"), "alice123")
// Remove control characters
XCTAssertEqual(NoiseSecurityValidator.sanitizePeerID("alice\0bob"), "alicebob")
XCTAssertEqual(NoiseSecurityValidator.sanitizePeerID("user\n\r\t"), "user")
// Truncate long IDs
let longID = String(repeating: "a", count: 300)
let sanitized = NoiseSecurityValidator.sanitizePeerID(longID)
XCTAssertEqual(sanitized.count, 255)
// Empty becomes placeholder
XCTAssertEqual(NoiseSecurityValidator.sanitizePeerID(""), "unknown")
}
func testSanitizeChannelName() {
// Normal case
XCTAssertEqual(NoiseSecurityValidator.sanitizeChannelName("#general"), "#general")
// Add # prefix if missing
XCTAssertEqual(NoiseSecurityValidator.sanitizeChannelName("general"), "#general")
// Remove invalid characters
XCTAssertEqual(NoiseSecurityValidator.sanitizeChannelName("#test\nchannel"), "#testchannel")
// Truncate long names
let longName = String(repeating: "a", count: 100)
let sanitized = NoiseSecurityValidator.sanitizeChannelName(longName)
XCTAssertTrue(sanitized.hasPrefix("#"))
XCTAssertLessThanOrEqual(sanitized.count, 50)
}
// MARK: - Security Pattern Detection Tests
func testDetectSuspiciousPatterns() {
// Path traversal
XCTAssertTrue(NoiseSecurityValidator.containsSuspiciousPattern("../../../etc/passwd"))
XCTAssertTrue(NoiseSecurityValidator.containsSuspiciousPattern("..\\..\\windows\\system32"))
// Script injection
XCTAssertTrue(NoiseSecurityValidator.containsSuspiciousPattern("<script>alert('xss')</script>"))
XCTAssertTrue(NoiseSecurityValidator.containsSuspiciousPattern("javascript:void(0)"))
// SQL injection patterns
XCTAssertTrue(NoiseSecurityValidator.containsSuspiciousPattern("'; DROP TABLE users; --"))
XCTAssertTrue(NoiseSecurityValidator.containsSuspiciousPattern("1' OR '1'='1"))
// Normal text should pass
XCTAssertFalse(NoiseSecurityValidator.containsSuspiciousPattern("Hello, this is a normal message!"))
XCTAssertFalse(NoiseSecurityValidator.containsSuspiciousPattern("Meeting at 3:00 PM"))
}
}
@@ -1,437 +0,0 @@
//
// PasswordProtectedChannelTests.swift
// bitchatTests
//
// This is free and unencumbered software released into the public domain.
// For more information, see <https://unlicense.org>
//
import XCTest
import CryptoKit
import CommonCrypto
@testable import bitchat
class PasswordProtectedChannelTests: XCTestCase {
var viewModel: ChatViewModel!
override func setUp() {
super.setUp()
// Clear UserDefaults to ensure test isolation
clearAllUserDefaults()
// Create a fresh view model for each test
viewModel = ChatViewModel()
// Ensure clean state
viewModel.passwordProtectedChannels.removeAll()
viewModel.channelCreators.removeAll()
viewModel.channelPasswords.removeAll()
viewModel.channelKeys.removeAll()
viewModel.joinedChannels.removeAll()
viewModel.channelMembers.removeAll()
viewModel.channelMessages.removeAll()
}
private func clearAllUserDefaults() {
let defaults = UserDefaults.standard
defaults.removeObject(forKey: "bitchat_nickname")
defaults.removeObject(forKey: "bitchat_joined_channels")
defaults.removeObject(forKey: "bitchat_password_protected_channels")
defaults.removeObject(forKey: "bitchat_channel_creators")
defaults.removeObject(forKey: "bitchat_channel_passwords")
defaults.removeObject(forKey: "bitchat_favorite_peers")
defaults.synchronize()
}
override func tearDown() {
// Clean up after tests
clearAllUserDefaults()
viewModel = nil
super.tearDown()
}
// MARK: - Password Key Derivation Tests
func testPasswordKeyDerivation() {
// Same password and channel should always produce same key
let password = "secretPassword123"
let channelName = "#testchannel"
let key1 = deriveChannelKey(from: password, channelName: channelName)
let key2 = deriveChannelKey(from: password, channelName: channelName)
// Keys should be identical
XCTAssertEqual(key1, key2, "Same password and channel should produce same key")
}
func testDifferentPasswordsProduceDifferentKeys() {
let channelName = "#testchannel"
let password1 = "password123"
let password2 = "different456"
let key1 = deriveChannelKey(from: password1, channelName: channelName)
let key2 = deriveChannelKey(from: password2, channelName: channelName)
XCTAssertNotEqual(key1, key2, "Different passwords should produce different keys")
}
func testDifferentChannelsProduceDifferentKeys() {
let password = "samePassword"
let channel1 = "#channel1"
let channel2 = "#channel2"
let key1 = deriveChannelKey(from: password, channelName: channel1)
let key2 = deriveChannelKey(from: password, channelName: channel2)
XCTAssertNotEqual(key1, key2, "Same password in different channels should produce different keys")
}
// MARK: - Channel Creation and Joining Tests
func testJoinUnprotectedChannel() {
let channelName = "#public"
let success = viewModel.joinChannel(channelName)
XCTAssertTrue(success, "Should be able to join unprotected channel")
XCTAssertTrue(viewModel.joinedChannels.contains(channelName))
XCTAssertEqual(viewModel.currentChannel, channelName)
XCTAssertTrue(viewModel.channelMembers[channelName]?.contains(viewModel.meshService.myPeerID) ?? false)
}
func testCreatePasswordProtectedChannel() {
let channelName = "#private"
let password = "secret123"
// Join channel first
let joinSuccess = viewModel.joinChannel(channelName)
XCTAssertTrue(joinSuccess)
// Set password
viewModel.setChannelPassword(password, for: channelName)
XCTAssertTrue(viewModel.passwordProtectedChannels.contains(channelName))
XCTAssertNotNil(viewModel.channelKeys[channelName])
XCTAssertEqual(viewModel.channelPasswords[channelName], password)
XCTAssertEqual(viewModel.channelCreators[channelName], viewModel.meshService.myPeerID)
}
func testJoinPasswordProtectedEmptyChannel() {
let channelName = "#protected"
let password = "test123"
// Simulate channel being marked as password protected
viewModel.passwordProtectedChannels.insert(channelName)
// Try to join with password - should be accepted tentatively for empty channel
let success = viewModel.joinChannel(channelName, password: password)
XCTAssertTrue(success, "Should accept tentative access to empty password-protected channel")
XCTAssertNotNil(viewModel.channelKeys[channelName], "Should store key tentatively")
XCTAssertEqual(viewModel.channelPasswords[channelName], password, "Should store password tentatively")
// Should have a system message explaining tentative access
let hasSystemMessage = viewModel.messages.contains { $0.sender == "system" && $0.content.contains("waiting for encrypted messages to verify password") }
XCTAssertTrue(hasSystemMessage, "Should add system message explaining tentative access")
}
func testJoinPasswordProtectedChannelWithMessages() {
let channelName = "#secure"
let correctPassword = "correct123"
let wrongPassword = "wrong456"
let testMessage = "Test encrypted message"
// First, create the channel and set password as creator
let _ = viewModel.joinChannel(channelName)
viewModel.setChannelPassword(correctPassword, for: channelName)
// Simulate an encrypted message in the channel
let key = viewModel.channelKeys[channelName]!
guard let messageData = testMessage.data(using: .utf8) else {
XCTFail("Failed to convert message to data")
return
}
do {
let sealedBox = try AES.GCM.seal(messageData, using: key)
let encryptedData = sealedBox.combined!
let encryptedMsg = BitchatMessage(
sender: "alice",
content: "[Encrypted message - password required]",
timestamp: Date(),
isRelay: false,
originalSender: nil,
isPrivate: false,
recipientNickname: nil,
senderPeerID: "alice123",
mentions: nil,
channel: channelName,
encryptedContent: encryptedData,
isEncrypted: true
)
// Add to channel messages
viewModel.channelMessages[channelName] = [encryptedMsg]
// Clear keys to simulate another user
viewModel.channelKeys.removeValue(forKey: channelName)
viewModel.channelPasswords.removeValue(forKey: channelName)
// Try to join with wrong password
let wrongSuccess = viewModel.joinChannel(channelName, password: wrongPassword)
XCTAssertFalse(wrongSuccess, "Should reject wrong password")
// Try to join with correct password
let correctSuccess = viewModel.joinChannel(channelName, password: correctPassword)
XCTAssertTrue(correctSuccess, "Should accept correct password")
XCTAssertNotNil(viewModel.channelKeys[channelName], "Should store key for correct password")
} catch {
XCTFail("Encryption failed: \(error)")
}
}
// MARK: - Password Verification Tests
func testEncryptDecryptChannelMessage() {
let channelName = "#crypto"
let password = "cryptoKey"
let testMessage = "This is a secret message"
// Derive key
let key = deriveChannelKey(from: password, channelName: channelName)
// Encrypt
guard let messageData = testMessage.data(using: .utf8) else {
XCTFail("Failed to convert message to data")
return
}
do {
let sealedBox = try AES.GCM.seal(messageData, using: key)
let encryptedData = sealedBox.combined!
// Store key and decrypt
viewModel.channelKeys[channelName] = key
let decrypted = viewModel.decryptChannelMessage(encryptedData, channel: channelName)
XCTAssertEqual(decrypted, testMessage, "Decrypted message should match original")
} catch {
XCTFail("Encryption failed: \(error)")
}
}
func testWrongPasswordFailsDecryption() {
let channelName = "#secure"
let correctPassword = "correct"
let wrongPassword = "wrong"
let testMessage = "Secret content"
// Encrypt with correct password
let correctKey = deriveChannelKey(from: correctPassword, channelName: channelName)
guard let messageData = testMessage.data(using: .utf8) else {
XCTFail("Failed to convert message to data")
return
}
do {
let sealedBox = try AES.GCM.seal(messageData, using: correctKey)
let encryptedData = sealedBox.combined!
// Try to decrypt with wrong password
let wrongKey = deriveChannelKey(from: wrongPassword, channelName: channelName)
let decrypted = viewModel.decryptChannelMessage(encryptedData, channel: channelName, testKey: wrongKey)
XCTAssertNil(decrypted, "Wrong password should fail to decrypt")
} catch {
XCTFail("Encryption failed: \(error)")
}
}
// MARK: - Channel Creator Tests
func testOnlyCreatorCanSetPassword() {
let channelName = "#owned"
let password = "ownerOnly"
// Join channel (becomes creator)
let _ = viewModel.joinChannel(channelName)
// Set password as creator
viewModel.setChannelPassword(password, for: channelName)
XCTAssertTrue(viewModel.passwordProtectedChannels.contains(channelName))
// Simulate another user trying to set password
viewModel.channelCreators[channelName] = "otherUser123"
viewModel.setChannelPassword("hackerPassword", for: channelName)
// Password should not change
XCTAssertEqual(viewModel.channelPasswords[channelName], password, "Non-creator should not be able to change password")
}
func testCreatorCanRemovePassword() {
let channelName = "#changeable"
let password = "temporary"
// Create protected channel
let _ = viewModel.joinChannel(channelName)
viewModel.setChannelPassword(password, for: channelName)
XCTAssertTrue(viewModel.passwordProtectedChannels.contains(channelName))
// Remove password
viewModel.removeChannelPassword(for: channelName)
XCTAssertFalse(viewModel.passwordProtectedChannels.contains(channelName))
XCTAssertNil(viewModel.channelKeys[channelName])
XCTAssertNil(viewModel.channelPasswords[channelName])
}
// MARK: - Message Handling Tests
func testReceiveEncryptedMessageWithoutKey() {
let channelName = "#encrypted"
// Join channel without password
let _ = viewModel.joinChannel(channelName)
// Simulate receiving encrypted message
let encryptedMessage = BitchatMessage(
sender: "alice",
content: "[Encrypted message - password required]",
timestamp: Date(),
isRelay: false,
originalSender: nil,
isPrivate: false,
recipientNickname: nil,
senderPeerID: "alice123",
mentions: nil,
channel: channelName,
encryptedContent: Data([1, 2, 3, 4]), // dummy encrypted data
isEncrypted: true
)
viewModel.didReceiveMessage(encryptedMessage)
// Should mark channel as password protected
XCTAssertTrue(viewModel.passwordProtectedChannels.contains(channelName))
// Should add system message
let channelMessages = viewModel.channelMessages[channelName] ?? []
let hasSystemMessage = channelMessages.contains { $0.sender == "system" && $0.content.contains("password protected") }
XCTAssertTrue(hasSystemMessage, "Should add system message about password protection")
}
// MARK: - Command Tests
func testJoinCommand() {
let input = "/join #testchannel"
viewModel.sendMessage(input)
XCTAssertTrue(viewModel.joinedChannels.contains("#testchannel"))
XCTAssertEqual(viewModel.currentChannel, "#testchannel")
}
func testJoinCommandAlias() {
let input = "/j #quick"
viewModel.sendMessage(input)
XCTAssertTrue(viewModel.joinedChannels.contains("#quick"))
XCTAssertEqual(viewModel.currentChannel, "#quick")
}
func testInvalidChannelName() {
let input = "/j #invalid-channel!"
viewModel.sendMessage(input)
XCTAssertFalse(viewModel.joinedChannels.contains("#invalid-channel!"))
// Should have system message about invalid name
let hasErrorMessage = viewModel.messages.contains { $0.sender == "system" && $0.content.contains("invalid channel name") }
XCTAssertTrue(hasErrorMessage)
}
// MARK: - Key Commitment Tests
func testKeyCommitmentVerification() {
let channelName = "#commitment"
let password = "testpass123"
// Join and set password
let _ = viewModel.joinChannel(channelName)
viewModel.setChannelPassword(password, for: channelName)
// Verify key commitment was stored
XCTAssertNotNil(viewModel.channelKeyCommitments[channelName], "Should store key commitment")
// Simulate another user with the stored commitment
let commitment = viewModel.channelKeyCommitments[channelName]!
viewModel.channelKeys.removeValue(forKey: channelName)
viewModel.channelPasswords.removeValue(forKey: channelName)
// Manually set the commitment as if received from network
viewModel.channelKeyCommitments[channelName] = commitment
// Try with wrong password - should fail immediately
let wrongSuccess = viewModel.joinChannel(channelName, password: "wrongpass")
XCTAssertFalse(wrongSuccess, "Should reject wrong password via commitment check")
// Try with correct password - should succeed
let correctSuccess = viewModel.joinChannel(channelName, password: password)
XCTAssertTrue(correctSuccess, "Should accept correct password via commitment check")
}
func testOwnershipTransfer() {
let channelName = "#transfertest"
let password = "ownerpass"
// Create channel and set password
let _ = viewModel.joinChannel(channelName)
viewModel.setChannelPassword(password, for: channelName)
// Verify creator is set
XCTAssertEqual(viewModel.channelCreators[channelName], viewModel.meshService.myPeerID)
// Simulate transfer (in real app would use /transfer command)
let newOwnerID = "newowner123"
viewModel.channelCreators[channelName] = newOwnerID
// Verify ownership changed
XCTAssertEqual(viewModel.channelCreators[channelName], newOwnerID)
XCTAssertNotEqual(viewModel.channelCreators[channelName], viewModel.meshService.myPeerID)
}
}
// MARK: - Helper Extensions for Testing
extension PasswordProtectedChannelTests {
// Helper method to derive channel key for testing
// This duplicates the logic from ChatViewModel for testing purposes
func deriveChannelKey(from password: String, channelName: String) -> SymmetricKey {
let salt = channelName.data(using: .utf8)!
let iterations = 100000
let keyLength = 32
var derivedKey = Data(count: keyLength)
let passwordData = password.data(using: .utf8)!
_ = derivedKey.withUnsafeMutableBytes { derivedKeyBytes in
salt.withUnsafeBytes { saltBytes in
passwordData.withUnsafeBytes { passwordBytes in
CCKeyDerivationPBKDF(
CCPBKDFAlgorithm(kCCPBKDF2),
passwordBytes.baseAddress, passwordData.count,
saltBytes.baseAddress, salt.count,
CCPseudoRandomAlgorithm(kCCPRFHmacAlgSHA256),
UInt32(iterations),
derivedKeyBytes.baseAddress, keyLength
)
}
}
}
return SymmetricKey(data: derivedKey)
}
}
@@ -1,357 +0,0 @@
//
// ProtocolVersionNegotiationTests.swift
// bitchatTests
//
// This is free and unencumbered software released into the public domain.
// For more information, see <https://unlicense.org>
//
import XCTest
@testable import bitchat
class ProtocolVersionNegotiationTests: XCTestCase {
// MARK: - VersionHello Tests
func testVersionHelloEncodingDecoding() {
let hello = VersionHello(
supportedVersions: [1, 2, 3],
preferredVersion: 3,
clientVersion: "1.2.3",
platform: "iOS",
capabilities: ["noise", "compression"]
)
// Encode
guard let encoded = hello.encode() else {
XCTFail("Failed to encode VersionHello")
return
}
// Decode
guard let decoded = VersionHello.decode(from: encoded) else {
XCTFail("Failed to decode VersionHello")
return
}
// Verify
XCTAssertEqual(decoded.supportedVersions, hello.supportedVersions)
XCTAssertEqual(decoded.preferredVersion, hello.preferredVersion)
XCTAssertEqual(decoded.clientVersion, hello.clientVersion)
XCTAssertEqual(decoded.platform, hello.platform)
XCTAssertEqual(decoded.capabilities, hello.capabilities)
}
func testVersionHelloDefaults() {
let hello = VersionHello(
clientVersion: "1.0.0",
platform: "macOS"
)
XCTAssertEqual(hello.supportedVersions, Array(ProtocolVersion.supportedVersions))
XCTAssertEqual(hello.preferredVersion, ProtocolVersion.current)
XCTAssertNil(hello.capabilities)
}
// MARK: - VersionAck Tests
func testVersionAckEncodingDecoding() {
let ack = VersionAck(
agreedVersion: 2,
serverVersion: "1.1.0",
platform: "iOS",
capabilities: ["noise"],
rejected: false,
reason: nil
)
// Encode
guard let encoded = ack.encode() else {
XCTFail("Failed to encode VersionAck")
return
}
// Decode
guard let decoded = VersionAck.decode(from: encoded) else {
XCTFail("Failed to decode VersionAck")
return
}
// Verify
XCTAssertEqual(decoded.agreedVersion, ack.agreedVersion)
XCTAssertEqual(decoded.serverVersion, ack.serverVersion)
XCTAssertEqual(decoded.platform, ack.platform)
XCTAssertEqual(decoded.capabilities, ack.capabilities)
XCTAssertEqual(decoded.rejected, ack.rejected)
XCTAssertNil(decoded.reason)
}
func testVersionAckRejection() {
let ack = VersionAck(
agreedVersion: 0,
serverVersion: "2.0.0",
platform: "macOS",
rejected: true,
reason: "No compatible version found"
)
guard let encoded = ack.encode(),
let decoded = VersionAck.decode(from: encoded) else {
XCTFail("Failed to encode/decode rejection VersionAck")
return
}
XCTAssertTrue(decoded.rejected)
XCTAssertEqual(decoded.reason, "No compatible version found")
XCTAssertEqual(decoded.agreedVersion, 0)
}
// MARK: - ProtocolVersion Tests
func testIsSupported() {
XCTAssertTrue(ProtocolVersion.isSupported(1))
XCTAssertFalse(ProtocolVersion.isSupported(99))
XCTAssertFalse(ProtocolVersion.isSupported(0))
}
func testVersionNegotiation() {
// Test successful negotiation
let clientVersions: [UInt8] = [1, 2, 3]
let serverVersions: [UInt8] = [1, 3, 4]
let agreed = ProtocolVersion.negotiateVersion(
clientVersions: clientVersions,
serverVersions: serverVersions
)
XCTAssertEqual(agreed, 3) // Should pick highest common version
}
func testVersionNegotiationNoCommon() {
// Test no common version
let clientVersions: [UInt8] = [2, 3]
let serverVersions: [UInt8] = [4, 5]
let agreed = ProtocolVersion.negotiateVersion(
clientVersions: clientVersions,
serverVersions: serverVersions
)
XCTAssertNil(agreed)
}
func testVersionNegotiationSingleCommon() {
// Test single common version
let clientVersions: [UInt8] = [1]
let serverVersions: [UInt8] = [1, 2, 3]
let agreed = ProtocolVersion.negotiateVersion(
clientVersions: clientVersions,
serverVersions: serverVersions
)
XCTAssertEqual(agreed, 1)
}
func testVersionNegotiationEmpty() {
// Test empty version lists
let agreed1 = ProtocolVersion.negotiateVersion(
clientVersions: [],
serverVersions: [1, 2]
)
XCTAssertNil(agreed1)
let agreed2 = ProtocolVersion.negotiateVersion(
clientVersions: [1, 2],
serverVersions: []
)
XCTAssertNil(agreed2)
}
// MARK: - Binary Protocol Integration Tests
func testVersionHelloPacketEncoding() {
let hello = VersionHello(
clientVersion: "1.0.0",
platform: "iOS"
)
guard let helloData = hello.encode() else {
XCTFail("Failed to encode VersionHello")
return
}
let packet = BitchatPacket(
type: MessageType.versionHello.rawValue,
ttl: 1,
senderID: "testpeer",
payload: helloData
)
guard let encoded = packet.toBinaryData() else {
XCTFail("Failed to encode packet")
return
}
guard let decoded = BitchatPacket.from(encoded) else {
XCTFail("Failed to decode packet")
return
}
XCTAssertEqual(decoded.type, MessageType.versionHello.rawValue)
XCTAssertEqual(decoded.ttl, 1)
// Verify payload can be decoded back to VersionHello
guard let decodedHello = VersionHello.decode(from: decoded.payload) else {
XCTFail("Failed to decode VersionHello from packet payload")
return
}
XCTAssertEqual(decodedHello.clientVersion, "1.0.0")
XCTAssertEqual(decodedHello.platform, "iOS")
}
func testVersionAckPacketEncoding() {
let ack = VersionAck(
agreedVersion: 1,
serverVersion: "1.0.0",
platform: "macOS"
)
guard let ackData = ack.encode() else {
XCTFail("Failed to encode VersionAck")
return
}
let packet = BitchatPacket(
type: MessageType.versionAck.rawValue,
senderID: Data("sender".utf8),
recipientID: Data("recipient".utf8),
timestamp: UInt64(Date().timeIntervalSince1970 * 1000),
payload: ackData,
signature: nil,
ttl: 1
)
guard let encoded = packet.toBinaryData() else {
XCTFail("Failed to encode packet")
return
}
guard let decoded = BitchatPacket.from(encoded) else {
XCTFail("Failed to decode packet")
return
}
XCTAssertEqual(decoded.type, MessageType.versionAck.rawValue)
// Verify payload can be decoded back to VersionAck
guard let decodedAck = VersionAck.decode(from: decoded.payload) else {
XCTFail("Failed to decode VersionAck from packet payload")
return
}
XCTAssertEqual(decodedAck.agreedVersion, 1)
XCTAssertEqual(decodedAck.serverVersion, "1.0.0")
XCTAssertEqual(decodedAck.platform, "macOS")
}
// MARK: - Version State Management Tests
func testVersionNegotiationStateTransitions() {
var state = VersionNegotiationState.none
// Test transition to helloSent
state = .helloSent
if case .helloSent = state {
// Success
} else {
XCTFail("State should be helloSent")
}
// Test transition to ackReceived
state = .ackReceived(version: 2)
if case .ackReceived(let version) = state {
XCTAssertEqual(version, 2)
} else {
XCTFail("State should be ackReceived")
}
// Test transition to failed
state = .failed(reason: "Version mismatch")
if case .failed(let reason) = state {
XCTAssertEqual(reason, "Version mismatch")
} else {
XCTFail("State should be failed")
}
}
// MARK: - Edge Cases
func testLargeVersionNumbers() {
let hello = VersionHello(
supportedVersions: [1, 127, 255],
preferredVersion: 255,
clientVersion: "99.99.99",
platform: "iOS"
)
guard let encoded = hello.encode(),
let decoded = VersionHello.decode(from: encoded) else {
XCTFail("Failed to encode/decode with large version numbers")
return
}
XCTAssertEqual(decoded.supportedVersions, [1, 127, 255])
XCTAssertEqual(decoded.preferredVersion, 255)
}
func testEmptyCapabilities() {
let hello = VersionHello(
clientVersion: "1.0.0",
platform: "iOS",
capabilities: []
)
guard let encoded = hello.encode(),
let decoded = VersionHello.decode(from: encoded) else {
XCTFail("Failed to encode/decode with empty capabilities")
return
}
XCTAssertEqual(decoded.capabilities, [])
}
func testLongCapabilityStrings() {
let longCapability = String(repeating: "a", count: 1000)
let hello = VersionHello(
clientVersion: "1.0.0",
platform: "iOS",
capabilities: [longCapability, "normal"]
)
guard let encoded = hello.encode(),
let decoded = VersionHello.decode(from: encoded) else {
XCTFail("Failed to encode/decode with long capability strings")
return
}
XCTAssertEqual(decoded.capabilities?.count, 2)
XCTAssertEqual(decoded.capabilities?[0], longCapability)
XCTAssertEqual(decoded.capabilities?[1], "normal")
}
func testInvalidJSON() {
let invalidData = Data("not json".utf8)
XCTAssertNil(VersionHello.decode(from: invalidData))
XCTAssertNil(VersionAck.decode(from: invalidData))
}
func testEmptyData() {
let emptyData = Data()
XCTAssertNil(VersionHello.decode(from: emptyData))
XCTAssertNil(VersionAck.decode(from: emptyData))
}
}
-287
View File
@@ -1,287 +0,0 @@
//
// SecureNoiseSessionTests.swift
// bitchatTests
//
// This is free and unencumbered software released into the public domain.
// For more information, see <https://unlicense.org>
//
import XCTest
import CryptoKit
@testable import bitchat
class SecureNoiseSessionTests: XCTestCase {
// MARK: - Session Timeout Tests
func testSessionTimesOutAfter30Minutes() {
let aliceKey = Curve25519.KeyAgreement.PrivateKey()
let session = SecureNoiseSession(peerID: "bob", role: .initiator, localStaticKey: aliceKey)
// Complete handshake
let bobKey = Curve25519.KeyAgreement.PrivateKey()
var bob = NoiseHandshakeState(role: .responder, pattern: .XX, localStaticKey: bobKey)
// Perform handshake
do {
let msg1 = try session.startHandshake()
_ = try bob.readMessage(msg1)
let msg2 = try bob.writeMessage()
_ = try session.processHandshakeMessage(msg2)
let msg3 = try session.writeMessage()
_ = try bob.readMessage(msg3)
XCTAssertTrue(session.isEstablished())
// Check initial state
XCTAssertFalse(session.needsRenegotiation())
// Fast-forward time by setting lastActivity to 31 minutes ago
let thirtyOneMinutesAgo = Date().addingTimeInterval(-31 * 60)
session.setLastActivityTimeForTesting(thirtyOneMinutesAgo)
// Should now need renegotiation
XCTAssertTrue(session.needsRenegotiation())
} catch {
XCTFail("Handshake failed: \(error)")
}
}
func testSessionRemainsValidUnder30Minutes() {
let aliceKey = Curve25519.KeyAgreement.PrivateKey()
let session = SecureNoiseSession(peerID: "bob", role: .initiator, localStaticKey: aliceKey)
// Complete handshake
let bobKey = Curve25519.KeyAgreement.PrivateKey()
var bob = NoiseHandshakeState(role: .responder, pattern: .XX, localStaticKey: bobKey)
do {
let msg1 = try session.startHandshake()
_ = try bob.readMessage(msg1)
let msg2 = try bob.writeMessage()
_ = try session.processHandshakeMessage(msg2)
let msg3 = try session.writeMessage()
_ = try bob.readMessage(msg3)
XCTAssertTrue(session.isEstablished())
// Set lastActivity to 29 minutes ago
let twentyNineMinutesAgo = Date().addingTimeInterval(-29 * 60)
session.setLastActivityTimeForTesting(twentyNineMinutesAgo)
// Should NOT need renegotiation
XCTAssertFalse(session.needsRenegotiation())
} catch {
XCTFail("Handshake failed: \(error)")
}
}
// MARK: - Message Count Limit Tests
func testSessionNeedsRekeyAfterMessageLimit() {
let aliceKey = Curve25519.KeyAgreement.PrivateKey()
let bobKey = Curve25519.KeyAgreement.PrivateKey()
let alice = SecureNoiseSession(peerID: "bob", role: .initiator, localStaticKey: aliceKey)
let bob = SecureNoiseSession(peerID: "alice", role: .responder, localStaticKey: bobKey)
// Complete handshake
do {
let msg1 = try alice.startHandshake()
_ = try bob.processHandshakeMessage(msg1)
let msg2 = try bob.writeMessage()
_ = try alice.processHandshakeMessage(msg2)
let msg3 = try alice.writeMessage()
_ = try bob.processHandshakeMessage(msg3)
XCTAssertTrue(alice.isEstablished())
XCTAssertTrue(bob.isEstablished())
// Check initial state
XCTAssertFalse(alice.needsRenegotiation())
// Set message count to just under 90% threshold (900,000)
alice.setMessageCountForTesting(899_999)
XCTAssertFalse(alice.needsRenegotiation())
// Set message count to 90% threshold
alice.setMessageCountForTesting(900_000)
XCTAssertTrue(alice.needsRenegotiation())
} catch {
XCTFail("Handshake failed: \(error)")
}
}
// MARK: - Activity Tracking Tests
func testActivityUpdatesOnEncryption() {
let aliceKey = Curve25519.KeyAgreement.PrivateKey()
let bobKey = Curve25519.KeyAgreement.PrivateKey()
let alice = SecureNoiseSession(peerID: "bob", role: .initiator, localStaticKey: aliceKey)
let bob = SecureNoiseSession(peerID: "alice", role: .responder, localStaticKey: bobKey)
// Complete handshake
do {
let msg1 = try alice.startHandshake()
_ = try bob.processHandshakeMessage(msg1)
let msg2 = try bob.writeMessage()
_ = try alice.processHandshakeMessage(msg2)
let msg3 = try alice.writeMessage()
_ = try bob.processHandshakeMessage(msg3)
// Set lastActivity to 5 minutes ago
let fiveMinutesAgo = Date().addingTimeInterval(-5 * 60)
alice.setLastActivityTimeForTesting(fiveMinutesAgo)
// Encrypt a message
let plaintext = Data("Hello Bob".utf8)
_ = try alice.encrypt(plaintext)
// Activity should be updated to now
let timeSinceUpdate = Date().timeIntervalSince(alice.lastActivityTime)
XCTAssertLessThan(timeSinceUpdate, 1.0) // Should be within 1 second
} catch {
XCTFail("Test failed: \(error)")
}
}
func testActivityUpdatesOnDecryption() {
let aliceKey = Curve25519.KeyAgreement.PrivateKey()
let bobKey = Curve25519.KeyAgreement.PrivateKey()
let alice = SecureNoiseSession(peerID: "bob", role: .initiator, localStaticKey: aliceKey)
let bob = SecureNoiseSession(peerID: "alice", role: .responder, localStaticKey: bobKey)
// Complete handshake
do {
let msg1 = try alice.startHandshake()
_ = try bob.processHandshakeMessage(msg1)
let msg2 = try bob.writeMessage()
_ = try alice.processHandshakeMessage(msg2)
let msg3 = try alice.writeMessage()
_ = try bob.processHandshakeMessage(msg3)
// Encrypt a message from Alice
let plaintext = Data("Hello Bob".utf8)
let ciphertext = try alice.encrypt(plaintext)
// Set Bob's lastActivity to 5 minutes ago
let fiveMinutesAgo = Date().addingTimeInterval(-5 * 60)
bob.setLastActivityTimeForTesting(fiveMinutesAgo)
// Decrypt the message
_ = try bob.decrypt(ciphertext)
// Activity should be updated to now
let timeSinceUpdate = Date().timeIntervalSince(bob.lastActivityTime)
XCTAssertLessThan(timeSinceUpdate, 1.0) // Should be within 1 second
} catch {
XCTFail("Test failed: \(error)")
}
}
// MARK: - Message Count Tracking Tests
func testMessageCountIncrements() {
let aliceKey = Curve25519.KeyAgreement.PrivateKey()
let bobKey = Curve25519.KeyAgreement.PrivateKey()
let alice = SecureNoiseSession(peerID: "bob", role: .initiator, localStaticKey: aliceKey)
let bob = SecureNoiseSession(peerID: "alice", role: .responder, localStaticKey: bobKey)
// Complete handshake
do {
let msg1 = try alice.startHandshake()
_ = try bob.processHandshakeMessage(msg1)
let msg2 = try bob.writeMessage()
_ = try alice.processHandshakeMessage(msg2)
let msg3 = try alice.writeMessage()
_ = try bob.processHandshakeMessage(msg3)
// Check initial message count
XCTAssertEqual(alice.messageCount, 0)
// Send multiple messages
for i in 1...5 {
let plaintext = Data("Message \(i)".utf8)
let ciphertext = try alice.encrypt(plaintext)
_ = try bob.decrypt(ciphertext)
}
// Check message count incremented
XCTAssertEqual(alice.messageCount, 5) // Alice sent 5 messages
XCTAssertEqual(bob.messageCount, 0) // Bob received but didn't send
} catch {
XCTFail("Test failed: \(error)")
}
}
// MARK: - Integration Tests
func testFullSessionLifecycle() {
let aliceKey = Curve25519.KeyAgreement.PrivateKey()
let bobKey = Curve25519.KeyAgreement.PrivateKey()
let alice = SecureNoiseSession(peerID: "bob", role: .initiator, localStaticKey: aliceKey)
let bob = SecureNoiseSession(peerID: "alice", role: .responder, localStaticKey: bobKey)
do {
// 1. Perform handshake
let msg1 = try alice.startHandshake()
_ = try bob.processHandshakeMessage(msg1)
let msg2 = try bob.writeMessage()
_ = try alice.processHandshakeMessage(msg2)
let msg3 = try alice.writeMessage()
_ = try bob.processHandshakeMessage(msg3)
XCTAssertTrue(alice.isEstablished())
XCTAssertTrue(bob.isEstablished())
// 2. Exchange messages
let message1 = "Hello from Alice"
let ciphertext1 = try alice.encrypt(Data(message1.utf8))
let decrypted1 = try bob.decrypt(ciphertext1)
XCTAssertEqual(String(data: decrypted1, encoding: .utf8), message1)
let message2 = "Hello from Bob"
let ciphertext2 = try bob.encrypt(Data(message2.utf8))
let decrypted2 = try alice.decrypt(ciphertext2)
XCTAssertEqual(String(data: decrypted2, encoding: .utf8), message2)
// 3. Check session health
XCTAssertFalse(alice.needsRenegotiation())
XCTAssertFalse(bob.needsRenegotiation())
// 4. Simulate time passing
let oldTime = Date().addingTimeInterval(-35 * 60)
alice.setLastActivityTimeForTesting(oldTime)
// 5. Check renegotiation needed
XCTAssertTrue(alice.needsRenegotiation())
} catch {
XCTFail("Test failed: \(error)")
}
}
}
@@ -1,323 +0,0 @@
//
// VersionNegotiationIntegrationTests.swift
// bitchatTests
//
// This is free and unencumbered software released into the public domain.
// For more information, see <https://unlicense.org>
//
import XCTest
import CoreBluetooth
@testable import bitchat
class VersionNegotiationIntegrationTests: XCTestCase {
var meshService: BluetoothMeshService!
var mockDelegate: MockBitchatDelegate!
override func setUp() {
super.setUp()
meshService = BluetoothMeshService()
mockDelegate = MockBitchatDelegate()
meshService.delegate = mockDelegate
}
override func tearDown() {
meshService = nil
mockDelegate = nil
super.tearDown()
}
// MARK: - Version Negotiation Flow Tests
func testVersionNegotiationSuccessFlow() {
let peerID = "testpeer12345678"
// Simulate receiving version hello
let hello = VersionHello(
supportedVersions: [1],
preferredVersion: 1,
clientVersion: "1.0.0",
platform: "iOS"
)
guard let helloData = hello.encode() else {
XCTFail("Failed to encode hello")
return
}
let helloPacket = BitchatPacket(
type: MessageType.versionHello.rawValue,
senderID: Data(hexString: peerID) ?? Data(),
recipientID: nil,
timestamp: UInt64(Date().timeIntervalSince1970 * 1000),
payload: helloData,
signature: nil,
ttl: 1
)
// Process the hello packet
meshService.handleReceivedPacket(helloPacket, from: peerID, peripheral: nil)
// Verify that version was negotiated
// Note: We'd need to expose negotiatedVersions or add a getter to properly test this
// For now, we're testing that the packet is processed without errors
// The service should have sent a version ack
// In a real test, we'd mock the broadcast mechanism to verify this
}
func testVersionNegotiationRejectionFlow() {
let peerID = "incompatiblepeer"
// Simulate receiving version hello with incompatible version
let hello = VersionHello(
supportedVersions: [99, 100], // Unsupported versions
preferredVersion: 100,
clientVersion: "99.0.0",
platform: "Unknown"
)
guard let helloData = hello.encode() else {
XCTFail("Failed to encode hello")
return
}
let helloPacket = BitchatPacket(
type: MessageType.versionHello.rawValue,
senderID: Data(hexString: peerID) ?? Data(),
recipientID: nil,
timestamp: UInt64(Date().timeIntervalSince1970 * 1000),
payload: helloData,
signature: nil,
ttl: 1
)
// Process the hello packet
meshService.handleReceivedPacket(helloPacket, from: peerID, peripheral: nil)
// The service should send a rejection ack
// In a real implementation, we'd verify the rejection was sent
}
func testBackwardCompatibilityWithLegacyPeer() {
let peerID = "legacypeer123456"
// Simulate receiving a Noise handshake init without prior version negotiation
let handshakePacket = BitchatPacket(
type: MessageType.noiseHandshakeInit.rawValue,
senderID: Data(hexString: peerID) ?? Data(),
recipientID: nil,
timestamp: UInt64(Date().timeIntervalSince1970 * 1000),
payload: Data("handshake_data".utf8),
signature: nil,
ttl: 3
)
// Process the handshake packet
meshService.handleReceivedPacket(handshakePacket, from: peerID, peripheral: nil)
// Should assume version 1 for backward compatibility
// The handshake should proceed normally
}
func testVersionAckHandling() {
let peerID = "ackpeer12345678"
// Simulate receiving version ack
let ack = VersionAck(
agreedVersion: 1,
serverVersion: "1.0.0",
platform: "macOS"
)
guard let ackData = ack.encode() else {
XCTFail("Failed to encode ack")
return
}
let ackPacket = BitchatPacket(
type: MessageType.versionAck.rawValue,
senderID: Data(hexString: peerID) ?? Data(),
recipientID: meshService.getMyPeerID().data(using: .utf8),
timestamp: UInt64(Date().timeIntervalSince1970 * 1000),
payload: ackData,
signature: nil,
ttl: 1
)
// Process the ack packet
meshService.handleReceivedPacket(ackPacket, from: peerID, peripheral: nil)
// Should update negotiated version and proceed with handshake
}
func testVersionAckRejectionHandling() {
let peerID = "rejectpeer123456"
// Simulate receiving rejection ack
let ack = VersionAck(
agreedVersion: 0,
serverVersion: "2.0.0",
platform: "iOS",
rejected: true,
reason: "No compatible protocol version"
)
guard let ackData = ack.encode() else {
XCTFail("Failed to encode rejection ack")
return
}
let ackPacket = BitchatPacket(
type: MessageType.versionAck.rawValue,
senderID: Data(hexString: peerID) ?? Data(),
recipientID: meshService.getMyPeerID().data(using: .utf8),
timestamp: UInt64(Date().timeIntervalSince1970 * 1000),
payload: ackData,
signature: nil,
ttl: 1
)
// Process the rejection ack
meshService.handleReceivedPacket(ackPacket, from: peerID, peripheral: nil)
// Should mark negotiation as failed
}
// MARK: - State Management Tests
func testVersionStateCleanupOnDisconnect() {
let peerID = "disconnectpeer12"
// First establish some version negotiation state
let hello = VersionHello(
clientVersion: "1.0.0",
platform: "iOS"
)
guard let helloData = hello.encode() else {
XCTFail("Failed to encode hello")
return
}
let helloPacket = BitchatPacket(
type: MessageType.versionHello.rawValue,
senderID: Data(hexString: peerID) ?? Data(),
recipientID: nil,
timestamp: UInt64(Date().timeIntervalSince1970 * 1000),
payload: helloData,
signature: nil,
ttl: 1
)
// Process hello to establish state
meshService.handleReceivedPacket(helloPacket, from: peerID, peripheral: nil)
// Simulate disconnect
// In real implementation, we'd trigger the disconnect logic
// and verify state is cleaned up
}
// MARK: - Error Handling Tests
func testMalformedVersionHello() {
let peerID = "malformedpeer123"
// Send malformed data
let malformedPacket = BitchatPacket(
type: MessageType.versionHello.rawValue,
senderID: Data(hexString: peerID) ?? Data(),
recipientID: nil,
timestamp: UInt64(Date().timeIntervalSince1970 * 1000),
payload: Data("not valid json".utf8),
signature: nil,
ttl: 1
)
// Should handle gracefully without crashing
meshService.handleReceivedPacket(malformedPacket, from: peerID, peripheral: nil)
}
func testMalformedVersionAck() {
let peerID = "malformedackpeer"
// Send malformed ack data
let malformedPacket = BitchatPacket(
type: MessageType.versionAck.rawValue,
senderID: Data(hexString: peerID) ?? Data(),
recipientID: meshService.getMyPeerID().data(using: .utf8),
timestamp: UInt64(Date().timeIntervalSince1970 * 1000),
payload: Data("{invalid json}".utf8),
signature: nil,
ttl: 1
)
// Should handle gracefully without crashing
meshService.handleReceivedPacket(malformedPacket, from: peerID, peripheral: nil)
}
// MARK: - Performance Tests
func testVersionNegotiationPerformance() {
measure {
// Test encoding/decoding performance
for i in 0..<1000 {
let hello = VersionHello(
supportedVersions: [1, 2, 3],
preferredVersion: 3,
clientVersion: "1.\(i).0",
platform: "iOS",
capabilities: ["cap1", "cap2", "cap3"]
)
if let data = hello.encode(),
let _ = VersionHello.decode(from: data) {
// Success
} else {
XCTFail("Failed at iteration \(i)")
}
}
}
}
}
// MARK: - Mock Delegate
class MockBitchatDelegate: BitchatDelegate {
var receivedMessages: [BitchatMessage] = []
var connectedPeers: [String] = []
var disconnectedPeers: [String] = []
func didReceiveMessage(_ message: BitchatMessage) {
receivedMessages.append(message)
}
func didConnectToPeer(_ peerID: String) {
connectedPeers.append(peerID)
}
func didDisconnectFromPeer(_ peerID: String) {
disconnectedPeers.append(peerID)
}
func didUpdatePeerList(_ peers: [String]) {
// Not used in these tests
}
func didReceiveChannelLeave(_ channel: String, from peerID: String) {
// Not used in these tests
}
func didReceivePasswordProtectedChannelAnnouncement(_ channel: String, isProtected: Bool, creatorID: String?, keyCommitment: String?) {
// Not used in these tests
}
func didReceiveChannelRetentionAnnouncement(_ channel: String, enabled: Bool, creatorID: String?) {
// Not used in these tests
}
func decryptChannelMessage(_ encryptedContent: Data, channel: String) -> String? {
return nil
}
}
@@ -1,297 +0,0 @@
//
// VersionNegotiationScenarioTests.swift
// bitchatTests
//
// This is free and unencumbered software released into the public domain.
// For more information, see <https://unlicense.org>
//
import XCTest
@testable import bitchat
class VersionNegotiationScenarioTests: XCTestCase {
// MARK: - Real-World Scenarios
func testOldClientConnectsToNewServer() {
// Scenario: Old client (v1 only) connects to new server (v1, v2, v3)
let oldClientVersions: [UInt8] = [1]
let newServerVersions: [UInt8] = [1, 2, 3]
let agreed = ProtocolVersion.negotiateVersion(
clientVersions: oldClientVersions,
serverVersions: newServerVersions
)
XCTAssertEqual(agreed, 1, "Should agree on v1 for backward compatibility")
}
func testNewClientConnectsToOldServer() {
// Scenario: New client (v1, v2, v3) connects to old server (v1 only)
let newClientVersions: [UInt8] = [1, 2, 3]
let oldServerVersions: [UInt8] = [1]
let agreed = ProtocolVersion.negotiateVersion(
clientVersions: newClientVersions,
serverVersions: oldServerVersions
)
XCTAssertEqual(agreed, 1, "Should agree on v1 for backward compatibility")
}
func testMixedVersionNetwork() {
// Scenario: Network with mixed client versions
let clients: [[UInt8]] = [
[1], // Old client
[1, 2], // Mid-version client
[1, 2, 3] // New client
]
// All should be able to negotiate with each other
for (i, client1) in clients.enumerated() {
for (j, client2) in clients.enumerated() {
let agreed = ProtocolVersion.negotiateVersion(
clientVersions: client1,
serverVersions: client2
)
XCTAssertNotNil(agreed, "Clients \(i) and \(j) should negotiate successfully")
XCTAssertGreaterThanOrEqual(agreed ?? 0, 1, "Should at least agree on v1")
}
}
}
func testFutureClientWithUnsupportedVersion() {
// Scenario: Future client with only unsupported versions
let futureClientVersions: [UInt8] = [10, 11, 12]
let currentServerVersions: [UInt8] = Array(ProtocolVersion.supportedVersions)
let agreed = ProtocolVersion.negotiateVersion(
clientVersions: futureClientVersions,
serverVersions: currentServerVersions
)
XCTAssertNil(agreed, "Should fail to negotiate with incompatible future client")
}
// MARK: - Race Condition Tests
func testSimultaneousVersionHello() {
// Scenario: Both peers send version hello at the same time
// This tests that the protocol handles simultaneous negotiation
let hello1 = VersionHello(
supportedVersions: [1, 2],
preferredVersion: 2,
clientVersion: "1.1.0",
platform: "iOS"
)
let hello2 = VersionHello(
supportedVersions: [1, 2, 3],
preferredVersion: 3,
clientVersion: "1.2.0",
platform: "macOS"
)
// Both should be able to encode/decode regardless of order
XCTAssertNotNil(hello1.encode())
XCTAssertNotNil(hello2.encode())
// Version negotiation should be deterministic
let agreed1 = ProtocolVersion.negotiateVersion(
clientVersions: hello1.supportedVersions,
serverVersions: hello2.supportedVersions
)
let agreed2 = ProtocolVersion.negotiateVersion(
clientVersions: hello2.supportedVersions,
serverVersions: hello1.supportedVersions
)
XCTAssertEqual(agreed1, agreed2, "Negotiation should be symmetric")
XCTAssertEqual(agreed1, 2, "Should agree on highest common version")
}
// MARK: - Error Recovery Tests
func testRecoveryFromFailedNegotiation() {
// Test that a peer can retry after failed negotiation
var state = VersionNegotiationState.failed(reason: "Network error")
// Reset state for retry
state = .none
// Should be able to start new negotiation
state = .helloSent
if case .helloSent = state {
// Success - can retry after failure
} else {
XCTFail("Should be able to retry after failed negotiation")
}
}
func testPartialMessageHandling() {
// Test handling of truncated version messages
let truncatedData = Data([123, 34]) // Partial JSON
XCTAssertNil(VersionHello.decode(from: truncatedData))
XCTAssertNil(VersionAck.decode(from: truncatedData))
// Should not crash, just return nil
}
// MARK: - Platform Compatibility Tests
func testCrossPlatformNegotiation() {
let platforms = ["iOS", "macOS", "iPadOS", "Unknown"]
for platform1 in platforms {
for platform2 in platforms {
let hello1 = VersionHello(
clientVersion: "1.0.0",
platform: platform1
)
let hello2 = VersionHello(
clientVersion: "1.0.0",
platform: platform2
)
// All platforms should be able to negotiate
XCTAssertNotNil(hello1.encode())
XCTAssertNotNil(hello2.encode())
// Platform difference should not affect version negotiation
let agreed = ProtocolVersion.negotiateVersion(
clientVersions: hello1.supportedVersions,
serverVersions: hello2.supportedVersions
)
XCTAssertNotNil(agreed, "\(platform1) and \(platform2) should negotiate")
}
}
}
// MARK: - Capability Tests
func testCapabilityNegotiation() {
// Test future capability negotiation
let clientCapabilities = ["noise", "compression", "multipath"]
let serverCapabilities = ["noise", "compression", "federation"]
_ = VersionHello(
clientVersion: "1.0.0",
platform: "iOS",
capabilities: clientCapabilities
)
_ = VersionAck(
agreedVersion: 1,
serverVersion: "1.0.0",
platform: "macOS",
capabilities: serverCapabilities
)
// Find common capabilities (for future use)
let commonCapabilities = Set(clientCapabilities).intersection(Set(serverCapabilities))
XCTAssertEqual(commonCapabilities, ["noise", "compression"])
}
func testEmptyCapabilityHandling() {
// Test peers with no capabilities
let hello1 = VersionHello(
clientVersion: "1.0.0",
platform: "iOS",
capabilities: nil
)
let hello2 = VersionHello(
clientVersion: "1.0.0",
platform: "macOS",
capabilities: []
)
XCTAssertNotNil(hello1.encode())
XCTAssertNotNil(hello2.encode())
// Should still negotiate successfully
let agreed = ProtocolVersion.negotiateVersion(
clientVersions: hello1.supportedVersions,
serverVersions: hello2.supportedVersions
)
XCTAssertNotNil(agreed)
}
// MARK: - Stress Tests
func testManyVersionsNegotiation() {
// Test with many supported versions
let manyVersions = Array<UInt8>(1...100)
let someVersions = Array<UInt8>([1, 50, 75, 100])
let agreed = ProtocolVersion.negotiateVersion(
clientVersions: manyVersions,
serverVersions: someVersions
)
XCTAssertEqual(agreed, 100, "Should pick highest common version")
}
func testRapidConnectionDisconnection() {
// Test rapid connect/disconnect cycles
var states: [String: VersionNegotiationState] = [:]
for i in 0..<100 {
let peerID = "peer\(i)"
// Connect
states[peerID] = .helloSent
// Negotiate
states[peerID] = .ackReceived(version: 1)
// Disconnect
states.removeValue(forKey: peerID)
}
XCTAssertTrue(states.isEmpty, "All states should be cleaned up")
}
// MARK: - Security Tests
func testLargeVersionListDoS() {
// Test protection against DoS with huge version lists
let hugeVersionList = Array<UInt8>(0...255) // All possible versions
let hello = VersionHello(
supportedVersions: hugeVersionList,
preferredVersion: 255,
clientVersion: "1.0.0",
platform: "iOS"
)
// Should handle without performance issues
let startTime = Date()
_ = hello.encode()
let encodingTime = Date().timeIntervalSince(startTime)
XCTAssertLessThan(encodingTime, 0.1, "Encoding should be fast even with large version list")
}
func testVersionDowngradeAttack() {
// Test that negotiation always picks highest common version
// to prevent downgrade attacks
let clientVersions: [UInt8] = [1, 2, 3]
let serverVersions: [UInt8] = [1, 2, 3]
let agreed = ProtocolVersion.negotiateVersion(
clientVersions: clientVersions,
serverVersions: serverVersions
)
XCTAssertEqual(agreed, 3, "Should not allow downgrade to lower version")
}
}