mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-26 16:25:23 +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
|
||||
let padForBLE = BLEOutboundPacketPolicy.padsBLEFrame(for: packetToSend.type)
|
||||
|
||||
// Cross-platform private-media v1 is bounded by Android's deployed
|
||||
// 256-fragment receive cap. Run the same planner the scheduler will
|
||||
// use, after route application, for both encrypted and consented raw
|
||||
// migration sends. Reject before reserving a transfer slot or writing
|
||||
// any fragment; public media is intentionally unaffected.
|
||||
// The 256-fragment ceiling exists to protect *current Android*
|
||||
// receivers, which only ever receive private media over the directed
|
||||
// raw-file migration fallback (they do not implement the encrypted
|
||||
// 0x20 path). Encrypted private media (`noiseEncrypted`) is sent only to
|
||||
// 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,
|
||||
let recipientPeerID = PeerID(hexData: packetToSend.recipientID),
|
||||
packetToSend.type == MessageType.noiseEncrypted.rawValue
|
||||
|| packetToSend.type == MessageType.fileTransfer.rawValue {
|
||||
packetToSend.type == MessageType.fileTransfer.rawValue {
|
||||
let compatibilityRequest = BLEOutboundFragmentTransferRequest(
|
||||
packet: packetToSend,
|
||||
pad: padForBLE,
|
||||
|
||||
@@ -741,7 +741,7 @@ struct PrivateMediaEndToEndTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
func encryptedAndConsentedLegacySendsRejectAboveAndroidFragmentCap() async throws {
|
||||
func consentedLegacySendRejectsAboveAndroidFragmentCapButEncryptedDoesNot() async throws {
|
||||
let root = FileManager.default.temporaryDirectory
|
||||
.appendingPathComponent("private-media-fragment-cap-\(UUID().uuidString)", isDirectory: true)
|
||||
defer { try? FileManager.default.removeItem(at: root) }
|
||||
@@ -790,14 +790,24 @@ struct PrivateMediaEndToEndTests {
|
||||
allowLegacyFallback: true
|
||||
)
|
||||
|
||||
let bothRejected = await TestHelpers.waitUntil(
|
||||
{ rejections.contains(encryptedID) && rejections.contains(legacyID) },
|
||||
// The directed raw-file migration fallback (Android-style peer without
|
||||
// the .privateMedia capability) still honors the 256-fragment ceiling.
|
||||
let legacyRejected = await TestHelpers.waitUntil(
|
||||
{ rejections.contains(legacyID) },
|
||||
timeout: TestConstants.longTimeout
|
||||
)
|
||||
#expect(bothRejected)
|
||||
#expect(rejections.reason(for: encryptedID)?.contains("256") == true)
|
||||
#expect(legacyRejected)
|
||||
#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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
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
|
||||
actual route-aware BLE fragment planner before both encrypted and consented
|
||||
legacy sends and rejects any plan above 256 fragments with a visible failure.
|
||||
This fragment-count contract, rather than a guessed byte threshold, stays
|
||||
correct as route overhead changes.
|
||||
well below iOS's absolute inbound ceiling. That cap only applies to those
|
||||
receivers, which take private media exclusively over the directed raw-file
|
||||
migration fallback (they do not implement the encrypted `0x20` path).
|
||||
Private-media v1 therefore runs the actual route-aware BLE fragment planner
|
||||
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