mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-25 03:05:19 +00:00
This major update replaces the basic encryption with the Noise Protocol Framework and adds ephemeral peer ID rotation for enhanced privacy. Key Changes: Security Infrastructure: - Implemented Noise Protocol Framework (XX handshake pattern) - End-to-end encryption with forward secrecy and identity hiding - Session management with automatic rekey support - Channel encryption with password-derived keys Privacy Enhancements: - Ephemeral peer ID rotation (5-15 minute random intervals) - Persistent identity through public key fingerprints - Favorites and verification persist across ID rotations - Block list based on fingerprints, not ephemeral IDs Core Components Added: - NoiseEncryptionService: Main encryption service - NoiseSession: Individual peer session management - NoiseChannelEncryption: Password-protected channel support - SecureIdentityStateManager: Persistent identity storage - FingerprintView: Visual fingerprint verification UI Bug Fixes: - Fixed handshake storm with tie-breaker mechanism - Fixed missing connect messages during peer rotation - Fixed delivery ACK compression issues - Fixed race conditions in message queue - Fixed nickname resolution for rotated peer IDs Testing: - Comprehensive test suite for Noise implementation - Security validator tests - Channel encryption tests - Identity persistence tests - Rate limiter tests Documentation: - BRING_THE_NOISE.md: Technical implementation details - Updated WHITEPAPER.md: Simplified and focused on core innovations - Removed temporary debug documentation The implementation maintains backward compatibility while significantly improving security and privacy. All existing features (channels, private messages, favorites, blocking) work seamlessly with the new system.
226 lines
8.3 KiB
Swift
226 lines
8.3 KiB
Swift
//
|
|
// 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
|
|
|
|
func testMessageRetentionKeyPersistence() {
|
|
// Create first instance
|
|
_ = MessageRetentionService.shared
|
|
|
|
// Get key from keychain
|
|
let keyData1 = KeychainManager.shared.getIdentityKey(forKey: "messageRetentionKey")
|
|
XCTAssertNotNil(keyData1, "Message retention key should be stored")
|
|
|
|
// Simulate app restart by clearing the singleton
|
|
// (In real app, this would be a new process)
|
|
|
|
// Get key again
|
|
let keyData2 = KeychainManager.shared.getIdentityKey(forKey: "messageRetentionKey")
|
|
XCTAssertEqual(keyData1, keyData2, "Message retention key should persist")
|
|
}
|
|
|
|
func testMessageRetentionKeyNotInUserDefaults() {
|
|
// Ensure service is initialized
|
|
_ = MessageRetentionService.shared
|
|
|
|
// Verify key is NOT in UserDefaults
|
|
let userDefaultsData = UserDefaults.standard.data(forKey: "bitchat.messageRetentionKey")
|
|
XCTAssertNil(userDefaultsData, "Message retention key should NOT be in UserDefaults")
|
|
}
|
|
|
|
// 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()
|
|
}
|
|
}
|
|
} |