mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-25 01:05:19 +00:00
* Require signed sender for broadcast file transfers (#1406 follow-up) Broadcast file transfers trusted the packet's claimed senderID whenever the peer was merely connected (resolveKnownPeer allowConnectedUnverified: true), unlike public messages and public voice frames, which both require a valid packet signature from the claimed sender. Codex flagged the consequence on PR #1406: a peer that observed a public voice burst could broadcast a spoofed voice_<burstID>.m4a note under the talker's senderID, and ChatLiveVoiceCoordinator.absorbFinalizedVoiceNote would replace the signature-verified live bubble with attacker audio (senderPeerID + scope were the only bindings, both attacker-forgeable on this path). Bring broadcast file transfers up to the same bar as public messages: verify the packet signature against the registry signing key, falling back to the persisted-identity signature lookup, before trusting the sender. Directed (private) transfers keep the lenient connected-peer path — they are addressed to us specifically and carry no broadcast exposure. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Exempt self broadcasts from the file-transfer signature gate Review of #1407 caught a regression: our own broadcast files replayed via gossip sync arrive with ttl==0 (so isSelfEcho does not drop them) and cannot be verified against the peer registry or identity cache, so the new broadcast signature guard would drop them. Mirror BLEPublicMessageHandler's self exemption — self packets are trivially authentic — and add a regression test. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Stop relaying broadcast file packets that fail sender authentication Codex review on #1407: the new signature gate dropped spoofed broadcast files locally, but BLEService's .fileTransfer case still fell through to scheduleRelayIfNeeded, so a forged file kept propagating to downstream (possibly older, ungated) nodes. Have the handler report failed sender authentication and skip the relay step, like invalid board posts and voice frames. Local-only drops (malformed payload, quota, save failure) and files directed to other peers still relay unchanged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Fix CI hang: sign the max-size reassembly file transfer, unhang timeouts Both CI runs on #1407 died at the 5-minute watchdog (SIGKILL, exit 137) with the test process fully idle. Root cause was a pair of issues in FragmentationTests: - "Max-sized file transfer survives reassembly" injected an UNSIGNED broadcast file from an unknown peer, which the new broadcast signature gate now drops by design. Sign the packet and preseed the sender's signing key, mirroring the public-message reassembly tests. - CaptureDelegate's wait helpers could never time out: the timeout task threw, but withThrowingTaskGroup then awaited the sibling child that was parked in a non-cancellable withCheckedContinuation, deadlocking the whole run (hang instead of a 5s failure). Resume the parked continuation from a cancellation handler so timeouts now fail fast. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: jack <jackjackbits@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
jack
Claude Opus 4.8
parent
35fb9fdd42
commit
d5cf64f99e
@@ -94,6 +94,11 @@ struct FragmentationTests {
|
||||
let capture = CaptureDelegate()
|
||||
ble.delegate = capture
|
||||
|
||||
// Broadcast file transfers must carry a valid sender signature (same
|
||||
// gate as public messages), so sign the packet and preseed the
|
||||
// sender's signing key into the registry.
|
||||
let signer = NoiseEncryptionService(keychain: MockKeychain())
|
||||
let signingKey = signer.getSigningPublicKeyData()
|
||||
let remoteID = PeerID(str: "CAFEBABECAFEBABE")
|
||||
let fileContent = Data(repeating: 0x42, count: FileTransferLimits.maxPayloadBytes)
|
||||
let filePacket = BitchatFilePacket(
|
||||
@@ -104,15 +109,18 @@ struct FragmentationTests {
|
||||
)
|
||||
let encoded = try #require(filePacket.encode(), "File packet encoding failed")
|
||||
|
||||
let packet = BitchatPacket(
|
||||
type: MessageType.fileTransfer.rawValue,
|
||||
senderID: Data(hexString: remoteID.id) ?? Data(),
|
||||
recipientID: nil,
|
||||
timestamp: UInt64(Date().timeIntervalSince1970 * 1000),
|
||||
payload: encoded,
|
||||
signature: nil,
|
||||
ttl: 7,
|
||||
version: 2
|
||||
let packet = try #require(
|
||||
signer.signPacket(BitchatPacket(
|
||||
type: MessageType.fileTransfer.rawValue,
|
||||
senderID: Data(hexString: remoteID.id) ?? Data(),
|
||||
recipientID: nil,
|
||||
timestamp: UInt64(Date().timeIntervalSince1970 * 1000),
|
||||
payload: encoded,
|
||||
signature: nil,
|
||||
ttl: 7,
|
||||
version: 2
|
||||
)),
|
||||
"Failed to sign file transfer packet"
|
||||
)
|
||||
|
||||
let fragments = fragmentPacket(packet, fragmentSize: 4096, pad: false)
|
||||
@@ -122,7 +130,7 @@ struct FragmentationTests {
|
||||
if i > 0 {
|
||||
try await Task.sleep(for: .milliseconds(5))
|
||||
}
|
||||
ble._test_handlePacket(fragment, fromPeerID: remoteID)
|
||||
ble._test_handlePacket(fragment, fromPeerID: remoteID, signingPublicKey: signingKey)
|
||||
}
|
||||
|
||||
try await capture.waitForReceivedMessages(count: 1, timeout: .seconds(5))
|
||||
@@ -265,19 +273,32 @@ extension FragmentationTests {
|
||||
|
||||
try await withThrowingTaskGroup(of: Void.self) { group in
|
||||
group.addTask {
|
||||
await withCheckedContinuation { continuation in
|
||||
let shouldResumeImmediately = self.withLock {
|
||||
// Recheck count after acquiring lock to avoid race condition
|
||||
// where message arrives between initial check and continuation install
|
||||
if self._publicMessages.count >= count {
|
||||
return true
|
||||
// withCheckedContinuation itself is not cancellable, so hook
|
||||
// group.cancelAll() to resume the parked continuation —
|
||||
// otherwise a timeout leaves the group awaiting this child
|
||||
// forever and the test run hangs instead of failing.
|
||||
await withTaskCancellationHandler {
|
||||
await withCheckedContinuation { continuation in
|
||||
let shouldResumeImmediately = self.withLock {
|
||||
// Recheck count after acquiring lock to avoid race condition
|
||||
// where message arrives between initial check and continuation install
|
||||
if self._publicMessages.count >= count {
|
||||
return true
|
||||
}
|
||||
self.publicMessageContinuation = continuation
|
||||
return false
|
||||
}
|
||||
if shouldResumeImmediately {
|
||||
continuation.resume()
|
||||
}
|
||||
self.publicMessageContinuation = continuation
|
||||
return false
|
||||
}
|
||||
if shouldResumeImmediately {
|
||||
continuation.resume()
|
||||
} onCancel: {
|
||||
let continuation = self.withLock {
|
||||
let parked = self.publicMessageContinuation
|
||||
self.publicMessageContinuation = nil
|
||||
return parked
|
||||
}
|
||||
continuation?.resume()
|
||||
}
|
||||
}
|
||||
group.addTask {
|
||||
@@ -304,19 +325,32 @@ extension FragmentationTests {
|
||||
|
||||
try await withThrowingTaskGroup(of: Void.self) { group in
|
||||
group.addTask {
|
||||
await withCheckedContinuation { continuation in
|
||||
let shouldResumeImmediately = self.withLock {
|
||||
// Recheck count after acquiring lock to avoid race condition
|
||||
// where message arrives between initial check and continuation install
|
||||
if self._receivedMessages.count >= count {
|
||||
return true
|
||||
// withCheckedContinuation itself is not cancellable, so hook
|
||||
// group.cancelAll() to resume the parked continuation —
|
||||
// otherwise a timeout leaves the group awaiting this child
|
||||
// forever and the test run hangs instead of failing.
|
||||
await withTaskCancellationHandler {
|
||||
await withCheckedContinuation { continuation in
|
||||
let shouldResumeImmediately = self.withLock {
|
||||
// Recheck count after acquiring lock to avoid race condition
|
||||
// where message arrives between initial check and continuation install
|
||||
if self._receivedMessages.count >= count {
|
||||
return true
|
||||
}
|
||||
self.receivedMessageContinuation = continuation
|
||||
return false
|
||||
}
|
||||
if shouldResumeImmediately {
|
||||
continuation.resume()
|
||||
}
|
||||
self.receivedMessageContinuation = continuation
|
||||
return false
|
||||
}
|
||||
if shouldResumeImmediately {
|
||||
continuation.resume()
|
||||
} onCancel: {
|
||||
let continuation = self.withLock {
|
||||
let parked = self.receivedMessageContinuation
|
||||
self.receivedMessageContinuation = nil
|
||||
return parked
|
||||
}
|
||||
continuation?.resume()
|
||||
}
|
||||
}
|
||||
group.addTask {
|
||||
|
||||
Reference in New Issue
Block a user