Merge branch 'codex/bind-noise-peer-identities' into codex/advertise-private-media-capability

# Conflicts:
#	app/src/main/java/com/bitchat/android/mesh/BluetoothMeshService.kt
#	app/src/main/java/com/bitchat/android/mesh/MeshCore.kt
#	app/src/main/java/com/bitchat/android/mesh/MessageHandler.kt
#	app/src/main/java/com/bitchat/android/model/RoutedPacket.kt
#	app/src/test/kotlin/com/bitchat/android/mesh/MessageHandlerTest.kt
This commit is contained in:
jack
2026-07-12 11:18:17 -04:00
23 changed files with 1416 additions and 277 deletions
@@ -3,7 +3,6 @@ package com.bitchat.android.mesh
import android.util.Log
import com.bitchat.android.model.BitchatMessage
import com.bitchat.android.model.BitchatMessageType
import com.bitchat.android.model.IdentityAnnouncement
import com.bitchat.android.model.RoutedPacket
import com.bitchat.android.protocol.BitchatPacket
import com.bitchat.android.protocol.MessageType
@@ -13,6 +12,11 @@ import kotlinx.coroutines.*
import java.util.*
import kotlin.random.Random
sealed class AnnounceHandlingResult {
data class Accepted(val isFirst: Boolean) : AnnounceHandlingResult()
object Rejected : AnnounceHandlingResult()
}
/**
* Handles processing of different message types
* Extracted from BluetoothMeshService for better separation of concerns
@@ -216,10 +220,14 @@ class MessageHandler(private val myPeerID: String, private val appContext: andro
* Handle announce message with TLV decoding and signature verification - exactly like iOS
*/
suspend fun handleAnnounce(routed: RoutedPacket): Boolean {
return (handleAnnounceWithResult(routed) as? AnnounceHandlingResult.Accepted)?.isFirst ?: false
}
suspend fun handleAnnounceWithResult(routed: RoutedPacket): AnnounceHandlingResult {
val packet = routed.packet
val peerID = routed.peerID ?: "unknown"
if (peerID == myPeerID) return false
if (peerID == myPeerID) return AnnounceHandlingResult.Rejected
// Peers use wall-clock packet timestamps; tolerate moderate device clock skew
// during identity learning, or later signed messages cannot be verified.
@@ -227,28 +235,21 @@ class MessageHandler(private val myPeerID: String, private val appContext: andro
val clockSkewMs = kotlin.math.abs(now - packet.timestamp.toLong())
if (clockSkewMs > ANNOUNCE_CLOCK_SKEW_TOLERANCE_MS) {
Log.w(TAG, "Ignoring ANNOUNCE from ${peerID.take(8)} with excessive clock skew (${clockSkewMs}ms > ${ANNOUNCE_CLOCK_SKEW_TOLERANCE_MS}ms)")
return false
return AnnounceHandlingResult.Rejected
} else if (clockSkewMs > com.bitchat.android.util.AppConstants.Mesh.STALE_PEER_TIMEOUT_MS) {
Log.w(TAG, "Accepting ANNOUNCE from ${peerID.take(8)} within clock skew tolerance (${clockSkewMs}ms)")
}
// Try to decode as iOS-compatible IdentityAnnouncement with TLV format
val announcement = IdentityAnnouncement.decode(packet.payload)
val announcement = AnnouncementIdentityValidator.verify(packet, peerID) { signature, data, key ->
delegate?.verifyEd25519Signature(signature, data, key) ?: false
}
if (announcement == null) {
Log.w(TAG, "Failed to decode announce from $peerID as iOS-compatible TLV format")
return false
}
// Verify packet signature using the announced signing public key
var verified = false
if (packet.signature != null) {
// Verify that the packet was signed by the signing private key corresponding to the announced signing public key
verified = delegate?.verifyEd25519Signature(packet.signature!!, packet.toBinaryDataForSigning()!!, announcement.signingPublicKey) ?: false
if (!verified) {
Log.w(TAG, "⚠️ Signature verification for announce failed ${peerID.take(8)}")
}
Log.w(TAG, "Rejecting malformed, unbound, or invalidly signed ANNOUNCE from ${peerID.take(8)}")
return AnnounceHandlingResult.Rejected
}
var verified = true
// Check for existing peer with different noise public key
// If existing peer has a different noise public key, do not consider this verified
val existingPeer = delegate?.getPeerInfo(peerID)
@@ -258,10 +259,21 @@ class MessageHandler(private val myPeerID: String, private val appContext: andro
verified = false
}
if (
existingPeer?.signingPublicKey != null &&
!existingPeer.signingPublicKey!!.contentEquals(announcement.signingPublicKey)
) {
Log.w(
TAG,
"Rejecting signing-key replacement for ${peerID.take(8)} without authenticated peer-state proof"
)
verified = false
}
// Require verified announce; ignore otherwise (no backward compatibility)
if (!verified) {
Log.w(TAG, "❌ Ignoring unverified announce from ${peerID.take(8)}...")
return false
return AnnounceHandlingResult.Rejected
}
// Successfully decoded TLV format exactly like iOS
@@ -284,19 +296,6 @@ class MessageHandler(private val myPeerID: String, private val appContext: andro
capabilities = announcement.capabilities
) ?: false
// Promotion of security-sensitive capabilities happens only after the
// signature check above and must additionally bind to the authenticated
// Noise remote-static key in the transport service.
delegate?.onVerifiedAnnouncementProcessed(peerID)
// Update peer ID binding with noise public key for identity management
delegate?.updatePeerIDBinding(
newPeerID = peerID,
nickname = nickname,
publicKey = noisePublicKey,
previousPeerID = null
)
// Update mesh graph from gossip neighbors (only if TLV present)
try {
val neighborsOrNull = com.bitchat.android.services.meshgraph.GossipTLV.decodeNeighborsFromAnnouncementPayload(packet.payload)
@@ -305,7 +304,7 @@ class MessageHandler(private val myPeerID: String, private val appContext: andro
} catch (_: Exception) { }
Log.d(TAG, "✅ Processed verified TLV announce: stored identity for $peerID")
return isFirstAnnounce
return AnnounceHandlingResult.Accepted(isFirstAnnounce)
}
/**
@@ -647,8 +646,6 @@ interface MessageHandlerDelegate {
fun hasNoiseSession(peerID: String): Boolean
fun initiateNoiseHandshake(peerID: String)
fun processNoiseHandshakeMessage(payload: ByteArray, peerID: String): ByteArray?
fun updatePeerIDBinding(newPeerID: String, nickname: String,
publicKey: ByteArray, previousPeerID: String?)
// Message operations
fun decryptChannelMessage(encryptedContent: ByteArray, channel: String): String?