mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-27 16:25:22 +00:00
Encrypt private media before BLE fragmentation (#1434)
Closes the last cleartext private-content path over BLE: private DM images/voice were sent as plaintext signed fileTransfer packets, TTL-relayed across the mesh, so every relay saw the full bytes. Now the complete BitchatFilePacket is encrypted as a single Noise AEAD message (inner type 0x20, matching Android) and the opaque ciphertext is fragmented. Adds an authenticated in-session capability proof (0x21 TLV: capabilities + Ed25519 key), TOFU-style downgrade pinning, a per-send consent dialog for the signed-cleartext fallback to legacy peers, and a cancellation/admission registry so cancel/delete cannot race a deferred cleartext send. Android wire constants (0x20 / 0x21 / capability bit 8) confirmed shipping. The 256-fragment preflight cap applies only to the directed fileTransfer migration fallback; encrypted media to capable peers uses the full receiver ceiling. Rebased over #1428/#1349: identity reads go through BLELocalIdentityStateStore; the session-bound authenticated signing-key check and the announce-path TOFU pin are kept as complementary checks. Full local suite green (1744+197 tests).
This commit is contained in:
@@ -156,6 +156,89 @@ struct AnnouncementPacket {
|
||||
}
|
||||
}
|
||||
|
||||
/// State that is authoritative only because it is carried inside an
|
||||
/// established Noise session. The public announce remains useful for
|
||||
/// discovery, but its self-signature cannot prove possession of the copied
|
||||
/// Noise public key it contains.
|
||||
///
|
||||
/// Wire format (v1):
|
||||
/// `[version=0x01][type][length][value]...`
|
||||
/// - TLV `0x01`: canonical minimal little-endian `PeerCapabilities`
|
||||
/// - TLV `0x02`: 32-byte Ed25519 signing public key
|
||||
///
|
||||
/// 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
|
||||
|
||||
let capabilities: PeerCapabilities
|
||||
let signingPublicKey: Data
|
||||
|
||||
private enum TLVType: UInt8 {
|
||||
case capabilities = 0x01
|
||||
case signingPublicKey = 0x02
|
||||
}
|
||||
|
||||
func encode() -> Data? {
|
||||
guard signingPublicKey.count == Self.signingPublicKeyLength else { return nil }
|
||||
let capabilityBytes = capabilities.encoded()
|
||||
guard !capabilityBytes.isEmpty, capabilityBytes.count <= 8 else { return nil }
|
||||
|
||||
var data = Data([Self.currentVersion])
|
||||
data.append(TLVType.capabilities.rawValue)
|
||||
data.append(UInt8(capabilityBytes.count))
|
||||
data.append(capabilityBytes)
|
||||
data.append(TLVType.signingPublicKey.rawValue)
|
||||
data.append(UInt8(signingPublicKey.count))
|
||||
data.append(signingPublicKey)
|
||||
return data
|
||||
}
|
||||
|
||||
static func decode(from data: Data) -> AuthenticatedPeerStatePacket? {
|
||||
guard data.first == Self.currentVersion else { return nil }
|
||||
|
||||
var offset = 1
|
||||
var capabilities: PeerCapabilities?
|
||||
var signingPublicKey: Data?
|
||||
|
||||
while offset < data.count {
|
||||
guard offset + 2 <= data.count else { return nil }
|
||||
let typeRaw = data[offset]
|
||||
let length = Int(data[offset + 1])
|
||||
offset += 2
|
||||
guard offset + length <= data.count else { return nil }
|
||||
let value = Data(data[offset..<(offset + length)])
|
||||
offset += length
|
||||
|
||||
guard let type = TLVType(rawValue: typeRaw) else {
|
||||
continue
|
||||
}
|
||||
switch type {
|
||||
case .capabilities:
|
||||
guard capabilities == nil,
|
||||
!value.isEmpty,
|
||||
value.count <= 8 else { return nil }
|
||||
let decoded = PeerCapabilities(encoded: value)
|
||||
guard decoded.encoded() == value else { return nil }
|
||||
capabilities = decoded
|
||||
|
||||
case .signingPublicKey:
|
||||
guard signingPublicKey == nil,
|
||||
value.count == Self.signingPublicKeyLength else { return nil }
|
||||
signingPublicKey = value
|
||||
}
|
||||
}
|
||||
|
||||
guard let capabilities, let signingPublicKey else { return nil }
|
||||
return AuthenticatedPeerStatePacket(
|
||||
capabilities: capabilities,
|
||||
signingPublicKey: signingPublicKey
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
struct PrivateMessagePacket {
|
||||
let messageID: String
|
||||
let content: String
|
||||
|
||||
Reference in New Issue
Block a user