mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-25 13:45:20 +00:00
Announces now carry an optional capabilities TLV (0x05): a little-endian bitfield with named bits for upcoming features (prekeys, wifiBulk, gateway, groups, board, vouch, meshDiagnostics). Old clients skip the unknown TLV; peers without it decode as nil so features can distinguish "legacy peer" from "advertises nothing". PeerCapabilities lives in BitFoundation with a minimal-length encoding that preserves unknown bits for forward compatibility. Peer capabilities are stored in the BLE peer registry on verified announce and exposed via BLEService.peerCapabilities(_:). The local advertisement set is empty until each feature ships its bit. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
193 lines
6.7 KiB
Swift
193 lines
6.7 KiB
Swift
import BitFoundation
|
|
import Foundation
|
|
|
|
// MARK: - Protocol TLV Packets
|
|
|
|
struct AnnouncementPacket {
|
|
let nickname: String
|
|
let noisePublicKey: Data // Noise static public key (Curve25519.KeyAgreement)
|
|
let signingPublicKey: Data // Ed25519 public key for signing
|
|
let directNeighbors: [Data]? // 8-byte peer IDs
|
|
let capabilities: PeerCapabilities? // advertised feature bits; nil when absent (old clients)
|
|
|
|
init(
|
|
nickname: String,
|
|
noisePublicKey: Data,
|
|
signingPublicKey: Data,
|
|
directNeighbors: [Data]?,
|
|
capabilities: PeerCapabilities? = nil
|
|
) {
|
|
self.nickname = nickname
|
|
self.noisePublicKey = noisePublicKey
|
|
self.signingPublicKey = signingPublicKey
|
|
self.directNeighbors = directNeighbors
|
|
self.capabilities = capabilities
|
|
}
|
|
|
|
private enum TLVType: UInt8 {
|
|
case nickname = 0x01
|
|
case noisePublicKey = 0x02
|
|
case signingPublicKey = 0x03
|
|
case directNeighbors = 0x04
|
|
case capabilities = 0x05
|
|
}
|
|
|
|
func encode() -> Data? {
|
|
var data = Data()
|
|
// Reserve: TLVs for nickname (2 + n), noise key (2 + 32), signing key (2 + 32)
|
|
data.reserveCapacity(2 + min(nickname.count, 255) + 2 + noisePublicKey.count + 2 + signingPublicKey.count)
|
|
|
|
// TLV for nickname
|
|
guard let nicknameData = nickname.data(using: .utf8), nicknameData.count <= 255 else { return nil }
|
|
data.append(TLVType.nickname.rawValue)
|
|
data.append(UInt8(nicknameData.count))
|
|
data.append(nicknameData)
|
|
|
|
// TLV for noise public key
|
|
guard noisePublicKey.count <= 255 else { return nil }
|
|
data.append(TLVType.noisePublicKey.rawValue)
|
|
data.append(UInt8(noisePublicKey.count))
|
|
data.append(noisePublicKey)
|
|
|
|
// TLV for signing public key
|
|
guard signingPublicKey.count <= 255 else { return nil }
|
|
data.append(TLVType.signingPublicKey.rawValue)
|
|
data.append(UInt8(signingPublicKey.count))
|
|
data.append(signingPublicKey)
|
|
|
|
// TLV for direct neighbors (optional)
|
|
if let neighbors = directNeighbors, !neighbors.isEmpty {
|
|
let neighborsData = neighbors.prefix(10).reduce(Data()) { $0 + $1 }
|
|
if !neighborsData.isEmpty && neighborsData.count % 8 == 0 {
|
|
data.append(TLVType.directNeighbors.rawValue)
|
|
data.append(UInt8(neighborsData.count))
|
|
data.append(neighborsData)
|
|
}
|
|
}
|
|
|
|
// TLV for capabilities (optional)
|
|
if let capabilities = capabilities {
|
|
let capabilityBytes = capabilities.encoded()
|
|
guard capabilityBytes.count <= 255 else { return nil }
|
|
data.append(TLVType.capabilities.rawValue)
|
|
data.append(UInt8(capabilityBytes.count))
|
|
data.append(capabilityBytes)
|
|
}
|
|
|
|
return data
|
|
}
|
|
|
|
static func decode(from data: Data) -> AnnouncementPacket? {
|
|
var offset = 0
|
|
var nickname: String?
|
|
var noisePublicKey: Data?
|
|
var signingPublicKey: Data?
|
|
var directNeighbors: [Data]?
|
|
var capabilities: PeerCapabilities?
|
|
|
|
while offset + 2 <= data.count {
|
|
let typeRaw = data[offset]
|
|
offset += 1
|
|
let length = Int(data[offset])
|
|
offset += 1
|
|
|
|
guard offset + length <= data.count else { return nil }
|
|
let value = data[offset..<offset + length]
|
|
offset += length
|
|
|
|
if let type = TLVType(rawValue: typeRaw) {
|
|
switch type {
|
|
case .nickname:
|
|
nickname = String(data: value, encoding: .utf8)
|
|
case .noisePublicKey:
|
|
noisePublicKey = Data(value)
|
|
case .signingPublicKey:
|
|
signingPublicKey = Data(value)
|
|
case .directNeighbors:
|
|
if length > 0 && length % 8 == 0 {
|
|
var neighbors = [Data]()
|
|
let count = length / 8
|
|
for i in 0..<count {
|
|
let start = value.startIndex + i * 8
|
|
let end = start + 8
|
|
neighbors.append(Data(value[start..<end]))
|
|
}
|
|
directNeighbors = neighbors
|
|
}
|
|
case .capabilities:
|
|
capabilities = PeerCapabilities(encoded: Data(value))
|
|
}
|
|
} else {
|
|
// Unknown TLV; skip (tolerant decoder for forward compatibility)
|
|
continue
|
|
}
|
|
}
|
|
|
|
guard let nickname = nickname, let noisePublicKey = noisePublicKey, let signingPublicKey = signingPublicKey else { return nil }
|
|
return AnnouncementPacket(
|
|
nickname: nickname,
|
|
noisePublicKey: noisePublicKey,
|
|
signingPublicKey: signingPublicKey,
|
|
directNeighbors: directNeighbors,
|
|
capabilities: capabilities
|
|
)
|
|
}
|
|
}
|
|
|
|
struct PrivateMessagePacket {
|
|
let messageID: String
|
|
let content: String
|
|
|
|
private enum TLVType: UInt8 {
|
|
case messageID = 0x00
|
|
case content = 0x01
|
|
}
|
|
|
|
func encode() -> Data? {
|
|
var data = Data()
|
|
data.reserveCapacity(2 + min(messageID.count, 255) + 2 + min(content.count, 255))
|
|
|
|
// TLV for messageID
|
|
guard let messageIDData = messageID.data(using: .utf8), messageIDData.count <= 255 else { return nil }
|
|
data.append(TLVType.messageID.rawValue)
|
|
data.append(UInt8(messageIDData.count))
|
|
data.append(messageIDData)
|
|
|
|
// TLV for content
|
|
guard let contentData = content.data(using: .utf8), contentData.count <= 255 else { return nil }
|
|
data.append(TLVType.content.rawValue)
|
|
data.append(UInt8(contentData.count))
|
|
data.append(contentData)
|
|
|
|
return data
|
|
}
|
|
|
|
static func decode(from data: Data) -> PrivateMessagePacket? {
|
|
var offset = 0
|
|
var messageID: String?
|
|
var content: String?
|
|
|
|
while offset + 2 <= data.count {
|
|
guard let type = TLVType(rawValue: data[offset]) else { return nil }
|
|
offset += 1
|
|
|
|
let length = Int(data[offset])
|
|
offset += 1
|
|
|
|
guard offset + length <= data.count else { return nil }
|
|
let value = data[offset..<offset + length]
|
|
offset += length
|
|
|
|
switch type {
|
|
case .messageID:
|
|
messageID = String(data: value, encoding: .utf8)
|
|
case .content:
|
|
content = String(data: value, encoding: .utf8)
|
|
}
|
|
}
|
|
|
|
guard let messageID = messageID, let content = content else { return nil }
|
|
return PrivateMessagePacket(messageID: messageID, content: content)
|
|
}
|
|
}
|