mirror of
https://github.com/permissionlesstech/bitchat-android.git
synced 2026-07-25 11:05:20 +00:00
Refactor QR verification: Extract VerificationHandler and fix concurrency issues
This commit is contained in:
@@ -35,6 +35,7 @@ class SecureIdentityStateManager(private val context: Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private val prefs: SharedPreferences
|
private val prefs: SharedPreferences
|
||||||
|
private val lock = Any()
|
||||||
|
|
||||||
init {
|
init {
|
||||||
// Create master key for encryption
|
// Create master key for encryption
|
||||||
@@ -199,17 +200,23 @@ class SecureIdentityStateManager(private val context: Context) {
|
|||||||
|
|
||||||
fun setVerifiedFingerprint(fingerprint: String, verified: Boolean) {
|
fun setVerifiedFingerprint(fingerprint: String, verified: Boolean) {
|
||||||
if (!isValidFingerprint(fingerprint)) return
|
if (!isValidFingerprint(fingerprint)) return
|
||||||
val current = prefs.getStringSet(KEY_VERIFIED_FINGERPRINTS, emptySet())?.toMutableSet() ?: mutableSetOf()
|
synchronized(lock) {
|
||||||
if (verified) {
|
val current = prefs.getStringSet(KEY_VERIFIED_FINGERPRINTS, emptySet())?.toMutableSet() ?: mutableSetOf()
|
||||||
current.add(fingerprint)
|
if (verified) {
|
||||||
} else {
|
current.add(fingerprint)
|
||||||
current.remove(fingerprint)
|
} else {
|
||||||
|
current.remove(fingerprint)
|
||||||
|
}
|
||||||
|
prefs.edit { putStringSet(KEY_VERIFIED_FINGERPRINTS, current) }
|
||||||
}
|
}
|
||||||
prefs.edit { putStringSet(KEY_VERIFIED_FINGERPRINTS, current) }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getCachedPeerFingerprint(peerID: String): String? {
|
fun getCachedPeerFingerprint(peerID: String): String? {
|
||||||
val pid = peerID.lowercase()
|
val pid = peerID.lowercase()
|
||||||
|
// Reading is safe without lock for SharedPreferences, but synchronizing ensures memory visibility
|
||||||
|
// if we are paranoid, but SharedPreferences is generally thread-safe for reads.
|
||||||
|
// However, to ensure we don't read a partial update (unlikely with SP), we can leave it.
|
||||||
|
// The critical part is the write.
|
||||||
val entries = prefs.getStringSet(KEY_CACHED_PEER_FINGERPRINTS, emptySet()) ?: return null
|
val entries = prefs.getStringSet(KEY_CACHED_PEER_FINGERPRINTS, emptySet()) ?: return null
|
||||||
val entry = entries.firstOrNull { it.startsWith("$pid:") } ?: return null
|
val entry = entries.firstOrNull { it.startsWith("$pid:") } ?: return null
|
||||||
return entry.substringAfter(':').takeIf { isValidFingerprint(it) }
|
return entry.substringAfter(':').takeIf { isValidFingerprint(it) }
|
||||||
@@ -218,10 +225,12 @@ class SecureIdentityStateManager(private val context: Context) {
|
|||||||
fun cachePeerFingerprint(peerID: String, fingerprint: String) {
|
fun cachePeerFingerprint(peerID: String, fingerprint: String) {
|
||||||
if (!isValidFingerprint(fingerprint)) return
|
if (!isValidFingerprint(fingerprint)) return
|
||||||
val pid = peerID.lowercase()
|
val pid = peerID.lowercase()
|
||||||
val current = prefs.getStringSet(KEY_CACHED_PEER_FINGERPRINTS, emptySet())?.toMutableSet() ?: mutableSetOf()
|
synchronized(lock) {
|
||||||
current.removeAll { it.startsWith("$pid:") }
|
val current = prefs.getStringSet(KEY_CACHED_PEER_FINGERPRINTS, emptySet())?.toMutableSet() ?: mutableSetOf()
|
||||||
current.add("$pid:$fingerprint")
|
current.removeAll { it.startsWith("$pid:") }
|
||||||
prefs.edit { putStringSet(KEY_CACHED_PEER_FINGERPRINTS, current) }
|
current.add("$pid:$fingerprint")
|
||||||
|
prefs.edit { putStringSet(KEY_CACHED_PEER_FINGERPRINTS, current) }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getCachedNoiseKey(peerID: String): String? {
|
fun getCachedNoiseKey(peerID: String): String? {
|
||||||
@@ -234,10 +243,12 @@ class SecureIdentityStateManager(private val context: Context) {
|
|||||||
fun cachePeerNoiseKey(peerID: String, noiseKeyHex: String) {
|
fun cachePeerNoiseKey(peerID: String, noiseKeyHex: String) {
|
||||||
if (!noiseKeyHex.matches(Regex("^[a-fA-F0-9]{64}$"))) return
|
if (!noiseKeyHex.matches(Regex("^[a-fA-F0-9]{64}$"))) return
|
||||||
val pid = peerID.lowercase()
|
val pid = peerID.lowercase()
|
||||||
val current = prefs.getStringSet(KEY_CACHED_PEER_NOISE_KEYS, emptySet())?.toMutableSet() ?: mutableSetOf()
|
synchronized(lock) {
|
||||||
current.removeAll { it.startsWith("$pid=") }
|
val current = prefs.getStringSet(KEY_CACHED_PEER_NOISE_KEYS, emptySet())?.toMutableSet() ?: mutableSetOf()
|
||||||
current.add("$pid=${noiseKeyHex.lowercase()}")
|
current.removeAll { it.startsWith("$pid=") }
|
||||||
prefs.edit { putStringSet(KEY_CACHED_PEER_NOISE_KEYS, current) }
|
current.add("$pid=${noiseKeyHex.lowercase()}")
|
||||||
|
prefs.edit { putStringSet(KEY_CACHED_PEER_NOISE_KEYS, current) }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getCachedNoiseFingerprint(noiseKeyHex: String): String? {
|
fun getCachedNoiseFingerprint(noiseKeyHex: String): String? {
|
||||||
@@ -251,10 +262,12 @@ class SecureIdentityStateManager(private val context: Context) {
|
|||||||
if (!isValidFingerprint(fingerprint)) return
|
if (!isValidFingerprint(fingerprint)) return
|
||||||
if (!noiseKeyHex.matches(Regex("^[a-fA-F0-9]{64}$"))) return
|
if (!noiseKeyHex.matches(Regex("^[a-fA-F0-9]{64}$"))) return
|
||||||
val key = noiseKeyHex.lowercase()
|
val key = noiseKeyHex.lowercase()
|
||||||
val current = prefs.getStringSet(KEY_CACHED_NOISE_FINGERPRINTS, emptySet())?.toMutableSet() ?: mutableSetOf()
|
synchronized(lock) {
|
||||||
current.removeAll { it.startsWith("$key=") }
|
val current = prefs.getStringSet(KEY_CACHED_NOISE_FINGERPRINTS, emptySet())?.toMutableSet() ?: mutableSetOf()
|
||||||
current.add("$key=$fingerprint")
|
current.removeAll { it.startsWith("$key=") }
|
||||||
prefs.edit { putStringSet(KEY_CACHED_NOISE_FINGERPRINTS, current) }
|
current.add("$key=$fingerprint")
|
||||||
|
prefs.edit { putStringSet(KEY_CACHED_NOISE_FINGERPRINTS, current) }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getCachedFingerprintNickname(fingerprint: String): String? {
|
fun getCachedFingerprintNickname(fingerprint: String): String? {
|
||||||
@@ -273,10 +286,12 @@ class SecureIdentityStateManager(private val context: Context) {
|
|||||||
if (!isValidFingerprint(fingerprint)) return
|
if (!isValidFingerprint(fingerprint)) return
|
||||||
val key = fingerprint.lowercase()
|
val key = fingerprint.lowercase()
|
||||||
val encoded = Base64.encodeToString(nickname.toByteArray(Charsets.UTF_8), Base64.NO_WRAP)
|
val encoded = Base64.encodeToString(nickname.toByteArray(Charsets.UTF_8), Base64.NO_WRAP)
|
||||||
val current = prefs.getStringSet(KEY_CACHED_FINGERPRINT_NICKNAMES, emptySet())?.toMutableSet() ?: mutableSetOf()
|
synchronized(lock) {
|
||||||
current.removeAll { it.startsWith("$key=") }
|
val current = prefs.getStringSet(KEY_CACHED_FINGERPRINT_NICKNAMES, emptySet())?.toMutableSet() ?: mutableSetOf()
|
||||||
current.add("$key=$encoded")
|
current.removeAll { it.startsWith("$key=") }
|
||||||
prefs.edit { putStringSet(KEY_CACHED_FINGERPRINT_NICKNAMES, current) }
|
current.add("$key=$encoded")
|
||||||
|
prefs.edit { putStringSet(KEY_CACHED_FINGERPRINT_NICKNAMES, current) }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Peer ID Rotation Management (removed)
|
// MARK: - Peer ID Rotation Management (removed)
|
||||||
|
|||||||
@@ -101,13 +101,15 @@ class ChatViewModel(
|
|||||||
NotificationIntervalManager()
|
NotificationIntervalManager()
|
||||||
)
|
)
|
||||||
|
|
||||||
// QR verification state
|
private val verificationHandler = VerificationHandler(
|
||||||
private val _verifiedFingerprints = MutableStateFlow<Set<String>>(emptySet())
|
scope = viewModelScope,
|
||||||
val verifiedFingerprints: StateFlow<Set<String>> = _verifiedFingerprints.asStateFlow()
|
meshService = meshService,
|
||||||
private val pendingQRVerifications = mutableMapOf<String, PendingVerification>()
|
identityManager = identityManager,
|
||||||
private val lastVerifyNonceByPeer = mutableMapOf<String, ByteArray>()
|
state = state,
|
||||||
private val lastInboundVerifyChallengeAt = mutableMapOf<String, Long>()
|
notificationManager = notificationManager,
|
||||||
private val lastMutualToastAt = mutableMapOf<String, Long>()
|
messageManager = messageManager
|
||||||
|
)
|
||||||
|
val verifiedFingerprints = verificationHandler.verifiedFingerprints
|
||||||
|
|
||||||
// Media file sending manager
|
// Media file sending manager
|
||||||
private val mediaSendingManager = MediaSendingManager(state, messageManager, channelManager, meshService)
|
private val mediaSendingManager = MediaSendingManager(state, messageManager, channelManager, meshService)
|
||||||
@@ -284,7 +286,7 @@ class ChatViewModel(
|
|||||||
com.bitchat.android.favorites.FavoritesPersistenceService.initialize(getApplication())
|
com.bitchat.android.favorites.FavoritesPersistenceService.initialize(getApplication())
|
||||||
|
|
||||||
// Load verified fingerprints from secure storage
|
// Load verified fingerprints from secure storage
|
||||||
loadVerifiedFingerprints()
|
verificationHandler.loadVerifiedFingerprints()
|
||||||
|
|
||||||
|
|
||||||
// Ensure NostrTransport knows our mesh peer ID for embedded packets
|
// Ensure NostrTransport knows our mesh peer ID for embedded packets
|
||||||
@@ -715,7 +717,7 @@ class ChatViewModel(
|
|||||||
// Flush any pending QR verification once a Noise session is established
|
// Flush any pending QR verification once a Noise session is established
|
||||||
currentPeers.forEach { peerID ->
|
currentPeers.forEach { peerID ->
|
||||||
if (meshService.getSessionState(peerID) is NoiseSession.NoiseSessionState.Established) {
|
if (meshService.getSessionState(peerID) is NoiseSession.NoiseSessionState.Established) {
|
||||||
sendPendingVerificationIfNeeded(peerID)
|
verificationHandler.sendPendingVerificationIfNeeded(peerID)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -724,125 +726,23 @@ class ChatViewModel(
|
|||||||
|
|
||||||
fun isPeerVerified(peerID: String, verifiedFingerprints: Set<String>): Boolean {
|
fun isPeerVerified(peerID: String, verifiedFingerprints: Set<String>): Boolean {
|
||||||
if (peerID.startsWith("nostr_") || peerID.startsWith("nostr:")) return false
|
if (peerID.startsWith("nostr_") || peerID.startsWith("nostr:")) return false
|
||||||
val fingerprint = getPeerFingerprintForDisplay(peerID)
|
val fingerprint = verificationHandler.getPeerFingerprintForDisplay(peerID)
|
||||||
return fingerprint != null && verifiedFingerprints.contains(fingerprint)
|
return fingerprint != null && verifiedFingerprints.contains(fingerprint)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun isNoisePublicKeyVerified(noisePublicKey: ByteArray, verifiedFingerprints: Set<String>): Boolean {
|
fun isNoisePublicKeyVerified(noisePublicKey: ByteArray, verifiedFingerprints: Set<String>): Boolean {
|
||||||
val fingerprint = fingerprintFromNoiseBytes(noisePublicKey)
|
val fingerprint = verificationHandler.fingerprintFromNoiseBytes(noisePublicKey)
|
||||||
return verifiedFingerprints.contains(fingerprint)
|
return verifiedFingerprints.contains(fingerprint)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun loadVerifiedFingerprints() {
|
|
||||||
val identityManager = SecureIdentityStateManager(getApplication())
|
|
||||||
_verifiedFingerprints.value = identityManager.getVerifiedFingerprints()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun unverifyFingerprint(peerID: String) {
|
fun unverifyFingerprint(peerID: String) {
|
||||||
val fingerprint = meshService.getPeerFingerprint(peerID) ?: return
|
verificationHandler.unverifyFingerprint(peerID)
|
||||||
val identityManager = SecureIdentityStateManager(getApplication())
|
|
||||||
identityManager.setVerifiedFingerprint(fingerprint, false)
|
|
||||||
_verifiedFingerprints.value -= fingerprint
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun beginQRVerification(qr: VerificationService.VerificationQR): Boolean {
|
fun beginQRVerification(qr: VerificationService.VerificationQR): Boolean {
|
||||||
val targetNoise = qr.noiseKeyHex.lowercase()
|
return verificationHandler.beginQRVerification(qr)
|
||||||
val peerID = state.getConnectedPeersValue().firstOrNull { pid ->
|
|
||||||
val noiseKeyHex = meshService.getPeerInfo(pid)?.noisePublicKey?.hexEncodedString()?.lowercase()
|
|
||||||
noiseKeyHex == targetNoise
|
|
||||||
} ?: return false
|
|
||||||
|
|
||||||
if (pendingQRVerifications.containsKey(peerID)) return true
|
|
||||||
val nonce = ByteArray(16)
|
|
||||||
java.security.SecureRandom().nextBytes(nonce)
|
|
||||||
val pending = PendingVerification(qr.noiseKeyHex, qr.signKeyHex, nonce, System.currentTimeMillis(), false)
|
|
||||||
pendingQRVerifications[peerID] = pending
|
|
||||||
|
|
||||||
if (meshService.getSessionState(peerID) is NoiseSession.NoiseSessionState.Established) {
|
|
||||||
meshService.sendVerifyChallenge(peerID, qr.noiseKeyHex, nonce)
|
|
||||||
pendingQRVerifications[peerID] = pending.copy(sent = true)
|
|
||||||
} else {
|
|
||||||
meshService.initiateNoiseHandshake(peerID)
|
|
||||||
}
|
|
||||||
fingerprintFromNoiseHex(qr.noiseKeyHex)?.let { fp ->
|
|
||||||
identityManager.cacheFingerprintNickname(fp, qr.nickname)
|
|
||||||
identityManager.cacheNoiseFingerprint(qr.noiseKeyHex, fp)
|
|
||||||
identityManager.cachePeerNoiseKey(peerID, qr.noiseKeyHex)
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun sendPendingVerificationIfNeeded(peerID: String) {
|
|
||||||
val pending = pendingQRVerifications[peerID] ?: return
|
|
||||||
if (pending.sent) return
|
|
||||||
meshService.sendVerifyChallenge(peerID, pending.noiseKeyHex, pending.nonceA)
|
|
||||||
pendingQRVerifications[peerID] = pending.copy(sent = true)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun addVerificationSystemMessage(peerID: String, text: String) {
|
|
||||||
val msg = BitchatMessage(
|
|
||||||
sender = "system",
|
|
||||||
content = text,
|
|
||||||
timestamp = Date(),
|
|
||||||
isRelay = false,
|
|
||||||
isPrivate = true,
|
|
||||||
senderPeerID = peerID
|
|
||||||
)
|
|
||||||
messageManager.addPrivateMessageNoUnread(peerID, msg)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun resolvePeerDisplayName(peerID: String): String {
|
|
||||||
val nick = try { meshService.getPeerInfo(peerID)?.nickname } catch (_: Exception) { null }
|
|
||||||
return nick ?: peerID.take(8)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun sendVerificationNotification(title: String, body: String, peerID: String) {
|
|
||||||
notificationManager.showVerificationNotification(title, body, peerID)
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Debug and Troubleshooting
|
|
||||||
|
|
||||||
fun getDebugStatus(): String {
|
|
||||||
return meshService.getDebugStatus()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Note: Mesh service restart is now handled by MainActivity
|
|
||||||
// This function is no longer needed
|
|
||||||
|
|
||||||
fun setAppBackgroundState(inBackground: Boolean) {
|
|
||||||
// Forward to notification manager for notification logic
|
|
||||||
notificationManager.setAppBackgroundState(inBackground)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setCurrentPrivateChatPeer(peerID: String?) {
|
|
||||||
// Update notification manager with current private chat peer
|
|
||||||
notificationManager.setCurrentPrivateChatPeer(peerID)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setCurrentGeohash(geohash: String?) {
|
|
||||||
// Update notification manager with current geohash for notification logic
|
|
||||||
notificationManager.setCurrentGeohash(geohash)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun clearNotificationsForSender(peerID: String) {
|
|
||||||
// Clear notifications when user opens a chat
|
|
||||||
notificationManager.clearNotificationsForSender(peerID)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun clearNotificationsForGeohash(geohash: String) {
|
|
||||||
// Clear notifications when user opens a geohash chat
|
|
||||||
notificationManager.clearNotificationsForGeohash(geohash)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Clear mesh mention notifications when user opens mesh chat
|
|
||||||
*/
|
|
||||||
fun clearMeshMentionNotifications() {
|
|
||||||
notificationManager.clearMeshMentionNotifications()
|
|
||||||
}
|
|
||||||
|
|
||||||
private var reopenSidebarAfterVerification = false
|
|
||||||
|
|
||||||
fun showVerificationSheet(fromSidebar: Boolean = false) {
|
fun showVerificationSheet(fromSidebar: Boolean = false) {
|
||||||
if (fromSidebar) {
|
if (fromSidebar) {
|
||||||
reopenSidebarAfterVerification = true
|
reopenSidebarAfterVerification = true
|
||||||
@@ -867,128 +767,23 @@ class ChatViewModel(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun getPeerFingerprintForDisplay(peerID: String): String? {
|
fun getPeerFingerprintForDisplay(peerID: String): String? {
|
||||||
val fromMap = peerFingerprints.value[peerID]
|
return verificationHandler.getPeerFingerprintForDisplay(peerID)
|
||||||
if (fromMap != null) return fromMap
|
|
||||||
val hexRegex = Regex("^[0-9a-fA-F]+$")
|
|
||||||
return try {
|
|
||||||
when {
|
|
||||||
peerID.length == 64 && peerID.matches(hexRegex) -> {
|
|
||||||
identityManager.getCachedNoiseFingerprint(peerID)?.let { return it }
|
|
||||||
fingerprintFromNoiseHex(peerID)?.also { identityManager.cacheNoiseFingerprint(peerID, it) }
|
|
||||||
}
|
|
||||||
peerID.length == 16 && peerID.matches(hexRegex) -> {
|
|
||||||
val meshFp = meshService.getPeerFingerprint(peerID)
|
|
||||||
if (meshFp != null) return meshFp
|
|
||||||
identityManager.getCachedPeerFingerprint(peerID)?.let { return it }
|
|
||||||
identityManager.getCachedNoiseKey(peerID)?.let { noiseHex ->
|
|
||||||
identityManager.getCachedNoiseFingerprint(noiseHex)?.let { return it }
|
|
||||||
return fingerprintFromNoiseHex(noiseHex)?.also { identityManager.cacheNoiseFingerprint(noiseHex, it) }
|
|
||||||
}
|
|
||||||
val favorite = try {
|
|
||||||
FavoritesPersistenceService.shared.getFavoriteStatus(peerID)
|
|
||||||
} catch (_: Exception) {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
favorite?.peerNoisePublicKey?.let { fingerprintFromNoiseBytes(it) }
|
|
||||||
}
|
|
||||||
peerID.startsWith("nostr_") -> {
|
|
||||||
val pubHex = GeohashAliasRegistry.get(peerID)
|
|
||||||
val noiseKey = pubHex?.let {
|
|
||||||
FavoritesPersistenceService.shared.findNoiseKey(it)
|
|
||||||
}
|
|
||||||
noiseKey?.let {
|
|
||||||
val noiseHex = it.hexEncodedString()
|
|
||||||
identityManager.getCachedNoiseFingerprint(noiseHex) ?: fingerprintFromNoiseBytes(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
peerID.startsWith("nostr:") -> {
|
|
||||||
val prefix = peerID.removePrefix("nostr:").lowercase()
|
|
||||||
val pubHex = GeohashAliasRegistry
|
|
||||||
.snapshot()
|
|
||||||
.values
|
|
||||||
.firstOrNull { it.lowercase().startsWith(prefix) }
|
|
||||||
val noiseKey = pubHex?.let {
|
|
||||||
FavoritesPersistenceService.shared.findNoiseKey(it)
|
|
||||||
}
|
|
||||||
noiseKey?.let {
|
|
||||||
val noiseHex = it.hexEncodedString()
|
|
||||||
identityManager.getCachedNoiseFingerprint(noiseHex) ?: fingerprintFromNoiseBytes(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else -> {
|
|
||||||
val meshFp = meshService.getPeerFingerprint(peerID)
|
|
||||||
if (meshFp != null) return meshFp
|
|
||||||
identityManager.getCachedPeerFingerprint(peerID)?.let { return it }
|
|
||||||
identityManager.getCachedNoiseKey(peerID)?.let { noiseHex ->
|
|
||||||
identityManager.getCachedNoiseFingerprint(noiseHex)?.let { return it }
|
|
||||||
return fingerprintFromNoiseHex(noiseHex)?.also { identityManager.cacheNoiseFingerprint(noiseHex, it) }
|
|
||||||
}
|
|
||||||
val favorite = try {
|
|
||||||
FavoritesPersistenceService.shared.getFavoriteStatus(peerID)
|
|
||||||
} catch (_: Exception) {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
favorite?.peerNoisePublicKey?.let { fingerprintFromNoiseBytes(it) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (_: Exception) {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getMyFingerprint(): String {
|
fun getMyFingerprint(): String {
|
||||||
return meshService.getIdentityFingerprint()
|
return verificationHandler.getMyFingerprint()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun resolvePeerDisplayNameForFingerprint(peerID: String): String {
|
fun resolvePeerDisplayNameForFingerprint(peerID: String): String {
|
||||||
val nicknameMap = peerNicknames.value
|
return verificationHandler.resolvePeerDisplayNameForFingerprint(peerID)
|
||||||
nicknameMap[peerID]?.let { return it }
|
|
||||||
try {
|
|
||||||
meshService.getPeerInfo(peerID)?.nickname?.let { return it }
|
|
||||||
} catch (_: Exception) { }
|
|
||||||
|
|
||||||
val fingerprint = getPeerFingerprintForDisplay(peerID)
|
|
||||||
fingerprint?.let { fp ->
|
|
||||||
identityManager.getCachedFingerprintNickname(fp)?.let { cached ->
|
|
||||||
if (cached.isNotBlank()) return cached
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val hexRegex = Regex("^[0-9a-fA-F]+$")
|
|
||||||
if (peerID.length == 64 && peerID.matches(hexRegex)) {
|
|
||||||
val noiseKeyBytes = try {
|
|
||||||
peerID.chunked(2).map { it.toInt(16).toByte() }.toByteArray()
|
|
||||||
} catch (_: Exception) { null }
|
|
||||||
val favorite = noiseKeyBytes?.let {
|
|
||||||
FavoritesPersistenceService.shared.getFavoriteStatus(it)
|
|
||||||
}
|
|
||||||
favorite?.peerNickname?.takeIf { it.isNotBlank() }?.let { return it }
|
|
||||||
}
|
|
||||||
|
|
||||||
if (peerID.length == 16 && peerID.matches(hexRegex)) {
|
|
||||||
val favorite = try {
|
|
||||||
FavoritesPersistenceService.shared.getFavoriteStatus(peerID)
|
|
||||||
} catch (_: Exception) {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
favorite?.peerNickname?.takeIf { it.isNotBlank() }?.let { return it }
|
|
||||||
}
|
|
||||||
|
|
||||||
return peerID.take(8)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun verifyFingerprintValue(fingerprint: String) {
|
fun verifyFingerprintValue(fingerprint: String) {
|
||||||
if (fingerprint.isBlank()) return
|
verificationHandler.verifyFingerprintValue(fingerprint)
|
||||||
val identityManager = SecureIdentityStateManager(getApplication())
|
|
||||||
identityManager.setVerifiedFingerprint(fingerprint, true)
|
|
||||||
_verifiedFingerprints.value += fingerprint
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun unverifyFingerprintValue(fingerprint: String) {
|
fun unverifyFingerprintValue(fingerprint: String) {
|
||||||
if (fingerprint.isBlank()) return
|
verificationHandler.unverifyFingerprintValue(fingerprint)
|
||||||
val identityManager = SecureIdentityStateManager(getApplication())
|
|
||||||
identityManager.setVerifiedFingerprint(fingerprint, false)
|
|
||||||
_verifiedFingerprints.value -= fingerprint
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Command Autocomplete (delegated)
|
// MARK: - Command Autocomplete (delegated)
|
||||||
@@ -1034,78 +829,11 @@ class ChatViewModel(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun didReceiveVerifyChallenge(peerID: String, payload: ByteArray, timestampMs: Long) {
|
override fun didReceiveVerifyChallenge(peerID: String, payload: ByteArray, timestampMs: Long) {
|
||||||
viewModelScope.launch {
|
verificationHandler.didReceiveVerifyChallenge(peerID, payload)
|
||||||
val parsed = VerificationService.parseVerifyChallenge(payload) ?: return@launch
|
|
||||||
val myNoiseHex = meshService.getStaticNoisePublicKey()?.hexEncodedString()?.lowercase() ?: return@launch
|
|
||||||
if (parsed.first.lowercase() != myNoiseHex) return@launch
|
|
||||||
|
|
||||||
val lastNonce = lastVerifyNonceByPeer[peerID]
|
|
||||||
if (lastNonce != null && lastNonce.contentEquals(parsed.second)) return@launch
|
|
||||||
lastVerifyNonceByPeer[peerID] = parsed.second
|
|
||||||
|
|
||||||
val fp = meshService.getPeerFingerprint(peerID)
|
|
||||||
if (fp != null) {
|
|
||||||
lastInboundVerifyChallengeAt[fp] = System.currentTimeMillis()
|
|
||||||
if (_verifiedFingerprints.value.contains(fp)) {
|
|
||||||
val lastToast = lastMutualToastAt[fp] ?: 0L
|
|
||||||
if (System.currentTimeMillis() - lastToast > 60_000L) {
|
|
||||||
lastMutualToastAt[fp] = System.currentTimeMillis()
|
|
||||||
val name = resolvePeerDisplayName(peerID)
|
|
||||||
val body = "You and $name verified each other"
|
|
||||||
addVerificationSystemMessage(peerID, "mutual verification with $name")
|
|
||||||
sendVerificationNotification("Mutual verification", body, peerID)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
meshService.sendVerifyResponse(peerID, parsed.first, parsed.second)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun didReceiveVerifyResponse(peerID: String, payload: ByteArray, timestampMs: Long) {
|
override fun didReceiveVerifyResponse(peerID: String, payload: ByteArray, timestampMs: Long) {
|
||||||
viewModelScope.launch {
|
verificationHandler.didReceiveVerifyResponse(peerID, payload)
|
||||||
val resp = VerificationService.parseVerifyResponse(payload) ?: return@launch
|
|
||||||
val pending = pendingQRVerifications[peerID] ?: return@launch
|
|
||||||
if (!resp.noiseKeyHex.equals(pending.noiseKeyHex, ignoreCase = true)) return@launch
|
|
||||||
if (!resp.nonceA.contentEquals(pending.nonceA)) return@launch
|
|
||||||
|
|
||||||
val ok = VerificationService.verifyResponseSignature(
|
|
||||||
noiseKeyHex = resp.noiseKeyHex,
|
|
||||||
nonceA = resp.nonceA,
|
|
||||||
signature = resp.signature,
|
|
||||||
signerPublicKeyHex = pending.signKeyHex
|
|
||||||
)
|
|
||||||
if (!ok) return@launch
|
|
||||||
|
|
||||||
pendingQRVerifications.remove(peerID)
|
|
||||||
val fp = meshService.getPeerFingerprint(peerID) ?: return@launch
|
|
||||||
identityManager.setVerifiedFingerprint(fp, true)
|
|
||||||
_verifiedFingerprints.value = _verifiedFingerprints.value + fp
|
|
||||||
val name = resolvePeerDisplayName(peerID)
|
|
||||||
identityManager.cacheFingerprintNickname(fp, name)
|
|
||||||
val noiseKeyHex = try {
|
|
||||||
meshService.getPeerInfo(peerID)?.noisePublicKey?.hexEncodedString()
|
|
||||||
} catch (_: Exception) {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
if (noiseKeyHex != null) {
|
|
||||||
identityManager.cachePeerNoiseKey(peerID, noiseKeyHex)
|
|
||||||
identityManager.cacheNoiseFingerprint(noiseKeyHex, fp)
|
|
||||||
}
|
|
||||||
addVerificationSystemMessage(peerID, "verified $name")
|
|
||||||
sendVerificationNotification("Verified", "You verified $name", peerID)
|
|
||||||
|
|
||||||
val lastChallenge = lastInboundVerifyChallengeAt[fp] ?: 0L
|
|
||||||
if (System.currentTimeMillis() - lastChallenge < 600_000L) {
|
|
||||||
val lastToast = lastMutualToastAt[fp] ?: 0L
|
|
||||||
if (System.currentTimeMillis() - lastToast > 60_000L) {
|
|
||||||
lastMutualToastAt[fp] = System.currentTimeMillis()
|
|
||||||
val body = "You and $name verified each other"
|
|
||||||
addVerificationSystemMessage(peerID, "mutual verification with $name")
|
|
||||||
sendVerificationNotification("Mutual verification", body, peerID)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun decryptChannelMessage(encryptedContent: ByteArray, channel: String): String? {
|
override fun decryptChannelMessage(encryptedContent: ByteArray, channel: String): String? {
|
||||||
@@ -1316,16 +1044,6 @@ class ChatViewModel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun fingerprintFromNoiseHex(noiseHex: String): String? {
|
|
||||||
val bytes = noiseHex.dataFromHexString() ?: return null
|
|
||||||
return fingerprintFromNoiseBytes(bytes)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun fingerprintFromNoiseBytes(bytes: ByteArray): String {
|
|
||||||
val hash = MessageDigest.getInstance("SHA-256").digest(bytes)
|
|
||||||
return hash.hexEncodedString()
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - iOS-Compatible Color System
|
// MARK: - iOS-Compatible Color System
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1344,36 +1062,6 @@ class ChatViewModel(
|
|||||||
return geohashViewModel.colorForNostrPubkey(pubkeyHex, isDark)
|
return geohashViewModel.colorForNostrPubkey(pubkeyHex, isDark)
|
||||||
}
|
}
|
||||||
|
|
||||||
private data class PendingVerification(
|
}
|
||||||
val noiseKeyHex: String,
|
|
||||||
val signKeyHex: String,
|
|
||||||
val nonceA: ByteArray,
|
|
||||||
val startedAtMs: Long,
|
|
||||||
val sent: Boolean
|
|
||||||
) {
|
|
||||||
override fun equals(other: Any?): Boolean {
|
|
||||||
if (this === other) return true
|
|
||||||
if (javaClass != other?.javaClass) return false
|
|
||||||
|
|
||||||
other as PendingVerification
|
|
||||||
|
|
||||||
if (startedAtMs != other.startedAtMs) return false
|
|
||||||
if (sent != other.sent) return false
|
|
||||||
if (noiseKeyHex != other.noiseKeyHex) return false
|
|
||||||
if (signKeyHex != other.signKeyHex) return false
|
|
||||||
if (!nonceA.contentEquals(other.nonceA)) return false
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun hashCode(): Int {
|
|
||||||
var result = startedAtMs.hashCode()
|
|
||||||
result = 31 * result + sent.hashCode()
|
|
||||||
result = 31 * result + noiseKeyHex.hashCode()
|
|
||||||
result = 31 * result + signKeyHex.hashCode()
|
|
||||||
result = 31 * result + nonceA.contentHashCode()
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,365 @@
|
|||||||
|
package com.bitchat.android.ui
|
||||||
|
|
||||||
|
import com.bitchat.android.favorites.FavoritesPersistenceService
|
||||||
|
import com.bitchat.android.identity.SecureIdentityStateManager
|
||||||
|
import com.bitchat.android.mesh.BluetoothMeshService
|
||||||
|
import com.bitchat.android.model.BitchatMessage
|
||||||
|
import com.bitchat.android.noise.NoiseSession
|
||||||
|
import com.bitchat.android.nostr.GeohashAliasRegistry
|
||||||
|
import com.bitchat.android.services.VerificationService
|
||||||
|
import com.bitchat.android.util.dataFromHexString
|
||||||
|
import com.bitchat.android.util.hexEncodedString
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import java.security.MessageDigest
|
||||||
|
import java.util.Date
|
||||||
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles QR verification logic and state, extracted from ChatViewModel.
|
||||||
|
*/
|
||||||
|
class VerificationHandler(
|
||||||
|
private val scope: CoroutineScope,
|
||||||
|
private val meshService: BluetoothMeshService,
|
||||||
|
private val identityManager: SecureIdentityStateManager,
|
||||||
|
private val state: ChatState,
|
||||||
|
private val notificationManager: NotificationManager,
|
||||||
|
private val messageManager: MessageManager
|
||||||
|
) {
|
||||||
|
|
||||||
|
private val _verifiedFingerprints = MutableStateFlow<Set<String>>(emptySet())
|
||||||
|
val verifiedFingerprints: StateFlow<Set<String>> = _verifiedFingerprints.asStateFlow()
|
||||||
|
|
||||||
|
private val pendingQRVerifications = ConcurrentHashMap<String, PendingVerification>()
|
||||||
|
private val lastVerifyNonceByPeer = ConcurrentHashMap<String, ByteArray>()
|
||||||
|
private val lastInboundVerifyChallengeAt = ConcurrentHashMap<String, Long>()
|
||||||
|
private val lastMutualToastAt = ConcurrentHashMap<String, Long>()
|
||||||
|
|
||||||
|
fun loadVerifiedFingerprints() {
|
||||||
|
_verifiedFingerprints.value = identityManager.getVerifiedFingerprints()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isPeerVerified(peerID: String): Boolean {
|
||||||
|
if (peerID.startsWith("nostr_") || peerID.startsWith("nostr:")) return false
|
||||||
|
val fingerprint = getPeerFingerprintForDisplay(peerID)
|
||||||
|
return fingerprint != null && _verifiedFingerprints.value.contains(fingerprint)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isNoisePublicKeyVerified(noisePublicKey: ByteArray): Boolean {
|
||||||
|
val fingerprint = fingerprintFromNoiseBytes(noisePublicKey)
|
||||||
|
return _verifiedFingerprints.value.contains(fingerprint)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun unverifyFingerprint(peerID: String) {
|
||||||
|
val fingerprint = meshService.getPeerFingerprint(peerID) ?: return
|
||||||
|
identityManager.setVerifiedFingerprint(fingerprint, false)
|
||||||
|
val current = _verifiedFingerprints.value.toMutableSet()
|
||||||
|
current.remove(fingerprint)
|
||||||
|
_verifiedFingerprints.value = current
|
||||||
|
}
|
||||||
|
|
||||||
|
fun beginQRVerification(qr: VerificationService.VerificationQR): Boolean {
|
||||||
|
val targetNoise = qr.noiseKeyHex.lowercase()
|
||||||
|
val peerID = state.getConnectedPeersValue().firstOrNull { pid ->
|
||||||
|
val noiseKeyHex = meshService.getPeerInfo(pid)?.noisePublicKey?.hexEncodedString()?.lowercase()
|
||||||
|
noiseKeyHex == targetNoise
|
||||||
|
} ?: return false
|
||||||
|
|
||||||
|
if (pendingQRVerifications.containsKey(peerID)) return true
|
||||||
|
val nonce = ByteArray(16)
|
||||||
|
java.security.SecureRandom().nextBytes(nonce)
|
||||||
|
val pending = PendingVerification(qr.noiseKeyHex, qr.signKeyHex, nonce, System.currentTimeMillis(), false)
|
||||||
|
pendingQRVerifications[peerID] = pending
|
||||||
|
|
||||||
|
if (meshService.getSessionState(peerID) is NoiseSession.NoiseSessionState.Established) {
|
||||||
|
meshService.sendVerifyChallenge(peerID, qr.noiseKeyHex, nonce)
|
||||||
|
pendingQRVerifications[peerID] = pending.copy(sent = true)
|
||||||
|
} else {
|
||||||
|
meshService.initiateNoiseHandshake(peerID)
|
||||||
|
}
|
||||||
|
fingerprintFromNoiseHex(qr.noiseKeyHex)?.let { fp ->
|
||||||
|
identityManager.cacheFingerprintNickname(fp, qr.nickname)
|
||||||
|
identityManager.cacheNoiseFingerprint(qr.noiseKeyHex, fp)
|
||||||
|
identityManager.cachePeerNoiseKey(peerID, qr.noiseKeyHex)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
fun sendPendingVerificationIfNeeded(peerID: String) {
|
||||||
|
val pending = pendingQRVerifications[peerID] ?: return
|
||||||
|
if (pending.sent) return
|
||||||
|
meshService.sendVerifyChallenge(peerID, pending.noiseKeyHex, pending.nonceA)
|
||||||
|
pendingQRVerifications[peerID] = pending.copy(sent = true)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun didReceiveVerifyChallenge(peerID: String, payload: ByteArray) {
|
||||||
|
scope.launch {
|
||||||
|
val parsed = VerificationService.parseVerifyChallenge(payload) ?: return@launch
|
||||||
|
val myNoiseHex = meshService.getStaticNoisePublicKey()?.hexEncodedString()?.lowercase() ?: return@launch
|
||||||
|
if (parsed.first.lowercase() != myNoiseHex) return@launch
|
||||||
|
|
||||||
|
val lastNonce = lastVerifyNonceByPeer[peerID]
|
||||||
|
if (lastNonce != null && lastNonce.contentEquals(parsed.second)) return@launch
|
||||||
|
lastVerifyNonceByPeer[peerID] = parsed.second
|
||||||
|
|
||||||
|
val fp = meshService.getPeerFingerprint(peerID)
|
||||||
|
if (fp != null) {
|
||||||
|
lastInboundVerifyChallengeAt[fp] = System.currentTimeMillis()
|
||||||
|
if (_verifiedFingerprints.value.contains(fp)) {
|
||||||
|
val lastToast = lastMutualToastAt[fp] ?: 0L
|
||||||
|
if (System.currentTimeMillis() - lastToast > 60_000L) {
|
||||||
|
lastMutualToastAt[fp] = System.currentTimeMillis()
|
||||||
|
val name = resolvePeerDisplayName(peerID)
|
||||||
|
val body = "You and $name verified each other"
|
||||||
|
addVerificationSystemMessage(peerID, "mutual verification with $name")
|
||||||
|
sendVerificationNotification("Mutual verification", body, peerID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
meshService.sendVerifyResponse(peerID, parsed.first, parsed.second)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun didReceiveVerifyResponse(peerID: String, payload: ByteArray) {
|
||||||
|
scope.launch {
|
||||||
|
val resp = VerificationService.parseVerifyResponse(payload) ?: return@launch
|
||||||
|
val pending = pendingQRVerifications[peerID] ?: return@launch
|
||||||
|
if (!resp.noiseKeyHex.equals(pending.noiseKeyHex, ignoreCase = true)) return@launch
|
||||||
|
if (!resp.nonceA.contentEquals(pending.nonceA)) return@launch
|
||||||
|
|
||||||
|
val ok = VerificationService.verifyResponseSignature(
|
||||||
|
noiseKeyHex = resp.noiseKeyHex,
|
||||||
|
nonceA = resp.nonceA,
|
||||||
|
signature = resp.signature,
|
||||||
|
signerPublicKeyHex = pending.signKeyHex
|
||||||
|
)
|
||||||
|
if (!ok) return@launch
|
||||||
|
|
||||||
|
pendingQRVerifications.remove(peerID)
|
||||||
|
val fp = meshService.getPeerFingerprint(peerID) ?: return@launch
|
||||||
|
identityManager.setVerifiedFingerprint(fp, true)
|
||||||
|
val current = _verifiedFingerprints.value.toMutableSet()
|
||||||
|
current.add(fp)
|
||||||
|
_verifiedFingerprints.value = current
|
||||||
|
|
||||||
|
val name = resolvePeerDisplayName(peerID)
|
||||||
|
identityManager.cacheFingerprintNickname(fp, name)
|
||||||
|
val noiseKeyHex = try {
|
||||||
|
meshService.getPeerInfo(peerID)?.noisePublicKey?.hexEncodedString()
|
||||||
|
} catch (_: Exception) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
if (noiseKeyHex != null) {
|
||||||
|
identityManager.cachePeerNoiseKey(peerID, noiseKeyHex)
|
||||||
|
identityManager.cacheNoiseFingerprint(noiseKeyHex, fp)
|
||||||
|
}
|
||||||
|
addVerificationSystemMessage(peerID, "verified $name")
|
||||||
|
sendVerificationNotification("Verified", "You verified $name", peerID)
|
||||||
|
|
||||||
|
val lastChallenge = lastInboundVerifyChallengeAt[fp] ?: 0L
|
||||||
|
if (System.currentTimeMillis() - lastChallenge < 600_000L) {
|
||||||
|
val lastToast = lastMutualToastAt[fp] ?: 0L
|
||||||
|
if (System.currentTimeMillis() - lastToast > 60_000L) {
|
||||||
|
lastMutualToastAt[fp] = System.currentTimeMillis()
|
||||||
|
val body = "You and $name verified each other"
|
||||||
|
addVerificationSystemMessage(peerID, "mutual verification with $name")
|
||||||
|
sendVerificationNotification("Mutual verification", body, peerID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getPeerFingerprintForDisplay(peerID: String): String? {
|
||||||
|
val fromMap = state.getPeerFingerprintsValue()[peerID]
|
||||||
|
if (fromMap != null) return fromMap
|
||||||
|
val hexRegex = Regex("^[0-9a-fA-F]+$")
|
||||||
|
return try {
|
||||||
|
when {
|
||||||
|
peerID.length == 64 && peerID.matches(hexRegex) -> {
|
||||||
|
identityManager.getCachedNoiseFingerprint(peerID)?.let { return it }
|
||||||
|
fingerprintFromNoiseHex(peerID)?.also { identityManager.cacheNoiseFingerprint(peerID, it) }
|
||||||
|
}
|
||||||
|
peerID.length == 16 && peerID.matches(hexRegex) -> {
|
||||||
|
val meshFp = meshService.getPeerFingerprint(peerID)
|
||||||
|
if (meshFp != null) return meshFp
|
||||||
|
identityManager.getCachedPeerFingerprint(peerID)?.let { return it }
|
||||||
|
identityManager.getCachedNoiseKey(peerID)?.let { noiseHex ->
|
||||||
|
identityManager.getCachedNoiseFingerprint(noiseHex)?.let { return it }
|
||||||
|
return fingerprintFromNoiseHex(noiseHex)?.also { identityManager.cacheNoiseFingerprint(noiseHex, it) }
|
||||||
|
}
|
||||||
|
val favorite = try {
|
||||||
|
FavoritesPersistenceService.shared.getFavoriteStatus(peerID)
|
||||||
|
} catch (_: Exception) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
favorite?.peerNoisePublicKey?.let { fingerprintFromNoiseBytes(it) }
|
||||||
|
}
|
||||||
|
peerID.startsWith("nostr_") -> {
|
||||||
|
val pubHex = GeohashAliasRegistry.get(peerID)
|
||||||
|
val noiseKey = pubHex?.let {
|
||||||
|
FavoritesPersistenceService.shared.findNoiseKey(it)
|
||||||
|
}
|
||||||
|
noiseKey?.let {
|
||||||
|
val noiseHex = it.hexEncodedString()
|
||||||
|
identityManager.getCachedNoiseFingerprint(noiseHex) ?: fingerprintFromNoiseBytes(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
peerID.startsWith("nostr:") -> {
|
||||||
|
val prefix = peerID.removePrefix("nostr:").lowercase()
|
||||||
|
val pubHex = GeohashAliasRegistry
|
||||||
|
.snapshot()
|
||||||
|
.values
|
||||||
|
.firstOrNull { it.lowercase().startsWith(prefix) }
|
||||||
|
val noiseKey = pubHex?.let {
|
||||||
|
FavoritesPersistenceService.shared.findNoiseKey(it)
|
||||||
|
}
|
||||||
|
noiseKey?.let {
|
||||||
|
val noiseHex = it.hexEncodedString()
|
||||||
|
identityManager.getCachedNoiseFingerprint(noiseHex) ?: fingerprintFromNoiseBytes(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
val meshFp = meshService.getPeerFingerprint(peerID)
|
||||||
|
if (meshFp != null) return meshFp
|
||||||
|
identityManager.getCachedPeerFingerprint(peerID)?.let { return it }
|
||||||
|
identityManager.getCachedNoiseKey(peerID)?.let { noiseHex ->
|
||||||
|
identityManager.getCachedNoiseFingerprint(noiseHex)?.let { return it }
|
||||||
|
return fingerprintFromNoiseHex(noiseHex)?.also { identityManager.cacheNoiseFingerprint(noiseHex, it) }
|
||||||
|
}
|
||||||
|
val favorite = try {
|
||||||
|
FavoritesPersistenceService.shared.getFavoriteStatus(peerID)
|
||||||
|
} catch (_: Exception) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
favorite?.peerNoisePublicKey?.let { fingerprintFromNoiseBytes(it) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (_: Exception) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun resolvePeerDisplayNameForFingerprint(peerID: String): String {
|
||||||
|
val nicknameMap = state.peerNicknames.value
|
||||||
|
nicknameMap[peerID]?.let { return it }
|
||||||
|
try {
|
||||||
|
meshService.getPeerInfo(peerID)?.nickname?.let { return it }
|
||||||
|
} catch (_: Exception) { }
|
||||||
|
|
||||||
|
val fingerprint = getPeerFingerprintForDisplay(peerID)
|
||||||
|
fingerprint?.let { fp ->
|
||||||
|
identityManager.getCachedFingerprintNickname(fp)?.let { cached ->
|
||||||
|
if (cached.isNotBlank()) return cached
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val hexRegex = Regex("^[0-9a-fA-F]+$")
|
||||||
|
if (peerID.length == 64 && peerID.matches(hexRegex)) {
|
||||||
|
val noiseKeyBytes = try {
|
||||||
|
peerID.chunked(2).map { it.toInt(16).toByte() }.toByteArray()
|
||||||
|
} catch (_: Exception) { null }
|
||||||
|
val favorite = noiseKeyBytes?.let {
|
||||||
|
FavoritesPersistenceService.shared.getFavoriteStatus(it)
|
||||||
|
}
|
||||||
|
favorite?.peerNickname?.takeIf { it.isNotBlank() }?.let { return it }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (peerID.length == 16 && peerID.matches(hexRegex)) {
|
||||||
|
val favorite = try {
|
||||||
|
FavoritesPersistenceService.shared.getFavoriteStatus(peerID)
|
||||||
|
} catch (_: Exception) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
favorite?.peerNickname?.takeIf { it.isNotBlank() }?.let { return it }
|
||||||
|
}
|
||||||
|
|
||||||
|
return peerID.take(8)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getMyFingerprint(): String {
|
||||||
|
return meshService.getIdentityFingerprint()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun verifyFingerprintValue(fingerprint: String) {
|
||||||
|
if (fingerprint.isBlank()) return
|
||||||
|
identityManager.setVerifiedFingerprint(fingerprint, true)
|
||||||
|
val current = _verifiedFingerprints.value.toMutableSet()
|
||||||
|
current.add(fingerprint)
|
||||||
|
_verifiedFingerprints.value = current
|
||||||
|
}
|
||||||
|
|
||||||
|
fun unverifyFingerprintValue(fingerprint: String) {
|
||||||
|
if (fingerprint.isBlank()) return
|
||||||
|
identityManager.setVerifiedFingerprint(fingerprint, false)
|
||||||
|
val current = _verifiedFingerprints.value.toMutableSet()
|
||||||
|
current.remove(fingerprint)
|
||||||
|
_verifiedFingerprints.value = current
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun addVerificationSystemMessage(peerID: String, text: String) {
|
||||||
|
val msg = BitchatMessage(
|
||||||
|
sender = "system",
|
||||||
|
content = text,
|
||||||
|
timestamp = Date(),
|
||||||
|
isRelay = false,
|
||||||
|
isPrivate = true,
|
||||||
|
senderPeerID = peerID
|
||||||
|
)
|
||||||
|
messageManager.addPrivateMessageNoUnread(peerID, msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun resolvePeerDisplayName(peerID: String): String {
|
||||||
|
val nick = try { meshService.getPeerInfo(peerID)?.nickname } catch (_: Exception) { null }
|
||||||
|
return nick ?: peerID.take(8)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun sendVerificationNotification(title: String, body: String, peerID: String) {
|
||||||
|
notificationManager.showVerificationNotification(title, body, peerID)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun fingerprintFromNoiseHex(noiseHex: String): String? {
|
||||||
|
val bytes = noiseHex.dataFromHexString() ?: return null
|
||||||
|
return fingerprintFromNoiseBytes(bytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun fingerprintFromNoiseBytes(bytes: ByteArray): String {
|
||||||
|
val hash = MessageDigest.getInstance("SHA-256").digest(bytes)
|
||||||
|
return hash.hexEncodedString()
|
||||||
|
}
|
||||||
|
|
||||||
|
private data class PendingVerification(
|
||||||
|
val noiseKeyHex: String,
|
||||||
|
val signKeyHex: String,
|
||||||
|
val nonceA: ByteArray,
|
||||||
|
val startedAtMs: Long,
|
||||||
|
val sent: Boolean
|
||||||
|
) {
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (this === other) return true
|
||||||
|
if (javaClass != other?.javaClass) return false
|
||||||
|
|
||||||
|
other as PendingVerification
|
||||||
|
|
||||||
|
if (startedAtMs != other.startedAtMs) return false
|
||||||
|
if (sent != other.sent) return false
|
||||||
|
if (noiseKeyHex != other.noiseKeyHex) return false
|
||||||
|
if (signKeyHex != other.signKeyHex) return false
|
||||||
|
if (!nonceA.contentEquals(other.nonceA)) return false
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
var result = startedAtMs.hashCode()
|
||||||
|
result = 31 * result + sent.hashCode()
|
||||||
|
result = 31 * result + noiseKeyHex.hashCode()
|
||||||
|
result = 31 * result + signKeyHex.hashCode()
|
||||||
|
result = 31 * result + nonceA.contentHashCode()
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user