From 0dd48445e1dc54687fc65c9f30034ee029ef60ad Mon Sep 17 00:00:00 2001 From: Ole-Morten Duesund Date: Thu, 11 Jun 2026 17:41:20 +0200 Subject: [PATCH] Require passphrase confirmation for autobackup setting and encrypted export MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../no/naiv/meddetsamme/ui/SettingsScreen.kt | 36 ++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/no/naiv/meddetsamme/ui/SettingsScreen.kt b/app/src/main/java/no/naiv/meddetsamme/ui/SettingsScreen.kt index 6e7d10b..92193e4 100644 --- a/app/src/main/java/no/naiv/meddetsamme/ui/SettingsScreen.kt +++ b/app/src/main/java/no/naiv/meddetsamme/ui/SettingsScreen.kt @@ -62,6 +62,7 @@ fun SettingsScreen(nav: Nav) { var accessKey by remember { mutableStateOf(settings.s3AccessKey ?: "") } var secretKey by remember { mutableStateOf(settings.s3SecretKey ?: "") } var autoPassphrase by remember { mutableStateOf(settings.autoBackupPassphrase ?: "") } + var autoPassphraseConfirm by remember { mutableStateOf(settings.autoBackupPassphrase ?: "") } var festUrl by remember { mutableStateOf(settings.festUrl ?: "") } // Export/import passphrase dialogs hold the pending document URI. @@ -134,8 +135,24 @@ fun SettingsScreen(nav: Nav) { }, 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( onClick = { + if (passphraseMismatch) { + toast("Passordfrasene er ikke like") + return@Button + } settings.s3Endpoint = endpoint.trim().ifBlank { null } settings.s3Region = region.trim().ifBlank { null } settings.s3Bucket = bucket.trim().ifBlank { null } @@ -144,6 +161,7 @@ fun SettingsScreen(nav: Nav) { settings.autoBackupPassphrase = autoPassphrase.ifBlank { null } toast("Lagret") }, + enabled = !passphraseMismatch, modifier = Modifier.fillMaxWidth(), ) { Text("Lagre backupinnstillinger") } @@ -214,6 +232,7 @@ fun SettingsScreen(nav: Nav) { title = "Krypter eksport", text = "Skriv en passordfrase (lagres ikke). La stå tom for ukryptert JSON.", allowEmpty = true, + requireConfirmation = true, // a typo here = an unrecoverable export onDismiss = { exportUri = null }, ) { passphrase -> exportUri = null @@ -306,10 +325,13 @@ private fun PassphraseDialog( title: String, text: String, allowEmpty: Boolean, + requireConfirmation: Boolean = false, onDismiss: () -> Unit, onConfirm: (CharArray) -> Unit, ) { var value by remember { mutableStateOf("") } + var repeated by remember { mutableStateOf("") } + val mismatch = requireConfirmation && value != repeated AlertDialog( onDismissRequest = onDismiss, title = { Text(title) }, @@ -324,12 +346,24 @@ private fun PassphraseDialog( visualTransformation = PasswordVisualTransformation(), 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 = { TextButton( onClick = { onConfirm(value.toCharArray()) }, - enabled = allowEmpty || value.isNotEmpty(), + enabled = (allowEmpty || value.isNotEmpty()) && !mismatch, ) { Text("OK") } }, dismissButton = { TextButton(onClick = onDismiss) { Text("Avbryt") } },