mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-04-20 22:23:37 +00:00
fix: improve PKI message routing and resolve database migration racecondition (#4996)
This commit is contained in:
parent
d0e3b682ab
commit
b3be9e2c38
14 changed files with 277 additions and 37 deletions
|
|
@ -95,23 +95,31 @@ class CommandSenderImpl(
|
|||
|
||||
private fun computeHopLimit(): Int = (localConfig.value.lora?.hop_limit ?: 0).takeIf { it > 0 } ?: DEFAULT_HOP_LIMIT
|
||||
|
||||
private fun getAdminChannelIndex(toNum: Int): Int {
|
||||
/**
|
||||
* Resolves the correct channel index for sending a packet to [toNum].
|
||||
*
|
||||
* When both the local node and the destination support PKC, returns [DataPacket.PKC_CHANNEL_INDEX] so that
|
||||
* [buildMeshPacket] enables PKI encryption. Otherwise falls back to the node's heard-on channel (for general
|
||||
* packets) or the dedicated admin channel (for admin packets).
|
||||
*/
|
||||
private fun getChannelIndex(toNum: Int, isAdmin: Boolean = false): Int {
|
||||
val myNum = nodeManager.myNodeNum.value ?: return 0
|
||||
val myNode = nodeManager.nodeDBbyNodeNum[myNum]
|
||||
val destNode = nodeManager.nodeDBbyNodeNum[toNum]
|
||||
|
||||
val adminChannelIndex =
|
||||
when {
|
||||
myNum == toNum -> 0
|
||||
myNode?.hasPKC == true && destNode?.hasPKC == true -> DataPacket.PKC_CHANNEL_INDEX
|
||||
else ->
|
||||
channelSet.value.settings
|
||||
.indexOfFirst { it.name.equals(ADMIN_CHANNEL_NAME, ignoreCase = true) }
|
||||
.coerceAtLeast(0)
|
||||
}
|
||||
return adminChannelIndex
|
||||
return when {
|
||||
myNum == toNum -> 0
|
||||
myNode?.hasPKC == true && destNode?.hasPKC == true -> DataPacket.PKC_CHANNEL_INDEX
|
||||
isAdmin ->
|
||||
channelSet.value.settings
|
||||
.indexOfFirst { it.name.equals(ADMIN_CHANNEL_NAME, ignoreCase = true) }
|
||||
.coerceAtLeast(0)
|
||||
else -> destNode?.channel ?: 0
|
||||
}
|
||||
}
|
||||
|
||||
private fun getAdminChannelIndex(toNum: Int): Int = getChannelIndex(toNum, isAdmin = true)
|
||||
|
||||
override fun sendData(p: DataPacket) {
|
||||
if (p.id == 0) p.id = generatePacketId()
|
||||
val bytes = p.bytes ?: ByteString.EMPTY
|
||||
|
|
@ -191,7 +199,7 @@ class CommandSenderImpl(
|
|||
packetHandler.sendToRadio(
|
||||
buildMeshPacket(
|
||||
to = idNum,
|
||||
channel = if (destNum == null) 0 else nodeManager.nodeDBbyNodeNum[destNum]?.channel ?: 0,
|
||||
channel = if (destNum == null) 0 else getChannelIndex(destNum),
|
||||
priority = MeshPacket.Priority.BACKGROUND,
|
||||
decoded =
|
||||
Data(
|
||||
|
|
@ -214,7 +222,7 @@ class CommandSenderImpl(
|
|||
packetHandler.sendToRadio(
|
||||
buildMeshPacket(
|
||||
to = destNum,
|
||||
channel = nodeManager.nodeDBbyNodeNum[destNum]?.channel ?: 0,
|
||||
channel = getChannelIndex(destNum),
|
||||
priority = MeshPacket.Priority.BACKGROUND,
|
||||
decoded =
|
||||
Data(
|
||||
|
|
@ -249,7 +257,7 @@ class CommandSenderImpl(
|
|||
packetHandler.sendToRadio(
|
||||
buildMeshPacket(
|
||||
to = destNum,
|
||||
channel = nodeManager.nodeDBbyNodeNum[destNum]?.channel ?: 0,
|
||||
channel = getChannelIndex(destNum),
|
||||
decoded =
|
||||
Data(
|
||||
portnum = PortNum.NODEINFO_APP,
|
||||
|
|
@ -267,7 +275,7 @@ class CommandSenderImpl(
|
|||
to = destNum,
|
||||
wantAck = true,
|
||||
id = requestId,
|
||||
channel = nodeManager.nodeDBbyNodeNum[destNum]?.channel ?: 0,
|
||||
channel = getChannelIndex(destNum),
|
||||
decoded = Data(portnum = PortNum.TRACEROUTE_APP, want_response = true, dest = destNum),
|
||||
),
|
||||
)
|
||||
|
|
@ -305,7 +313,7 @@ class CommandSenderImpl(
|
|||
buildMeshPacket(
|
||||
to = destNum,
|
||||
id = requestId,
|
||||
channel = nodeManager.nodeDBbyNodeNum[destNum]?.channel ?: 0,
|
||||
channel = getChannelIndex(destNum),
|
||||
decoded = Data(portnum = portNum, payload = payloadBytes, want_response = true, dest = destNum),
|
||||
),
|
||||
)
|
||||
|
|
@ -342,7 +350,7 @@ class CommandSenderImpl(
|
|||
to = destNum,
|
||||
wantAck = true,
|
||||
id = requestId,
|
||||
channel = nodeManager.nodeDBbyNodeNum[destNum]?.channel ?: 0,
|
||||
channel = getChannelIndex(destNum),
|
||||
decoded =
|
||||
Data(
|
||||
portnum = PortNum.NEIGHBORINFO_APP,
|
||||
|
|
@ -358,7 +366,7 @@ class CommandSenderImpl(
|
|||
to = destNum,
|
||||
wantAck = true,
|
||||
id = requestId,
|
||||
channel = nodeManager.nodeDBbyNodeNum[destNum]?.channel ?: 0,
|
||||
channel = getChannelIndex(destNum),
|
||||
decoded = Data(portnum = PortNum.NEIGHBORINFO_APP, want_response = true, dest = destNum),
|
||||
),
|
||||
)
|
||||
|
|
@ -397,7 +405,14 @@ class CommandSenderImpl(
|
|||
|
||||
if (channel == DataPacket.PKC_CHANNEL_INDEX) {
|
||||
pkiEncrypted = true
|
||||
publicKey = nodeManager.nodeDBbyNodeNum[to]?.user?.public_key ?: ByteString.EMPTY
|
||||
val destNode = nodeManager.nodeDBbyNodeNum[to]
|
||||
// Resolve the public key using the same fallback as Node.hasPKC:
|
||||
// standalone publicKey (populated after Room round-trip) first, then
|
||||
// the embedded user.public_key (always available in-memory).
|
||||
publicKey = destNode?.let { it.publicKey ?: it.user.public_key } ?: ByteString.EMPTY
|
||||
if (publicKey.size == 0) {
|
||||
Logger.w { "buildMeshPacket: no public key for node ${to.toUInt()}, PKI encryption will fail" }
|
||||
}
|
||||
actualChannel = 0
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -99,6 +99,7 @@ class HistoryManagerImpl(private val meshPrefs: MeshPrefs, private val packetHan
|
|||
MeshPacket(
|
||||
from = myNodeNum,
|
||||
to = myNodeNum,
|
||||
id = kotlin.random.Random.nextInt(1, Int.MAX_VALUE),
|
||||
decoded = Data(portnum = PortNum.STORE_FORWARD_APP, payload = request.encode().toByteString()),
|
||||
priority = MeshPacket.Priority.BACKGROUND,
|
||||
),
|
||||
|
|
|
|||
|
|
@ -79,7 +79,14 @@ class MeshActionHandlerImpl(
|
|||
override suspend fun onServiceAction(action: ServiceAction) {
|
||||
Logger.d { "ServiceAction dispatched: ${action::class.simpleName}" }
|
||||
ignoreExceptionSuspend {
|
||||
val myNodeNum = nodeManager.myNodeNum.value ?: return@ignoreExceptionSuspend
|
||||
val myNodeNum = nodeManager.myNodeNum.value
|
||||
if (myNodeNum == null) {
|
||||
Logger.w { "MeshActionHandlerImpl: myNodeNum is null, skipping ServiceAction!" }
|
||||
if (action is ServiceAction.SendContact) {
|
||||
action.result.complete(false)
|
||||
}
|
||||
return@ignoreExceptionSuspend
|
||||
}
|
||||
when (action) {
|
||||
is ServiceAction.Favorite -> handleFavorite(action, myNodeNum)
|
||||
is ServiceAction.Ignore -> handleIgnore(action, myNodeNum)
|
||||
|
|
|
|||
|
|
@ -304,7 +304,7 @@ class MeshDataHandlerImpl(
|
|||
if (p != null && p.status != MessageStatus.RECEIVED) {
|
||||
val updatedPacket =
|
||||
p.copy(status = m, relays = if (isAck) p.relays + 1 else p.relays, relayNode = relayNode)
|
||||
packetRepository.value.update(updatedPacket)
|
||||
packetRepository.value.update(updatedPacket, routingError = routingError)
|
||||
}
|
||||
|
||||
reaction?.let { r ->
|
||||
|
|
|
|||
|
|
@ -103,7 +103,9 @@ class NodeManagerImpl(
|
|||
val byId = mutableMapOf<String, Node>()
|
||||
nodes.values.forEach { byId[it.user.id] = it }
|
||||
_nodeDBbyID.value = persistentMapOf<String, Node>().putAll(byId)
|
||||
myNodeNum.value = nodeRepository.myNodeInfo.value?.myNodeNum
|
||||
if (myNodeNum.value == null) {
|
||||
myNodeNum.value = nodeRepository.myNodeInfo.value?.myNodeNum
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -195,7 +197,12 @@ class NodeManagerImpl(
|
|||
} else {
|
||||
val keyMatch = !node.hasPKC || node.user.public_key == p.public_key
|
||||
val newUser = if (keyMatch) p else p.copy(public_key = ByteString.EMPTY)
|
||||
node.copy(user = newUser, channel = channel, manuallyVerified = manuallyVerified)
|
||||
node.copy(
|
||||
user = newUser,
|
||||
publicKey = newUser.public_key,
|
||||
channel = channel,
|
||||
manuallyVerified = manuallyVerified,
|
||||
)
|
||||
}
|
||||
if (newNode && !shouldPreserve) {
|
||||
scope.handledLaunch {
|
||||
|
|
@ -278,7 +285,7 @@ class NodeManagerImpl(
|
|||
if (info.via_mqtt) {
|
||||
newUser = newUser.copy(long_name = "${newUser.long_name} (MQTT)")
|
||||
}
|
||||
next = next.copy(user = newUser)
|
||||
next = next.copy(user = newUser, publicKey = newUser.public_key)
|
||||
}
|
||||
}
|
||||
val position = info.position
|
||||
|
|
|
|||
|
|
@ -256,12 +256,20 @@ class PacketRepositoryImpl(private val dbManager: DatabaseProvider, private val
|
|||
insertRoomPacket(packetToSave)
|
||||
}
|
||||
|
||||
override suspend fun update(packet: DataPacket): Unit = withContext(dispatchers.io) {
|
||||
override suspend fun update(packet: DataPacket, routingError: Int): Unit = withContext(dispatchers.io) {
|
||||
val dao = dbManager.currentDb.value.packetDao()
|
||||
// Match on key fields that identify the packet, rather than the entire data object
|
||||
dao.findPacketsWithId(packet.id)
|
||||
.find { it.data.id == packet.id && it.data.from == packet.from && it.data.to == packet.to }
|
||||
?.let { dao.update(it.copy(data = packet)) }
|
||||
?.let { existing ->
|
||||
val updated =
|
||||
if (routingError >= 0) {
|
||||
existing.copy(data = packet, routingError = routingError)
|
||||
} else {
|
||||
existing.copy(data = packet)
|
||||
}
|
||||
dao.update(updated)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun insertReaction(reaction: Reaction, myNodeNum: Int) =
|
||||
|
|
|
|||
|
|
@ -18,6 +18,8 @@ package org.meshtastic.core.data.manager
|
|||
|
||||
import dev.mokkery.MockMode
|
||||
import dev.mokkery.mock
|
||||
import okio.ByteString
|
||||
import okio.ByteString.Companion.toByteString
|
||||
import org.meshtastic.core.model.DataPacket
|
||||
import org.meshtastic.core.model.Node
|
||||
import org.meshtastic.core.repository.NodeRepository
|
||||
|
|
@ -34,6 +36,7 @@ import kotlin.test.assertEquals
|
|||
import kotlin.test.assertNotNull
|
||||
import kotlin.test.assertNull
|
||||
import kotlin.test.assertTrue
|
||||
import org.meshtastic.proto.NodeInfo as ProtoNodeInfo
|
||||
import org.meshtastic.proto.Position as ProtoPosition
|
||||
|
||||
class NodeManagerImplTest {
|
||||
|
|
@ -226,4 +229,103 @@ class NodeManagerImplTest {
|
|||
assertTrue(!nodeManager.nodeDBbyNodeNum.containsKey(nodeNum))
|
||||
assertTrue(!nodeManager.nodeDBbyID.containsKey("!testnode"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `handleReceivedUser sets publicKey from user public_key`() {
|
||||
val nodeNum = 1234
|
||||
val pk = ByteArray(32) { (it + 1).toByte() }.toByteString()
|
||||
val existingUser =
|
||||
User(id = "!12345678", long_name = "Existing", short_name = "EX", hw_model = HardwareModel.TLORA_V2)
|
||||
nodeManager.updateNode(nodeNum) { it.copy(user = existingUser) }
|
||||
|
||||
val incomingUser =
|
||||
User(
|
||||
id = "!12345678",
|
||||
long_name = "Updated",
|
||||
short_name = "UP",
|
||||
hw_model = HardwareModel.TLORA_V2,
|
||||
public_key = pk,
|
||||
)
|
||||
nodeManager.handleReceivedUser(nodeNum, incomingUser)
|
||||
|
||||
val result = nodeManager.nodeDBbyNodeNum[nodeNum]!!
|
||||
assertEquals(pk, result.publicKey)
|
||||
assertEquals(pk, result.user.public_key)
|
||||
assertTrue(result.hasPKC)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `handleReceivedUser sets empty publicKey when key mismatch clears user key`() {
|
||||
val nodeNum = 1234
|
||||
val existingPk = ByteArray(32) { (it + 1).toByte() }.toByteString()
|
||||
val existingUser =
|
||||
User(
|
||||
id = "!12345678",
|
||||
long_name = "Existing",
|
||||
short_name = "EX",
|
||||
hw_model = HardwareModel.TLORA_V2,
|
||||
public_key = existingPk,
|
||||
)
|
||||
nodeManager.updateNode(nodeNum) { it.copy(user = existingUser, publicKey = existingPk) }
|
||||
|
||||
val differentPk = ByteArray(32) { (it + 10).toByte() }.toByteString()
|
||||
val incomingUser =
|
||||
User(
|
||||
id = "!12345678",
|
||||
long_name = "Updated",
|
||||
short_name = "UP",
|
||||
hw_model = HardwareModel.TLORA_V2,
|
||||
public_key = differentPk,
|
||||
)
|
||||
nodeManager.handleReceivedUser(nodeNum, incomingUser)
|
||||
|
||||
val result = nodeManager.nodeDBbyNodeNum[nodeNum]!!
|
||||
// Key mismatch: newUser gets public_key cleared to EMPTY, and publicKey should match
|
||||
assertEquals(ByteString.EMPTY, result.publicKey)
|
||||
assertEquals(ByteString.EMPTY, result.user.public_key)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `installNodeInfo sets publicKey from user public_key`() {
|
||||
val nodeNum = 5678
|
||||
val pk = ByteArray(32) { (it + 1).toByte() }.toByteString()
|
||||
val user =
|
||||
User(
|
||||
id = "!abcd1234",
|
||||
long_name = "Remote Node",
|
||||
short_name = "RN",
|
||||
hw_model = HardwareModel.HELTEC_V3,
|
||||
public_key = pk,
|
||||
)
|
||||
val info = ProtoNodeInfo(num = nodeNum, user = user, last_heard = 1000, channel = 0)
|
||||
|
||||
nodeManager.installNodeInfo(info)
|
||||
|
||||
val result = nodeManager.nodeDBbyNodeNum[nodeNum]!!
|
||||
assertEquals(pk, result.publicKey)
|
||||
assertEquals(pk, result.user.public_key)
|
||||
assertTrue(result.hasPKC)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `installNodeInfo clears publicKey for licensed users`() {
|
||||
val nodeNum = 5678
|
||||
val pk = ByteArray(32) { (it + 1).toByte() }.toByteString()
|
||||
val user =
|
||||
User(
|
||||
id = "!abcd1234",
|
||||
long_name = "Licensed Op",
|
||||
short_name = "LO",
|
||||
hw_model = HardwareModel.HELTEC_V3,
|
||||
public_key = pk,
|
||||
is_licensed = true,
|
||||
)
|
||||
val info = ProtoNodeInfo(num = nodeNum, user = user, last_heard = 1000, channel = 0)
|
||||
|
||||
nodeManager.installNodeInfo(info)
|
||||
|
||||
val result = nodeManager.nodeDBbyNodeNum[nodeNum]!!
|
||||
assertEquals(ByteString.EMPTY, result.publicKey)
|
||||
assertEquals(ByteString.EMPTY, result.user.public_key)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue