Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 10 additions & 4 deletions app/src/main/java/com/bitchat/android/ui/ChatHeader.kt
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,9 @@ fun ChatHeaderContent(
onBackClick: () -> Unit,
onSidebarClick: () -> Unit,
onTripleClick: () -> Unit,
onShowAppInfo: () -> Unit
onShowAppInfo: () -> Unit,
showSecurityVerification: Boolean,
onSecurityVerificationChange: (Boolean) -> Unit
) {
val colorScheme = MaterialTheme.colorScheme

Expand All @@ -228,7 +230,8 @@ fun ChatHeaderContent(
isFavorite = isFavorite,
sessionState = sessionState,
onBackClick = onBackClick,
onToggleFavorite = { viewModel.toggleFavorite(selectedPrivatePeer) }
onToggleFavorite = { viewModel.toggleFavorite(selectedPrivatePeer) },
onShowSecurityVerification = { onSecurityVerificationChange(true) }
)
}
currentChannel != null -> {
Expand Down Expand Up @@ -261,7 +264,8 @@ private fun PrivateChatHeader(
isFavorite: Boolean,
sessionState: String?,
onBackClick: () -> Unit,
onToggleFavorite: () -> Unit
onToggleFavorite: () -> Unit,
onShowSecurityVerification: () -> Unit
) {
val colorScheme = MaterialTheme.colorScheme
val peerNickname = peerNicknames[peerID] ?: peerID
Expand Down Expand Up @@ -300,7 +304,9 @@ private fun PrivateChatHeader(
// Title - perfectly centered regardless of other elements
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.align(Alignment.Center)
modifier = Modifier
.align(Alignment.Center)
.clickable { onShowSecurityVerification() }
) {

Text(
Expand Down
43 changes: 38 additions & 5 deletions app/src/main/java/com/bitchat/android/ui/ChatScreen.kt
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ fun ChatScreen(viewModel: ChatViewModel) {
var showPasswordPrompt by remember { mutableStateOf(false) }
var showPasswordDialog by remember { mutableStateOf(false) }
var passwordInput by remember { mutableStateOf("") }
var showSecurityVerification by remember { mutableStateOf(false) }

// Show password dialog when needed
LaunchedEffect(showPasswordPrompt) {
Expand Down Expand Up @@ -147,7 +148,9 @@ fun ChatScreen(viewModel: ChatViewModel) {
colorScheme = colorScheme,
onSidebarToggle = { viewModel.showSidebar() },
onShowAppInfo = { viewModel.showAppInfo() },
onPanicClear = { viewModel.panicClearAllData() }
onPanicClear = { viewModel.panicClearAllData() },
showSecurityVerification = showSecurityVerification,
onSecurityVerificationChange = { showSecurityVerification = it }
)

// Sidebar overlay
Expand Down Expand Up @@ -191,7 +194,12 @@ fun ChatScreen(viewModel: ChatViewModel) {
passwordInput = ""
},
showAppInfo = showAppInfo,
onAppInfoDismiss = { viewModel.hideAppInfo() }
onAppInfoDismiss = { viewModel.hideAppInfo() },
showSecurityVerification = showSecurityVerification,
selectedPrivatePeer = selectedPrivatePeer,
viewModel = viewModel,
onSecurityVerificationDismiss = { showSecurityVerification = false },
onSecurityVerificationVerify = { showSecurityVerification = false }
)
}

Expand Down Expand Up @@ -251,7 +259,9 @@ private fun ChatFloatingHeader(
colorScheme: ColorScheme,
onSidebarToggle: () -> Unit,
onShowAppInfo: () -> Unit,
onPanicClear: () -> Unit
onPanicClear: () -> Unit,
showSecurityVerification: Boolean,
onSecurityVerificationChange: (Boolean) -> Unit
) {
Surface(
modifier = Modifier
Expand All @@ -277,7 +287,9 @@ private fun ChatFloatingHeader(
},
onSidebarClick = onSidebarToggle,
onTripleClick = onPanicClear,
onShowAppInfo = onShowAppInfo
onShowAppInfo = onShowAppInfo,
showSecurityVerification = showSecurityVerification,
onSecurityVerificationChange = onSecurityVerificationChange
)
},
colors = TopAppBarDefaults.topAppBarColors(
Expand Down Expand Up @@ -305,8 +317,16 @@ private fun ChatDialogs(
onPasswordConfirm: () -> Unit,
onPasswordDismiss: () -> Unit,
showAppInfo: Boolean,
onAppInfoDismiss: () -> Unit
onAppInfoDismiss: () -> Unit,
showSecurityVerification: Boolean,
selectedPrivatePeer: String?,
viewModel: ChatViewModel,
onSecurityVerificationDismiss: () -> Unit,
onSecurityVerificationVerify: () -> Unit
) {
// Observe verified fingerprints to ensure dialog updates when verification state changes
val verifiedFingerprints by viewModel.verifiedFingerprints.observeAsState()

// Password dialog
PasswordPromptDialog(
show = showPasswordDialog,
Expand All @@ -322,4 +342,17 @@ private fun ChatDialogs(
show = showAppInfo,
onDismiss = onAppInfoDismiss
)

// Security verification dialog
if (showSecurityVerification && selectedPrivatePeer != null) {
SecurityVerificationDialog(
peerID = selectedPrivatePeer,
peerNicknames = viewModel.meshService.getPeerNicknames(),
peerFingerprints = viewModel.peerFingerprints.value ?: emptyMap(),
verifiedFingerprints = verifiedFingerprints ?: emptySet(),
viewModel = viewModel,
onDismiss = onSecurityVerificationDismiss,
onVerify = onSecurityVerificationVerify
)
}
}
9 changes: 9 additions & 0 deletions app/src/main/java/com/bitchat/android/ui/ChatState.kt
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,10 @@ class ChatState {
private val _peerFingerprints = MutableLiveData<Map<String, String>>(emptyMap())
val peerFingerprints: LiveData<Map<String, String>> = _peerFingerprints

// Verified fingerprints - tracks which peer fingerprints have been verified by the user
private val _verifiedFingerprints = MutableLiveData<Set<String>>(emptySet())
val verifiedFingerprints: LiveData<Set<String>> = _verifiedFingerprints

// peerIDToPublicKeyFingerprint REMOVED - fingerprints now handled centrally in PeerManager

// Navigation state
Expand Down Expand Up @@ -132,6 +136,7 @@ class ChatState {
fun getFavoritePeersValue() = _favoritePeers.value ?: emptySet()
fun getPeerSessionStatesValue() = _peerSessionStates.value ?: emptyMap()
fun getPeerFingerprintsValue() = _peerFingerprints.value ?: emptyMap()
fun getVerifiedFingerprintsValue() = _verifiedFingerprints.value ?: emptySet()
fun getShowAppInfoValue() = _showAppInfo.value ?: false

// Setters for state updates
Expand Down Expand Up @@ -225,6 +230,10 @@ class ChatState {
_peerFingerprints.value = fingerprints
}

fun setVerifiedFingerprints(fingerprints: Set<String>) {
_verifiedFingerprints.value = fingerprints
}

fun setShowAppInfo(show: Boolean) {
_showAppInfo.value = show
}
Expand Down
85 changes: 85 additions & 0 deletions app/src/main/java/com/bitchat/android/ui/ChatViewModel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import com.bitchat.android.mesh.BluetoothMeshService
import com.bitchat.android.model.BitchatMessage
import com.bitchat.android.model.DeliveryAck
import com.bitchat.android.model.ReadReceipt
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import kotlinx.coroutines.launch
import kotlinx.coroutines.delay
import java.util.*
Expand Down Expand Up @@ -86,6 +88,7 @@ class ChatViewModel(
val favoritePeers: LiveData<Set<String>> = state.favoritePeers
val peerSessionStates: LiveData<Map<String, String>> = state.peerSessionStates
val peerFingerprints: LiveData<Map<String, String>> = state.peerFingerprints
val verifiedFingerprints: LiveData<Set<String>> = state.verifiedFingerprints
val showAppInfo: LiveData<Boolean> = state.showAppInfo

init {
Expand All @@ -98,6 +101,10 @@ class ChatViewModel(
val nickname = dataManager.loadNickname()
state.setNickname(nickname)

// Load verified fingerprints from persistent storage
val verifiedFingerprints = loadVerifiedFingerprintsFromStorage()
state.setVerifiedFingerprints(verifiedFingerprints)

// Load data
val (joinedChannels, protectedChannels) = channelManager.loadChannelData()
state.setJoinedChannels(joinedChannels)
Expand Down Expand Up @@ -412,6 +419,9 @@ class ChatViewModel(
// Clear all cryptographic data
clearAllCryptographicData()

// Clear verified fingerprints
clearVerifiedFingerprints()

// Clear all notifications
notificationManager.clearAllNotifications()

Expand All @@ -426,6 +436,20 @@ class ChatViewModel(
// This method now only clears data, not mesh service lifecycle
}

/**
* Clear verified fingerprints (used for panic clear)
*/
private fun clearVerifiedFingerprints() {
try {
val prefs = getApplication<Application>().getSharedPreferences("bitchat_security", Context.MODE_PRIVATE)
prefs.edit().remove("verified_fingerprints").apply()
state.setVerifiedFingerprints(emptySet())
Log.d(TAG, "✅ Cleared verified fingerprints")
} catch (e: Exception) {
Log.e(TAG, "❌ Error clearing verified fingerprints: ${e.message}")
}
}

/**
* Clear all mesh service related data
*/
Expand Down Expand Up @@ -481,6 +505,67 @@ class ChatViewModel(
state.setShowSidebar(false)
}

// MARK: - Security Verification

fun getMyFingerprint(): String {
return meshService.getIdentityFingerprint()
}

fun verifyFingerprint(peerID: String) {
// Get the peer's fingerprint from PrivateChatManager
val fingerprint = privateChatManager.getPeerFingerprint(peerID)
if (fingerprint != null) {
// Get current verified fingerprints
val currentVerified = state.getVerifiedFingerprintsValue().toMutableSet()
// Add this fingerprint to verified set
currentVerified.add(fingerprint)
// Update the state
state.setVerifiedFingerprints(currentVerified)
// Save to persistent storage
saveVerifiedFingerprintsToStorage(currentVerified)
Log.d("ChatViewModel", "User verified fingerprint for peer: $peerID, fingerprint: $fingerprint")
} else {
Log.w("ChatViewModel", "Could not verify fingerprint for peer: $peerID - no fingerprint found")
}
}

fun isFingerprintVerified(peerID: String): Boolean {
val fingerprint = privateChatManager.getPeerFingerprint(peerID)
return fingerprint?.let { state.getVerifiedFingerprintsValue().contains(it) } ?: false
}

private fun saveVerifiedFingerprintsToStorage(verifiedFingerprints: Set<String>) {
try {
val prefs = getApplication<Application>().getSharedPreferences("bitchat_security", Context.MODE_PRIVATE)
val editor = prefs.edit()
// Convert set to JSON string
val json = Gson().toJson(verifiedFingerprints)
editor.putString("verified_fingerprints", json)
editor.apply()
Log.d("ChatViewModel", "Saved ${verifiedFingerprints.size} verified fingerprints to storage")
} catch (e: Exception) {
Log.e("ChatViewModel", "Failed to save verified fingerprints: ${e.message}")
}
}

private fun loadVerifiedFingerprintsFromStorage(): Set<String> {
return try {
val prefs = getApplication<Application>().getSharedPreferences("bitchat_security", Context.MODE_PRIVATE)
val json = prefs.getString("verified_fingerprints", null)
if (json != null) {
val type = object : TypeToken<Set<String>>() {}.type
val fingerprints = Gson().fromJson<Set<String>>(json, type)
Log.d("ChatViewModel", "Loaded ${fingerprints?.size ?: 0} verified fingerprints from storage")
fingerprints ?: emptySet()
} else {
emptySet()
}
} catch (e: Exception) {
Log.e("ChatViewModel", "Failed to load verified fingerprints: ${e.message}")
emptySet()
}
}

/**
* Handle Android back navigation
* Returns true if the back press was handled, false if it should be passed to the system
Expand Down
Loading
Loading