Secure Export/Import with device credential auth
This commit is contained in:
parent
c821cdc37b
commit
15e075b293
@ -2,13 +2,13 @@ package cz.bugsy.passwordzebra
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.WindowManager.LayoutParams.FLAG_SECURE
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import cz.bugsy.passwordzebra.ui.navigation.AppNavigation
|
||||
import cz.bugsy.passwordzebra.ui.theme.PasswordZebraTheme
|
||||
|
||||
class MainActivity : ComponentActivity() {
|
||||
class MainActivity : AppCompatActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
window.addFlags(FLAG_SECURE)
|
||||
|
||||
@ -1,38 +1,32 @@
|
||||
package cz.bugsy.passwordzebra.ui.screens
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.graphics.Bitmap
|
||||
import android.net.Uri
|
||||
import android.util.Base64
|
||||
import org.json.JSONArray
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.biometric.BiometricManager
|
||||
import androidx.biometric.BiometricPrompt
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Tab
|
||||
import androidx.compose.material3.TabRow
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
@ -47,8 +41,9 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.asImageBitmap
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import com.google.zxing.BarcodeFormat
|
||||
import com.google.zxing.MultiFormatWriter
|
||||
import com.journeyapps.barcodescanner.ScanContract
|
||||
@ -56,17 +51,36 @@ import com.journeyapps.barcodescanner.ScanOptions
|
||||
import cz.bugsy.passwordzebra.R
|
||||
import cz.bugsy.passwordzebra.deterministic.DeterministicViewModel
|
||||
|
||||
private val AUTH = BiometricManager.Authenticators.BIOMETRIC_STRONG or
|
||||
BiometricManager.Authenticators.DEVICE_CREDENTIAL
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun ExportImportScreen(
|
||||
viewModel: DeterministicViewModel,
|
||||
onNavigateBack: () -> Unit,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
var selectedTab by rememberSaveable { mutableIntStateOf(0) }
|
||||
var showConfirmDialog by remember { mutableStateOf(false) }
|
||||
var pendingImportSecret by remember { mutableStateOf<ByteArray?>(null) }
|
||||
var confirmPassword by remember { mutableStateOf("") }
|
||||
var confirmError by remember { mutableStateOf(false) }
|
||||
|
||||
fun launchImportAuth(secret: ByteArray) {
|
||||
val activity = context as? FragmentActivity ?: return
|
||||
val executor = ContextCompat.getMainExecutor(context)
|
||||
val prompt = BiometricPrompt(
|
||||
activity, executor,
|
||||
object : BiometricPrompt.AuthenticationCallback() {
|
||||
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
|
||||
viewModel.importDeviceSecret(secret)
|
||||
}
|
||||
},
|
||||
)
|
||||
BiometricPrompt.PromptInfo.Builder()
|
||||
.setTitle(context.getString(R.string.det_import_confirm_title))
|
||||
.setSubtitle(context.getString(R.string.det_import_biometric_subtitle))
|
||||
.setAllowedAuthenticators(AUTH)
|
||||
.build()
|
||||
.let { prompt.authenticate(it) }
|
||||
}
|
||||
|
||||
val scanLauncher = rememberLauncherForActivityResult(ScanContract()) { result ->
|
||||
val rawValue = result.contents ?: return@rememberLauncherForActivityResult
|
||||
@ -77,8 +91,6 @@ fun ExportImportScreen(
|
||||
Base64.decode(secretParam, Base64.URL_SAFE or Base64.NO_WRAP or Base64.NO_PADDING)
|
||||
} catch (_: Exception) { null }
|
||||
if (decoded != null && decoded.size == 32) {
|
||||
pendingImportSecret = decoded
|
||||
// Parse v2 services if present
|
||||
val servicesParam = uri.getQueryParameter("services")
|
||||
if (servicesParam != null) {
|
||||
try {
|
||||
@ -92,7 +104,7 @@ fun ExportImportScreen(
|
||||
}
|
||||
} catch (_: Exception) { /* ignore malformed services */ }
|
||||
}
|
||||
showConfirmDialog = true
|
||||
launchImportAuth(decoded)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -136,60 +148,6 @@ fun ExportImportScreen(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (showConfirmDialog) {
|
||||
AlertDialog(
|
||||
onDismissRequest = {
|
||||
showConfirmDialog = false
|
||||
pendingImportSecret = null
|
||||
confirmPassword = ""
|
||||
confirmError = false
|
||||
},
|
||||
title = { Text(stringResource(R.string.det_import_confirm_title)) },
|
||||
text = {
|
||||
Column {
|
||||
Text(stringResource(R.string.det_import_confirm_body))
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
OutlinedTextField(
|
||||
value = confirmPassword,
|
||||
onValueChange = { confirmPassword = it; confirmError = false },
|
||||
label = { Text(stringResource(R.string.det_master_password)) },
|
||||
visualTransformation = PasswordVisualTransformation(),
|
||||
isError = confirmError,
|
||||
supportingText = if (confirmError) {
|
||||
{ Text(stringResource(R.string.det_import_wrong_password)) }
|
||||
} else null,
|
||||
)
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(onClick = {
|
||||
// We just require any non-empty master password as confirmation
|
||||
if (confirmPassword.isNotEmpty()) {
|
||||
pendingImportSecret?.let { viewModel.importDeviceSecret(it) }
|
||||
showConfirmDialog = false
|
||||
pendingImportSecret = null
|
||||
confirmPassword = ""
|
||||
confirmError = false
|
||||
} else {
|
||||
confirmError = true
|
||||
}
|
||||
}) {
|
||||
Text(stringResource(R.string.det_import_confirm))
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = {
|
||||
showConfirmDialog = false
|
||||
pendingImportSecret = null
|
||||
confirmPassword = ""
|
||||
confirmError = false
|
||||
}) {
|
||||
Text(stringResource(R.string.cancel))
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
|
||||
@ -44,6 +44,9 @@ import androidx.fragment.app.FragmentActivity
|
||||
import cz.bugsy.passwordzebra.R
|
||||
import cz.bugsy.passwordzebra.deterministic.DeterministicViewModel
|
||||
|
||||
private val AUTH = BiometricManager.Authenticators.BIOMETRIC_STRONG or
|
||||
BiometricManager.Authenticators.DEVICE_CREDENTIAL
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun SettingsScreen(
|
||||
@ -62,6 +65,25 @@ fun SettingsScreen(
|
||||
else -> context.getString(R.string.det_timeout_minutes, minutes)
|
||||
}
|
||||
|
||||
fun launchExportImportAuth() {
|
||||
val activity = context as? FragmentActivity ?: return
|
||||
val executor = ContextCompat.getMainExecutor(context)
|
||||
val prompt = BiometricPrompt(
|
||||
activity, executor,
|
||||
object : BiometricPrompt.AuthenticationCallback() {
|
||||
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
|
||||
onNavigateToExportImport()
|
||||
}
|
||||
},
|
||||
)
|
||||
BiometricPrompt.PromptInfo.Builder()
|
||||
.setTitle(context.getString(R.string.det_export_import_title))
|
||||
.setSubtitle(context.getString(R.string.det_export_locked_body))
|
||||
.setAllowedAuthenticators(AUTH)
|
||||
.build()
|
||||
.let { prompt.authenticate(it) }
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
@ -170,7 +192,7 @@ fun SettingsScreen(
|
||||
|
||||
// Export / Import link
|
||||
Button(
|
||||
onClick = onNavigateToExportImport,
|
||||
onClick = { launchExportImportAuth() },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
Text(stringResource(R.string.det_export_import_title))
|
||||
|
||||
@ -72,4 +72,6 @@
|
||||
<string name="det_unlock_biometric">Odemknout biometrikou</string>
|
||||
<string name="det_biometric_prompt_title">Odemknout trezor</string>
|
||||
<string name="det_biometric_prompt_subtitle">Ověřte se pro přístup do trezoru</string>
|
||||
<string name="det_export_locked_body">Ověřte se pro zobrazení QR zálohy vašeho device secret.</string>
|
||||
<string name="det_import_biometric_subtitle">Potvrzení nahrazení device secret</string>
|
||||
</resources>
|
||||
|
||||
@ -71,4 +71,6 @@
|
||||
<string name="det_unlock_biometric">Unlock with Biometrics</string>
|
||||
<string name="det_biometric_prompt_title">Unlock Vault</string>
|
||||
<string name="det_biometric_prompt_subtitle">Authenticate to access your vault</string>
|
||||
<string name="det_export_locked_body">Authenticate to view your device secret QR backup.</string>
|
||||
<string name="det_import_biometric_subtitle">Confirm replacing your device secret</string>
|
||||
</resources>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user