mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-25 03:45:20 +00:00
Expand coverage for transport, chat, and media flows (#1056)
* Expand coverage for transport, chat, and media flows * Stabilize transport and media coverage tests --------- Co-authored-by: jack <jackjackbits@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,231 @@
|
||||
import Foundation
|
||||
import Testing
|
||||
@testable import bitchat
|
||||
|
||||
@Suite("NoiseEncryptionService Tests")
|
||||
struct NoiseEncryptionServiceTests {
|
||||
|
||||
@Test("Encryption status accessors cover all cases")
|
||||
func encryptionStatusAccessorsCoverAllCases() {
|
||||
#expect(EncryptionStatus.none.icon == "lock.slash")
|
||||
#expect(EncryptionStatus.noHandshake.icon == nil)
|
||||
#expect(EncryptionStatus.noiseHandshaking.icon == "lock.rotation")
|
||||
#expect(EncryptionStatus.noiseSecured.icon == "lock.fill")
|
||||
#expect(EncryptionStatus.noiseVerified.icon == "checkmark.seal.fill")
|
||||
|
||||
#expect(!EncryptionStatus.none.description.isEmpty)
|
||||
#expect(!EncryptionStatus.noHandshake.description.isEmpty)
|
||||
#expect(!EncryptionStatus.noiseHandshaking.description.isEmpty)
|
||||
#expect(!EncryptionStatus.noiseSecured.description.isEmpty)
|
||||
#expect(!EncryptionStatus.noiseVerified.description.isEmpty)
|
||||
|
||||
#expect(!EncryptionStatus.none.accessibilityDescription.isEmpty)
|
||||
#expect(!EncryptionStatus.noHandshake.accessibilityDescription.isEmpty)
|
||||
#expect(!EncryptionStatus.noiseHandshaking.accessibilityDescription.isEmpty)
|
||||
#expect(!EncryptionStatus.noiseSecured.accessibilityDescription.isEmpty)
|
||||
#expect(!EncryptionStatus.noiseVerified.accessibilityDescription.isEmpty)
|
||||
}
|
||||
|
||||
@Test("Announce and packet signatures round-trip and detect tampering")
|
||||
func announceAndPacketSignaturesRoundTrip() throws {
|
||||
let service = NoiseEncryptionService(keychain: MockKeychain())
|
||||
let signingPublicKey = service.getSigningPublicKeyData()
|
||||
let noisePublicKey = service.getStaticPublicKeyData()
|
||||
|
||||
let signature = try #require(
|
||||
service.buildAnnounceSignature(
|
||||
peerID: Data([0xAA, 0xBB]),
|
||||
noiseKey: noisePublicKey,
|
||||
ed25519Key: signingPublicKey,
|
||||
nickname: "Alice",
|
||||
timestampMs: 12345
|
||||
),
|
||||
"Expected announce signature"
|
||||
)
|
||||
|
||||
#expect(
|
||||
service.verifyAnnounceSignature(
|
||||
signature: signature,
|
||||
peerID: Data([0xAA, 0xBB]),
|
||||
noiseKey: noisePublicKey,
|
||||
ed25519Key: signingPublicKey,
|
||||
nickname: "Alice",
|
||||
timestampMs: 12345,
|
||||
publicKey: signingPublicKey
|
||||
)
|
||||
)
|
||||
#expect(
|
||||
!service.verifyAnnounceSignature(
|
||||
signature: signature,
|
||||
peerID: Data([0xAA, 0xBB]),
|
||||
noiseKey: noisePublicKey,
|
||||
ed25519Key: signingPublicKey,
|
||||
nickname: "Mallory",
|
||||
timestampMs: 12345,
|
||||
publicKey: signingPublicKey
|
||||
)
|
||||
)
|
||||
#expect(!service.verifySignature(signature, for: Data("data".utf8), publicKey: Data([1, 2, 3])))
|
||||
|
||||
let packet = BitchatPacket(
|
||||
type: MessageType.announce.rawValue,
|
||||
senderID: Data([0, 1, 2, 3, 4, 5, 6, 7]),
|
||||
recipientID: nil,
|
||||
timestamp: 42,
|
||||
payload: Data("payload".utf8),
|
||||
signature: nil,
|
||||
ttl: 7
|
||||
)
|
||||
let signedPacket = try #require(service.signPacket(packet), "Expected signed packet")
|
||||
|
||||
#expect(service.verifyPacketSignature(signedPacket, publicKey: signingPublicKey))
|
||||
#expect(!service.verifyPacketSignature(packet, publicKey: signingPublicKey))
|
||||
|
||||
var tampered = signedPacket
|
||||
tampered.signature = Data(repeating: 0xFF, count: 64)
|
||||
#expect(!service.verifyPacketSignature(tampered, publicKey: signingPublicKey))
|
||||
}
|
||||
|
||||
@Test("Service-level handshake, encryption, and fingerprint lifecycle work")
|
||||
func handshakeEncryptionAndFingerprintLifecycle() async throws {
|
||||
let alice = NoiseEncryptionService(keychain: MockKeychain())
|
||||
let bob = NoiseEncryptionService(keychain: MockKeychain())
|
||||
let alicePeerID = PeerID(str: "0011223344556677")
|
||||
let bobPeerID = PeerID(str: "8899aabbccddeeff")
|
||||
let recorder = AuthenticationRecorder()
|
||||
|
||||
#expect(alice.onPeerAuthenticated == nil)
|
||||
alice.addOnPeerAuthenticatedHandler(recorder.record(peerID:fingerprint:))
|
||||
bob.onPeerAuthenticated = recorder.record(peerID:fingerprint:)
|
||||
|
||||
try establishSessions(alice: alice, bob: bob, alicePeerID: alicePeerID, bobPeerID: bobPeerID)
|
||||
|
||||
let authenticated = await TestHelpers.waitUntil({ recorder.count >= 2 }, timeout: 0.5)
|
||||
#expect(authenticated)
|
||||
#expect(alice.hasEstablishedSession(with: alicePeerID))
|
||||
#expect(bob.hasEstablishedSession(with: bobPeerID))
|
||||
#expect(alice.hasSession(with: alicePeerID))
|
||||
#expect(bob.hasSession(with: bobPeerID))
|
||||
#expect(alice.getPeerPublicKeyData(alicePeerID)?.count == 32)
|
||||
#expect(bob.getPeerPublicKeyData(bobPeerID)?.count == 32)
|
||||
#expect(alice.getPeerFingerprint(alicePeerID) != nil)
|
||||
#expect(bob.getPeerFingerprint(bobPeerID) != nil)
|
||||
|
||||
let plaintext = Data("secret payload".utf8)
|
||||
let ciphertext = try alice.encrypt(plaintext, for: alicePeerID)
|
||||
let decrypted = try bob.decrypt(ciphertext, from: bobPeerID)
|
||||
#expect(decrypted == plaintext)
|
||||
|
||||
alice.clearSession(for: alicePeerID)
|
||||
#expect(!alice.hasSession(with: alicePeerID))
|
||||
#expect(alice.getPeerFingerprint(alicePeerID) == nil)
|
||||
|
||||
bob.clearEphemeralStateForPanic()
|
||||
#expect(!bob.hasSession(with: bobPeerID))
|
||||
#expect(bob.getPeerFingerprint(bobPeerID) == nil)
|
||||
}
|
||||
|
||||
@Test("Encrypt without a session requests handshake and decrypt without session fails")
|
||||
func handshakeRequiredAndSessionNotEstablishedErrors() throws {
|
||||
let service = NoiseEncryptionService(keychain: MockKeychain())
|
||||
let peerID = PeerID(str: "1021324354657687")
|
||||
var requestedPeerID: PeerID?
|
||||
|
||||
service.onHandshakeRequired = { requestedPeerID = $0 }
|
||||
|
||||
do {
|
||||
_ = try service.encrypt(Data("hello".utf8), for: peerID)
|
||||
Issue.record("Expected handshakeRequired error")
|
||||
} catch NoiseEncryptionError.handshakeRequired {
|
||||
#expect(requestedPeerID == peerID)
|
||||
} catch {
|
||||
Issue.record("Unexpected error: \(error)")
|
||||
}
|
||||
|
||||
do {
|
||||
_ = try service.decrypt(Data("hello".utf8), from: peerID)
|
||||
Issue.record("Expected sessionNotEstablished error")
|
||||
} catch NoiseEncryptionError.sessionNotEstablished {
|
||||
// Expected
|
||||
} catch {
|
||||
Issue.record("Unexpected error: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
@Test("Clearing persistent identity removes saved keys")
|
||||
func clearPersistentIdentityRemovesSavedKeys() {
|
||||
let keychain = MockKeychain()
|
||||
let service = NoiseEncryptionService(keychain: keychain)
|
||||
|
||||
#expect(service.getStaticPublicKeyData().count == 32)
|
||||
#expect(service.getSigningPublicKeyData().count == 32)
|
||||
|
||||
service.clearPersistentIdentity()
|
||||
|
||||
if case .itemNotFound = keychain.getIdentityKeyWithResult(forKey: "noiseStaticKey") {
|
||||
} else {
|
||||
Issue.record("Expected noiseStaticKey to be removed")
|
||||
}
|
||||
|
||||
if case .itemNotFound = keychain.getIdentityKeyWithResult(forKey: "ed25519SigningKey") {
|
||||
} else {
|
||||
Issue.record("Expected ed25519SigningKey to be removed")
|
||||
}
|
||||
}
|
||||
|
||||
@Test("NoiseMessage JSON and binary encoding round-trip")
|
||||
func noiseMessageRoundTrips() throws {
|
||||
let message = NoiseMessage(
|
||||
type: .encryptedMessage,
|
||||
sessionID: UUID().uuidString,
|
||||
payload: Data([1, 2, 3, 4])
|
||||
)
|
||||
|
||||
let encoded = try #require(message.encode(), "Expected JSON encoding")
|
||||
let decoded = try #require(NoiseMessage.decode(from: encoded), "Expected JSON decode")
|
||||
#expect(decoded.type == message.type)
|
||||
#expect(decoded.sessionID == message.sessionID)
|
||||
#expect(decoded.payload == message.payload)
|
||||
|
||||
#expect(NoiseMessage.decodeWithError(from: Data("bad".utf8)) == nil)
|
||||
|
||||
let binary = message.toBinaryData()
|
||||
let roundTripped = try #require(NoiseMessage.fromBinaryData(binary), "Expected binary decode")
|
||||
#expect(roundTripped.type == message.type)
|
||||
#expect(roundTripped.sessionID == message.sessionID)
|
||||
#expect(roundTripped.payload == message.payload)
|
||||
#expect(NoiseMessage.fromBinaryData(Data()) == nil)
|
||||
}
|
||||
|
||||
private func establishSessions(
|
||||
alice: NoiseEncryptionService,
|
||||
bob: NoiseEncryptionService,
|
||||
alicePeerID: PeerID,
|
||||
bobPeerID: PeerID
|
||||
) throws {
|
||||
let message1 = try alice.initiateHandshake(with: alicePeerID)
|
||||
let response = try bob.processHandshakeMessage(from: bobPeerID, message: message1)
|
||||
let message2 = try #require(response, "Expected handshake response")
|
||||
let final = try alice.processHandshakeMessage(from: alicePeerID, message: message2)
|
||||
let message3 = try #require(final, "Expected handshake final")
|
||||
let finalMessage = try bob.processHandshakeMessage(from: bobPeerID, message: message3)
|
||||
#expect(finalMessage == nil)
|
||||
}
|
||||
}
|
||||
|
||||
private final class AuthenticationRecorder: @unchecked Sendable {
|
||||
private let lock = NSLock()
|
||||
private var entries: [(PeerID, String)] = []
|
||||
|
||||
var count: Int {
|
||||
lock.lock()
|
||||
defer { lock.unlock() }
|
||||
return entries.count
|
||||
}
|
||||
|
||||
func record(peerID: PeerID, fingerprint: String) {
|
||||
lock.lock()
|
||||
entries.append((peerID, fingerprint))
|
||||
lock.unlock()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user