Require passphrase confirmation for autobackup setting and encrypted export
A typo in a write-only passphrase field encrypts every backup with a phrase the user cannot reproduce — unrecoverable by design. Both the settings field and the manual-export dialog now demand a matching repeat before accepting. Import is unchanged (wrong passphrase there just fails loudly). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
3ce003d336
commit
0dd48445e1
1 changed files with 35 additions and 1 deletions
|
|
@ -62,6 +62,7 @@ fun SettingsScreen(nav: Nav) {
|
||||||
var accessKey by remember { mutableStateOf(settings.s3AccessKey ?: "") }
|
var accessKey by remember { mutableStateOf(settings.s3AccessKey ?: "") }
|
||||||
var secretKey by remember { mutableStateOf(settings.s3SecretKey ?: "") }
|
var secretKey by remember { mutableStateOf(settings.s3SecretKey ?: "") }
|
||||||
var autoPassphrase by remember { mutableStateOf(settings.autoBackupPassphrase ?: "") }
|
var autoPassphrase by remember { mutableStateOf(settings.autoBackupPassphrase ?: "") }
|
||||||
|
var autoPassphraseConfirm by remember { mutableStateOf(settings.autoBackupPassphrase ?: "") }
|
||||||
var festUrl by remember { mutableStateOf(settings.festUrl ?: "") }
|
var festUrl by remember { mutableStateOf(settings.festUrl ?: "") }
|
||||||
|
|
||||||
// Export/import passphrase dialogs hold the pending document URI.
|
// Export/import passphrase dialogs hold the pending document URI.
|
||||||
|
|
@ -134,8 +135,24 @@ fun SettingsScreen(nav: Nav) {
|
||||||
},
|
},
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
)
|
)
|
||||||
|
// Confirmation field: a typo here would encrypt every backup with a
|
||||||
|
// passphrase the user can't reproduce — unrecoverable by design.
|
||||||
|
val passphraseMismatch = autoPassphrase != autoPassphraseConfirm
|
||||||
|
OutlinedTextField(
|
||||||
|
value = autoPassphraseConfirm, onValueChange = { autoPassphraseConfirm = it },
|
||||||
|
label = { Text("Gjenta passordfrase") }, singleLine = true,
|
||||||
|
visualTransformation = PasswordVisualTransformation(),
|
||||||
|
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password),
|
||||||
|
isError = passphraseMismatch,
|
||||||
|
supportingText = { if (passphraseMismatch) Text("Passordfrasene er ikke like") },
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
)
|
||||||
Button(
|
Button(
|
||||||
onClick = {
|
onClick = {
|
||||||
|
if (passphraseMismatch) {
|
||||||
|
toast("Passordfrasene er ikke like")
|
||||||
|
return@Button
|
||||||
|
}
|
||||||
settings.s3Endpoint = endpoint.trim().ifBlank { null }
|
settings.s3Endpoint = endpoint.trim().ifBlank { null }
|
||||||
settings.s3Region = region.trim().ifBlank { null }
|
settings.s3Region = region.trim().ifBlank { null }
|
||||||
settings.s3Bucket = bucket.trim().ifBlank { null }
|
settings.s3Bucket = bucket.trim().ifBlank { null }
|
||||||
|
|
@ -144,6 +161,7 @@ fun SettingsScreen(nav: Nav) {
|
||||||
settings.autoBackupPassphrase = autoPassphrase.ifBlank { null }
|
settings.autoBackupPassphrase = autoPassphrase.ifBlank { null }
|
||||||
toast("Lagret")
|
toast("Lagret")
|
||||||
},
|
},
|
||||||
|
enabled = !passphraseMismatch,
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
) { Text("Lagre backupinnstillinger") }
|
) { Text("Lagre backupinnstillinger") }
|
||||||
|
|
||||||
|
|
@ -214,6 +232,7 @@ fun SettingsScreen(nav: Nav) {
|
||||||
title = "Krypter eksport",
|
title = "Krypter eksport",
|
||||||
text = "Skriv en passordfrase (lagres ikke). La stå tom for ukryptert JSON.",
|
text = "Skriv en passordfrase (lagres ikke). La stå tom for ukryptert JSON.",
|
||||||
allowEmpty = true,
|
allowEmpty = true,
|
||||||
|
requireConfirmation = true, // a typo here = an unrecoverable export
|
||||||
onDismiss = { exportUri = null },
|
onDismiss = { exportUri = null },
|
||||||
) { passphrase ->
|
) { passphrase ->
|
||||||
exportUri = null
|
exportUri = null
|
||||||
|
|
@ -306,10 +325,13 @@ private fun PassphraseDialog(
|
||||||
title: String,
|
title: String,
|
||||||
text: String,
|
text: String,
|
||||||
allowEmpty: Boolean,
|
allowEmpty: Boolean,
|
||||||
|
requireConfirmation: Boolean = false,
|
||||||
onDismiss: () -> Unit,
|
onDismiss: () -> Unit,
|
||||||
onConfirm: (CharArray) -> Unit,
|
onConfirm: (CharArray) -> Unit,
|
||||||
) {
|
) {
|
||||||
var value by remember { mutableStateOf("") }
|
var value by remember { mutableStateOf("") }
|
||||||
|
var repeated by remember { mutableStateOf("") }
|
||||||
|
val mismatch = requireConfirmation && value != repeated
|
||||||
AlertDialog(
|
AlertDialog(
|
||||||
onDismissRequest = onDismiss,
|
onDismissRequest = onDismiss,
|
||||||
title = { Text(title) },
|
title = { Text(title) },
|
||||||
|
|
@ -324,12 +346,24 @@ private fun PassphraseDialog(
|
||||||
visualTransformation = PasswordVisualTransformation(),
|
visualTransformation = PasswordVisualTransformation(),
|
||||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password),
|
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password),
|
||||||
)
|
)
|
||||||
|
if (requireConfirmation) {
|
||||||
|
OutlinedTextField(
|
||||||
|
value = repeated,
|
||||||
|
onValueChange = { repeated = it },
|
||||||
|
label = { Text("Gjenta passordfrase") },
|
||||||
|
singleLine = true,
|
||||||
|
isError = mismatch,
|
||||||
|
supportingText = { if (mismatch) Text("Passordfrasene er ikke like") },
|
||||||
|
visualTransformation = PasswordVisualTransformation(),
|
||||||
|
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password),
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
confirmButton = {
|
confirmButton = {
|
||||||
TextButton(
|
TextButton(
|
||||||
onClick = { onConfirm(value.toCharArray()) },
|
onClick = { onConfirm(value.toCharArray()) },
|
||||||
enabled = allowEmpty || value.isNotEmpty(),
|
enabled = (allowEmpty || value.isNotEmpty()) && !mismatch,
|
||||||
) { Text("OK") }
|
) { Text("OK") }
|
||||||
},
|
},
|
||||||
dismissButton = { TextButton(onClick = onDismiss) { Text("Avbryt") } },
|
dismissButton = { TextButton(onClick = onDismiss) { Text("Avbryt") } },
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue