mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-26 16:45:22 +00:00
Cap only migration-fallback media at Android's 256 fragments
The private-media preflight applied Android's 256-fragment reassembly cap to every directed media send, including the encrypted 0x20 path. That regressed iOS→iOS private photos in the ~120-512 KiB range, which work today: iOS reassembles up to 10,000 fragments and main compresses images to <=512 KiB. Encrypted private media is only ever sent to peers that advertised the .privateMedia capability (modern clients with the full receiver ceiling); current Android receives private media exclusively over the directed raw-file migration fallback. Restrict the 256-fragment cap to that fileTransfer fallback path so capable peers use the normal ceiling. Leaves a TODO(#1434) to negotiate an explicit per-peer fragment limit for a future Android client that adopts 0x20 but keeps a small reassembler. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1950,15 +1950,26 @@ final class BLEService: NSObject {
|
|||||||
// Encode once using a small per-type padding policy, then delegate by type
|
// Encode once using a small per-type padding policy, then delegate by type
|
||||||
let padForBLE = BLEOutboundPacketPolicy.padsBLEFrame(for: packetToSend.type)
|
let padForBLE = BLEOutboundPacketPolicy.padsBLEFrame(for: packetToSend.type)
|
||||||
|
|
||||||
// Cross-platform private-media v1 is bounded by Android's deployed
|
// The 256-fragment ceiling exists to protect *current Android*
|
||||||
// 256-fragment receive cap. Run the same planner the scheduler will
|
// receivers, which only ever receive private media over the directed
|
||||||
// use, after route application, for both encrypted and consented raw
|
// raw-file migration fallback (they do not implement the encrypted
|
||||||
// migration sends. Reject before reserving a transfer slot or writing
|
// 0x20 path). Encrypted private media (`noiseEncrypted`) is sent only to
|
||||||
// any fragment; public media is intentionally unaffected.
|
// peers that advertised the `.privateMedia` capability — modern clients
|
||||||
|
// that assemble up to the full receiver ceiling (see
|
||||||
|
// `BLEFragmentAssemblyBuffer`'s 10,000-fragment guard) — so forcing them
|
||||||
|
// down to Android's 256 cap would needlessly reject iOS→iOS photos in
|
||||||
|
// the ~120–512 KiB range that work today. Restrict the low cap to the
|
||||||
|
// migration fallback (directed `fileTransfer`); public media is
|
||||||
|
// unaffected. Run the same planner the scheduler will use, after route
|
||||||
|
// application, and reject before reserving a transfer slot or writing
|
||||||
|
// any fragment.
|
||||||
|
// TODO(#1434): negotiate an explicit per-peer fragment limit so a future
|
||||||
|
// Android client that adopts the encrypted 0x20 path but still caps its
|
||||||
|
// reassembler can advertise its own ceiling instead of relying on the
|
||||||
|
// capability/type proxy above.
|
||||||
if let transferId,
|
if let transferId,
|
||||||
let recipientPeerID = PeerID(hexData: packetToSend.recipientID),
|
let recipientPeerID = PeerID(hexData: packetToSend.recipientID),
|
||||||
packetToSend.type == MessageType.noiseEncrypted.rawValue
|
packetToSend.type == MessageType.fileTransfer.rawValue {
|
||||||
|| packetToSend.type == MessageType.fileTransfer.rawValue {
|
|
||||||
let compatibilityRequest = BLEOutboundFragmentTransferRequest(
|
let compatibilityRequest = BLEOutboundFragmentTransferRequest(
|
||||||
packet: packetToSend,
|
packet: packetToSend,
|
||||||
pad: padForBLE,
|
pad: padForBLE,
|
||||||
|
|||||||
@@ -741,7 +741,7 @@ struct PrivateMediaEndToEndTests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
func encryptedAndConsentedLegacySendsRejectAboveAndroidFragmentCap() async throws {
|
func consentedLegacySendRejectsAboveAndroidFragmentCapButEncryptedDoesNot() async throws {
|
||||||
let root = FileManager.default.temporaryDirectory
|
let root = FileManager.default.temporaryDirectory
|
||||||
.appendingPathComponent("private-media-fragment-cap-\(UUID().uuidString)", isDirectory: true)
|
.appendingPathComponent("private-media-fragment-cap-\(UUID().uuidString)", isDirectory: true)
|
||||||
defer { try? FileManager.default.removeItem(at: root) }
|
defer { try? FileManager.default.removeItem(at: root) }
|
||||||
@@ -790,14 +790,24 @@ struct PrivateMediaEndToEndTests {
|
|||||||
allowLegacyFallback: true
|
allowLegacyFallback: true
|
||||||
)
|
)
|
||||||
|
|
||||||
let bothRejected = await TestHelpers.waitUntil(
|
// The directed raw-file migration fallback (Android-style peer without
|
||||||
{ rejections.contains(encryptedID) && rejections.contains(legacyID) },
|
// the .privateMedia capability) still honors the 256-fragment ceiling.
|
||||||
|
let legacyRejected = await TestHelpers.waitUntil(
|
||||||
|
{ rejections.contains(legacyID) },
|
||||||
timeout: TestConstants.longTimeout
|
timeout: TestConstants.longTimeout
|
||||||
)
|
)
|
||||||
#expect(bothRejected)
|
#expect(legacyRejected)
|
||||||
#expect(rejections.reason(for: encryptedID)?.contains("256") == true)
|
|
||||||
#expect(rejections.reason(for: legacyID)?.contains("256") == true)
|
#expect(rejections.reason(for: legacyID)?.contains("256") == true)
|
||||||
#expect(tap.snapshot().isEmpty, "No outer packet or fragment may be exposed before size rejection")
|
|
||||||
|
// Encrypted private media to a .privateMedia-capable peer is NOT forced
|
||||||
|
// down to Android's 256 cap: it uses the full receiver ceiling and
|
||||||
|
// proceeds to fragment/emit (a 130 KiB file exceeds 256 fragments).
|
||||||
|
let encryptedEmitted = await TestHelpers.waitUntil(
|
||||||
|
{ !tap.snapshot().isEmpty },
|
||||||
|
timeout: TestConstants.longTimeout
|
||||||
|
)
|
||||||
|
#expect(encryptedEmitted, "Encrypted send to a capable peer must not be blocked by the Android cap")
|
||||||
|
#expect(!rejections.contains(encryptedID))
|
||||||
_ = cancellable
|
_ = cancellable
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -100,8 +100,16 @@ temporary `0x09` alias. Ordinary Noise messages retain their 64 KiB limit.
|
|||||||
|
|
||||||
Current Android builds cap each reassembly at 256 fragments. Depending on the
|
Current Android builds cap each reassembly at 256 fragments. Depending on the
|
||||||
negotiated BLE packet size and routing overhead, that is roughly 110-120 KiB,
|
negotiated BLE packet size and routing overhead, that is roughly 110-120 KiB,
|
||||||
well below iOS's absolute inbound ceiling. Private-media v1 therefore runs the
|
well below iOS's absolute inbound ceiling. That cap only applies to those
|
||||||
actual route-aware BLE fragment planner before both encrypted and consented
|
receivers, which take private media exclusively over the directed raw-file
|
||||||
legacy sends and rejects any plan above 256 fragments with a visible failure.
|
migration fallback (they do not implement the encrypted `0x20` path).
|
||||||
This fragment-count contract, rather than a guessed byte threshold, stays
|
Private-media v1 therefore runs the actual route-aware BLE fragment planner
|
||||||
correct as route overhead changes.
|
before a consented legacy send and rejects any plan above 256 fragments with a
|
||||||
|
visible failure. Encrypted sends go only to peers that advertised the
|
||||||
|
`privateMedia` capability — modern clients that reassemble up to the full
|
||||||
|
receiver ceiling (10,000 fragments) — so they are not held to Android's cap and
|
||||||
|
iOS→iOS photos in the ~120-512 KiB range keep working. This fragment-count
|
||||||
|
contract, rather than a guessed byte threshold, stays correct as route overhead
|
||||||
|
changes. A future Android client that adopts `0x20` but still caps its
|
||||||
|
reassembler would need to negotiate an explicit per-peer fragment limit
|
||||||
|
(tracked as a #1434 follow-up).
|
||||||
|
|||||||
Reference in New Issue
Block a user