From 777bcc08f1931f16ff40a350087563bd9e6aa400 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Wed, 20 Aug 2025 00:28:02 +0200 Subject: [PATCH] announcements WIP --- .../mesh/BluetoothGattClientManager.kt | 2 +- .../mesh/BluetoothGattServerManager.kt | 2 +- .../android/mesh/BluetoothMeshService.kt | 38 ++++- .../bitchat/android/mesh/MessageHandler.kt | 56 ++++++- .../com/bitchat/android/mesh/PeerManager.kt | 139 +++++++++++++++++- .../android/noise/NoiseEncryptionService.kt | 62 ++++---- 6 files changed, 250 insertions(+), 49 deletions(-) diff --git a/app/src/main/java/com/bitchat/android/mesh/BluetoothGattClientManager.kt b/app/src/main/java/com/bitchat/android/mesh/BluetoothGattClientManager.kt index f47aba15..2f8b410c 100644 --- a/app/src/main/java/com/bitchat/android/mesh/BluetoothGattClientManager.kt +++ b/app/src/main/java/com/bitchat/android/mesh/BluetoothGattClientManager.kt @@ -30,7 +30,7 @@ class BluetoothGattClientManager( companion object { private const val TAG = "BluetoothGattClientManager" // 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 DESCRIPTOR_UUID = UUID.fromString("00002902-0000-1000-8000-00805f9b34fb") diff --git a/app/src/main/java/com/bitchat/android/mesh/BluetoothGattServerManager.kt b/app/src/main/java/com/bitchat/android/mesh/BluetoothGattServerManager.kt index 99c23f0c..1e2759c0 100644 --- a/app/src/main/java/com/bitchat/android/mesh/BluetoothGattServerManager.kt +++ b/app/src/main/java/com/bitchat/android/mesh/BluetoothGattServerManager.kt @@ -29,7 +29,7 @@ class BluetoothGattServerManager( companion object { private const val TAG = "BluetoothGattServerManager" // 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 DESCRIPTOR_UUID = UUID.fromString("00002902-0000-1000-8000-00805f9b34fb") } diff --git a/app/src/main/java/com/bitchat/android/mesh/BluetoothMeshService.kt b/app/src/main/java/com/bitchat/android/mesh/BluetoothMeshService.kt index ca7a3f57..87bac85a 100644 --- a/app/src/main/java/com/bitchat/android/mesh/BluetoothMeshService.kt +++ b/app/src/main/java/com/bitchat/android/mesh/BluetoothMeshService.kt @@ -597,8 +597,13 @@ class BluetoothMeshService(private val context: Context) { payload = tlvPayload ) - connectionManager.broadcastPacket(RoutedPacket(announcePacket)) - Log.d(TAG, "Sent iOS-compatible TLV announce (${tlvPayload.size} bytes)") + // Sign the packet using our signing key (exactly like iOS) + 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 ) - 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) - 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)") } /** @@ -697,6 +707,26 @@ class BluetoothMeshService(private val context: Context) { fun getPeerFingerprint(peerID: String): String? { 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 diff --git a/app/src/main/java/com/bitchat/android/mesh/MessageHandler.kt b/app/src/main/java/com/bitchat/android/mesh/MessageHandler.kt index 68139cf9..f9498773 100644 --- a/app/src/main/java/com/bitchat/android/mesh/MessageHandler.kt +++ b/app/src/main/java/com/bitchat/android/mesh/MessageHandler.kt @@ -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 { val packet = routed.packet @@ -179,8 +179,36 @@ class MessageHandler(private val myPeerID: String) { 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 - 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)}..., " + "signingPublicKey=${announcement.signingPublicKey.joinToString("") { "%02x".format(it) }.take(16)}...") @@ -189,8 +217,16 @@ class MessageHandler(private val myPeerID: String) { val noisePublicKey = announcement.noisePublicKey val signingPublicKey = announcement.signingPublicKey - // Notify delegate to handle peer management with nickname - val isFirstAnnounce = delegate?.addOrUpdatePeer(peerID, nickname) ?: false + // Update peer info with verification status through new method + 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 delegate?.updatePeerIDBinding( @@ -200,7 +236,7 @@ class MessageHandler(private val myPeerID: String) { 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 } @@ -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) { val packet = routed.packet 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 { // Parse message val message = BitchatMessage( diff --git a/app/src/main/java/com/bitchat/android/mesh/PeerManager.kt b/app/src/main/java/com/bitchat/android/mesh/PeerManager.kt index b383df63..26a0ef87 100644 --- a/app/src/main/java/com/bitchat/android/mesh/PeerManager.kt +++ b/app/src/main/java/com/bitchat/android/mesh/PeerManager.kt @@ -5,11 +5,60 @@ import kotlinx.coroutines.* import java.util.concurrent.ConcurrentHashMap 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 * Extracted from BluetoothMeshService for better separation of concerns * * Now includes centralized peer fingerprint management via PeerFingerprintManager singleton + * and support for signed announcement verification */ class PeerManager { @@ -19,13 +68,18 @@ class PeerManager { private const val CLEANUP_INTERVAL = 60000L // 1 minute } - // Peer tracking data - private val peerNicknames = ConcurrentHashMap() - private val activePeers = ConcurrentHashMap() // peerID -> lastSeen timestamp + // Peer tracking data - enhanced with verification status + private val peers = ConcurrentHashMap() // peerID -> PeerInfo private val peerRSSI = ConcurrentHashMap() private val announcedPeers = CopyOnWriteArrayList() private val announcedToPeers = CopyOnWriteArrayList() + // Legacy support for existing code + @Deprecated("Use PeerInfo structure instead") + private val peerNicknames = ConcurrentHashMap() + @Deprecated("Use PeerInfo structure instead") + private val activePeers = ConcurrentHashMap() // peerID -> lastSeen timestamp + // Centralized fingerprint management private val fingerprintManager = PeerFingerprintManager.getInstance() @@ -38,13 +92,90 @@ class PeerManager { init { 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 { + return peers.filterValues { it.isVerifiedNickname } + } + + // MARK: - Legacy Methods (maintained for compatibility) + /** * Update peer last seen timestamp */ fun updatePeerLastSeen(peerID: String) { if (peerID != "unknown") { activePeers[peerID] = System.currentTimeMillis() + // Also update PeerInfo if it exists + peers[peerID]?.let { info -> + peers[peerID] = info.copy(lastSeen = System.currentTimeMillis()) + } } } diff --git a/app/src/main/java/com/bitchat/android/noise/NoiseEncryptionService.kt b/app/src/main/java/com/bitchat/android/noise/NoiseEncryptionService.kt index 4cf76f70..a3d469be 100644 --- a/app/src/main/java/com/bitchat/android/noise/NoiseEncryptionService.kt +++ b/app/src/main/java/com/bitchat/android/noise/NoiseEncryptionService.kt @@ -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 */ private fun generateEd25519KeyPair(): Pair { try { - // Generate a simple Ed25519 key pair using secure random - // This is a simplified implementation - in production you'd use proper Ed25519 library - val secureRandom = SecureRandom() - val privateKey = ByteArray(32) - secureRandom.nextBytes(privateKey) + // Use BouncyCastle for proper Ed25519 key generation + val keyGen = org.bouncycastle.crypto.generators.Ed25519KeyPairGenerator() + keyGen.init(org.bouncycastle.crypto.params.Ed25519KeyGenerationParameters(SecureRandom())) + val keyPair = keyGen.generateKeyPair() - // Generate public key from private key (simplified) - val publicKey = deriveEd25519PublicKey(privateKey) + val privateKey = (keyPair.private as org.bouncycastle.crypto.params.Ed25519PrivateKeyParameters).encoded + val publicKey = (keyPair.public as org.bouncycastle.crypto.params.Ed25519PublicKeyParameters).encoded return Pair(privateKey, publicKey) } catch (e: Exception) { @@ -444,38 +443,35 @@ class NoiseEncryptionService(private val context: Context) { } /** - * Derive Ed25519 public key from private key (simplified implementation) - */ - 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) + * Sign data with Ed25519 private key using BouncyCastle */ private fun signWithEd25519(data: ByteArray, privateKey: ByteArray): ByteArray { - // This is a placeholder implementation - // In production, you'd use a proper Ed25519 library like BouncyCastle - val digest = MessageDigest.getInstance("SHA-256") - val combined = ByteArray(privateKey.size + data.size) - System.arraycopy(privateKey, 0, combined, 0, privateKey.size) - System.arraycopy(data, 0, combined, privateKey.size, data.size) - return digest.digest(combined) + try { + val privateKeyParams = org.bouncycastle.crypto.params.Ed25519PrivateKeyParameters(privateKey, 0) + val signer = org.bouncycastle.crypto.signers.Ed25519Signer() + signer.init(true, privateKeyParams) + signer.update(data, 0, data.size) + return signer.generateSignature() + } 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 { - // This is a placeholder implementation - // In production, you'd use a proper Ed25519 library like BouncyCastle - val privateKeyFromPublic = publicKey // This is obviously not correct, but for demo - val expectedSignature = signWithEd25519(data, privateKeyFromPublic) - return signature.contentEquals(expectedSignature) + try { + val publicKeyParams = org.bouncycastle.crypto.params.Ed25519PublicKeyParameters(publicKey, 0) + val verifier = org.bouncycastle.crypto.signers.Ed25519Signer() + verifier.init(false, publicKeyParams) + 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 + } } /**