feat: prompt user to regenerate compromised keys (#2131)

This commit is contained in:
James Rich 2025-06-16 17:06:23 +00:00 committed by GitHub
parent 86905942de
commit 3ef504c567
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 109 additions and 18 deletions

View file

@ -206,13 +206,19 @@ class UIViewModel @Inject constructor(
private val _lastTraceRouteTime = MutableStateFlow<Long?>(null)
val lastTraceRouteTime: StateFlow<Long?> = _lastTraceRouteTime.asStateFlow()
val clientNotification: StateFlow<MeshProtos.ClientNotification?> = radioConfigRepository.clientNotification
fun clearClientNotification(notification: MeshProtos.ClientNotification) {
radioConfigRepository.clearClientNotification()
meshServiceNotifications.clearClientNotification(notification)
}
data class AlertData(
val title: String,
val message: String? = null,
val html: String? = null,
val onConfirm: (() -> Unit)? = null,
val onDismiss: (() -> Unit)? = null,
val choices: Map<String, () -> Unit> = emptyMap()
val choices: Map<String, () -> Unit> = emptyMap(),
)
private val _currentAlert: MutableStateFlow<AlertData?> = MutableStateFlow(null)
@ -224,7 +230,7 @@ class UIViewModel @Inject constructor(
html: String? = null,
onConfirm: (() -> Unit)? = {},
dismissable: Boolean = true,
choices: Map<String, () -> Unit> = emptyMap()
choices: Map<String, () -> Unit> = emptyMap(),
) {
_currentAlert.value =
AlertData(
@ -238,7 +244,7 @@ class UIViewModel @Inject constructor(
onDismiss = {
if (dismissable) dismissAlert()
},
choices = choices
choices = choices,
)
}
@ -285,7 +291,8 @@ class UIViewModel @Inject constructor(
private val onlyDirect = MutableStateFlow(preferences.getBoolean("only-direct", false))
private val onlyFavorites = MutableStateFlow(preferences.getBoolean("only-favorites", false))
private val showWaypointsOnMap = MutableStateFlow(preferences.getBoolean("show-waypoints-on-map", true))
private val showWaypointsOnMap =
MutableStateFlow(preferences.getBoolean("show-waypoints-on-map", true))
private val showPrecisionCircleOnMap =
MutableStateFlow(preferences.getBoolean("show-precision-circle-on-map", true))
@ -441,18 +448,6 @@ class UIViewModel @Inject constructor(
)
}.launchIn(viewModelScope)
radioConfigRepository.clientNotification.filterNotNull().onEach { notification ->
showAlert(
title = app.getString(R.string.client_notification),
message = notification.message,
onConfirm = {
radioConfigRepository.clearClientNotification()
meshServiceNotifications.clearClientNotification(notification)
},
dismissable = false
)
}.launchIn(viewModelScope)
radioConfigRepository.localConfigFlow.onEach { config ->
_localConfig.value = config
}.launchIn(viewModelScope)
@ -663,7 +658,8 @@ class UIViewModel @Inject constructor(
showSnackbar(R.string.channel_invalid)
}
val latestStableFirmwareRelease = firmwareReleaseRepository.stableRelease.mapNotNull { it?.asDeviceVersion() }
val latestStableFirmwareRelease =
firmwareReleaseRepository.stableRelease.mapNotNull { it?.asDeviceVersion() }
/**
* Called immediately after activity observes requestChannelUrl

View file

@ -148,6 +148,30 @@ fun MainScreen(
}
}
val clientNotification by viewModel.clientNotification.collectAsStateWithLifecycle()
clientNotification?.let { notification ->
var message = notification.message
val compromisedKeys =
if (notification.hasLowEntropyKey() || notification.hasDuplicatedPublicKey()) {
message = stringResource(R.string.compromised_keys)
true
} else {
false
}
SimpleAlertDialog(
title = R.string.client_notification,
text = {
Text(text = message)
},
onConfirm = {
if (compromisedKeys) {
navController.navigate(RadioConfigRoutes.Security)
}
viewModel.clearClientNotification(notification)
},
)
}
val traceRouteResponse by viewModel.tracerouteResponse.observeAsState()
traceRouteResponse?.let { response ->
SimpleAlertDialog(

View file

@ -952,6 +952,7 @@ fun TracerouteActionButton(
@Suppress("LongMethod")
@Composable
fun NodeActionButton(
modifier: Modifier = Modifier,
title: String,
enabled: Boolean,
icon: ImageVector? = null,
@ -964,7 +965,7 @@ fun NodeActionButton(
onClick()
},
enabled = enabled,
modifier = Modifier
modifier = modifier
.fillMaxWidth()
.padding(vertical = 4.dp)
.height(48.dp),

View file

@ -18,9 +18,15 @@
package com.geeksville.mesh.ui.radioconfig.components
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.twotone.Warning
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
@ -30,6 +36,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.geeksville.mesh.ConfigProtos.Config.SecurityConfig
@ -42,6 +49,7 @@ import com.geeksville.mesh.ui.common.components.EditListPreference
import com.geeksville.mesh.ui.common.components.PreferenceCategory
import com.geeksville.mesh.ui.common.components.PreferenceFooter
import com.geeksville.mesh.ui.common.components.SwitchPreference
import com.geeksville.mesh.ui.node.NodeActionButton
import com.geeksville.mesh.ui.radioconfig.RadioConfigViewModel
import com.geeksville.mesh.util.encodeToString
@ -78,6 +86,17 @@ fun SecurityConfigItemList(
val focusManager = LocalFocusManager.current
var securityInput by rememberSaveable { mutableStateOf(securityConfig) }
var showKeyGenerationDialog by rememberSaveable { mutableStateOf(false) }
PrivateKeyRegenerateDialog(
showKeyGenerationDialog = showKeyGenerationDialog,
config = securityInput,
onConfirm = { newConfig ->
securityInput = newConfig
showKeyGenerationDialog = false
},
onDismiss = { showKeyGenerationDialog = false }
)
LazyColumn(
modifier = Modifier.fillMaxSize()
) {
@ -122,6 +141,18 @@ fun SecurityConfigItemList(
)
}
item {
NodeActionButton(
modifier = Modifier.padding(16.dp),
title = stringResource(R.string.regenerate_private_key),
enabled = enabled,
icon = Icons.TwoTone.Warning,
onClick = {
showKeyGenerationDialog = true
}
)
}
item {
EditListPreference(
title = stringResource(R.string.admin_key),
@ -200,6 +231,42 @@ fun SecurityConfigItemList(
}
}
@Composable
fun PrivateKeyRegenerateDialog(
showKeyGenerationDialog: Boolean,
config: SecurityConfig,
onConfirm: (SecurityConfig) -> Unit,
onDismiss: () -> Unit = {},
) {
var securityInput by rememberSaveable { mutableStateOf(config) }
if (showKeyGenerationDialog) {
AlertDialog(
onDismissRequest = onDismiss,
title = { Text(text = stringResource(R.string.regenerate_private_key)) },
text = { Text(text = stringResource(R.string.regenerate_keys_confirmation)) },
confirmButton = {
TextButton(
onClick = {
securityInput = securityInput.copy {
clearPrivateKey()
}
onConfirm(securityInput)
},
) {
Text(stringResource(R.string.okay))
}
},
dismissButton = {
TextButton(
onClick = onDismiss,
) {
Text(stringResource(R.string.cancel))
}
}
)
}
}
@Preview(showBackground = true)
@Composable
private fun SecurityConfigPreview() {

View file

@ -680,4 +680,7 @@
<string name="show_waypoints">Show Waypoints</string>
<string name="show_precision_circle">Show Precision Circles</string>
<string name="client_notification">Client Notification</string>
<string name="compromised_keys">Compromised keys detected, select OK to regenerate.</string>
<string name="regenerate_private_key">Regenerate Private Key</string>
<string name="regenerate_keys_confirmation">Are you sure you want to regenerate your Private Key?</string>
</resources>