mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-26 18:25:21 +00:00
Retry confirmed private media after reconnect
This commit is contained in:
@@ -364,6 +364,217 @@ struct PrivateMediaEndToEndTests {
|
||||
#expect(alice.privateMediaSendPolicy(to: bob.myPeerID) == .legacyRequiresConsent)
|
||||
}
|
||||
|
||||
@Test
|
||||
func privateMediaRetryRequiresExactAuthenticatedBit9Proof() async throws {
|
||||
let root = FileManager.default.temporaryDirectory
|
||||
.appendingPathComponent(
|
||||
"private-media-receipt-proof-\(UUID().uuidString)",
|
||||
isDirectory: true
|
||||
)
|
||||
defer { try? FileManager.default.removeItem(at: root) }
|
||||
let alice = makeService(
|
||||
baseDirectory: root.appendingPathComponent(
|
||||
"alice",
|
||||
isDirectory: true
|
||||
)
|
||||
)
|
||||
let bob = makeService(
|
||||
baseDirectory: root.appendingPathComponent(
|
||||
"bob",
|
||||
isDirectory: true
|
||||
)
|
||||
)
|
||||
let bothCapabilities: PeerCapabilities = [
|
||||
.privateMedia,
|
||||
.privateMediaReceipts
|
||||
]
|
||||
|
||||
// A public bit-9 announce is discovery only.
|
||||
alice._test_seedConnectedPeer(
|
||||
bob.myPeerID,
|
||||
nickname: "Bob",
|
||||
capabilities: bothCapabilities,
|
||||
noisePublicKey: bob.noiseStaticPublicKeyData()
|
||||
)
|
||||
#expect(
|
||||
alice.authenticatedPrivateMediaReceiptSessionGeneration(
|
||||
to: bob.myPeerID
|
||||
) == nil
|
||||
)
|
||||
|
||||
let proofs = try await establishSessionCapturingPeerState(
|
||||
alice: alice,
|
||||
bob: bob
|
||||
)
|
||||
#expect(
|
||||
alice.authenticatedPrivateMediaReceiptSessionGeneration(
|
||||
to: bob.myPeerID
|
||||
) == nil
|
||||
)
|
||||
|
||||
// Bit 8 alone preserves encrypted transfer compatibility but cannot
|
||||
// authorize automatic resend.
|
||||
let privateMediaOnly = try authenticatedPeerStatePacket(
|
||||
from: bob,
|
||||
to: alice,
|
||||
capabilities: .privateMedia
|
||||
)
|
||||
alice._test_handlePacket(
|
||||
privateMediaOnly,
|
||||
fromPeerID: bob.myPeerID
|
||||
)
|
||||
#expect(await TestHelpers.waitUntil(
|
||||
{
|
||||
alice.privateMediaSendPolicy(to: bob.myPeerID)
|
||||
== .encrypted
|
||||
},
|
||||
timeout: TestConstants.longTimeout
|
||||
))
|
||||
#expect(
|
||||
alice.authenticatedPrivateMediaReceiptSessionGeneration(
|
||||
to: bob.myPeerID
|
||||
) == nil
|
||||
)
|
||||
|
||||
let receiptCapable = try authenticatedPeerStatePacket(
|
||||
from: bob,
|
||||
to: alice,
|
||||
capabilities: bothCapabilities
|
||||
)
|
||||
alice._test_handlePacket(
|
||||
receiptCapable,
|
||||
fromPeerID: bob.myPeerID
|
||||
)
|
||||
#expect(await TestHelpers.waitUntil(
|
||||
{
|
||||
alice.authenticatedPrivateMediaReceiptSessionGeneration(
|
||||
to: bob.myPeerID
|
||||
) != nil
|
||||
},
|
||||
timeout: TestConstants.longTimeout
|
||||
))
|
||||
|
||||
bob._test_handlePacket(
|
||||
proofs.alice,
|
||||
fromPeerID: alice.myPeerID
|
||||
)
|
||||
alice._test_onOutboundPacket = nil
|
||||
bob._test_onOutboundPacket = nil
|
||||
}
|
||||
|
||||
@Test
|
||||
func receiptRetryRechecksBit9AtDeferredTransportBoundary() async throws {
|
||||
let root = FileManager.default.temporaryDirectory
|
||||
.appendingPathComponent(
|
||||
"private-media-retry-proof-race-\(UUID().uuidString)",
|
||||
isDirectory: true
|
||||
)
|
||||
defer { try? FileManager.default.removeItem(at: root) }
|
||||
let alice = makeService(
|
||||
baseDirectory: root.appendingPathComponent(
|
||||
"alice",
|
||||
isDirectory: true
|
||||
)
|
||||
)
|
||||
let bob = makeService(
|
||||
baseDirectory: root.appendingPathComponent(
|
||||
"bob",
|
||||
isDirectory: true
|
||||
)
|
||||
)
|
||||
let receiptCapabilities: PeerCapabilities = [
|
||||
.privateMedia,
|
||||
.privateMediaReceipts
|
||||
]
|
||||
alice._test_seedConnectedPeer(
|
||||
bob.myPeerID,
|
||||
nickname: "Bob",
|
||||
capabilities: receiptCapabilities,
|
||||
noisePublicKey: bob.noiseStaticPublicKeyData()
|
||||
)
|
||||
bob._test_seedConnectedPeer(
|
||||
alice.myPeerID,
|
||||
nickname: "Alice",
|
||||
capabilities: receiptCapabilities,
|
||||
noisePublicKey: alice.noiseStaticPublicKeyData()
|
||||
)
|
||||
try await establishSession(alice: alice, bob: bob)
|
||||
#expect(
|
||||
alice.authenticatedPrivateMediaReceiptSessionGeneration(
|
||||
to: bob.myPeerID
|
||||
) != nil
|
||||
)
|
||||
|
||||
let privateMediaOnly = try authenticatedPeerStatePacket(
|
||||
from: bob,
|
||||
to: alice,
|
||||
capabilities: .privateMedia
|
||||
)
|
||||
let transferID =
|
||||
"receipt-proof-race-\(UUID().uuidString)"
|
||||
let tap = PacketTap()
|
||||
let boundaryProofs = ReceiptCapabilityRecorder()
|
||||
let rejections = TransferCancellationRecorder()
|
||||
let cancellable = TransferProgressManager.shared.publisher.sink {
|
||||
rejections.record($0)
|
||||
}
|
||||
alice._test_onOutboundPacket = tap.record
|
||||
alice._test_beforePrivateMediaDeferredSend = { id in
|
||||
guard id == transferID else { return }
|
||||
boundaryProofs.record(
|
||||
alice
|
||||
.authenticatedPrivateMediaReceiptSessionGeneration(
|
||||
to: bob.myPeerID
|
||||
) != nil
|
||||
)
|
||||
}
|
||||
defer {
|
||||
alice._test_beforePrivateMediaDeferredSend = nil
|
||||
alice._test_onOutboundPacket = nil
|
||||
}
|
||||
|
||||
// Rotate authenticated state before the deferred retry reaches its
|
||||
// admission boundary.
|
||||
alice._test_handlePacket(
|
||||
privateMediaOnly,
|
||||
fromPeerID: bob.myPeerID
|
||||
)
|
||||
let content = Data("%PDF-1.7\nreceipt-proof-race".utf8)
|
||||
alice.sendFilePrivateReceiptRetry(
|
||||
BitchatFilePacket(
|
||||
fileName: "receipt-proof-race.pdf",
|
||||
fileSize: UInt64(content.count),
|
||||
mimeType: "application/pdf",
|
||||
content: content
|
||||
),
|
||||
to: bob.myPeerID,
|
||||
transferId: transferID
|
||||
)
|
||||
#expect(await TestHelpers.waitUntil(
|
||||
{ boundaryProofs.snapshot() == [false] },
|
||||
timeout: TestConstants.longTimeout
|
||||
))
|
||||
await alice._test_drainPrivateMediaSendPipeline()
|
||||
|
||||
#expect(await TestHelpers.waitUntil(
|
||||
{ rejections.contains(transferID) },
|
||||
timeout: TestConstants.longTimeout
|
||||
))
|
||||
#expect(tap.snapshot().allSatisfy {
|
||||
$0.type != MessageType.fileTransfer.rawValue
|
||||
&& !(
|
||||
$0.type == MessageType.noiseEncrypted.rawValue
|
||||
&& $0.version == 2
|
||||
)
|
||||
})
|
||||
let state = alice._test_privateMediaTransferState(
|
||||
transferId: transferID
|
||||
)
|
||||
#expect(!state.admissionActive)
|
||||
#expect(!state.pendingNoise)
|
||||
_ = cancellable
|
||||
}
|
||||
|
||||
@Test
|
||||
func capabilityAnnounceCannotPoisonPinWithoutMatchingNoiseAuthentication() async throws {
|
||||
let root = FileManager.default.temporaryDirectory
|
||||
@@ -1397,6 +1608,23 @@ private final class PrivateMediaPolicyRecorder: @unchecked Sendable {
|
||||
}
|
||||
}
|
||||
|
||||
private final class ReceiptCapabilityRecorder: @unchecked Sendable {
|
||||
private let lock = NSLock()
|
||||
private var values: [Bool] = []
|
||||
|
||||
func record(_ value: Bool) {
|
||||
lock.lock()
|
||||
values.append(value)
|
||||
lock.unlock()
|
||||
}
|
||||
|
||||
func snapshot() -> [Bool] {
|
||||
lock.lock()
|
||||
defer { lock.unlock() }
|
||||
return values
|
||||
}
|
||||
}
|
||||
|
||||
private final class MessageCaptureDelegate: BitchatDelegate, @unchecked Sendable {
|
||||
private let lock = NSLock()
|
||||
private var messages: [BitchatMessage] = []
|
||||
|
||||
Reference in New Issue
Block a user