announcements WIP

This commit is contained in:
callebtc
2025-08-20 00:28:02 +02:00
parent cc4316bf2b
commit 777bcc08f1
6 changed files with 250 additions and 49 deletions
@@ -30,7 +30,7 @@ class BluetoothGattClientManager(
companion object { companion object {
private const val TAG = "BluetoothGattClientManager" private const val TAG = "BluetoothGattClientManager"
// Use exact same UUIDs as iOS version // Use exact same UUIDs as iOS version
private val SERVICE_UUID = UUID.fromString("F47B5E2D-4A9E-4C5A-9B3F-8E1D2C3A4B5C") private val SERVICE_UUID = UUID.fromString("F47B5E2D-4A9E-4C5A-9B3F-8E1D2C3A4B5A")
private val CHARACTERISTIC_UUID = UUID.fromString("A1B2C3D4-E5F6-4A5B-8C9D-0E1F2A3B4C5D") private val CHARACTERISTIC_UUID = UUID.fromString("A1B2C3D4-E5F6-4A5B-8C9D-0E1F2A3B4C5D")
private val DESCRIPTOR_UUID = UUID.fromString("00002902-0000-1000-8000-00805f9b34fb") private val DESCRIPTOR_UUID = UUID.fromString("00002902-0000-1000-8000-00805f9b34fb")
@@ -29,7 +29,7 @@ class BluetoothGattServerManager(
companion object { companion object {
private const val TAG = "BluetoothGattServerManager" private const val TAG = "BluetoothGattServerManager"
// Use exact same UUIDs as iOS version // Use exact same UUIDs as iOS version
private val SERVICE_UUID = UUID.fromString("F47B5E2D-4A9E-4C5A-9B3F-8E1D2C3A4B5C") private val SERVICE_UUID = UUID.fromString("F47B5E2D-4A9E-4C5A-9B3F-8E1D2C3A4B5A")
private val CHARACTERISTIC_UUID = UUID.fromString("A1B2C3D4-E5F6-4A5B-8C9D-0E1F2A3B4C5D") private val CHARACTERISTIC_UUID = UUID.fromString("A1B2C3D4-E5F6-4A5B-8C9D-0E1F2A3B4C5D")
private val DESCRIPTOR_UUID = UUID.fromString("00002902-0000-1000-8000-00805f9b34fb") private val DESCRIPTOR_UUID = UUID.fromString("00002902-0000-1000-8000-00805f9b34fb")
} }
@@ -597,8 +597,13 @@ class BluetoothMeshService(private val context: Context) {
payload = tlvPayload payload = tlvPayload
) )
connectionManager.broadcastPacket(RoutedPacket(announcePacket)) // Sign the packet using our signing key (exactly like iOS)
Log.d(TAG, "Sent iOS-compatible TLV announce (${tlvPayload.size} bytes)") val signedPacket = encryptionService.signData(announcePacket.toBinaryDataForSigning()!!)?.let { signature ->
announcePacket.copy(signature = signature)
} ?: announcePacket
connectionManager.broadcastPacket(RoutedPacket(signedPacket))
Log.d(TAG, "Sent iOS-compatible signed TLV announce (${tlvPayload.size} bytes)")
} }
} }
@@ -639,9 +644,14 @@ class BluetoothMeshService(private val context: Context) {
payload = tlvPayload payload = tlvPayload
) )
connectionManager.broadcastPacket(RoutedPacket(packet)) // Sign the packet using our signing key (exactly like iOS)
val signedPacket = encryptionService.signData(packet.toBinaryDataForSigning()!!)?.let { signature ->
packet.copy(signature = signature)
} ?: packet
connectionManager.broadcastPacket(RoutedPacket(signedPacket))
peerManager.markPeerAsAnnouncedTo(peerID) peerManager.markPeerAsAnnouncedTo(peerID)
Log.d(TAG, "Sent iOS-compatible TLV peer announce to $peerID (${tlvPayload.size} bytes)") Log.d(TAG, "Sent iOS-compatible signed TLV peer announce to $peerID (${tlvPayload.size} bytes)")
} }
/** /**
@@ -698,6 +708,26 @@ class BluetoothMeshService(private val context: Context) {
return peerManager.getFingerprintForPeer(peerID) return peerManager.getFingerprintForPeer(peerID)
} }
/**
* Get peer info for verification purposes
*/
fun getPeerInfo(peerID: String): PeerInfo? {
return peerManager.getPeerInfo(peerID)
}
/**
* Update peer information with verification data
*/
fun updatePeerInfo(
peerID: String,
nickname: String,
noisePublicKey: ByteArray,
signingPublicKey: ByteArray,
isVerified: Boolean
): Boolean {
return peerManager.updatePeerInfo(peerID, nickname, noisePublicKey, signingPublicKey, isVerified)
}
/** /**
* Get our identity fingerprint * Get our identity fingerprint
*/ */
@@ -164,7 +164,7 @@ class MessageHandler(private val myPeerID: String) {
} }
/** /**
* Handle announce message with TLV decoding support - exactly like iOS * Handle announce message with TLV decoding and signature verification - exactly like iOS
*/ */
suspend fun handleAnnounce(routed: RoutedPacket): Boolean { suspend fun handleAnnounce(routed: RoutedPacket): Boolean {
val packet = routed.packet val packet = routed.packet
@@ -179,8 +179,36 @@ class MessageHandler(private val myPeerID: String) {
return false 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)}")
}
}
// 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 as? BluetoothMeshService)?.let { service ->
// Access PeerManager through service
service.getPeerInfo(peerID)
}
if (existingPeer != null && existingPeer.noisePublicKey != null && !existingPeer.noisePublicKey!!.contentEquals(announcement.noisePublicKey)) {
Log.w(TAG, "⚠️ Announce key mismatch for ${peerID.take(8)}... — keeping unverified")
verified = false
}
// Require verified announce; ignore otherwise (no backward compatibility)
if (!verified) {
Log.w(TAG, "❌ Ignoring unverified announce from ${peerID.take(8)}...")
return false
}
// Successfully decoded TLV format exactly like iOS // Successfully decoded TLV format exactly like iOS
Log.d(TAG, "Received iOS-compatible announce from $peerID: nickname=${announcement.nickname}, " + Log.d(TAG, "✅ Verified announce from $peerID: nickname=${announcement.nickname}, " +
"noisePublicKey=${announcement.noisePublicKey.joinToString("") { "%02x".format(it) }.take(16)}..., " + "noisePublicKey=${announcement.noisePublicKey.joinToString("") { "%02x".format(it) }.take(16)}..., " +
"signingPublicKey=${announcement.signingPublicKey.joinToString("") { "%02x".format(it) }.take(16)}...") "signingPublicKey=${announcement.signingPublicKey.joinToString("") { "%02x".format(it) }.take(16)}...")
@@ -189,8 +217,16 @@ class MessageHandler(private val myPeerID: String) {
val noisePublicKey = announcement.noisePublicKey val noisePublicKey = announcement.noisePublicKey
val signingPublicKey = announcement.signingPublicKey val signingPublicKey = announcement.signingPublicKey
// Notify delegate to handle peer management with nickname // Update peer info with verification status through new method
val isFirstAnnounce = delegate?.addOrUpdatePeer(peerID, nickname) ?: false val isFirstAnnounce = (delegate as? BluetoothMeshService)?.let { service ->
service.updatePeerInfo(
peerID = peerID,
nickname = nickname,
noisePublicKey = noisePublicKey,
signingPublicKey = signingPublicKey,
isVerified = true
)
} ?: false
// Update peer ID binding with noise public key for identity management // Update peer ID binding with noise public key for identity management
delegate?.updatePeerIDBinding( delegate?.updatePeerIDBinding(
@@ -200,7 +236,7 @@ class MessageHandler(private val myPeerID: String) {
previousPeerID = null previousPeerID = null
) )
Log.d(TAG, "Processed iOS-compatible TLV announce: stored public key for $peerID") Log.d(TAG, "Processed verified TLV announce: stored identity for $peerID")
return isFirstAnnounce return isFirstAnnounce
} }
@@ -284,11 +320,19 @@ class MessageHandler(private val myPeerID: String) {
} }
/** /**
* Handle broadcast message * Handle broadcast message with verification enforcement
*/ */
private suspend fun handleBroadcastMessage(routed: RoutedPacket) { private suspend fun handleBroadcastMessage(routed: RoutedPacket) {
val packet = routed.packet val packet = routed.packet
val peerID = routed.peerID ?: "unknown" val peerID = routed.peerID ?: "unknown"
// Enforce: only accept public messages from verified peers we know
val peerInfo = (delegate as? BluetoothMeshService)?.getPeerInfo(peerID)
if (peerInfo == null || !peerInfo.isVerifiedNickname) {
Log.w(TAG, "🚫 Dropping public message from unverified or unknown peer ${peerID.take(8)}...")
return
}
try { try {
// Parse message // Parse message
val message = BitchatMessage( val message = BitchatMessage(
@@ -5,11 +5,60 @@ import kotlinx.coroutines.*
import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.CopyOnWriteArrayList import java.util.concurrent.CopyOnWriteArrayList
/**
* Peer information structure with verification status
* Compatible with iOS PeerInfo structure
*/
data class PeerInfo(
val id: String,
var nickname: String,
var isConnected: Boolean,
var noisePublicKey: ByteArray?,
var signingPublicKey: ByteArray?, // NEW: Ed25519 public key for verification
var isVerifiedNickname: Boolean, // NEW: Verification status flag
var lastSeen: Long // Using Long instead of Date for simplicity
) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as PeerInfo
if (id != other.id) return false
if (nickname != other.nickname) return false
if (isConnected != other.isConnected) return false
if (noisePublicKey != null) {
if (other.noisePublicKey == null) return false
if (!noisePublicKey.contentEquals(other.noisePublicKey)) return false
} else if (other.noisePublicKey != null) return false
if (signingPublicKey != null) {
if (other.signingPublicKey == null) return false
if (!signingPublicKey.contentEquals(other.signingPublicKey)) return false
} else if (other.signingPublicKey != null) return false
if (isVerifiedNickname != other.isVerifiedNickname) return false
if (lastSeen != other.lastSeen) return false
return true
}
override fun hashCode(): Int {
var result = id.hashCode()
result = 31 * result + nickname.hashCode()
result = 31 * result + isConnected.hashCode()
result = 31 * result + (noisePublicKey?.contentHashCode() ?: 0)
result = 31 * result + (signingPublicKey?.contentHashCode() ?: 0)
result = 31 * result + isVerifiedNickname.hashCode()
result = 31 * result + lastSeen.hashCode()
return result
}
}
/** /**
* Manages active peers, nicknames, RSSI tracking, and peer fingerprints * Manages active peers, nicknames, RSSI tracking, and peer fingerprints
* Extracted from BluetoothMeshService for better separation of concerns * Extracted from BluetoothMeshService for better separation of concerns
* *
* Now includes centralized peer fingerprint management via PeerFingerprintManager singleton * Now includes centralized peer fingerprint management via PeerFingerprintManager singleton
* and support for signed announcement verification
*/ */
class PeerManager { class PeerManager {
@@ -19,13 +68,18 @@ class PeerManager {
private const val CLEANUP_INTERVAL = 60000L // 1 minute private const val CLEANUP_INTERVAL = 60000L // 1 minute
} }
// Peer tracking data // Peer tracking data - enhanced with verification status
private val peerNicknames = ConcurrentHashMap<String, String>() private val peers = ConcurrentHashMap<String, PeerInfo>() // peerID -> PeerInfo
private val activePeers = ConcurrentHashMap<String, Long>() // peerID -> lastSeen timestamp
private val peerRSSI = ConcurrentHashMap<String, Int>() private val peerRSSI = ConcurrentHashMap<String, Int>()
private val announcedPeers = CopyOnWriteArrayList<String>() private val announcedPeers = CopyOnWriteArrayList<String>()
private val announcedToPeers = CopyOnWriteArrayList<String>() private val announcedToPeers = CopyOnWriteArrayList<String>()
// Legacy support for existing code
@Deprecated("Use PeerInfo structure instead")
private val peerNicknames = ConcurrentHashMap<String, String>()
@Deprecated("Use PeerInfo structure instead")
private val activePeers = ConcurrentHashMap<String, Long>() // peerID -> lastSeen timestamp
// Centralized fingerprint management // Centralized fingerprint management
private val fingerprintManager = PeerFingerprintManager.getInstance() private val fingerprintManager = PeerFingerprintManager.getInstance()
@@ -39,12 +93,89 @@ class PeerManager {
startPeriodicCleanup() startPeriodicCleanup()
} }
// MARK: - New PeerInfo-based methods
/**
* Update peer information with verification data
* Similar to iOS updatePeer method
*/
fun updatePeerInfo(
peerID: String,
nickname: String,
noisePublicKey: ByteArray,
signingPublicKey: ByteArray,
isVerified: Boolean
): Boolean {
if (peerID == "unknown") return false
val now = System.currentTimeMillis()
val existingPeer = peers[peerID]
val isNewPeer = existingPeer == null
// Update or create peer info
val peerInfo = PeerInfo(
id = peerID,
nickname = nickname,
isConnected = true,
noisePublicKey = noisePublicKey,
signingPublicKey = signingPublicKey,
isVerifiedNickname = isVerified,
lastSeen = now
)
peers[peerID] = peerInfo
// Update legacy structures for compatibility
peerNicknames[peerID] = nickname
activePeers[peerID] = now
if (isNewPeer && isVerified) {
announcedPeers.add(peerID)
notifyPeerListUpdate()
Log.d(TAG, "🆕 New verified peer: $nickname ($peerID)")
return true
} else if (isVerified) {
Log.d(TAG, "🔄 Updated verified peer: $nickname ($peerID)")
} else {
Log.d(TAG, "⚠️ Unverified peer announcement from: $nickname ($peerID)")
}
return false
}
/**
* Get peer info
*/
fun getPeerInfo(peerID: String): PeerInfo? {
return peers[peerID]
}
/**
* Check if peer is verified
*/
fun isPeerVerified(peerID: String): Boolean {
return peers[peerID]?.isVerifiedNickname == true
}
/**
* Get all verified peers
*/
fun getVerifiedPeers(): Map<String, PeerInfo> {
return peers.filterValues { it.isVerifiedNickname }
}
// MARK: - Legacy Methods (maintained for compatibility)
/** /**
* Update peer last seen timestamp * Update peer last seen timestamp
*/ */
fun updatePeerLastSeen(peerID: String) { fun updatePeerLastSeen(peerID: String) {
if (peerID != "unknown") { if (peerID != "unknown") {
activePeers[peerID] = System.currentTimeMillis() activePeers[peerID] = System.currentTimeMillis()
// Also update PeerInfo if it exists
peers[peerID]?.let { info ->
peers[peerID] = info.copy(lastSeen = System.currentTimeMillis())
}
} }
} }
@@ -422,19 +422,18 @@ class NoiseEncryptionService(private val context: Context) {
} }
/** /**
* Generate a new Ed25519 key pair for signing * Generate a new Ed25519 key pair for signing using BouncyCastle
* Returns (privateKey, publicKey) as 32-byte arrays * Returns (privateKey, publicKey) as 32-byte arrays
*/ */
private fun generateEd25519KeyPair(): Pair<ByteArray, ByteArray> { private fun generateEd25519KeyPair(): Pair<ByteArray, ByteArray> {
try { try {
// Generate a simple Ed25519 key pair using secure random // Use BouncyCastle for proper Ed25519 key generation
// This is a simplified implementation - in production you'd use proper Ed25519 library val keyGen = org.bouncycastle.crypto.generators.Ed25519KeyPairGenerator()
val secureRandom = SecureRandom() keyGen.init(org.bouncycastle.crypto.params.Ed25519KeyGenerationParameters(SecureRandom()))
val privateKey = ByteArray(32) val keyPair = keyGen.generateKeyPair()
secureRandom.nextBytes(privateKey)
// Generate public key from private key (simplified) val privateKey = (keyPair.private as org.bouncycastle.crypto.params.Ed25519PrivateKeyParameters).encoded
val publicKey = deriveEd25519PublicKey(privateKey) val publicKey = (keyPair.public as org.bouncycastle.crypto.params.Ed25519PublicKeyParameters).encoded
return Pair(privateKey, publicKey) return Pair(privateKey, publicKey)
} catch (e: Exception) { } catch (e: Exception) {
@@ -444,38 +443,35 @@ class NoiseEncryptionService(private val context: Context) {
} }
/** /**
* Derive Ed25519 public key from private key (simplified implementation) * Sign data with Ed25519 private key using BouncyCastle
*/
private fun deriveEd25519PublicKey(privateKey: ByteArray): ByteArray {
// This is a placeholder - in a real implementation you'd use proper Ed25519 math
// For now, we'll use a hash-based derivation which is not cryptographically correct
// but will work for the protocol demonstration
val digest = MessageDigest.getInstance("SHA-256")
return digest.digest(privateKey)
}
/**
* Sign data with Ed25519 private key (simplified implementation)
*/ */
private fun signWithEd25519(data: ByteArray, privateKey: ByteArray): ByteArray { private fun signWithEd25519(data: ByteArray, privateKey: ByteArray): ByteArray {
// This is a placeholder implementation try {
// In production, you'd use a proper Ed25519 library like BouncyCastle val privateKeyParams = org.bouncycastle.crypto.params.Ed25519PrivateKeyParameters(privateKey, 0)
val digest = MessageDigest.getInstance("SHA-256") val signer = org.bouncycastle.crypto.signers.Ed25519Signer()
val combined = ByteArray(privateKey.size + data.size) signer.init(true, privateKeyParams)
System.arraycopy(privateKey, 0, combined, 0, privateKey.size) signer.update(data, 0, data.size)
System.arraycopy(data, 0, combined, privateKey.size, data.size) return signer.generateSignature()
return digest.digest(combined) } catch (e: Exception) {
Log.e(TAG, "Failed to sign data with Ed25519: ${e.message}")
throw e
}
} }
/** /**
* Verify Ed25519 signature (simplified implementation) * Verify Ed25519 signature using BouncyCastle
*/ */
private fun verifyWithEd25519(signature: ByteArray, data: ByteArray, publicKey: ByteArray): Boolean { private fun verifyWithEd25519(signature: ByteArray, data: ByteArray, publicKey: ByteArray): Boolean {
// This is a placeholder implementation try {
// In production, you'd use a proper Ed25519 library like BouncyCastle val publicKeyParams = org.bouncycastle.crypto.params.Ed25519PublicKeyParameters(publicKey, 0)
val privateKeyFromPublic = publicKey // This is obviously not correct, but for demo val verifier = org.bouncycastle.crypto.signers.Ed25519Signer()
val expectedSignature = signWithEd25519(data, privateKeyFromPublic) verifier.init(false, publicKeyParams)
return signature.contentEquals(expectedSignature) verifier.update(data, 0, data.size)
return verifier.verifySignature(signature)
} catch (e: Exception) {
Log.e(TAG, "Failed to verify Ed25519 signature: ${e.message}")
return false
}
} }
/** /**