Refactor fingerprint manager (#185)

* centralized fingerprint manager

* centralized fingerprintmanager

* update fingerprint only if no collision
This commit is contained in:
callebtc
2025-07-25 15:43:19 +02:00
committed by GitHub
parent 9b7009137c
commit 534be7e613
10 changed files with 425 additions and 106 deletions
@@ -66,7 +66,7 @@ class BluetoothMeshService(private val context: Context) {
// Wire up PacketProcessor reference for recursive handling in MessageHandler
messageHandler.packetProcessor = packetProcessor
sendPeriodicBroadcastAnnounce()
//startPeriodicDebugLogging()
startPeriodicDebugLogging()
}
/**
@@ -126,9 +126,6 @@ class BluetoothMeshService(private val context: Context) {
// SecurityManager delegate for key exchange notifications
securityManager.delegate = object : SecurityManagerDelegate {
override fun onKeyExchangeCompleted(peerID: String, peerPublicKeyData: ByteArray, receivedAddress: String?) {
// Notify delegate about key exchange completion so it can register peer fingerprint
delegate?.registerPeerPublicKey(peerID, peerPublicKeyData)
receivedAddress?.let { address ->
connectionManager.addressPeerMap[address] = peerID
}
@@ -259,19 +256,21 @@ class BluetoothMeshService(private val context: Context) {
}
}
override fun updatePeerIDBinding(newPeerID: String, fingerprint: String, nickname: String,
override fun updatePeerIDBinding(newPeerID: String, nickname: String,
publicKey: ByteArray, previousPeerID: String?) {
Log.d(TAG, "Updating peer ID binding: $newPeerID (was: $previousPeerID) with nickname: $nickname and public key: ${publicKey.toHexString().take(16)}...")
// Update peer mapping in the PeerManager for peer ID rotation support
peerManager.addOrUpdatePeer(newPeerID, nickname)
// Store fingerprint for the peer via centralized fingerprint manager
val fingerprint = peerManager.storeFingerprintForPeer(newPeerID, publicKey)
// If there was a previous peer ID, remove it to avoid duplicates
previousPeerID?.let { oldPeerID ->
peerManager.removePeer(oldPeerID)
}
// Register the public key with the delegate (ChatViewModel)
delegate?.registerPeerPublicKey(newPeerID, publicKey)
Log.d(TAG, "Updated peer ID binding: $newPeerID (was: $previousPeerID), fingerprint: ${fingerprint.take(16)}...")
}
@@ -794,7 +793,7 @@ class BluetoothMeshService(private val context: Context) {
* Get peer fingerprint for identity management
*/
fun getPeerFingerprint(peerID: String): String? {
return encryptionService.getPeerFingerprint(peerID)
return peerManager.getFingerprintForPeer(peerID)
}
/**
@@ -853,6 +852,8 @@ class BluetoothMeshService(private val context: Context) {
appendLine()
appendLine(peerManager.getDebugInfo(connectionManager.addressPeerMap))
appendLine()
appendLine(peerManager.getFingerprintDebugInfo())
appendLine()
appendLine(fragmentManager.getDebugInfo())
appendLine()
appendLine(securityManager.getDebugInfo())
@@ -910,5 +911,5 @@ interface BluetoothMeshDelegate {
fun decryptChannelMessage(encryptedContent: ByteArray, channel: String): String?
fun getNickname(): String?
fun isFavorite(peerID: String): Boolean
fun registerPeerPublicKey(peerID: String, publicKeyData: ByteArray)
// registerPeerPublicKey REMOVED - fingerprints now handled centrally in PeerManager
}
@@ -138,7 +138,6 @@ class MessageHandler(private val myPeerID: String) {
// Update peer binding in the delegate (ChatViewModel/BluetoothMeshService)
delegate?.updatePeerIDBinding(
newPeerID = announcement.peerID,
fingerprint = announcement.fingerprint ?: "",
nickname = announcement.nickname,
publicKey = announcement.publicKey,
previousPeerID = announcement.previousPeerID
@@ -409,7 +408,7 @@ interface MessageHandlerDelegate {
// Noise protocol operations
fun hasNoiseSession(peerID: String): Boolean
fun initiateNoiseHandshake(peerID: String)
fun updatePeerIDBinding(newPeerID: String, fingerprint: String, nickname: String,
fun updatePeerIDBinding(newPeerID: String, nickname: String,
publicKey: ByteArray, previousPeerID: String?)
// Message operations
@@ -0,0 +1,247 @@
package com.bitchat.android.mesh
import android.util.Log
import java.security.MessageDigest
import java.util.concurrent.ConcurrentHashMap
/**
* Centralized peer fingerprint management singleton
*
* This class manages all peer fingerprint storage and retrieval operations,
* providing a single source of truth for peer identity across the entire application.
*
* Fingerprints are SHA-256 hashes of peer static public keys and are only stored
* after successful Noise handshake session establishment.
*
* Key Design Principles:
* - Thread-safe operations using ConcurrentHashMap
* - Bidirectional mapping (peerID ↔ fingerprint)
* - Support for peer ID rotation while maintaining persistent identity
* - Centralized logging for debugging identity management
*/
class PeerFingerprintManager private constructor() {
companion object {
private const val TAG = "PeerFingerprintManager"
@Volatile
private var INSTANCE: PeerFingerprintManager? = null
/**
* Get the singleton instance
*/
fun getInstance(): PeerFingerprintManager {
return INSTANCE ?: synchronized(this) {
INSTANCE ?: PeerFingerprintManager().also { INSTANCE = it }
}
}
}
// Bidirectional mapping for efficient lookups
private val peerIDToFingerprint = ConcurrentHashMap<String, String>() // peerID -> fingerprint
private val fingerprintToPeerID = ConcurrentHashMap<String, String>() // fingerprint -> current peerID
// MARK: - Fingerprint Storage (Only called after successful Noise handshake)
/**
* Store fingerprint mapping after successful Noise handshake session establishment
* This is the ONLY place where fingerprints should be stored
*
* @param peerID The peer's current ID
* @param publicKey The peer's static public key from Noise handshake
*/
fun storeFingerprintForPeer(peerID: String, publicKey: ByteArray): String {
// get existing fingerprint for this peer and compare
val existingFingerprint = getFingerprintForPeer(peerID)
val fingerprint = calculateFingerprint(publicKey)
if (existingFingerprint != null && existingFingerprint != fingerprint) {
Log.w(TAG, "Fingerprint mismatch for peer $peerID: $existingFingerprint != $fingerprint")
throw IllegalStateException("Fingerprint mismatch for peer $peerID: $existingFingerprint != $fingerprint")
}
// Store bidirectional mapping
peerIDToFingerprint[peerID] = fingerprint
fingerprintToPeerID[fingerprint] = peerID
Log.d(TAG, "Stored fingerprint for peer $peerID: ${fingerprint.take(16)}...")
return fingerprint
}
/**
* Update peer ID mapping while preserving fingerprint identity
* Used for peer ID rotation - when a peer changes their ID but maintains the same static key
*
* @param oldPeerID The previous peer ID (nullable if this is a fresh mapping)
* @param newPeerID The new peer ID
* @param fingerprint The persistent fingerprint (should match existing one)
*/
fun updatePeerIDMapping(oldPeerID: String?, newPeerID: String, fingerprint: String) {
if (newPeerID.isBlank()) {
Log.w(TAG, "Attempted to update mapping with blank newPeerID")
return
}
if (fingerprint.isBlank()) {
Log.w(TAG, "Attempted to update mapping with blank fingerprint")
return
}
// Remove old mapping if exists
oldPeerID?.takeIf { it.isNotBlank() }?.let { oldID ->
val removedFingerprint = peerIDToFingerprint.remove(oldID)
if (removedFingerprint != null && removedFingerprint == fingerprint) {
Log.d(TAG, "Removed old mapping: $oldID -> ${removedFingerprint.take(16)}...")
}
}
// Add new mapping
peerIDToFingerprint[newPeerID] = fingerprint
fingerprintToPeerID[fingerprint] = newPeerID
Log.d(TAG, "Updated peer ID mapping: $newPeerID (was: $oldPeerID), fingerprint: ${fingerprint.take(16)}...")
}
// MARK: - Fingerprint Retrieval
/**
* Get fingerprint for a specific peer ID
*
* @param peerID The peer ID to look up
* @return The fingerprint if found, null otherwise
*/
fun getFingerprintForPeer(peerID: String): String? {
if (peerID.isBlank()) return null
return peerIDToFingerprint[peerID]
}
/**
* Get current peer ID for a specific fingerprint
*
* @param fingerprint The fingerprint to look up
* @return The current peer ID if found, null otherwise
*/
fun getPeerIDForFingerprint(fingerprint: String): String? {
if (fingerprint.isBlank()) return null
return fingerprintToPeerID[fingerprint]
}
/**
* Check if we have a fingerprint for a specific peer
*
* @param peerID The peer ID to check
* @return True if we have a fingerprint for this peer, false otherwise
*/
fun hasFingerprintForPeer(peerID: String): Boolean {
return getFingerprintForPeer(peerID) != null
}
/**
* Get all current peer ID to fingerprint mappings
*
* @return Immutable copy of all mappings
*/
fun getAllPeerFingerprints(): Map<String, String> {
return peerIDToFingerprint.toMap()
}
/**
* Get all current fingerprint to peer ID mappings
*
* @return Immutable copy of all reverse mappings
*/
fun getAllFingerprintMappings(): Map<String, String> {
return fingerprintToPeerID.toMap()
}
// MARK: - Peer Management
/**
* Remove all mappings for a specific peer (called when peer disconnects)
*
* @param peerID The peer ID to remove
*/
fun removePeer(peerID: String) {
if (peerID.isBlank()) return
val fingerprint = peerIDToFingerprint.remove(peerID)
if (fingerprint != null) {
fingerprintToPeerID.remove(fingerprint)
Log.d(TAG, "Removed peer mappings for $peerID: ${fingerprint.take(16)}...")
}
}
/**
* Remove all mappings for a specific fingerprint
*
* @param fingerprint The fingerprint to remove
*/
fun removeFingerprint(fingerprint: String) {
if (fingerprint.isBlank()) return
val peerID = fingerprintToPeerID.remove(fingerprint)
if (peerID != null) {
peerIDToFingerprint.remove(peerID)
Log.d(TAG, "Removed fingerprint mappings for ${fingerprint.take(16)}...: $peerID")
}
}
/**
* Clear all fingerprint mappings (used for emergency clear/panic mode)
*/
fun clearAllFingerprints() {
val count = peerIDToFingerprint.size
peerIDToFingerprint.clear()
fingerprintToPeerID.clear()
Log.d(TAG, "Cleared all fingerprint mappings ($count entries)")
}
// MARK: - Utility Functions
/**
* Calculate SHA-256 fingerprint from public key
*
* @param publicKey The peer's static public key
* @return The hex-encoded SHA-256 hash
*/
private fun calculateFingerprint(publicKey: ByteArray): String {
val digest = MessageDigest.getInstance("SHA-256")
val hash = digest.digest(publicKey)
return hash.joinToString("") { "%02x".format(it) }
}
/**
* Get debug information about current fingerprint mappings
*
* @return Debug string with mapping counts and summary
*/
fun getDebugInfo(): String {
return buildString {
appendLine("=== PeerFingerprintManager Debug Info ===")
appendLine("Total mappings: ${peerIDToFingerprint.size}")
if (peerIDToFingerprint.isNotEmpty()) {
appendLine("Peer ID -> Fingerprint mappings:")
peerIDToFingerprint.forEach { (peerID, fingerprint) ->
appendLine(" $peerID -> ${fingerprint.take(16)}...")
}
} else {
appendLine("No fingerprint mappings stored")
}
// Verify bidirectional mapping consistency
val inconsistentMappings = mutableListOf<String>()
peerIDToFingerprint.forEach { (peerID, fingerprint) ->
val reversePeerID = fingerprintToPeerID[fingerprint]
if (reversePeerID != peerID) {
inconsistentMappings.add("$peerID -> $fingerprint -> $reversePeerID")
}
}
if (inconsistentMappings.isNotEmpty()) {
appendLine("⚠️ INCONSISTENT MAPPINGS DETECTED:")
inconsistentMappings.forEach { appendLine(" $it") }
}
}
}
}
@@ -7,8 +7,10 @@ import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.CopyOnWriteArrayList
/**
* Manages active peers, nicknames, and RSSI tracking
* 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
*/
class PeerManager {
@@ -25,6 +27,9 @@ class PeerManager {
private val announcedPeers = CopyOnWriteArrayList<String>()
private val announcedToPeers = CopyOnWriteArrayList<String>()
// Centralized fingerprint management
private val fingerprintManager = PeerFingerprintManager.getInstance()
// Delegate for callbacks
var delegate: PeerManagerDelegate? = null
@@ -81,7 +86,7 @@ class PeerManager {
notifyPeerListUpdate()
return true
}
Log.d(TAG, "Updated peer: $peerID ($nickname)")
return false
}
@@ -95,6 +100,9 @@ class PeerManager {
announcedPeers.remove(peerID)
announcedToPeers.remove(peerID)
// Also remove fingerprint mappings
fingerprintManager.removePeer(peerID)
if (notifyDelegate && nickname != null) {
delegate?.onPeerDisconnected(nickname)
notifyPeerListUpdate()
@@ -177,6 +185,10 @@ class PeerManager {
peerRSSI.clear()
announcedPeers.clear()
announcedToPeers.clear()
// Also clear fingerprint mappings
fingerprintManager.clearAllFingerprints()
notifyPeerListUpdate()
}
@@ -264,6 +276,83 @@ class PeerManager {
}
}
// MARK: - Fingerprint Management (Centralized)
/**
* Store fingerprint for a peer after successful Noise handshake
* This should only be called when a Noise session is established
*
* @param peerID The peer's ID
* @param publicKey The peer's static public key from Noise handshake
*/
fun storeFingerprintForPeer(peerID: String, publicKey: ByteArray): String {
return fingerprintManager.storeFingerprintForPeer(peerID, publicKey)
}
/**
* Update peer ID mapping for peer ID rotation
*
* @param oldPeerID The previous peer ID (nullable)
* @param newPeerID The new peer ID
* @param fingerprint The persistent fingerprint
*/
fun updatePeerIDMapping(oldPeerID: String?, newPeerID: String, fingerprint: String) {
fingerprintManager.updatePeerIDMapping(oldPeerID, newPeerID, fingerprint)
}
/**
* Get fingerprint for a specific peer
*
* @param peerID The peer ID to look up
* @return The fingerprint if found, null otherwise
*/
fun getFingerprintForPeer(peerID: String): String? {
return fingerprintManager.getFingerprintForPeer(peerID)
}
/**
* Get current peer ID for a specific fingerprint
*
* @param fingerprint The fingerprint to look up
* @return The current peer ID if found, null otherwise
*/
fun getPeerIDForFingerprint(fingerprint: String): String? {
return fingerprintManager.getPeerIDForFingerprint(fingerprint)
}
/**
* Check if we have a fingerprint for a specific peer
*
* @param peerID The peer ID to check
* @return True if we have a fingerprint for this peer, false otherwise
*/
fun hasFingerprintForPeer(peerID: String): Boolean {
return fingerprintManager.hasFingerprintForPeer(peerID)
}
/**
* Get all current peer ID to fingerprint mappings
*
* @return Immutable copy of all mappings
*/
fun getAllPeerFingerprints(): Map<String, String> {
return fingerprintManager.getAllPeerFingerprints()
}
/**
* Clear all fingerprint mappings (used for emergency clear)
*/
fun clearAllFingerprints() {
fingerprintManager.clearAllFingerprints()
}
/**
* Get fingerprint manager debug info
*/
fun getFingerprintDebugInfo(): String {
return fingerprintManager.getDebugInfo()
}
/**
* Shutdown the manager
*/
@@ -146,46 +146,46 @@ class SecurityManager(private val encryptionService: EncryptionService, private
}
}
/**
* Handle key exchange packet
*/
suspend fun handleKeyExchange(routed: RoutedPacket): Boolean {
val packet = routed.packet
val peerID = routed.peerID ?: "unknown"
if (peerID == myPeerID) return false
if (packet.payload.isEmpty()) {
Log.w(TAG, "Key exchange packet has empty payload")
return false
}
// Prevent duplicate key exchange processing
val exchangeKey = "$peerID-${packet.payload.sliceArray(0 until minOf(16, packet.payload.size)).contentHashCode()}"
if (processedKeyExchanges.contains(exchangeKey)) {
Log.d(TAG, "Already processed key exchange: $exchangeKey")
return false
}
processedKeyExchanges.add(exchangeKey)
try {
// Process the key exchange
encryptionService.addPeerPublicKey(peerID, packet.payload)
Log.d(TAG, "Successfully processed key exchange from $peerID")
// Notify delegate
delegate?.onKeyExchangeCompleted(peerID, packet.payload, routed.relayAddress)
return true
} catch (e: Exception) {
Log.e(TAG, "Failed to process key exchange from $peerID: ${e.message}")
return false
}
}
// /**
// * Handle key exchange packet
// */
// suspend fun handleKeyExchange(routed: RoutedPacket): Boolean {
// val packet = routed.packet
// val peerID = routed.peerID ?: "unknown"
//
// if (peerID == myPeerID) return false
//
// if (packet.payload.isEmpty()) {
// Log.w(TAG, "Key exchange packet has empty payload")
// return false
// }
//
// // Prevent duplicate key exchange processing
// val exchangeKey = "$peerID-${packet.payload.sliceArray(0 until minOf(16, packet.payload.size)).contentHashCode()}"
//
// if (processedKeyExchanges.contains(exchangeKey)) {
// Log.d(TAG, "Already processed key exchange: $exchangeKey")
// return false
// }
//
// processedKeyExchanges.add(exchangeKey)
//
// try {
// // Process the key exchange
// encryptionService.addPeerPublicKey(peerID, packet.payload)
//
// Log.d(TAG, "Successfully processed key exchange from $peerID")
//
// // Notify delegate
// delegate?.onKeyExchangeCompleted(peerID, packet.payload, routed.relayAddress)
//
// return true
//
// } catch (e: Exception) {
// Log.e(TAG, "Failed to process key exchange from $peerID: ${e.message}")
// return false
// }
// }
/**
* Verify packet signature