mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-25 22:05:21 +00:00
Wi-Fi bulk: normalize peer ID before eligibility checks
Stable/verified private chats address a peer by its full 64-hex Noise key, but the Noise session, advertised capabilities, and connection state are keyed by the short (SHA256-derived 16-hex) routing ID. sendFilePrivate resolved the Wi-Fi bulk SendCandidate with the raw peerID, so a 64-hex key made hasEstablishedSession return false; shouldOffer never selected Wi-Fi and a >BLE-cap payload cancelled as "Wi-Fi unavailable" even when the direct peer advertised .wifiBulk. Normalize with peerID.toShort() once before the session/capability checks (extracted into wifiBulkSendCandidate) and use the normalized ID for the negotiation packet and the BLE fallback too. Test: eligibility + offer path resolves when addressed by a 64-hex Noise key — the session is established under the short ID, the raw-key lookup misses it, and the normalized send path still offers Wi-Fi. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -793,15 +793,19 @@ final class BLEService: NSObject {
|
|||||||
}
|
}
|
||||||
let fitsBLECaps = filePacket.encode() != nil
|
let fitsBLECaps = filePacket.encode() != nil
|
||||||
|
|
||||||
let candidate = WifiBulkPolicy.SendCandidate(
|
// Normalize to the short routing ID (SHA256-derived 16-hex) once.
|
||||||
payloadBytes: payload.count,
|
// Stable/verified private chats address the peer by its full
|
||||||
peerCapabilities: self.peerCapabilities(peerID),
|
// 64-hex Noise key, but the Noise session and the negotiation
|
||||||
isDirectlyConnected: self.isPeerConnected(peerID),
|
// packet are keyed by the short ID — so the capability/session
|
||||||
hasEstablishedNoiseSession: self.noiseService.hasEstablishedSession(with: peerID)
|
// eligibility checks (and the Wi-Fi offer) must all use it, or a
|
||||||
)
|
// 64-hex key makes `hasEstablishedSession` return false and Wi-Fi
|
||||||
|
// is never selected even when the peer advertises `.wifiBulk`.
|
||||||
|
let routingID = peerID.toShort()
|
||||||
|
|
||||||
|
let candidate = self.wifiBulkSendCandidate(payloadBytes: payload.count, to: routingID)
|
||||||
if WifiBulkPolicy.shouldOffer(candidate) {
|
if WifiBulkPolicy.shouldOffer(candidate) {
|
||||||
self.wifiBulkService.sendFile(payload: payload, to: peerID, transferId: transferId) { [weak self] in
|
self.wifiBulkService.sendFile(payload: payload, to: routingID, transferId: transferId) { [weak self] in
|
||||||
self?.sendFilePrivateViaBLE(payload: payload, to: peerID, transferId: transferId, fitsBLECaps: fitsBLECaps)
|
self?.sendFilePrivateViaBLE(payload: payload, to: routingID, transferId: transferId, fitsBLECaps: fitsBLECaps)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -811,10 +815,29 @@ final class BLEService: NSObject {
|
|||||||
self.surfaceUndeliverableTransfer(transferId)
|
self.surfaceUndeliverableTransfer(transferId)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
self.sendFilePrivateViaBLE(payload: payload, to: peerID, transferId: transferId, fitsBLECaps: true)
|
self.sendFilePrivateViaBLE(payload: payload, to: routingID, transferId: transferId, fitsBLECaps: true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Builds the Wi-Fi bulk eligibility candidate for a private send.
|
||||||
|
///
|
||||||
|
/// Normalizes `peerID` to its short routing ID first: stable/verified
|
||||||
|
/// private chats address the peer by its full 64-hex Noise key, but the
|
||||||
|
/// Noise session, advertised capabilities, and connection state are all
|
||||||
|
/// keyed by the short (SHA256-derived 16-hex) ID. Resolving them with the
|
||||||
|
/// 64-hex key would miss the session (`hasEstablishedSession` returns
|
||||||
|
/// false), so Wi-Fi would never be offered even to a directly-connected
|
||||||
|
/// `.wifiBulk` peer.
|
||||||
|
private func wifiBulkSendCandidate(payloadBytes: Int, to peerID: PeerID) -> WifiBulkPolicy.SendCandidate {
|
||||||
|
let routingID = peerID.toShort()
|
||||||
|
return WifiBulkPolicy.SendCandidate(
|
||||||
|
payloadBytes: payloadBytes,
|
||||||
|
peerCapabilities: peerCapabilities(routingID),
|
||||||
|
isDirectlyConnected: isPeerConnected(routingID),
|
||||||
|
hasEstablishedNoiseSession: noiseService.hasEstablishedSession(with: routingID)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
/// BLE fragmentation path for private file transfers; also the fallback
|
/// BLE fragmentation path for private file transfers; also the fallback
|
||||||
/// target when a Wi-Fi bulk attempt declines, times out, or errors.
|
/// target when a Wi-Fi bulk attempt declines, times out, or errors.
|
||||||
/// May be invoked from the Wi-Fi bulk queue.
|
/// May be invoked from the Wi-Fi bulk queue.
|
||||||
@@ -1833,6 +1856,34 @@ extension BLEService {
|
|||||||
cachedServiceUUIDs: cachedServiceUUIDs
|
cachedServiceUUIDs: cachedServiceUUIDs
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The internal Noise service, so tests can drive a real handshake and
|
||||||
|
/// establish a session keyed by a chosen (short) routing ID.
|
||||||
|
var _test_noiseService: NoiseEncryptionService { noiseService }
|
||||||
|
|
||||||
|
/// Registers a directly-connected peer with the given advertised
|
||||||
|
/// capabilities, keyed by its short routing ID (as production does).
|
||||||
|
func _test_registerConnectedPeer(_ peerID: PeerID, capabilities: PeerCapabilities) {
|
||||||
|
let shortID = peerID.toShort()
|
||||||
|
collectionsQueue.sync(flags: .barrier) {
|
||||||
|
peerRegistry.upsert(BLEPeerInfo(
|
||||||
|
peerID: shortID,
|
||||||
|
nickname: "TestPeer_\(shortID.id.prefix(4))",
|
||||||
|
isConnected: true,
|
||||||
|
noisePublicKey: peerID.noiseKey,
|
||||||
|
signingPublicKey: nil,
|
||||||
|
isVerifiedNickname: true,
|
||||||
|
lastSeen: Date(),
|
||||||
|
capabilities: capabilities
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Runs the production Wi-Fi bulk eligibility resolution (including peer-ID
|
||||||
|
/// normalization) for the given payload size and recipient.
|
||||||
|
func _test_wifiBulkSendCandidate(payloadBytes: Int, to peerID: PeerID) -> WifiBulkPolicy.SendCandidate {
|
||||||
|
wifiBulkSendCandidate(payloadBytes: payloadBytes, to: peerID)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
|||||||
@@ -216,6 +216,51 @@ struct BLEServiceCoreTests {
|
|||||||
cachedServiceUUIDs: [BLEService.serviceUUID, otherService]
|
cachedServiceUUIDs: [BLEService.serviceUUID, otherService]
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Regression: a stable/verified private chat addresses the peer by its
|
||||||
|
// full 64-hex Noise key, but the Noise session (and capabilities/
|
||||||
|
// connection) are keyed by the short routing ID. The Wi-Fi bulk send path
|
||||||
|
// must normalize the ID first, or `hasEstablishedSession` misses and
|
||||||
|
// Wi-Fi is never offered for a large payload to a direct `.wifiBulk` peer.
|
||||||
|
@Test
|
||||||
|
func wifiBulkEligibility_resolvesWhenAddressedBy64HexNoiseKey() throws {
|
||||||
|
let ble = makeService()
|
||||||
|
|
||||||
|
let noiseKey = Data((0..<32).map { UInt8(($0 &* 7) &+ 3) })
|
||||||
|
let fullKey = PeerID(str: noiseKey.hexEncodedString()) // 64-hex Noise key
|
||||||
|
#expect(fullKey.noiseKey != nil)
|
||||||
|
let shortID = fullKey.toShort() // 16-hex routing ID
|
||||||
|
#expect(shortID != fullKey)
|
||||||
|
|
||||||
|
// Establish a real Noise session in the service's noise engine, keyed
|
||||||
|
// by the short routing ID (exactly as a completed handshake would).
|
||||||
|
let peer = NoiseEncryptionService(keychain: MockKeychain())
|
||||||
|
let noise = ble._test_noiseService
|
||||||
|
let m1 = try noise.initiateHandshake(with: shortID)
|
||||||
|
let m2 = try #require(try peer.processHandshakeMessage(from: shortID, message: m1))
|
||||||
|
let m3 = try #require(try noise.processHandshakeMessage(from: shortID, message: m2))
|
||||||
|
_ = try peer.processHandshakeMessage(from: shortID, message: m3)
|
||||||
|
|
||||||
|
#expect(noise.hasEstablishedSession(with: shortID))
|
||||||
|
// The bug in raw form: querying by the 64-hex key misses the session.
|
||||||
|
#expect(!noise.hasEstablishedSession(with: fullKey))
|
||||||
|
|
||||||
|
// Register the peer as a directly-connected `.wifiBulk` neighbor.
|
||||||
|
ble._test_registerConnectedPeer(fullKey, capabilities: [.wifiBulk])
|
||||||
|
|
||||||
|
let bigPayload = FileTransferLimits.maxWifiBulkPayloadBytes
|
||||||
|
|
||||||
|
// The send path normalizes first, so eligibility + offer resolve for
|
||||||
|
// both the short ID and the full 64-hex Noise key.
|
||||||
|
let viaShort = ble._test_wifiBulkSendCandidate(payloadBytes: bigPayload, to: shortID)
|
||||||
|
let viaFull = ble._test_wifiBulkSendCandidate(payloadBytes: bigPayload, to: fullKey)
|
||||||
|
#expect(viaShort.hasEstablishedNoiseSession)
|
||||||
|
#expect(viaFull.hasEstablishedNoiseSession)
|
||||||
|
#expect(viaFull.isDirectlyConnected)
|
||||||
|
#expect(viaFull.peerCapabilities.contains(.wifiBulk))
|
||||||
|
#expect(WifiBulkPolicy.shouldOffer(viaShort, enabled: true))
|
||||||
|
#expect(WifiBulkPolicy.shouldOffer(viaFull, enabled: true))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func makeService() -> BLEService {
|
private func makeService() -> BLEService {
|
||||||
|
|||||||
Reference in New Issue
Block a user