Enhancement - Add 'show all meanings' to node key encryption dialog (#3437)

Co-authored-by: ChrisDeardeuff <chris.deardeuff@proton.me>
This commit is contained in:
ChrisDeardeuff 2025-10-15 13:04:18 -07:00 committed by GitHub
parent 241b46da3d
commit f6487518f8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 161 additions and 51 deletions

View file

@ -354,6 +354,7 @@
<string name="ch_util_definition">Utilization for the current channel, including well formed TX, RX and malformed RX (aka noise).</string>
<string name="air_util_definition">Percent of airtime for transmission used within the last hour.</string>
<string name="iaq">IAQ</string>
<string name="show_all_key_title">Encryption Key Meanings</string>
<string name="encryption_psk">Shared Key</string>
<string name="encryption_psk_text">Only channel messages can be sent/received. Direct Messages require the Public Key Infrastructure feature in 2.5+ firmware.</string>
<string name="encryption_pkc">Public Key Encryption</string>

View file

@ -20,40 +20,45 @@ package org.meshtastic.core.ui.component
import android.util.Base64
import androidx.annotation.StringRes
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.KeyOff
import androidx.compose.material.icons.filled.Lock
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material.icons.filled.LockOpen
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.MaterialTheme.colorScheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewLightDark
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import com.google.protobuf.ByteString
import org.meshtastic.core.model.Channel
import org.meshtastic.core.strings.R
@ -62,56 +67,19 @@ import org.meshtastic.core.ui.theme.StatusColors.StatusGreen
import org.meshtastic.core.ui.theme.StatusColors.StatusRed
import org.meshtastic.core.ui.theme.StatusColors.StatusYellow
@Composable
private fun KeyStatusDialog(@StringRes title: Int, @StringRes text: Int, key: ByteString?, onDismiss: () -> Unit = {}) =
Dialog(onDismissRequest = onDismiss) {
Surface(
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(16.dp),
color = MaterialTheme.colorScheme.background,
) {
LazyColumn(
contentPadding = PaddingValues(horizontal = 24.dp, vertical = 16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
) {
item {
Text(text = stringResource(id = title), textAlign = TextAlign.Center)
Spacer(Modifier.height(16.dp))
Text(text = stringResource(id = text), textAlign = TextAlign.Center)
Spacer(Modifier.height(16.dp))
if (key != null && title == R.string.encryption_pkc) {
val keyString = Base64.encodeToString(key.toByteArray(), Base64.NO_WRAP)
Text(
text = stringResource(id = R.string.config_security_public_key) + ":",
textAlign = TextAlign.Center,
)
Spacer(Modifier.height(8.dp))
SelectionContainer { Text(text = keyString, textAlign = TextAlign.Center) }
Spacer(Modifier.height(8.dp))
CopyIconButton(valueToCopy = keyString, modifier = Modifier.padding(start = 8.dp))
Spacer(Modifier.height(16.dp))
}
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.End) {
TextButton(
onClick = onDismiss,
colors = ButtonDefaults.textButtonColors(
contentColor = MaterialTheme.colorScheme.onSurface,
),
) {
Text(text = stringResource(id = R.string.close))
}
}
}
}
}
}
/**
* function to display information about the current node's encryption key.
*
* @property hasPKC boolean if the node has public key encryption
* @property mismatchKey boolean if the public key does not match the recorded key.
* @property publicKey boolean if the node has a shared public key.
*/
@Composable
fun NodeKeyStatusIcon(
modifier: Modifier = Modifier,
hasPKC: Boolean,
mismatchKey: Boolean,
publicKey: ByteString? = null,
modifier: Modifier = Modifier,
) {
var showEncryptionDialog by remember { mutableStateOf(false) }
if (showEncryptionDialog) {
@ -150,6 +118,141 @@ fun NodeKeyStatusIcon(
}
}
/**
* Represents the various visual states of the node key as an enum. Each enum constant encapsulates the icon, color,
* descriptive text, and optional badge details.
*
* @property icon The primary vector graphic for the icon.
* @property color The tint color for the primary icon.
* @property descriptionResId The string resource ID for the accessibility description of the icon's state.
* @property helpTextResId The string resource ID for the detailed help text associated with this state.
* @property title The string resource ID for the title associated with this state.
*/
@Immutable
enum class NodeKeySecurityState(
@Stable val icon: ImageVector,
@Stable val color: @Composable () -> Color,
@StringRes val descriptionResId: Int,
@StringRes val helpTextResId: Int,
@Stable val title: Int,
) {
// State for public key mismatch
PKM(
icon = Icons.Default.KeyOff,
color = { colorScheme.StatusRed },
descriptionResId = R.string.encryption_error,
helpTextResId = R.string.encryption_error_text,
title = R.string.encryption_error,
),
// State for public key encryption
PKC(
icon = Icons.Default.Lock,
color = { colorScheme.StatusGreen },
title = R.string.encryption_pkc,
helpTextResId = R.string.encryption_pkc_text,
descriptionResId = R.string.encryption_pkc,
),
// State for shared key encryption
PSK(
icon = Icons.Default.LockOpen,
color = { colorScheme.StatusYellow },
title = R.string.encryption_psk,
helpTextResId = R.string.encryption_psk_text,
descriptionResId = R.string.encryption_psk,
),
}
@Composable
private fun KeyStatusDialog(@StringRes title: Int, @StringRes text: Int, key: ByteString?, onDismiss: () -> Unit = {}) {
var showAll by rememberSaveable { mutableStateOf(false) }
AlertDialog(
modifier = Modifier,
onDismissRequest = onDismiss,
title = {
if (showAll) {
Text(stringResource(R.string.show_all_key_title))
} else {
Text(stringResource(id = title))
}
},
text = {
if (showAll) {
AllKeyStates()
} else {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Text(text = stringResource(id = text), textAlign = TextAlign.Center)
Spacer(Modifier.height(16.dp))
if (key != null && title == R.string.encryption_pkc) {
val keyString = Base64.encodeToString(key.toByteArray(), Base64.NO_WRAP)
Text(
text = stringResource(id = R.string.config_security_public_key) + ":",
textAlign = TextAlign.Center,
)
Spacer(Modifier.height(8.dp))
SelectionContainer { Text(text = keyString, textAlign = TextAlign.Center) }
Spacer(Modifier.height(8.dp))
CopyIconButton(valueToCopy = keyString, modifier = Modifier.padding(start = 8.dp))
Spacer(Modifier.height(16.dp))
}
}
}
},
confirmButton = {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
) {
TextButton(onClick = { showAll = !showAll }) {
Text(
if (showAll) {
stringResource(R.string.security_icon_help_show_less)
} else {
stringResource(R.string.security_icon_help_show_all)
},
)
}
TextButton(onClick = onDismiss) { Text(stringResource(R.string.security_icon_help_dismiss)) }
}
},
)
}
/**
* Displays a list of all possible node key states with their icons and descriptions within the help dialog. Iterates
* over `NodeKeySecurityState.entries` which is provided by the enum class.
*/
@Composable
private fun AllKeyStates() {
Column(
verticalArrangement = Arrangement.spacedBy(16.dp),
modifier = Modifier.verticalScroll(rememberScrollState()),
) {
NodeKeySecurityState.entries.forEach { state ->
// Uses enum entries
Row(verticalAlignment = Alignment.CenterVertically) {
when (state) {
NodeKeySecurityState.PKM -> NodeKeyStatusIcon(hasPKC = false, mismatchKey = true)
NodeKeySecurityState.PKC -> NodeKeyStatusIcon(hasPKC = true, mismatchKey = false)
else -> NodeKeyStatusIcon(hasPKC = false, mismatchKey = false)
}
Column(modifier = Modifier.padding(start = 16.dp)) {
Text(text = stringResource(state.descriptionResId), style = MaterialTheme.typography.titleMedium)
Text(text = stringResource(state.helpTextResId), style = MaterialTheme.typography.bodyMedium)
}
}
if (state != NodeKeySecurityState.entries.lastOrNull()) {
HorizontalDivider(modifier = Modifier.padding(top = 8.dp))
}
}
}
}
@PreviewLightDark
@Composable
private fun KeyStatusDialogErrorPreview() {
@ -173,3 +276,9 @@ private fun KeyStatusDialogPkcPreview() {
private fun KeyStatusDialogPskPreview() {
AppTheme { KeyStatusDialog(title = R.string.encryption_psk, text = R.string.encryption_psk_text, key = null) }
}
@Preview
@Composable
private fun AllKeyStatusDialogPreview() {
AppTheme { AllKeyStates() }
}