mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-24 23:45:18 +00:00
Drop couriered mail from blocked senders at envelope open
The UI-layer block check (isPeerBlocked in the transport event coordinator) resolves a fingerprint from the live session or peer list, but a couriered message arrives precisely when its sender is absent — no session, no registry entry — so the check failed open and a blocked identity's mail was delivered anyway. Gate in openCourierEnvelope, where the sealed sender's full static key is in hand. End-to-end test ferries a full deposit→carry→handover round and verifies the envelope from a blocked sender never reaches the delegate (confirmed failing without the gate). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -2570,6 +2570,13 @@ extension BLEService {
|
|||||||
SecureLogger.warning("⚠️ Courier envelope carried unsupported payload type", category: .session)
|
SecureLogger.warning("⚠️ Courier envelope carried unsupported payload type", category: .session)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
// Couriered mail arrives while the sender is absent, so the UI's
|
||||||
|
// block check can't resolve their fingerprint from a live session.
|
||||||
|
// Gate here, where the full static key is in hand.
|
||||||
|
guard !identityManager.isBlocked(fingerprint: senderStaticKey.sha256Fingerprint()) else {
|
||||||
|
SecureLogger.debug("🚫 Dropping courier envelope from blocked sender", category: .security)
|
||||||
|
return
|
||||||
|
}
|
||||||
// Mesh peer IDs are derived from Noise static keys, so the sender
|
// Mesh peer IDs are derived from Noise static keys, so the sender
|
||||||
// resolves to the same DM thread whether or not they're present.
|
// resolves to the same DM thread whether or not they're present.
|
||||||
let senderPeerID = PeerID(publicKey: senderStaticKey)
|
let senderPeerID = PeerID(publicKey: senderStaticKey)
|
||||||
|
|||||||
@@ -67,9 +67,9 @@ struct CourierEndToEndTests {
|
|||||||
func didReceivePublicMessage(from peerID: PeerID, nickname: String, content: String, timestamp: Date, messageID: String?) {}
|
func didReceivePublicMessage(from peerID: PeerID, nickname: String, content: String, timestamp: Date, messageID: String?) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func makeService() -> BLEService {
|
private func makeService(identityManager: MockIdentityManager? = nil) -> BLEService {
|
||||||
let keychain = MockKeychain()
|
let keychain = MockKeychain()
|
||||||
let identityManager = MockIdentityManager(keychain)
|
let identityManager = identityManager ?? MockIdentityManager(keychain)
|
||||||
let idBridge = NostrIdentityBridge(keychain: MockKeychainHelper())
|
let idBridge = NostrIdentityBridge(keychain: MockKeychainHelper())
|
||||||
let service = BLEService(
|
let service = BLEService(
|
||||||
keychain: keychain,
|
keychain: keychain,
|
||||||
@@ -177,6 +177,75 @@ struct CourierEndToEndTests {
|
|||||||
#expect(message.content == "the camp moved north")
|
#expect(message.content == "the camp moved north")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test func courieredMailFromBlockedSenderIsDropped() async throws {
|
||||||
|
let alice = makeService()
|
||||||
|
let carol = makeService()
|
||||||
|
let bobIdentity = MockIdentityManager(MockKeychain())
|
||||||
|
let bob = makeService(identityManager: bobIdentity)
|
||||||
|
carol.courierDepositPolicy = { _ in true }
|
||||||
|
|
||||||
|
let bobDelegate = NoiseCaptureDelegate()
|
||||||
|
bob.delegate = bobDelegate
|
||||||
|
let aliceOut = PacketTap()
|
||||||
|
alice._test_onOutboundPacket = aliceOut.record
|
||||||
|
let carolOut = PacketTap()
|
||||||
|
carol._test_onOutboundPacket = carolOut.record
|
||||||
|
let bobOut = PacketTap()
|
||||||
|
bob._test_onOutboundPacket = bobOut.record
|
||||||
|
|
||||||
|
preseedConnectedPeer(carol, in: alice)
|
||||||
|
|
||||||
|
// Bob blocked Alice by her stable Noise identity while she was away.
|
||||||
|
bobIdentity.setBlocked(alice.noiseStaticPublicKeyData().sha256Fingerprint(), isBlocked: true)
|
||||||
|
|
||||||
|
#expect(alice.sendCourierMessage(
|
||||||
|
"you should not see this",
|
||||||
|
messageID: "courier-msg-blocked-sender",
|
||||||
|
recipientNoiseKey: bob.noiseStaticPublicKeyData(),
|
||||||
|
via: [carol.myPeerID]
|
||||||
|
))
|
||||||
|
let deposited = await TestHelpers.waitUntil(
|
||||||
|
{ aliceOut.first(ofType: .courierEnvelope) != nil },
|
||||||
|
timeout: TestConstants.defaultTimeout
|
||||||
|
)
|
||||||
|
#expect(deposited)
|
||||||
|
let depositPacket = try #require(aliceOut.first(ofType: .courierEnvelope))
|
||||||
|
|
||||||
|
carol._test_handlePacket(depositPacket, fromPeerID: alice.myPeerID)
|
||||||
|
let carried = await TestHelpers.waitUntil(
|
||||||
|
{ !carol.courierStore.isEmpty },
|
||||||
|
timeout: TestConstants.defaultTimeout
|
||||||
|
)
|
||||||
|
#expect(carried)
|
||||||
|
|
||||||
|
bob.sendBroadcastAnnounce()
|
||||||
|
let announced = await TestHelpers.waitUntil(
|
||||||
|
{ bobOut.first(ofType: .announce) != nil },
|
||||||
|
timeout: TestConstants.defaultTimeout
|
||||||
|
)
|
||||||
|
#expect(announced)
|
||||||
|
let announcePacket = try #require(bobOut.first(ofType: .announce))
|
||||||
|
carol._test_handlePacket(announcePacket, fromPeerID: bob.myPeerID, preseedPeer: false)
|
||||||
|
|
||||||
|
let handedOver = await TestHelpers.waitUntil(
|
||||||
|
{ carolOut.first(ofType: .courierEnvelope) != nil },
|
||||||
|
timeout: TestConstants.defaultTimeout
|
||||||
|
)
|
||||||
|
#expect(handedOver)
|
||||||
|
let handoverPacket = try #require(carolOut.first(ofType: .courierEnvelope))
|
||||||
|
|
||||||
|
// Bob opens the envelope — but the sealed sender is blocked, and it
|
||||||
|
// must never reach the UI. The live block check can't cover this: the
|
||||||
|
// sender is absent from Bob's registry, so no fingerprint resolves at
|
||||||
|
// delivery time.
|
||||||
|
bob._test_handlePacket(handoverPacket, fromPeerID: carol.myPeerID)
|
||||||
|
let delivered = await TestHelpers.waitUntil(
|
||||||
|
{ !bobDelegate.snapshot().isEmpty },
|
||||||
|
timeout: TestConstants.shortTimeout
|
||||||
|
)
|
||||||
|
#expect(!delivered)
|
||||||
|
}
|
||||||
|
|
||||||
@Test func unverifiedAnnounceDoesNotTriggerCourierHandover() async throws {
|
@Test func unverifiedAnnounceDoesNotTriggerCourierHandover() async throws {
|
||||||
let alice = makeService()
|
let alice = makeService()
|
||||||
let carol = makeService()
|
let carol = makeService()
|
||||||
|
|||||||
Reference in New Issue
Block a user