mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-25 11:45: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.
222 lines
8.4 KiB
Swift
222 lines
8.4 KiB
Swift
//
|
|
// 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)")
|
|
}
|
|
}
|
|
}
|
|
} |