mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-04-20 22:23:37 +00:00
feat: Add key export functionality (#2158)
This commit is contained in:
parent
9b045cee5f
commit
e205f1d6d6
3 changed files with 111 additions and 3 deletions
|
|
@ -20,6 +20,7 @@ package com.geeksville.mesh.ui.radioconfig
|
|||
import android.app.Application
|
||||
import android.net.Uri
|
||||
import android.os.RemoteException
|
||||
import android.util.Base64
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.core.net.toUri
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
|
|
@ -30,6 +31,7 @@ import com.geeksville.mesh.AdminProtos
|
|||
import com.geeksville.mesh.ChannelProtos
|
||||
import com.geeksville.mesh.ClientOnlyProtos.DeviceProfile
|
||||
import com.geeksville.mesh.ConfigProtos
|
||||
import com.geeksville.mesh.ConfigProtos.Config.SecurityConfig
|
||||
import com.geeksville.mesh.IMeshService
|
||||
import com.geeksville.mesh.MeshProtos
|
||||
import com.geeksville.mesh.ModuleConfigProtos
|
||||
|
|
@ -66,6 +68,7 @@ import kotlinx.coroutines.flow.onEach
|
|||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.json.JSONObject
|
||||
import java.io.FileOutputStream
|
||||
import javax.inject.Inject
|
||||
|
||||
|
|
@ -379,7 +382,6 @@ class RadioConfigViewModel @Inject constructor(
|
|||
fun exportProfile(uri: Uri, profile: DeviceProfile) = viewModelScope.launch {
|
||||
writeToUri(uri, profile)
|
||||
}
|
||||
|
||||
private suspend fun writeToUri(uri: Uri, message: MessageLite) = withContext(Dispatchers.IO) {
|
||||
try {
|
||||
app.contentResolver.openFileDescriptor(uri, "wt")?.use { parcelFileDescriptor ->
|
||||
|
|
@ -394,6 +396,49 @@ class RadioConfigViewModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
fun exportSecurityConfig(uri: Uri, securityConfig: SecurityConfig) = viewModelScope.launch {
|
||||
writeSecurityKeysJsonToUri(uri, securityConfig)
|
||||
}
|
||||
|
||||
private suspend fun writeSecurityKeysJsonToUri(uri: Uri, securityConfig: SecurityConfig) =
|
||||
withContext(Dispatchers.IO) {
|
||||
try {
|
||||
val publicKeyBytes =
|
||||
securityConfig.publicKey.toByteArray()
|
||||
val privateKeyBytes =
|
||||
securityConfig.privateKey.toByteArray()
|
||||
|
||||
|
||||
// Convert byte arrays to Base64 strings for human readability in JSON
|
||||
val publicKeyBase64 = Base64.encodeToString(publicKeyBytes, Base64.NO_WRAP)
|
||||
val privateKeyBase64 = Base64.encodeToString(privateKeyBytes, Base64.NO_WRAP)
|
||||
|
||||
// Create a JSON object
|
||||
val jsonObject = JSONObject().apply {
|
||||
put("timestamp", System.currentTimeMillis())
|
||||
put("public_key", publicKeyBase64)
|
||||
put("private_key", privateKeyBase64)
|
||||
}
|
||||
|
||||
// Convert JSON object to a string
|
||||
val jsonString =
|
||||
jsonObject.toString(4)
|
||||
|
||||
app.contentResolver.openFileDescriptor(uri, "wt")?.use { parcelFileDescriptor ->
|
||||
FileOutputStream(parcelFileDescriptor.fileDescriptor).use { outputStream ->
|
||||
outputStream.write(jsonString.toByteArray(Charsets.UTF_8))
|
||||
}
|
||||
}
|
||||
setResponseStateSuccess()
|
||||
|
||||
} catch (ex: Exception) {
|
||||
val errorMessage = "Can't write security keys JSON error: ${ex.message}"
|
||||
errormsg(errorMessage)
|
||||
sendError(ex.customMessage)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun installProfile(protobuf: DeviceProfile) = with(protobuf) {
|
||||
meshService?.beginEditSettings()
|
||||
if (hasLongName() || hasShortName()) {
|
||||
|
|
|
|||
|
|
@ -17,6 +17,11 @@
|
|||
|
||||
package com.geeksville.mesh.ui.radioconfig.components
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
|
|
@ -24,6 +29,7 @@ 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.ExperimentalMaterial3ExpressiveApi
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
|
|
@ -40,6 +46,7 @@ import androidx.compose.ui.unit.dp
|
|||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.geeksville.mesh.ConfigProtos.Config.SecurityConfig
|
||||
import com.geeksville.mesh.MeshProtos
|
||||
import com.geeksville.mesh.R
|
||||
import com.geeksville.mesh.config
|
||||
import com.geeksville.mesh.copy
|
||||
|
|
@ -60,6 +67,7 @@ fun SecurityConfigScreen(
|
|||
viewModel: RadioConfigViewModel = hiltViewModel(),
|
||||
) {
|
||||
val state by viewModel.radioConfigState.collectAsStateWithLifecycle()
|
||||
val node by viewModel.destNode.collectAsStateWithLifecycle()
|
||||
|
||||
if (state.responseState.isWaiting()) {
|
||||
PacketResponseStateDialog(
|
||||
|
|
@ -69,25 +77,40 @@ fun SecurityConfigScreen(
|
|||
}
|
||||
|
||||
SecurityConfigItemList(
|
||||
user = node?.user,
|
||||
securityConfig = state.radioConfig.security,
|
||||
enabled = state.connected,
|
||||
onConfirm = { securityInput ->
|
||||
val config = config { security = securityInput }
|
||||
viewModel.setConfig(config)
|
||||
}
|
||||
},
|
||||
onExport = { uri, securityConfig ->
|
||||
viewModel.exportSecurityConfig(uri, securityConfig)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
||||
@Suppress("LongMethod")
|
||||
@Composable
|
||||
fun SecurityConfigItemList(
|
||||
user: MeshProtos.User? = null,
|
||||
securityConfig: SecurityConfig,
|
||||
enabled: Boolean,
|
||||
onConfirm: (config: SecurityConfig) -> Unit,
|
||||
onExport: (uri: Uri, securityConfig: SecurityConfig) -> Unit = { _, _ -> },
|
||||
) {
|
||||
val focusManager = LocalFocusManager.current
|
||||
var securityInput by rememberSaveable { mutableStateOf(securityConfig) }
|
||||
|
||||
val exportConfigLauncher = rememberLauncherForActivityResult(
|
||||
ActivityResultContracts.StartActivityForResult()
|
||||
) {
|
||||
if (it.resultCode == Activity.RESULT_OK) {
|
||||
it.data?.data?.let { uri -> onExport(uri, securityConfig) }
|
||||
}
|
||||
}
|
||||
|
||||
var showKeyGenerationDialog by rememberSaveable { mutableStateOf(false) }
|
||||
PrivateKeyRegenerateDialog(
|
||||
showKeyGenerationDialog = showKeyGenerationDialog,
|
||||
|
|
@ -98,6 +121,32 @@ fun SecurityConfigItemList(
|
|||
},
|
||||
onDismiss = { showKeyGenerationDialog = false }
|
||||
)
|
||||
var showEditSecurityConfigDialog by rememberSaveable { mutableStateOf(false) }
|
||||
if (showEditSecurityConfigDialog) {
|
||||
AlertDialog(
|
||||
title = { Text(text = stringResource(R.string.export_keys)) },
|
||||
text = { Text(text = stringResource(R.string.export_keys_confirmation)) },
|
||||
onDismissRequest = { showEditSecurityConfigDialog = false },
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
onClick = {
|
||||
showEditSecurityConfigDialog = false
|
||||
val intent = Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
|
||||
addCategory(Intent.CATEGORY_OPENABLE)
|
||||
type = "application/*"
|
||||
putExtra(
|
||||
Intent.EXTRA_TITLE,
|
||||
"${user?.shortName}_keys_${System.currentTimeMillis()}.json"
|
||||
)
|
||||
}
|
||||
exportConfigLauncher.launch(intent)
|
||||
},
|
||||
) {
|
||||
Text(stringResource(R.string.okay))
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxSize()
|
||||
|
|
@ -145,7 +194,7 @@ fun SecurityConfigItemList(
|
|||
|
||||
item {
|
||||
NodeActionButton(
|
||||
modifier = Modifier.padding(16.dp),
|
||||
modifier = Modifier.padding(horizontal = 8.dp),
|
||||
title = stringResource(R.string.regenerate_private_key),
|
||||
enabled = enabled,
|
||||
icon = Icons.TwoTone.Warning,
|
||||
|
|
@ -155,6 +204,18 @@ fun SecurityConfigItemList(
|
|||
)
|
||||
}
|
||||
|
||||
item {
|
||||
NodeActionButton(
|
||||
modifier = Modifier.padding(horizontal = 8.dp),
|
||||
title = stringResource(R.string.export_keys),
|
||||
enabled = enabled,
|
||||
icon = Icons.TwoTone.Warning,
|
||||
onClick = {
|
||||
showEditSecurityConfigDialog = true
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
EditListPreference(
|
||||
title = stringResource(R.string.admin_key),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue