mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-25 23:25:20 +00:00
1311 lines
48 KiB
Swift
1311 lines
48 KiB
Swift
import BitFoundation
|
|
import CryptoKit
|
|
import Foundation
|
|
import Testing
|
|
@testable import bitchat
|
|
|
|
struct BLENoisePacketHandlerTests {
|
|
private struct TestError: Error {}
|
|
|
|
private final class Recorder {
|
|
var handshakeResult: Result<Data?, Error> = .success(nil)
|
|
var handshakeAuthenticated = false
|
|
var hasSession = false
|
|
let sessionGeneration = UUID()
|
|
var awaitingResponderHandshake = false
|
|
var decryptResult: Result<Data, Error> = .success(Data())
|
|
var currentDate = Date(timeIntervalSince1970: 1_000)
|
|
var transportGenerationReady = false
|
|
var forcedServiceDecryptError: Error?
|
|
|
|
var processedHandshakes: [(peerID: PeerID, message: Data)] = []
|
|
var hasSessionQueries: [PeerID] = []
|
|
var initiatedHandshakes: [PeerID] = []
|
|
var broadcastPackets: [BitchatPacket] = []
|
|
var lastSeenUpdates: [PeerID] = []
|
|
var decryptCalls: [(payload: Data, peerID: PeerID)] = []
|
|
var clearedSessions: [PeerID] = []
|
|
var authenticatedPeerStates: [(peerID: PeerID, payload: Data, generation: UUID)] = []
|
|
var deliveries: [(peerID: PeerID, type: NoisePayloadType, payload: Data, timestamp: Date)] = []
|
|
/// Ordered side-effect log to assert recovery sequencing.
|
|
var events: [String] = []
|
|
}
|
|
|
|
private let localPeerID = PeerID(str: "0102030405060708")
|
|
private let remotePeerID = PeerID(str: "1122334455667788")
|
|
private let localPeerIDData = Data(hexString: "0102030405060708") ?? Data()
|
|
|
|
private func makeHandler(
|
|
recorder: Recorder,
|
|
now: Date = Date(timeIntervalSince1970: 1_000)
|
|
) -> BLENoisePacketHandler {
|
|
recorder.currentDate = now
|
|
let environment = BLENoisePacketHandlerEnvironment(
|
|
localPeerID: { [localPeerID] in localPeerID },
|
|
localPeerIDData: { [localPeerIDData] in localPeerIDData },
|
|
messageTTL: TransportConfig.messageTTLDefault,
|
|
now: { recorder.currentDate },
|
|
processHandshakeMessage: { peerID, message in
|
|
recorder.processedHandshakes.append((peerID, message))
|
|
return NoiseHandshakeProcessingResult(
|
|
response: try recorder.handshakeResult.get(),
|
|
didEstablishAuthenticatedSession:
|
|
recorder.handshakeAuthenticated
|
|
)
|
|
},
|
|
hasNoiseSession: { peerID in
|
|
recorder.hasSessionQueries.append(peerID)
|
|
return recorder.hasSession
|
|
},
|
|
isAwaitingResponderHandshakeCompletion: { _ in
|
|
recorder.awaitingResponderHandshake
|
|
},
|
|
initiateHandshake: { peerID in
|
|
recorder.initiatedHandshakes.append(peerID)
|
|
recorder.events.append("initiateHandshake")
|
|
},
|
|
broadcastPacket: { packet in
|
|
recorder.broadcastPackets.append(packet)
|
|
},
|
|
updatePeerLastSeen: { peerID in
|
|
recorder.lastSeenUpdates.append(peerID)
|
|
},
|
|
decrypt: { payload, peerID in
|
|
recorder.decryptCalls.append((payload, peerID))
|
|
return BLENoiseDecryptionResult(
|
|
plaintext: try recorder.decryptResult.get(),
|
|
sessionGeneration: recorder.sessionGeneration
|
|
)
|
|
},
|
|
clearSession: { peerID in
|
|
recorder.clearedSessions.append(peerID)
|
|
recorder.events.append("clearSession")
|
|
},
|
|
handleAuthenticatedPeerState: { peerID, payload, generation in
|
|
recorder.authenticatedPeerStates.append((peerID, payload, generation))
|
|
},
|
|
deliverNoisePayload: { peerID, type, payload, timestamp in
|
|
recorder.deliveries.append((peerID, type, payload, timestamp))
|
|
}
|
|
)
|
|
return BLENoisePacketHandler(environment: environment)
|
|
}
|
|
|
|
private func makeServiceBackedHandler(
|
|
service: NoiseEncryptionService,
|
|
localPeerID: PeerID,
|
|
recorder: Recorder,
|
|
transportGenerationIsReady:
|
|
@escaping (UUID) -> Bool
|
|
) -> BLENoisePacketHandler {
|
|
BLENoisePacketHandler(
|
|
environment: BLENoisePacketHandlerEnvironment(
|
|
localPeerID: { localPeerID },
|
|
localPeerIDData: {
|
|
Data(hexString: localPeerID.id) ?? Data()
|
|
},
|
|
messageTTL: TransportConfig.messageTTLDefault,
|
|
now: { recorder.currentDate },
|
|
processHandshakeMessage: { peerID, message in
|
|
try service.processHandshakeMessageWithResult(
|
|
from: peerID,
|
|
message: message
|
|
)
|
|
},
|
|
hasNoiseSession: { peerID in
|
|
service.hasSession(with: peerID)
|
|
},
|
|
isAwaitingResponderHandshakeCompletion: { peerID in
|
|
service.isAwaitingResponderHandshakeCompletion(
|
|
with: peerID
|
|
)
|
|
},
|
|
initiateHandshake: { peerID in
|
|
recorder.initiatedHandshakes.append(peerID)
|
|
},
|
|
broadcastPacket: { packet in
|
|
recorder.broadcastPackets.append(packet)
|
|
},
|
|
updatePeerLastSeen: { peerID in
|
|
recorder.lastSeenUpdates.append(peerID)
|
|
},
|
|
decrypt: { payload, peerID in
|
|
recorder.decryptCalls.append((payload, peerID))
|
|
if let error = recorder.forcedServiceDecryptError {
|
|
throw error
|
|
}
|
|
let result =
|
|
try service.decryptWithSessionGeneration(
|
|
payload,
|
|
from: peerID,
|
|
establishedGenerationIsReady:
|
|
transportGenerationIsReady
|
|
)
|
|
return BLENoiseDecryptionResult(
|
|
plaintext: result.plaintext,
|
|
sessionGeneration: result.sessionGeneration
|
|
)
|
|
},
|
|
clearSession: { peerID in
|
|
recorder.clearedSessions.append(peerID)
|
|
service.clearSession(for: peerID)
|
|
},
|
|
handleAuthenticatedPeerState: {
|
|
peerID, payload, generation in
|
|
recorder.authenticatedPeerStates.append(
|
|
(peerID, payload, generation)
|
|
)
|
|
},
|
|
deliverNoisePayload: {
|
|
peerID, type, payload, timestamp in
|
|
recorder.deliveries.append(
|
|
(peerID, type, payload, timestamp)
|
|
)
|
|
}
|
|
)
|
|
)
|
|
}
|
|
|
|
private func establishedServices() throws -> (
|
|
sender: NoiseEncryptionService,
|
|
receiver: NoiseEncryptionService,
|
|
senderPeerID: PeerID,
|
|
receiverPeerID: PeerID
|
|
) {
|
|
let sender = NoiseEncryptionService(keychain: MockKeychain())
|
|
let receiver = NoiseEncryptionService(keychain: MockKeychain())
|
|
let senderPeerID = PeerID(
|
|
publicKey: sender.getStaticPublicKeyData()
|
|
)
|
|
let receiverPeerID = PeerID(
|
|
publicKey: receiver.getStaticPublicKeyData()
|
|
)
|
|
let message1 = try sender.initiateHandshake(with: receiverPeerID)
|
|
let message2 = try #require(
|
|
try receiver.processHandshakeMessage(
|
|
from: senderPeerID,
|
|
message: message1
|
|
)
|
|
)
|
|
let message3 = try #require(
|
|
try sender.processHandshakeMessage(
|
|
from: receiverPeerID,
|
|
message: message2
|
|
)
|
|
)
|
|
_ = try receiver.processHandshakeMessage(
|
|
from: senderPeerID,
|
|
message: message3
|
|
)
|
|
return (
|
|
sender,
|
|
receiver,
|
|
senderPeerID,
|
|
receiverPeerID
|
|
)
|
|
}
|
|
|
|
// MARK: Handshake
|
|
|
|
@Test
|
|
func handshakeForUsBroadcastsResponsePacket() {
|
|
let now = Date(timeIntervalSince1970: 1_000)
|
|
let recorder = Recorder()
|
|
recorder.handshakeResult = .success(Data([0xAA, 0xBB]))
|
|
let handler = makeHandler(recorder: recorder, now: now)
|
|
let packet = makeHandshakePacket(recipientID: Data(hexString: localPeerID.id))
|
|
|
|
handler.handleHandshake(packet, from: remotePeerID)
|
|
|
|
#expect(recorder.processedHandshakes.count == 1)
|
|
#expect(recorder.processedHandshakes.first?.peerID == remotePeerID)
|
|
#expect(recorder.processedHandshakes.first?.message == packet.payload)
|
|
#expect(recorder.broadcastPackets.count == 1)
|
|
let response = recorder.broadcastPackets.first
|
|
#expect(response?.type == MessageType.noiseHandshake.rawValue)
|
|
#expect(response?.senderID == localPeerIDData)
|
|
#expect(response?.recipientID == Data(hexString: remotePeerID.id))
|
|
#expect(response?.payload == Data([0xAA, 0xBB]))
|
|
#expect(response?.signature == nil)
|
|
#expect(response?.ttl == TransportConfig.messageTTLDefault)
|
|
#expect(response?.timestamp == UInt64(now.timeIntervalSince1970 * 1000))
|
|
#expect(recorder.initiatedHandshakes.isEmpty)
|
|
}
|
|
|
|
@Test
|
|
func handshakeWithoutResponseDoesNotBroadcast() {
|
|
let recorder = Recorder()
|
|
recorder.handshakeResult = .success(nil)
|
|
let handler = makeHandler(recorder: recorder)
|
|
let packet = makeHandshakePacket(recipientID: Data(hexString: localPeerID.id))
|
|
|
|
handler.handleHandshake(packet, from: remotePeerID)
|
|
|
|
#expect(recorder.processedHandshakes.count == 1)
|
|
#expect(recorder.broadcastPackets.isEmpty)
|
|
#expect(recorder.initiatedHandshakes.isEmpty)
|
|
}
|
|
|
|
@Test
|
|
func handshakeResultPreservesExactCandidateAuthentication() {
|
|
let recorder = Recorder()
|
|
recorder.handshakeAuthenticated = true
|
|
let handler = makeHandler(recorder: recorder)
|
|
let packet = makeHandshakePacket(
|
|
recipientID: Data(hexString: localPeerID.id)
|
|
)
|
|
|
|
let result = handler.handleHandshakeWithResult(
|
|
packet,
|
|
from: remotePeerID
|
|
)
|
|
|
|
#expect(result.processed)
|
|
#expect(result.didEstablishAuthenticatedSession)
|
|
}
|
|
|
|
@Test
|
|
func handshakeForAnotherPeerIsIgnored() {
|
|
let recorder = Recorder()
|
|
let handler = makeHandler(recorder: recorder)
|
|
let packet = makeHandshakePacket(recipientID: Data(hexString: remotePeerID.id))
|
|
|
|
handler.handleHandshake(packet, from: remotePeerID)
|
|
|
|
#expect(recorder.processedHandshakes.isEmpty)
|
|
#expect(recorder.broadcastPackets.isEmpty)
|
|
#expect(recorder.initiatedHandshakes.isEmpty)
|
|
}
|
|
|
|
@Test
|
|
func handshakeFailureInitiatesNewHandshakeWhenNoSession() {
|
|
let recorder = Recorder()
|
|
recorder.handshakeResult = .failure(TestError())
|
|
recorder.hasSession = false
|
|
let handler = makeHandler(recorder: recorder)
|
|
let packet = makeHandshakePacket(recipientID: Data(hexString: localPeerID.id))
|
|
|
|
handler.handleHandshake(packet, from: remotePeerID)
|
|
|
|
#expect(recorder.hasSessionQueries == [remotePeerID])
|
|
#expect(recorder.initiatedHandshakes == [remotePeerID])
|
|
#expect(recorder.broadcastPackets.isEmpty)
|
|
}
|
|
|
|
@Test
|
|
func handshakeFailureSkipsInitiateWhenSessionExists() {
|
|
let recorder = Recorder()
|
|
recorder.handshakeResult = .failure(TestError())
|
|
recorder.hasSession = true
|
|
let handler = makeHandler(recorder: recorder)
|
|
let packet = makeHandshakePacket(recipientID: Data(hexString: localPeerID.id))
|
|
|
|
handler.handleHandshake(packet, from: remotePeerID)
|
|
|
|
#expect(recorder.hasSessionQueries == [remotePeerID])
|
|
#expect(recorder.initiatedHandshakes.isEmpty)
|
|
}
|
|
|
|
@Test
|
|
func peerIdentityMismatchDoesNotRecreateHandshakeState() {
|
|
let recorder = Recorder()
|
|
recorder.handshakeResult = .failure(NoiseSessionError.peerIdentityMismatch)
|
|
recorder.hasSession = false
|
|
let handler = makeHandler(recorder: recorder)
|
|
let packet = makeHandshakePacket(recipientID: Data(hexString: localPeerID.id))
|
|
|
|
#expect(!handler.handleHandshake(packet, from: remotePeerID))
|
|
|
|
#expect(recorder.hasSessionQueries.isEmpty)
|
|
#expect(recorder.initiatedHandshakes.isEmpty)
|
|
#expect(recorder.broadcastPackets.isEmpty)
|
|
}
|
|
|
|
@Test
|
|
func managedHandshakeFailureDoesNotStartASecondRecovery() {
|
|
let recorder = Recorder()
|
|
recorder.handshakeResult = .failure(
|
|
NoiseManagedHandshakeFailure(underlying: TestError())
|
|
)
|
|
recorder.hasSession = false
|
|
let handler = makeHandler(recorder: recorder)
|
|
let packet = makeHandshakePacket(
|
|
recipientID: Data(hexString: localPeerID.id)
|
|
)
|
|
|
|
#expect(!handler.handleHandshake(packet, from: remotePeerID))
|
|
#expect(recorder.hasSessionQueries.isEmpty)
|
|
#expect(recorder.initiatedHandshakes.isEmpty)
|
|
#expect(recorder.broadcastPackets.isEmpty)
|
|
}
|
|
|
|
// MARK: Encrypted
|
|
|
|
@Test
|
|
func encryptedWithoutRecipientIsDropped() {
|
|
let recorder = Recorder()
|
|
let handler = makeHandler(recorder: recorder)
|
|
let packet = makeEncryptedPacket(recipientID: nil)
|
|
|
|
handler.handleEncrypted(packet, from: remotePeerID)
|
|
|
|
#expect(recorder.lastSeenUpdates.isEmpty)
|
|
#expect(recorder.decryptCalls.isEmpty)
|
|
#expect(recorder.deliveries.isEmpty)
|
|
}
|
|
|
|
@Test
|
|
func encryptedForAnotherPeerIsDropped() {
|
|
let recorder = Recorder()
|
|
let handler = makeHandler(recorder: recorder)
|
|
let packet = makeEncryptedPacket(recipientID: Data(hexString: remotePeerID.id))
|
|
|
|
handler.handleEncrypted(packet, from: remotePeerID)
|
|
|
|
#expect(recorder.lastSeenUpdates.isEmpty)
|
|
#expect(recorder.decryptCalls.isEmpty)
|
|
#expect(recorder.deliveries.isEmpty)
|
|
}
|
|
|
|
@Test
|
|
func decryptedPayloadIsDeliveredWithTypeAndTimestamp() {
|
|
let now = Date(timeIntervalSince1970: 1_000)
|
|
let recorder = Recorder()
|
|
recorder.decryptResult = .success(Data([NoisePayloadType.privateMessage.rawValue, 0x01, 0x02, 0x03]))
|
|
let handler = makeHandler(recorder: recorder, now: now)
|
|
let sentAt = Date(timeIntervalSince1970: 900)
|
|
let packet = makeEncryptedPacket(
|
|
recipientID: Data(hexString: localPeerID.id),
|
|
timestamp: UInt64(sentAt.timeIntervalSince1970 * 1000)
|
|
)
|
|
|
|
handler.handleEncrypted(packet, from: remotePeerID)
|
|
|
|
#expect(recorder.lastSeenUpdates == [remotePeerID])
|
|
#expect(recorder.decryptCalls.count == 1)
|
|
#expect(recorder.decryptCalls.first?.payload == packet.payload)
|
|
#expect(recorder.deliveries.count == 1)
|
|
#expect(recorder.deliveries.first?.peerID == remotePeerID)
|
|
#expect(recorder.deliveries.first?.type == .privateMessage)
|
|
#expect(recorder.deliveries.first?.payload == Data([0x01, 0x02, 0x03]))
|
|
#expect(recorder.deliveries.first?.timestamp == sentAt)
|
|
#expect(recorder.clearedSessions.isEmpty)
|
|
#expect(recorder.initiatedHandshakes.isEmpty)
|
|
}
|
|
|
|
@Test
|
|
func authenticatedPeerStateIsConsumedByTransportNotDeliveredToUI() {
|
|
let recorder = Recorder()
|
|
recorder.decryptResult = .success(Data([
|
|
NoisePayloadType.authenticatedPeerState.rawValue,
|
|
0x01, 0x02, 0x03
|
|
]))
|
|
let handler = makeHandler(recorder: recorder)
|
|
let packet = makeEncryptedPacket(recipientID: Data(hexString: localPeerID.id))
|
|
|
|
handler.handleEncrypted(packet, from: remotePeerID)
|
|
|
|
#expect(recorder.authenticatedPeerStates.count == 1)
|
|
#expect(recorder.authenticatedPeerStates.first?.peerID == remotePeerID)
|
|
#expect(recorder.authenticatedPeerStates.first?.payload == Data([0x01, 0x02, 0x03]))
|
|
#expect(recorder.authenticatedPeerStates.first?.generation == recorder.sessionGeneration)
|
|
#expect(recorder.deliveries.isEmpty)
|
|
}
|
|
|
|
@Test
|
|
func emptyDecryptedPayloadIsIgnored() {
|
|
let recorder = Recorder()
|
|
recorder.decryptResult = .success(Data())
|
|
let handler = makeHandler(recorder: recorder)
|
|
let packet = makeEncryptedPacket(recipientID: Data(hexString: localPeerID.id))
|
|
|
|
handler.handleEncrypted(packet, from: remotePeerID)
|
|
|
|
#expect(recorder.decryptCalls.count == 1)
|
|
#expect(recorder.deliveries.isEmpty)
|
|
#expect(recorder.clearedSessions.isEmpty)
|
|
}
|
|
|
|
@Test
|
|
func unknownNoisePayloadTypeIsIgnored() {
|
|
let recorder = Recorder()
|
|
recorder.decryptResult = .success(Data([0xEE, 0x01]))
|
|
let handler = makeHandler(recorder: recorder)
|
|
let packet = makeEncryptedPacket(recipientID: Data(hexString: localPeerID.id))
|
|
|
|
handler.handleEncrypted(packet, from: remotePeerID)
|
|
|
|
#expect(recorder.decryptCalls.count == 1)
|
|
#expect(recorder.deliveries.isEmpty)
|
|
}
|
|
|
|
@Test
|
|
func missingSessionInitiatesHandshakeWithoutClearing() {
|
|
let recorder = Recorder()
|
|
recorder.decryptResult = .failure(NoiseEncryptionError.sessionNotEstablished)
|
|
recorder.hasSession = false
|
|
let handler = makeHandler(recorder: recorder)
|
|
let packet = makeEncryptedPacket(recipientID: Data(hexString: localPeerID.id))
|
|
|
|
handler.handleEncrypted(packet, from: remotePeerID)
|
|
|
|
#expect(recorder.hasSessionQueries == [remotePeerID])
|
|
#expect(recorder.initiatedHandshakes == [remotePeerID])
|
|
#expect(recorder.clearedSessions.isEmpty)
|
|
#expect(recorder.deliveries.isEmpty)
|
|
}
|
|
|
|
@Test
|
|
func missingSessionSkipsInitiateWhenSessionAppeared() {
|
|
let recorder = Recorder()
|
|
recorder.decryptResult = .failure(NoiseEncryptionError.sessionNotEstablished)
|
|
recorder.hasSession = true
|
|
let handler = makeHandler(recorder: recorder)
|
|
let packet = makeEncryptedPacket(recipientID: Data(hexString: localPeerID.id))
|
|
|
|
handler.handleEncrypted(packet, from: remotePeerID)
|
|
|
|
#expect(recorder.initiatedHandshakes.isEmpty)
|
|
#expect(recorder.clearedSessions.isEmpty)
|
|
}
|
|
|
|
@Test
|
|
func decryptFailureClearsSessionThenReinitiatesHandshake() {
|
|
let recorder = Recorder()
|
|
recorder.decryptResult = .failure(TestError())
|
|
// Even with a live session, recovery clears it and re-initiates unconditionally.
|
|
recorder.hasSession = true
|
|
let handler = makeHandler(recorder: recorder)
|
|
let packet = makeEncryptedPacket(recipientID: Data(hexString: localPeerID.id))
|
|
|
|
handler.handleEncrypted(packet, from: remotePeerID)
|
|
|
|
#expect(recorder.clearedSessions == [remotePeerID])
|
|
#expect(recorder.initiatedHandshakes == [remotePeerID])
|
|
// Session-recovery order must stay clear → re-initiate.
|
|
#expect(recorder.events == ["clearSession", "initiateHandshake"])
|
|
#expect(recorder.deliveries.isEmpty)
|
|
}
|
|
|
|
@Test
|
|
func earlyCiphertextIsRetriedAfterResponderHandshakeCompletes() {
|
|
let recorder = Recorder()
|
|
recorder.hasSession = true
|
|
recorder.awaitingResponderHandshake = true
|
|
recorder.decryptResult = .failure(
|
|
CryptoKitError.authenticationFailure
|
|
)
|
|
let handler = makeHandler(recorder: recorder)
|
|
let encrypted = makeEncryptedPacket(
|
|
recipientID: Data(hexString: localPeerID.id)
|
|
)
|
|
|
|
handler.handleEncrypted(encrypted, from: remotePeerID)
|
|
|
|
#expect(recorder.decryptCalls.count == 1)
|
|
#expect(recorder.clearedSessions.isEmpty)
|
|
#expect(recorder.initiatedHandshakes.isEmpty)
|
|
|
|
recorder.awaitingResponderHandshake = false
|
|
recorder.decryptResult = .success(
|
|
Data([NoisePayloadType.privateMessage.rawValue, 0xCA, 0xFE])
|
|
)
|
|
handler.handleSessionAuthenticated(remotePeerID)
|
|
handler.handleSessionAuthenticated(remotePeerID)
|
|
|
|
#expect(recorder.decryptCalls.count == 2)
|
|
#expect(recorder.deliveries.count == 1)
|
|
#expect(recorder.deliveries.first?.type == .privateMessage)
|
|
#expect(recorder.deliveries.first?.payload == Data([0xCA, 0xFE]))
|
|
#expect(recorder.clearedSessions.isEmpty)
|
|
#expect(recorder.initiatedHandshakes.isEmpty)
|
|
}
|
|
|
|
@Test
|
|
func panicResetDiscardsDeferredCiphertextBeforeFutureAuthentication() {
|
|
let recorder = Recorder()
|
|
recorder.hasSession = true
|
|
recorder.awaitingResponderHandshake = true
|
|
recorder.decryptResult = .failure(
|
|
CryptoKitError.authenticationFailure
|
|
)
|
|
let handler = makeHandler(recorder: recorder)
|
|
let prePanicCiphertext = makeEncryptedPacket(
|
|
recipientID: Data(hexString: localPeerID.id),
|
|
payload: Data(
|
|
count: NoiseSecurityConstants.maxMessageSize
|
|
+ NoiseSecurityConstants.transportCiphertextOverhead
|
|
)
|
|
)
|
|
|
|
handler.handleEncrypted(prePanicCiphertext, from: remotePeerID)
|
|
#expect(recorder.decryptCalls.count == 1)
|
|
|
|
handler.resetForPanic()
|
|
|
|
// Three maximum-sized packets fit only when reset also zeroed the
|
|
// global byte accounting. They model ciphertext received under the
|
|
// replacement identity before that responder handshake completes.
|
|
for index in 0..<3 {
|
|
handler.handleEncrypted(
|
|
makeEncryptedPacket(
|
|
recipientID: Data(hexString: localPeerID.id),
|
|
timestamp: UInt64(901_000 + index),
|
|
payload: Data(
|
|
count: NoiseSecurityConstants.maxMessageSize
|
|
+ NoiseSecurityConstants.transportCiphertextOverhead
|
|
)
|
|
),
|
|
from: remotePeerID
|
|
)
|
|
}
|
|
#expect(recorder.decryptCalls.count == 4)
|
|
|
|
recorder.awaitingResponderHandshake = false
|
|
recorder.decryptResult = .success(
|
|
Data([NoisePayloadType.privateMessage.rawValue, 0xCA, 0xFE])
|
|
)
|
|
handler.handleSessionAuthenticated(remotePeerID)
|
|
|
|
// Only the three post-reset packets replay; the pre-panic packet does
|
|
// not survive into the replacement session.
|
|
#expect(recorder.decryptCalls.count == 7)
|
|
#expect(recorder.deliveries.count == 3)
|
|
#expect(recorder.clearedSessions.isEmpty)
|
|
#expect(recorder.initiatedHandshakes.isEmpty)
|
|
}
|
|
|
|
@Test
|
|
func ciphertextQueuedAheadOfEstablishmentCallbackDoesNotConsumeNonce()
|
|
throws {
|
|
let alice = NoiseEncryptionService(keychain: MockKeychain())
|
|
let bob = NoiseEncryptionService(keychain: MockKeychain())
|
|
let alicePeerID = PeerID(
|
|
publicKey: alice.getStaticPublicKeyData()
|
|
)
|
|
let bobPeerID = PeerID(publicKey: bob.getStaticPublicKeyData())
|
|
|
|
let message1 = try alice.initiateHandshake(with: bobPeerID)
|
|
let message2 = try #require(
|
|
try bob.processHandshakeMessage(
|
|
from: alicePeerID,
|
|
message: message1
|
|
)
|
|
)
|
|
let message3 = try #require(
|
|
try alice.processHandshakeMessage(
|
|
from: bobPeerID,
|
|
message: message2
|
|
)
|
|
)
|
|
let typedPayload = Data([
|
|
NoisePayloadType.privateMessage.rawValue,
|
|
0xCA, 0xFE
|
|
])
|
|
let ciphertext = try alice.encrypt(
|
|
typedPayload,
|
|
for: bobPeerID
|
|
)
|
|
|
|
// Manager promotion has completed, but the serialized BLE callback is
|
|
// deliberately still behind this ciphertext.
|
|
_ = try bob.processHandshakeMessage(
|
|
from: alicePeerID,
|
|
message: message3
|
|
)
|
|
let recorder = Recorder()
|
|
recorder.transportGenerationReady = false
|
|
let handler = makeServiceBackedHandler(
|
|
service: bob,
|
|
localPeerID: bobPeerID,
|
|
recorder: recorder,
|
|
transportGenerationIsReady: { _ in
|
|
recorder.transportGenerationReady
|
|
}
|
|
)
|
|
let packet = makeEncryptedPacket(
|
|
recipientID: Data(hexString: bobPeerID.id),
|
|
payload: ciphertext
|
|
)
|
|
|
|
handler.handleEncrypted(packet, from: alicePeerID)
|
|
#expect(recorder.deliveries.isEmpty)
|
|
#expect(recorder.clearedSessions.isEmpty)
|
|
|
|
// The exact ciphertext must still authenticate, proving the readiness
|
|
// rejection happened before the receive nonce was consumed.
|
|
recorder.transportGenerationReady = true
|
|
handler.handleSessionAuthenticated(alicePeerID)
|
|
|
|
#expect(recorder.decryptCalls.count == 2)
|
|
#expect(recorder.deliveries.count == 1)
|
|
#expect(recorder.deliveries.first?.type == .privateMessage)
|
|
#expect(recorder.deliveries.first?.payload == Data([0xCA, 0xFE]))
|
|
#expect(recorder.clearedSessions.isEmpty)
|
|
#expect(recorder.initiatedHandshakes.isEmpty)
|
|
}
|
|
|
|
@Test
|
|
func ciphertextQueuedAheadOfRestoreCallbackDoesNotConsumeNonce()
|
|
throws {
|
|
let alice = NoiseEncryptionService(keychain: MockKeychain())
|
|
let bob = NoiseEncryptionService(keychain: MockKeychain())
|
|
let mallory = NoiseEncryptionService(keychain: MockKeychain())
|
|
let alicePeerID = PeerID(
|
|
publicKey: alice.getStaticPublicKeyData()
|
|
)
|
|
let bobPeerID = PeerID(publicKey: bob.getStaticPublicKeyData())
|
|
|
|
let initial1 = try alice.initiateHandshake(with: bobPeerID)
|
|
let initial2 = try #require(
|
|
try bob.processHandshakeMessage(
|
|
from: alicePeerID,
|
|
message: initial1
|
|
)
|
|
)
|
|
let initial3 = try #require(
|
|
try alice.processHandshakeMessage(
|
|
from: bobPeerID,
|
|
message: initial2
|
|
)
|
|
)
|
|
_ = try bob.processHandshakeMessage(
|
|
from: alicePeerID,
|
|
message: initial3
|
|
)
|
|
let typedPayload = Data([
|
|
NoisePayloadType.privateMessage.rawValue,
|
|
0xBE, 0xEF
|
|
])
|
|
let delayedCiphertext = try alice.encrypt(
|
|
typedPayload,
|
|
for: bobPeerID
|
|
)
|
|
|
|
let forged1 = try mallory.initiateHandshake(with: bobPeerID)
|
|
let forged2 = try #require(
|
|
try bob.processHandshakeMessage(
|
|
from: alicePeerID,
|
|
message: forged1
|
|
)
|
|
)
|
|
let forged3 = try #require(
|
|
try mallory.processHandshakeMessage(
|
|
from: bobPeerID,
|
|
message: forged2
|
|
)
|
|
)
|
|
#expect(throws: NoiseSessionError.peerIdentityMismatch) {
|
|
try bob.processHandshakeMessage(
|
|
from: alicePeerID,
|
|
message: forged3
|
|
)
|
|
}
|
|
#expect(bob.hasEstablishedSession(with: alicePeerID))
|
|
|
|
let recorder = Recorder()
|
|
recorder.transportGenerationReady = false
|
|
let handler = makeServiceBackedHandler(
|
|
service: bob,
|
|
localPeerID: bobPeerID,
|
|
recorder: recorder,
|
|
transportGenerationIsReady: { _ in
|
|
recorder.transportGenerationReady
|
|
}
|
|
)
|
|
let packet = makeEncryptedPacket(
|
|
recipientID: Data(hexString: bobPeerID.id),
|
|
payload: delayedCiphertext
|
|
)
|
|
|
|
// Manager rollback is visible, while the BLE restore callback is
|
|
// deliberately still queued behind this ciphertext.
|
|
handler.handleEncrypted(packet, from: alicePeerID)
|
|
#expect(recorder.deliveries.isEmpty)
|
|
#expect(recorder.clearedSessions.isEmpty)
|
|
|
|
recorder.transportGenerationReady = true
|
|
handler.handleSessionAuthenticated(alicePeerID)
|
|
|
|
#expect(recorder.decryptCalls.count == 2)
|
|
#expect(recorder.deliveries.count == 1)
|
|
#expect(recorder.deliveries.first?.type == .privateMessage)
|
|
#expect(recorder.deliveries.first?.payload == Data([0xBE, 0xEF]))
|
|
#expect(recorder.clearedSessions.isEmpty)
|
|
#expect(recorder.initiatedHandshakes.isEmpty)
|
|
}
|
|
|
|
@Test
|
|
func oversizedCiphertextCannotEvictEstablishedTransport() throws {
|
|
let pair = try establishedServices()
|
|
let recorder = Recorder()
|
|
recorder.transportGenerationReady = true
|
|
let handler = makeServiceBackedHandler(
|
|
service: pair.receiver,
|
|
localPeerID: pair.receiverPeerID,
|
|
recorder: recorder,
|
|
transportGenerationIsReady: { _ in
|
|
recorder.transportGenerationReady
|
|
}
|
|
)
|
|
|
|
handler.handleEncrypted(
|
|
makeEncryptedPacket(
|
|
recipientID: Data(hexString: pair.receiverPeerID.id),
|
|
payload: Data(
|
|
count:
|
|
NoiseSecurityConstants
|
|
.maxPrivateFileCiphertextSize + 1
|
|
)
|
|
),
|
|
from: pair.senderPeerID
|
|
)
|
|
#expect(
|
|
pair.receiver.hasEstablishedSession(with: pair.senderPeerID)
|
|
)
|
|
#expect(recorder.clearedSessions.isEmpty)
|
|
|
|
let valid = try pair.sender.encrypt(
|
|
Data([NoisePayloadType.privateMessage.rawValue, 0x01]),
|
|
for: pair.receiverPeerID
|
|
)
|
|
handler.handleEncrypted(
|
|
makeEncryptedPacket(
|
|
recipientID: Data(hexString: pair.receiverPeerID.id),
|
|
payload: valid
|
|
),
|
|
from: pair.senderPeerID
|
|
)
|
|
|
|
#expect(recorder.deliveries.count == 1)
|
|
#expect(recorder.deliveries.first?.payload == Data([0x01]))
|
|
#expect(recorder.clearedSessions.isEmpty)
|
|
#expect(recorder.initiatedHandshakes.isEmpty)
|
|
}
|
|
|
|
@Test
|
|
func forgedAuthenticationFailureCannotEvictEstablishedTransport()
|
|
throws {
|
|
let pair = try establishedServices()
|
|
let recorder = Recorder()
|
|
recorder.transportGenerationReady = true
|
|
let handler = makeServiceBackedHandler(
|
|
service: pair.receiver,
|
|
localPeerID: pair.receiverPeerID,
|
|
recorder: recorder,
|
|
transportGenerationIsReady: { _ in
|
|
recorder.transportGenerationReady
|
|
}
|
|
)
|
|
let valid = try pair.sender.encrypt(
|
|
Data([NoisePayloadType.privateMessage.rawValue, 0x02]),
|
|
for: pair.receiverPeerID
|
|
)
|
|
var forged = valid
|
|
forged[forged.index(before: forged.endIndex)] ^= 0xFF
|
|
|
|
handler.handleEncrypted(
|
|
makeEncryptedPacket(
|
|
recipientID: Data(hexString: pair.receiverPeerID.id),
|
|
payload: forged
|
|
),
|
|
from: pair.senderPeerID
|
|
)
|
|
#expect(
|
|
pair.receiver.hasEstablishedSession(with: pair.senderPeerID)
|
|
)
|
|
#expect(recorder.clearedSessions.isEmpty)
|
|
|
|
// Authentication failure leaves nonce state untouched, so the exact
|
|
// original ciphertext remains valid.
|
|
handler.handleEncrypted(
|
|
makeEncryptedPacket(
|
|
recipientID: Data(hexString: pair.receiverPeerID.id),
|
|
payload: valid
|
|
),
|
|
from: pair.senderPeerID
|
|
)
|
|
|
|
#expect(recorder.deliveries.count == 1)
|
|
#expect(recorder.deliveries.first?.payload == Data([0x02]))
|
|
#expect(recorder.clearedSessions.isEmpty)
|
|
#expect(recorder.initiatedHandshakes.isEmpty)
|
|
}
|
|
|
|
@Test
|
|
func replayCannotEvictEstablishedTransportOrBlockNextNonce() throws {
|
|
let pair = try establishedServices()
|
|
let recorder = Recorder()
|
|
recorder.transportGenerationReady = true
|
|
let handler = makeServiceBackedHandler(
|
|
service: pair.receiver,
|
|
localPeerID: pair.receiverPeerID,
|
|
recorder: recorder,
|
|
transportGenerationIsReady: { _ in
|
|
recorder.transportGenerationReady
|
|
}
|
|
)
|
|
let first = try pair.sender.encrypt(
|
|
Data([NoisePayloadType.privateMessage.rawValue, 0x03]),
|
|
for: pair.receiverPeerID
|
|
)
|
|
let firstPacket = makeEncryptedPacket(
|
|
recipientID: Data(hexString: pair.receiverPeerID.id),
|
|
payload: first
|
|
)
|
|
|
|
handler.handleEncrypted(firstPacket, from: pair.senderPeerID)
|
|
handler.handleEncrypted(firstPacket, from: pair.senderPeerID)
|
|
#expect(
|
|
pair.receiver.hasEstablishedSession(with: pair.senderPeerID)
|
|
)
|
|
#expect(recorder.clearedSessions.isEmpty)
|
|
|
|
let next = try pair.sender.encrypt(
|
|
Data([NoisePayloadType.privateMessage.rawValue, 0x04]),
|
|
for: pair.receiverPeerID
|
|
)
|
|
handler.handleEncrypted(
|
|
makeEncryptedPacket(
|
|
recipientID: Data(hexString: pair.receiverPeerID.id),
|
|
payload: next
|
|
),
|
|
from: pair.senderPeerID
|
|
)
|
|
|
|
#expect(recorder.deliveries.count == 2)
|
|
#expect(recorder.deliveries.map { $0.payload } == [
|
|
Data([0x03]), Data([0x04])
|
|
])
|
|
#expect(recorder.clearedSessions.isEmpty)
|
|
#expect(recorder.initiatedHandshakes.isEmpty)
|
|
}
|
|
|
|
@Test
|
|
func rateLimitFailureCannotEvictEstablishedTransportOrConsumeNonce()
|
|
throws {
|
|
let pair = try establishedServices()
|
|
let recorder = Recorder()
|
|
recorder.transportGenerationReady = true
|
|
recorder.forcedServiceDecryptError =
|
|
NoiseSecurityError.rateLimitExceeded
|
|
let handler = makeServiceBackedHandler(
|
|
service: pair.receiver,
|
|
localPeerID: pair.receiverPeerID,
|
|
recorder: recorder,
|
|
transportGenerationIsReady: { _ in
|
|
recorder.transportGenerationReady
|
|
}
|
|
)
|
|
let valid = try pair.sender.encrypt(
|
|
Data([NoisePayloadType.privateMessage.rawValue, 0x05]),
|
|
for: pair.receiverPeerID
|
|
)
|
|
|
|
handler.handleEncrypted(
|
|
makeEncryptedPacket(
|
|
recipientID: Data(hexString: pair.receiverPeerID.id),
|
|
payload: Data(repeating: 0xA5, count: 20)
|
|
),
|
|
from: pair.senderPeerID
|
|
)
|
|
#expect(
|
|
pair.receiver.hasEstablishedSession(with: pair.senderPeerID)
|
|
)
|
|
#expect(recorder.clearedSessions.isEmpty)
|
|
|
|
recorder.forcedServiceDecryptError = nil
|
|
handler.handleEncrypted(
|
|
makeEncryptedPacket(
|
|
recipientID: Data(hexString: pair.receiverPeerID.id),
|
|
payload: valid
|
|
),
|
|
from: pair.senderPeerID
|
|
)
|
|
|
|
#expect(recorder.deliveries.count == 1)
|
|
#expect(recorder.deliveries.first?.payload == Data([0x05]))
|
|
#expect(recorder.clearedSessions.isEmpty)
|
|
#expect(recorder.initiatedHandshakes.isEmpty)
|
|
}
|
|
|
|
@Test
|
|
func maximumPrivateFileCiphertextIsEligibleForDeferredRetry() {
|
|
let recorder = Recorder()
|
|
recorder.hasSession = true
|
|
recorder.awaitingResponderHandshake = true
|
|
recorder.decryptResult = .failure(
|
|
CryptoKitError.authenticationFailure
|
|
)
|
|
let handler = makeHandler(recorder: recorder)
|
|
let encrypted = makeEncryptedPacket(
|
|
recipientID: Data(hexString: localPeerID.id),
|
|
payload: Data(
|
|
count: NoiseSecurityConstants.maxPrivateFileCiphertextSize
|
|
)
|
|
)
|
|
|
|
handler.handleEncrypted(encrypted, from: remotePeerID)
|
|
recorder.awaitingResponderHandshake = false
|
|
recorder.decryptResult = .success(
|
|
Data([NoisePayloadType.privateFile.rawValue, 0x01])
|
|
)
|
|
handler.handleSessionAuthenticated(remotePeerID)
|
|
|
|
#expect(recorder.decryptCalls.count == 2)
|
|
#expect(recorder.deliveries.count == 1)
|
|
#expect(recorder.deliveries.first?.type == .privateFile)
|
|
#expect(recorder.clearedSessions.isEmpty)
|
|
#expect(recorder.initiatedHandshakes.isEmpty)
|
|
}
|
|
|
|
@Test
|
|
func oversizedEarlyCiphertextIsNotDeferred() {
|
|
let recorder = Recorder()
|
|
recorder.hasSession = true
|
|
recorder.awaitingResponderHandshake = true
|
|
recorder.decryptResult = .failure(
|
|
CryptoKitError.authenticationFailure
|
|
)
|
|
let handler = makeHandler(recorder: recorder)
|
|
let encrypted = makeEncryptedPacket(
|
|
recipientID: Data(hexString: localPeerID.id),
|
|
payload: Data(
|
|
count:
|
|
NoiseSecurityConstants.maxPrivateFileCiphertextSize + 1
|
|
)
|
|
)
|
|
|
|
handler.handleEncrypted(encrypted, from: remotePeerID)
|
|
recorder.awaitingResponderHandshake = false
|
|
recorder.decryptResult = .success(
|
|
Data([NoisePayloadType.delivered.rawValue, 0x01])
|
|
)
|
|
handler.handleSessionAuthenticated(remotePeerID)
|
|
|
|
#expect(recorder.decryptCalls.count == 1)
|
|
#expect(recorder.deliveries.isEmpty)
|
|
#expect(recorder.clearedSessions.isEmpty)
|
|
#expect(recorder.initiatedHandshakes.isEmpty)
|
|
}
|
|
|
|
@Test
|
|
func missingSessionCiphertextIsRetriedAfterResponderHandshakeCompletes() {
|
|
let recorder = Recorder()
|
|
recorder.hasSession = true
|
|
recorder.awaitingResponderHandshake = true
|
|
recorder.decryptResult = .failure(
|
|
NoiseEncryptionError.sessionNotEstablished
|
|
)
|
|
let handler = makeHandler(recorder: recorder)
|
|
let encrypted = makeEncryptedPacket(
|
|
recipientID: Data(hexString: localPeerID.id)
|
|
)
|
|
|
|
handler.handleEncrypted(encrypted, from: remotePeerID)
|
|
#expect(recorder.decryptCalls.count == 1)
|
|
#expect(recorder.initiatedHandshakes.isEmpty)
|
|
|
|
recorder.awaitingResponderHandshake = false
|
|
recorder.decryptResult = .success(
|
|
Data([NoisePayloadType.delivered.rawValue, 0x01])
|
|
)
|
|
handler.handleSessionAuthenticated(remotePeerID)
|
|
|
|
#expect(recorder.decryptCalls.count == 2)
|
|
#expect(recorder.deliveries.count == 1)
|
|
#expect(recorder.clearedSessions.isEmpty)
|
|
#expect(recorder.initiatedHandshakes.isEmpty)
|
|
}
|
|
|
|
@Test
|
|
func lowNonceCiphertextIsRetriedAfterResponderHandshakeCompletes() {
|
|
let recorder = Recorder()
|
|
recorder.hasSession = true
|
|
recorder.awaitingResponderHandshake = true
|
|
recorder.decryptResult = .failure(NoiseError.replayDetected)
|
|
let handler = makeHandler(recorder: recorder)
|
|
let encrypted = makeEncryptedPacket(
|
|
recipientID: Data(hexString: localPeerID.id)
|
|
)
|
|
|
|
handler.handleEncrypted(encrypted, from: remotePeerID)
|
|
recorder.awaitingResponderHandshake = false
|
|
recorder.decryptResult = .success(
|
|
Data([NoisePayloadType.readReceipt.rawValue, 0x02])
|
|
)
|
|
handler.handleSessionAuthenticated(remotePeerID)
|
|
|
|
#expect(recorder.decryptCalls.count == 2)
|
|
#expect(recorder.deliveries.count == 1)
|
|
#expect(recorder.deliveries.first?.type == .readReceipt)
|
|
#expect(recorder.clearedSessions.isEmpty)
|
|
#expect(recorder.initiatedHandshakes.isEmpty)
|
|
}
|
|
|
|
@Test
|
|
func invalidDeferredCiphertextDoesNotClearAuthenticatedSession() {
|
|
let recorder = Recorder()
|
|
recorder.hasSession = true
|
|
recorder.awaitingResponderHandshake = true
|
|
recorder.decryptResult = .failure(
|
|
CryptoKitError.authenticationFailure
|
|
)
|
|
let handler = makeHandler(recorder: recorder)
|
|
let encrypted = makeEncryptedPacket(
|
|
recipientID: Data(hexString: localPeerID.id)
|
|
)
|
|
|
|
handler.handleEncrypted(encrypted, from: remotePeerID)
|
|
recorder.awaitingResponderHandshake = false
|
|
handler.handleSessionAuthenticated(remotePeerID)
|
|
|
|
#expect(recorder.decryptCalls.count == 2)
|
|
#expect(recorder.deliveries.isEmpty)
|
|
#expect(recorder.clearedSessions.isEmpty)
|
|
#expect(recorder.initiatedHandshakes.isEmpty)
|
|
}
|
|
|
|
@Test
|
|
func nonCipherFailureDuringResponderHandshakeIsDroppedNotDeferred() {
|
|
let recorder = Recorder()
|
|
recorder.hasSession = true
|
|
recorder.awaitingResponderHandshake = true
|
|
recorder.decryptResult = .failure(TestError())
|
|
let handler = makeHandler(recorder: recorder)
|
|
handler.handleEncrypted(
|
|
makeEncryptedPacket(
|
|
recipientID: Data(hexString: localPeerID.id)
|
|
),
|
|
from: remotePeerID
|
|
)
|
|
|
|
recorder.awaitingResponderHandshake = false
|
|
recorder.decryptResult = .success(
|
|
Data([NoisePayloadType.delivered.rawValue, 0x01])
|
|
)
|
|
handler.handleSessionAuthenticated(remotePeerID)
|
|
|
|
#expect(recorder.decryptCalls.count == 1)
|
|
#expect(recorder.deliveries.isEmpty)
|
|
#expect(recorder.clearedSessions.isEmpty)
|
|
#expect(recorder.initiatedHandshakes.isEmpty)
|
|
}
|
|
|
|
@Test
|
|
func earlyCiphertextBufferIsBoundedPerPeer() {
|
|
let recorder = Recorder()
|
|
recorder.hasSession = true
|
|
recorder.awaitingResponderHandshake = true
|
|
recorder.decryptResult = .failure(
|
|
CryptoKitError.authenticationFailure
|
|
)
|
|
let handler = makeHandler(recorder: recorder)
|
|
|
|
for index in 0..<5 {
|
|
handler.handleEncrypted(
|
|
makeEncryptedPacket(
|
|
recipientID: Data(hexString: localPeerID.id),
|
|
timestamp: UInt64(900_000 + index)
|
|
),
|
|
from: remotePeerID
|
|
)
|
|
}
|
|
#expect(recorder.decryptCalls.count == 5)
|
|
|
|
recorder.awaitingResponderHandshake = false
|
|
recorder.decryptResult = .success(
|
|
Data([NoisePayloadType.delivered.rawValue, 0x01])
|
|
)
|
|
handler.handleSessionAuthenticated(remotePeerID)
|
|
|
|
#expect(recorder.decryptCalls.count == 9)
|
|
#expect(recorder.deliveries.count == 4)
|
|
#expect(recorder.clearedSessions.isEmpty)
|
|
#expect(recorder.initiatedHandshakes.isEmpty)
|
|
}
|
|
|
|
@Test
|
|
func earlyCiphertextBufferIsBoundedGlobally() {
|
|
let recorder = Recorder()
|
|
recorder.hasSession = true
|
|
recorder.awaitingResponderHandshake = true
|
|
recorder.decryptResult = .failure(
|
|
CryptoKitError.authenticationFailure
|
|
)
|
|
let handler = makeHandler(recorder: recorder)
|
|
let peers = (1...33).map {
|
|
PeerID(str: String(format: "%016llx", UInt64($0)))
|
|
}
|
|
let packet = makeEncryptedPacket(
|
|
recipientID: Data(hexString: localPeerID.id)
|
|
)
|
|
|
|
for peerID in peers {
|
|
handler.handleEncrypted(packet, from: peerID)
|
|
}
|
|
#expect(recorder.decryptCalls.count == 33)
|
|
|
|
recorder.awaitingResponderHandshake = false
|
|
recorder.decryptResult = .success(
|
|
Data([NoisePayloadType.delivered.rawValue, 0x01])
|
|
)
|
|
for peerID in peers {
|
|
handler.handleSessionAuthenticated(peerID)
|
|
}
|
|
|
|
#expect(recorder.decryptCalls.count == 65)
|
|
#expect(recorder.deliveries.count == 32)
|
|
#expect(recorder.clearedSessions.isEmpty)
|
|
#expect(recorder.initiatedHandshakes.isEmpty)
|
|
}
|
|
|
|
@Test
|
|
func earlyCiphertextBufferKeepsPrivateFileRoomAndByteBound() {
|
|
let recorder = Recorder()
|
|
recorder.hasSession = true
|
|
recorder.awaitingResponderHandshake = true
|
|
recorder.decryptResult = .failure(
|
|
CryptoKitError.authenticationFailure
|
|
)
|
|
let handler = makeHandler(recorder: recorder)
|
|
let peers = [
|
|
PeerID(str: "0000000000000001"),
|
|
PeerID(str: "0000000000000002"),
|
|
PeerID(str: "0000000000000003")
|
|
]
|
|
|
|
handler.handleEncrypted(
|
|
makeEncryptedPacket(
|
|
recipientID: Data(hexString: localPeerID.id),
|
|
payload: Data(
|
|
count:
|
|
NoiseSecurityConstants.maxPrivateFileCiphertextSize
|
|
)
|
|
),
|
|
from: peers[0]
|
|
)
|
|
handler.handleEncrypted(
|
|
makeEncryptedPacket(
|
|
recipientID: Data(hexString: localPeerID.id),
|
|
payload: Data(count: 256 * 1024)
|
|
),
|
|
from: peers[1]
|
|
)
|
|
handler.handleEncrypted(
|
|
makeEncryptedPacket(
|
|
recipientID: Data(hexString: localPeerID.id),
|
|
payload: Data([0x01])
|
|
),
|
|
from: peers[2]
|
|
)
|
|
|
|
recorder.awaitingResponderHandshake = false
|
|
recorder.decryptResult = .success(
|
|
Data([NoisePayloadType.delivered.rawValue, 0x01])
|
|
)
|
|
for peerID in peers {
|
|
handler.handleSessionAuthenticated(peerID)
|
|
}
|
|
|
|
#expect(recorder.decryptCalls.count == 5)
|
|
#expect(recorder.deliveries.count == 2)
|
|
#expect(recorder.clearedSessions.isEmpty)
|
|
#expect(recorder.initiatedHandshakes.isEmpty)
|
|
}
|
|
|
|
@Test
|
|
func expiredEarlyCiphertextIsNotRetried() {
|
|
let recorder = Recorder()
|
|
recorder.hasSession = true
|
|
recorder.awaitingResponderHandshake = true
|
|
recorder.decryptResult = .failure(
|
|
CryptoKitError.authenticationFailure
|
|
)
|
|
let handler = makeHandler(recorder: recorder)
|
|
handler.handleEncrypted(
|
|
makeEncryptedPacket(
|
|
recipientID: Data(hexString: localPeerID.id)
|
|
),
|
|
from: remotePeerID
|
|
)
|
|
|
|
recorder.currentDate =
|
|
recorder.currentDate.addingTimeInterval(
|
|
NoiseSecurityConstants.ordinaryResponderHandshakeTimeout
|
|
+ 0.001
|
|
)
|
|
recorder.awaitingResponderHandshake = false
|
|
recorder.decryptResult = .success(
|
|
Data([NoisePayloadType.delivered.rawValue, 0x01])
|
|
)
|
|
handler.handleSessionAuthenticated(remotePeerID)
|
|
|
|
#expect(recorder.decryptCalls.count == 1)
|
|
#expect(recorder.deliveries.isEmpty)
|
|
#expect(recorder.clearedSessions.isEmpty)
|
|
#expect(recorder.initiatedHandshakes.isEmpty)
|
|
}
|
|
|
|
@Test
|
|
func earlyCiphertextSurvivesResponderHandshakeWindow() {
|
|
let recorder = Recorder()
|
|
recorder.hasSession = true
|
|
recorder.awaitingResponderHandshake = true
|
|
recorder.decryptResult = .failure(
|
|
CryptoKitError.authenticationFailure
|
|
)
|
|
let handler = makeHandler(recorder: recorder)
|
|
handler.handleEncrypted(
|
|
makeEncryptedPacket(
|
|
recipientID: Data(hexString: localPeerID.id)
|
|
),
|
|
from: remotePeerID
|
|
)
|
|
|
|
recorder.currentDate =
|
|
recorder.currentDate.addingTimeInterval(
|
|
NoiseSecurityConstants.ordinaryResponderHandshakeTimeout
|
|
- 0.001
|
|
)
|
|
recorder.awaitingResponderHandshake = false
|
|
recorder.decryptResult = .success(
|
|
Data([NoisePayloadType.delivered.rawValue, 0x01])
|
|
)
|
|
handler.handleSessionAuthenticated(remotePeerID)
|
|
|
|
#expect(recorder.decryptCalls.count == 2)
|
|
#expect(recorder.deliveries.count == 1)
|
|
#expect(recorder.clearedSessions.isEmpty)
|
|
#expect(recorder.initiatedHandshakes.isEmpty)
|
|
}
|
|
|
|
private func makeHandshakePacket(recipientID: Data?) -> BitchatPacket {
|
|
BitchatPacket(
|
|
type: MessageType.noiseHandshake.rawValue,
|
|
senderID: Data(hexString: remotePeerID.id) ?? Data(),
|
|
recipientID: recipientID,
|
|
timestamp: 900_000,
|
|
payload: Data([0x01, 0x02, 0x03]),
|
|
signature: nil,
|
|
ttl: TransportConfig.messageTTLDefault
|
|
)
|
|
}
|
|
|
|
private func makeEncryptedPacket(
|
|
recipientID: Data?,
|
|
timestamp: UInt64 = 900_000,
|
|
payload: Data = Data([0xC0, 0xFF, 0xEE])
|
|
) -> BitchatPacket {
|
|
BitchatPacket(
|
|
type: MessageType.noiseEncrypted.rawValue,
|
|
senderID: Data(hexString: remotePeerID.id) ?? Data(),
|
|
recipientID: recipientID,
|
|
timestamp: timestamp,
|
|
payload: payload,
|
|
signature: nil,
|
|
ttl: TransportConfig.messageTTLDefault
|
|
)
|
|
}
|
|
}
|