Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 31 additions & 31 deletions bitchat/Localizable.xcstrings
Original file line number Diff line number Diff line change
Expand Up @@ -16184,187 +16184,187 @@
}
},
"content.delivery.reason.private_media_too_many_fragments" : {
"comment" : "Failure reason when private media exceeds the Android-compatible fragment limit",
"comment" : "Failure reason when private media exceeds the recipient's fragment limit; %lld is that limit",
"extractionState" : "manual",
"localizations" : {
"ar" : {
"stringUnit" : {
"state" : "translated",
"value" : "الملف كبير جدًا على تطبيق جهة الاتصال هذه (أكثر من 256 جزءًا عبر الشبكة المتداخلة)"
"value" : "الملف كبير جدًا على تطبيق جهة الاتصال هذه (أكثر من %lld جزءًا عبر الشبكة المتداخلة)"
}
},
"bn" : {
"stringUnit" : {
"state" : "translated",
"value" : "এই কন্টাক্টের ক্লায়েন্টের জন্য ফাইলটি খুব বড় (256টির বেশি মেশ ফ্র্যাগমেন্ট)"
"value" : "এই কন্টাক্টের ক্লায়েন্টের জন্য ফাইলটি খুব বড় (%lldটির বেশি মেশ ফ্র্যাগমেন্ট)"
}
},
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Datei ist zu groß für den Client dieses Kontakts (mehr als 256 Mesh-Fragmente)"
"value" : "Datei ist zu groß für den Client dieses Kontakts (mehr als %lld Mesh-Fragmente)"
}
},
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "File is too large for this contact's client (more than 256 mesh fragments)"
"value" : "File is too large for this contact's client (more than %lld mesh fragments)"
}
},
"es" : {
"stringUnit" : {
"state" : "translated",
"value" : "El archivo es demasiado grande para el cliente de este contacto (más de 256 fragmentos de malla)"
"value" : "El archivo es demasiado grande para el cliente de este contacto (más de %lld fragmentos de malla)"
}
},
"fa" : {
"stringUnit" : {
"state" : "translated",
"value" : "فایل برای کلاینت این مخاطب خیلی بزرگ است (بیش از 256 قطعهٔ مش)"
"value" : "فایل برای کلاینت این مخاطب خیلی بزرگ است (بیش از %lld قطعهٔ مش)"
}
},
"fil" : {
"stringUnit" : {
"state" : "translated",
"value" : "Masyadong malaki ang file para sa client ng contact na ito (mahigit 256 mesh fragment)"
"value" : "Masyadong malaki ang file para sa client ng contact na ito (mahigit %lld mesh fragment)"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
"value" : "Le fichier est trop volumineux pour le client de ce contact (plus de 256 fragments mesh)"
"value" : "Le fichier est trop volumineux pour le client de ce contact (plus de %lld fragments mesh)"
}
},
"he" : {
"stringUnit" : {
"state" : "translated",
"value" : "הקובץ גדול מדי עבור הקליינט של איש הקשר הזה (יותר מ-256 מקטעי mesh)"
"value" : "הקובץ גדול מדי עבור הקליינט של איש הקשר הזה (יותר מ-%lld מקטעי mesh)"
}
},
"hi" : {
"stringUnit" : {
"state" : "translated",
"value" : "यह फ़ाइल इस संपर्क के क्लाइंट के लिए बहुत बड़ी है (256 से ज़्यादा मेश फ़्रैगमेंट)"
"value" : "यह फ़ाइल इस संपर्क के क्लाइंट के लिए बहुत बड़ी है (%lld से ज़्यादा मेश फ़्रैगमेंट)"
}
},
"id" : {
"stringUnit" : {
"state" : "translated",
"value" : "File terlalu besar untuk klien kontak ini (lebih dari 256 fragmen mesh)"
"value" : "File terlalu besar untuk klien kontak ini (lebih dari %lld fragmen mesh)"
}
},
"it" : {
"stringUnit" : {
"state" : "translated",
"value" : "Il file è troppo grande per il client di questo contatto (più di 256 frammenti mesh)"
"value" : "Il file è troppo grande per il client di questo contatto (più di %lld frammenti mesh)"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : "このファイルはこの連絡先のクライアントには大きすぎます(メッシュフラグメントが256個を超えています)"
"value" : "このファイルはこの連絡先のクライアントには大きすぎます(メッシュフラグメントが%lld個を超えています)"
}
},
"ko" : {
"stringUnit" : {
"state" : "translated",
"value" : "이 연락처의 클라이언트에는 파일이 너무 커요(메시 조각 256개 초과)"
"value" : "이 연락처의 클라이언트에는 파일이 너무 커요(메시 조각 %lld개 초과)"
}
},
"ms" : {
"stringUnit" : {
"state" : "translated",
"value" : "Fail terlalu besar untuk klien kenalan ini (lebih daripada 256 fragmen mesh)"
"value" : "Fail terlalu besar untuk klien kenalan ini (lebih daripada %lld fragmen mesh)"
}
},
"ne" : {
"stringUnit" : {
"state" : "translated",
"value" : "यो सम्पर्कको क्लाइन्टका लागि फाइल धेरै ठूलो छ (256 भन्दा बढी मेश खण्डहरू)"
"value" : "यो सम्पर्कको क्लाइन्टका लागि फाइल धेरै ठूलो छ (%lld भन्दा बढी मेश खण्डहरू)"
}
},
"nl" : {
"stringUnit" : {
"state" : "translated",
"value" : "Het bestand is te groot voor de client van dit contact (meer dan 256 mesh-fragmenten)"
"value" : "Het bestand is te groot voor de client van dit contact (meer dan %lld mesh-fragmenten)"
}
},
"pl" : {
"stringUnit" : {
"state" : "translated",
"value" : "Plik jest za duży dla klienta tego kontaktu (ponad 256 fragmentów mesh)"
"value" : "Plik jest za duży dla klienta tego kontaktu (ponad %lld fragmentów mesh)"
}
},
"pt" : {
"stringUnit" : {
"state" : "translated",
"value" : "O ficheiro é demasiado grande para o cliente deste contacto (mais de 256 fragmentos da malha)"
"value" : "O ficheiro é demasiado grande para o cliente deste contacto (mais de %lld fragmentos da malha)"
}
},
"pt-BR" : {
"stringUnit" : {
"state" : "translated",
"value" : "O arquivo é grande demais para o cliente deste contato (mais de 256 fragmentos da malha)"
"value" : "O arquivo é grande demais para o cliente deste contato (mais de %lld fragmentos da malha)"
}
},
"ru" : {
"stringUnit" : {
"state" : "translated",
"value" : "Файл слишком большой для клиента этого контакта (больше 256 фрагментов mesh-сети)"
"value" : "Файл слишком большой для клиента этого контакта (больше %lld фрагментов mesh-сети)"
}
},
"sv" : {
"stringUnit" : {
"state" : "translated",
"value" : "Filen är för stor för den här kontaktens klient (fler än 256 mesh-fragment)"
"value" : "Filen är för stor för den här kontaktens klient (fler än %lld mesh-fragment)"
}
},
"ta" : {
"stringUnit" : {
"state" : "translated",
"value" : "இந்தத் தொடர்பின் கிளையண்டுக்கு இந்தக் கோப்பு மிகப் பெரியது (256-க்கும் மேற்பட்ட மெஷ் துண்டுகள்)"
"value" : "இந்தத் தொடர்பின் கிளையண்டுக்கு இந்தக் கோப்பு மிகப் பெரியது (%lld-க்கும் மேற்பட்ட மெஷ் துண்டுகள்)"
}
},
"th" : {
"stringUnit" : {
"state" : "translated",
"value" : "ไฟล์ใหญ่เกินไปสำหรับไคลเอ็นต์ของผู้ติดต่อรายนี้ (เกิน 256 ส่วนย่อยของ mesh)"
"value" : "ไฟล์ใหญ่เกินไปสำหรับไคลเอ็นต์ของผู้ติดต่อรายนี้ (เกิน %lld ส่วนย่อยของ mesh)"
}
},
"tr" : {
"stringUnit" : {
"state" : "translated",
"value" : "Dosya bu kişinin istemcisi için çok büyük (256'dan fazla mesh parçası)"
"value" : "Dosya bu kişinin istemcisi için çok büyük (mesh parça sınırı: %lld)"
}
},
"uk" : {
"stringUnit" : {
"state" : "translated",
"value" : "Файл завеликий для клієнта цього контакту (понад 256 фрагментів mesh-мережі)"
"value" : "Файл завеликий для клієнта цього контакту (понад %lld фрагментів mesh-мережі)"
}
},
"ur" : {
"stringUnit" : {
"state" : "translated",
"value" : "یہ فائل اس رابطے کے کلائنٹ کے لیے بہت بڑی ہے (256 سے زیادہ میش ٹکڑے)"
"value" : "یہ فائل اس رابطے کے کلائنٹ کے لیے بہت بڑی ہے (%lld سے زیادہ میش ٹکڑے)"
}
},
"vi" : {
"stringUnit" : {
"state" : "translated",
"value" : "Tệp quá lớn so với ứng dụng của liên hệ này (hơn 256 phân mảnh mesh)"
"value" : "Tệp quá lớn so với ứng dụng của liên hệ này (hơn %lld phân mảnh mesh)"
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "文件太大,这位联系人的客户端无法接收(超过 256 个网状网络分片)"
"value" : "文件太大,这位联系人的客户端无法接收(超过 %lld 个网状网络分片)"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
"value" : "檔案太大,這位聯絡人的用戶端無法接收(超過 256 個網狀網路分段)"
"value" : "檔案太大,這位聯絡人的用戶端無法接收(超過 %lld 個網狀網路分段)"
}
}
}
Expand Down
52 changes: 51 additions & 1 deletion bitchat/Protocols/Packets.swift
Original file line number Diff line number Diff line change
Expand Up @@ -165,26 +165,57 @@ struct AnnouncementPacket {
/// `[version=0x01][type][length][value]...`
/// - TLV `0x01`: canonical minimal little-endian `PeerCapabilities`
/// - TLV `0x02`: 32-byte Ed25519 signing public key
/// - TLV `0x03`: optional 2-byte big-endian reassembly fragment ceiling
///
/// Unknown TLVs are skipped for forward compatibility. Unknown versions,
/// duplicates, non-canonical capability fields, and malformed lengths are
/// rejected without changing authenticated state.
struct AuthenticatedPeerStatePacket: Equatable {
static let currentVersion: UInt8 = 1
static let signingPublicKeyLength = 32
static let maxReassemblyFragmentsLength = 2

let capabilities: PeerCapabilities
let signingPublicKey: Data
/// How many BLE fragments this peer will reassemble for one packet, as it
/// reported inside the established Noise session.
///
/// The `.privateMedia` capability bit says a peer understands encrypted
/// media; it says nothing about how much of it that peer can hold. Before
/// this TLV existed, senders inferred the ceiling from the packet type —
/// 256 for the directed migration fallback, the full local ceiling for
/// anything encrypted — which is only correct while every client that
/// implements `0x20` also has a large reassembler.
///
/// `nil` means the peer did not advertise one, which is the case for every
/// client released before this TLV. Senders fall back to the type proxy
/// there; see `BLEFragmentCeilingPolicy`.
let maxReassemblyFragments: UInt16?

init(
capabilities: PeerCapabilities,
signingPublicKey: Data,
maxReassemblyFragments: UInt16? = nil
) {
self.capabilities = capabilities
self.signingPublicKey = signingPublicKey
self.maxReassemblyFragments = maxReassemblyFragments
}

private enum TLVType: UInt8 {
case capabilities = 0x01
case signingPublicKey = 0x02
case maxReassemblyFragments = 0x03
}

func encode() -> Data? {
guard signingPublicKey.count == Self.signingPublicKeyLength else { return nil }
let capabilityBytes = capabilities.encoded()
guard !capabilityBytes.isEmpty, capabilityBytes.count <= 8 else { return nil }
// Zero would advertise a peer that can reassemble nothing, which is
// indistinguishable in effect from refusing every fragmented packet.
// Omit the TLV instead of putting a meaningless number on the wire.
if let maxReassemblyFragments { guard maxReassemblyFragments > 0 else { return nil } }

var data = Data([Self.currentVersion])
data.append(TLVType.capabilities.rawValue)
Expand All @@ -193,6 +224,11 @@ struct AuthenticatedPeerStatePacket: Equatable {
data.append(TLVType.signingPublicKey.rawValue)
data.append(UInt8(signingPublicKey.count))
data.append(signingPublicKey)
if let maxReassemblyFragments {
data.append(TLVType.maxReassemblyFragments.rawValue)
data.append(UInt8(Self.maxReassemblyFragmentsLength))
data.append(contentsOf: withUnsafeBytes(of: maxReassemblyFragments.bigEndian) { Data($0) })
}
return data
}

Expand All @@ -202,6 +238,7 @@ struct AuthenticatedPeerStatePacket: Equatable {
var offset = 1
var capabilities: PeerCapabilities?
var signingPublicKey: Data?
var maxReassemblyFragments: UInt16?

while offset < data.count {
guard offset + 2 <= data.count else { return nil }
Expand All @@ -228,13 +265,26 @@ struct AuthenticatedPeerStatePacket: Equatable {
guard signingPublicKey == nil,
value.count == Self.signingPublicKeyLength else { return nil }
signingPublicKey = value

case .maxReassemblyFragments:
// Rejected rather than skipped: a peer that meant to constrain
// us but sent a field we cannot read must not be treated as
// having said nothing, because "said nothing" falls back to
// the permissive type proxy.
guard maxReassemblyFragments == nil,
value.count == Self.maxReassemblyFragmentsLength else { return nil }
let decoded = (UInt16(value[value.startIndex]) << 8)
| UInt16(value[value.startIndex + 1])
guard decoded > 0 else { return nil }
maxReassemblyFragments = decoded
}
}

guard let capabilities, let signingPublicKey else { return nil }
return AuthenticatedPeerStatePacket(
capabilities: capabilities,
signingPublicKey: signingPublicKey
signingPublicKey: signingPublicKey,
maxReassemblyFragments: maxReassemblyFragments
)
}
}
Expand Down
16 changes: 15 additions & 1 deletion bitchat/Services/BLE/BLEFragmentAssemblyBuffer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@ struct BLEFragmentHeader: Equatable {
let index = Int((UInt16(packet.payload[8]) << 8) | UInt16(packet.payload[9]))
let total = Int((UInt16(packet.payload[10]) << 8) | UInt16(packet.payload[11]))

guard total > 0 && total <= 10_000 && index >= 0 && index < total else {
guard total > 0 && total <= BLEFragmentAssemblyBuffer.maxReassemblyFragments
&& index >= 0 && index < total else {
return nil
}

Expand All @@ -54,6 +55,19 @@ struct BLEFragmentHeader: Equatable {
}

struct BLEFragmentAssemblyBuffer {
/// The largest fragment count this client will ever begin reassembling.
///
/// A sender cannot know this by inspection, so it is also the value we
/// advertise to authenticated peers (`AuthenticatedPeerStatePacket`'s
/// `maxReassemblyFragments` TLV). Naming it keeps the guard below and the
/// advertised number from drifting apart: a client that raises one and
/// forgets the other either rejects media it promised to accept, or
/// promises more than it can hold.
///
/// Bounded by `UInt16.max` because the wire header carries `total` as a
/// big-endian `UInt16`.
static let maxReassemblyFragments = 10_000

enum AppendResult: Equatable {
case stored(header: BLEFragmentHeader, started: Bool)
case complete(header: BLEFragmentHeader, reassembledData: Data, started: Bool)
Expand Down
Loading
Loading