mirror of
https://github.com/permissionlesstech/bitchat-android.git
synced 2026-07-26 08:45:22 +00:00
Refactor fingerprint manager (#185)
* centralized fingerprint manager * centralized fingerprintmanager * update fingerprint only if no collision
This commit is contained in:
@@ -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") }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user