fix(ble): require a verified ANNOUNCE before granting broadcast notifications

A peer could subscribe to the full mesh feed by writing the CCCD alone.
Gate subscription on a signature-valid announce from that address and
fail closed with GATT_INSUFFICIENT_AUTHORIZATION until one arrives.
This commit is contained in:
Taksh 2026-08-25 07:39:23 +05:30
parent eab894ce78
commit 6e53bef71e
5 changed files with 56 additions and 0 deletions

View File

@ -101,6 +101,10 @@ class BluetoothConnectionManager(
// Public property for address-peer mapping
val addressPeerMap get() = connectionTracker.addressPeerMap
fun noteVerifiedAnnounceReceived(deviceAddress: String) {
connectionTracker.noteAnnounceReceived(deviceAddress)
}
fun observePeerIfCurrent(deviceAddress: String, linkID: String, peerID: String): Boolean =
connectionTracker.observePeerIfCurrent(deviceAddress, linkID, peerID)

View File

@ -283,6 +283,26 @@ class BluetoothGattServerManager(
}
if (BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE.contentEquals(value)) {
if (!GattNotificationEligibility.maySubscribe(
connectionTracker.hasSeenFirstAnnounce(device.address)
)
) {
Log.w(
TAG,
"Rejecting broadcast subscription from ${device.address} before verified ANNOUNCE"
)
if (responseNeeded) {
gattServer?.sendResponse(
device,
requestId,
BluetoothGatt.GATT_INSUFFICIENT_AUTHORIZATION,
0,
null
)
}
return
}
connectionTracker.addSubscribedDevice(device)
connectionScope.launch {

View File

@ -579,6 +579,10 @@ class BluetoothMeshService(private val context: Context) : TransportBridgeServic
val result = messageHandler.handleAnnounceWithResult(routed)
if (result !is AnnounceHandlingResult.Accepted) return false
routed.relayAddress?.let { deviceAddress ->
connectionManager.noteVerifiedAnnounceReceived(deviceAddress)
}
DirectLinkAnnouncementPolicy.observationFor(routed, MAX_TTL)?.let { observation ->
if (connectionManager.observePeerIfCurrent(
observation.relayAddress,

View File

@ -0,0 +1,11 @@
package com.bitchat.android.mesh
/**
* Whether a BLE central may subscribe to the mesh broadcast notification feed.
*
* Granting notifications before the peer has sent a verified ANNOUNCE would let
* a passive listener harvest the full mesh feed with no participation (#901).
*/
internal object GattNotificationEligibility {
fun maySubscribe(hasVerifiedAnnounce: Boolean): Boolean = hasVerifiedAnnounce
}

View File

@ -0,0 +1,17 @@
package com.bitchat.android.mesh
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test
class GattNotificationEligibilityTest {
@Test
fun `subscription denied before a verified announce`() {
assertFalse(GattNotificationEligibility.maySubscribe(hasVerifiedAnnounce = false))
}
@Test
fun `subscription allowed after a verified announce`() {
assertTrue(GattNotificationEligibility.maySubscribe(hasVerifiedAnnounce = true))
}
}