Files
bitchat/bitchatTests/SecureNoiseSessionTests.swift
T
jack 3070a4d307 Implement Noise Protocol Framework and peer ID rotation for enhanced security and privacy
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.
2025-07-15 13:15:31 +02:00

287 lines
11 KiB
Swift

//
// 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)")
}
}
}