Enforce Android lifecycle and presentation security (#100)
Android ARM verification / rust (push) Failing after 47m6s
Android ARM verification / android (push) Failing after 8s
Dependency security audit / rustsec (push) Successful in 23s

This commit is contained in:
hugo committed 2026-10-09 18:59:18 +02:00
1 parent c0ba6d43d8
commit a06f39af23
37 files changed
+1129 -110

No files matched your search

@@ -1,6 +1,5 @@
package de.rfc1437.ironstorage
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.Column
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
@@ -11,6 +10,7 @@ import androidx.test.core.app.ActivityScenario
import androidx.test.platform.app.InstrumentationRegistry
import de.rfc1437.ironstorage.core.AndroidKeystore
import de.rfc1437.ironstorage.core.AuthenticationFailureKind
import de.rfc1437.ironstorage.core.AuthenticationLifecycle
import de.rfc1437.ironstorage.core.KeystoreAuthorization
import de.rfc1437.ironstorage.core.KeystoreCiphertext
import de.rfc1437.ironstorage.core.KeystoreKeySpecification
@@ -47,7 +47,12 @@ class AuthenticationLeaseTest {
private val context = instrumentation.targetContext
@Test fun strongBiometricLeaseCancellationBackgroundAndManualRecovery() = runBlocking {
assumeTrue(InstrumentationRegistry.getArguments().getString("biometricLease") == "true")
val securityOnly =
InstrumentationRegistry.getArguments().getString("biometricSecurity") == "true"
assumeTrue(
securityOnly ||
InstrumentationRegistry.getArguments().getString("biometricLease") == "true"
)
val os = AndroidKeystoreAdapter(context)
assertTrue(
"Enroll a synthetic strong fingerprint for this opt-in test",
@@ -98,7 +103,7 @@ class AuthenticationLeaseTest {
scenario.onActivity {
host = it
os.attach(it)
it.setContent {
it.setProtectedTestContent {
MaterialTheme {
Column {
Text(stage.collectAsState().value)
@@ -140,9 +145,24 @@ class AuthenticationLeaseTest {
request { authentication.unlock() }
instrumentation.waitForIdleSync()
scenario.moveToState(Lifecycle.State.CREATED)
request { authentication.lock() }
request { authentication.lifecycle(AuthenticationLifecycle.Background) }
assertFalse(settled().unlocked)
scenario.moveToState(Lifecycle.State.RESUMED)
if (securityOnly) {
request { authentication.lifecycle(AuthenticationLifecycle.Foreground) }
assertFalse(settled().unlocked)
show("100:unlock-after-background")
request { authentication.unlock() }
assertTrue(settled().unlocked)
request {
authentication.lifecycle(AuthenticationLifecycle.MemoryPressure)
assertFalse(authentication.state.value.unlocked)
}
request { authentication.disable() }
assertFalse(settled().biometricEnabled)
show("100:security-complete")
return@use
}
request { authentication.disable() }
assertFalse(settled().biometricEnabled)
show("98:reenroll")
@@ -1,6 +1,5 @@
package de.rfc1437.ironstorage
import androidx.activity.compose.setContent
import androidx.compose.material3.MaterialTheme
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.assertIsNotEnabled
@@ -26,7 +25,9 @@ class AuthenticationScreenTest {
@Test fun enrollmentRequiresExplicitSubmissionAndTypedSuccessBeforeDisplayingEnabled() {
val repository = Fixture()
compose.activity.runOnUiThread {
compose.activity.setContent { MaterialTheme { AuthenticationControls(repository) } }
compose.activity.setProtectedTestContent {
MaterialTheme { AuthenticationControls(repository) }
}
}
compose.onNodeWithText("Biometric unlock disabled").assertIsDisplayed()
compose.onNodeWithText("Enable biometric unlock").performClick()
@@ -59,7 +60,9 @@ class AuthenticationScreenTest {
@Test fun cancellationBackgroundAndRemountNeverRestoreThePassphrase() {
val repository = Fixture()
fun mount() = compose.activity.runOnUiThread {
compose.activity.setContent { MaterialTheme { AuthenticationControls(repository) } }
compose.activity.setProtectedTestContent {
MaterialTheme { AuthenticationControls(repository) }
}
}
mount()
compose.onNodeWithText("Enable biometric unlock").performClick()
@@ -73,7 +76,9 @@ class AuthenticationScreenTest {
compose.onNodeWithText("GPG passphrase").assertDoesNotExist()
compose.onNodeWithText("Enable biometric unlock").performClick()
compose.onNodeWithText("Continue").assertIsNotEnabled()
compose.activity.runOnUiThread { compose.activity.setContent { MaterialTheme { } } }
compose.activity.runOnUiThread {
compose.activity.setProtectedTestContent { MaterialTheme { } }
}
mount()
compose.onNodeWithText("Enable biometric unlock").performClick()
compose.onNodeWithText("Continue").assertIsNotEnabled()
@@ -82,6 +87,14 @@ class AuthenticationScreenTest {
}
private class Fixture : AuthenticationRepository {
override val presentation = de.rfc1437.ironstorage.core.ProtectedPresentation()
override fun lifecycle(event: de.rfc1437.ironstorage.core.AuthenticationLifecycle) {
if (event !=
de.rfc1437.ironstorage.core.AuthenticationLifecycle.Foreground
) {
lock()
}
}
override val state = MutableStateFlow(AuthenticationState())
var submitted: String? = null
var locked = false
@@ -2,7 +2,6 @@ package de.rfc1437.ironstorage
import android.content.ClipboardManager
import android.view.WindowManager
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.MaterialTheme
@@ -58,7 +57,7 @@ class EntryViewerTest {
@Test fun maskedFieldsExplicitActionsEditorDiagnosticsAndRevocation() {
val repository = Fixture()
compose.activity.runOnUiThread {
compose.activity.setContent {
compose.activity.setProtectedTestContent {
MaterialTheme { PasswordScreen(repository, repository.authentication) }
}
}
@@ -101,7 +100,7 @@ class EntryViewerTest {
val repository = Fixture()
repository.unlock("")
compose.activity.runOnUiThread {
compose.activity.setContent {
compose.activity.setProtectedTestContent {
MaterialTheme { PasswordScreen(repository, repository.authentication) }
}
}
@@ -137,7 +136,7 @@ class EntryViewerTest {
val repository = Fixture()
val done = CountDownLatch(1)
compose.activity.runOnUiThread {
compose.activity.setContent {
compose.activity.setProtectedTestContent {
val density = LocalDensity.current
CompositionLocalProvider(
LocalDensity provides Density(
@@ -184,6 +183,14 @@ class EntryViewerTest {
MutableStateFlow(PasswordState(rows = listOf(row), selected = row, revision = "r"))
val lease = MutableStateFlow(AuthenticationState())
val authentication = object : AuthenticationRepository {
override val presentation = de.rfc1437.ironstorage.core.ProtectedPresentation()
override fun lifecycle(event: de.rfc1437.ironstorage.core.AuthenticationLifecycle) {
if (event !=
de.rfc1437.ironstorage.core.AuthenticationLifecycle.Foreground
) {
lock()
}
}
override val state = lease
override fun reload() = Unit
override fun unlock() = Unit
@@ -1,6 +1,5 @@
package de.rfc1437.ironstorage
import androidx.activity.compose.setContent
import androidx.compose.material3.MaterialTheme
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.assertIsNotEnabled
@@ -40,7 +39,7 @@ class HomeScreenTest {
@get:Rule val compose = createAndroidComposeRule<MainActivity>()
private fun show(repository: Fixture) {
compose.activity.runOnUiThread {
compose.activity.setContent { MaterialTheme { HomeScreen(repository) } }
compose.activity.setProtectedTestContent { MaterialTheme { HomeScreen(repository) } }
}
}
@@ -1,6 +1,5 @@
package de.rfc1437.ironstorage
import androidx.activity.compose.setContent
import androidx.compose.material3.MaterialTheme
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.junit4.v2.createAndroidComposeRule
@@ -26,7 +25,9 @@ class OnboardingScreenTest {
val repository = FixtureRepository()
val secrets = SetupFormViewModel()
compose.activity.runOnUiThread {
compose.activity.setContent { MaterialTheme { OnboardingScreen(repository, secrets) } }
compose.activity.setProtectedTestContent {
MaterialTheme { OnboardingScreen(repository, secrets) }
}
}
compose.waitForIdle()
compose.onNodeWithText("Create local store").performClick()
@@ -2,7 +2,6 @@ package de.rfc1437.ironstorage
import android.content.pm.ActivityInfo
import android.content.res.Configuration
import androidx.activity.compose.setContent
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.input.InputMode
@@ -56,7 +55,7 @@ class PasswordScreenTest {
val repository = Fixture()
lateinit var inputMode: InputModeManager
compose.activity.runOnUiThread {
compose.activity.setContent {
compose.activity.setProtectedTestContent {
inputMode = LocalInputModeManager.current
MaterialTheme { PasswordScreen(repository, repository.authentication) }
}
@@ -125,7 +124,7 @@ class PasswordScreenTest {
repository.activate("folder-id", "root")
repository.activate("entry-id", "folder")
compose.activity.runOnUiThread {
compose.activity.setContent {
compose.activity.setProtectedTestContent {
val density = LocalDensity.current
CompositionLocalProvider(LocalDensity provides Density(density.density, 2f)) {
MaterialTheme { PasswordScreen(repository, repository.authentication) }
@@ -164,6 +163,14 @@ class PasswordScreenTest {
MutableStateFlow(PasswordState(revision = "root", rows = listOf(folder)))
val lease = MutableStateFlow(AuthenticationState())
val authentication = object : AuthenticationRepository {
override val presentation = de.rfc1437.ironstorage.core.ProtectedPresentation()
override fun lifecycle(event: de.rfc1437.ironstorage.core.AuthenticationLifecycle) {
if (event !=
de.rfc1437.ironstorage.core.AuthenticationLifecycle.Foreground
) {
lock()
}
}
override val state = lease
override fun reload() = Unit
override fun unlock() = error("Unexpected direct unlock")
@@ -0,0 +1,224 @@
package de.rfc1437.ironstorage
import android.content.ClipData
import android.content.ClipboardManager
import android.content.ComponentCallbacks2
import android.os.Build
import android.os.Bundle
import android.os.Parcel
import android.view.View
import android.view.WindowManager
import android.view.inputmethod.EditorInfo
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.Column
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.ui.platform.InterceptPlatformTextInput
import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.hasSetTextAction
import androidx.compose.ui.test.junit4.v2.createAndroidComposeRule
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import androidx.lifecycle.ViewModelProvider
import androidx.test.platform.app.InstrumentationRegistry
import de.rfc1437.ironstorage.features.ProtectedContent
import de.rfc1437.ironstorage.features.SetupFormViewModel
import de.rfc1437.ironstorage.platform.AndroidSecretClipboard
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicReference
import kotlinx.coroutines.awaitCancellation
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.Assume.assumeTrue
import org.junit.Rule
import org.junit.Test
@OptIn(ExperimentalTestApi::class)
class PresentationSecurityTest {
@get:Rule val compose = createAndroidComposeRule<MainActivity>()
@Test fun inspectTaskSwitchingWithDisposableProtectedInput() {
assumeTrue(InstrumentationRegistry.getArguments().getString("securityInspect") == "true")
val done = CountDownLatch(1)
lateinit var form: SetupFormViewModel
compose.runOnIdle {
form = ViewModelProvider(compose.activity)[SetupFormViewModel::class.java]
form.passphrase = "public-task-switch-marker-100"
compose.activity.setProtectedTestContent {
MaterialTheme {
Column {
Text("Task switching security fixture")
OutlinedTextField(
form.passphrase,
{ form.passphrase = it },
label = { Text("Disposable protected input") }
)
if (form.passphrase.isEmpty()) Text("Protected value cleared")
TextButton(onClick = {
done.countDown()
}) { Text("Finish security inspection") }
}
}
}
}
val started = android.os.SystemClock.elapsedRealtime()
while (!done.await(100, TimeUnit.MILLISECONDS)) {
compose.mainClock.advanceTimeBy(100)
compose.waitForIdle()
assertTrue(android.os.SystemClock.elapsedRealtime() - started < 600000)
}
compose.runOnIdle {
assertEquals("", form.passphrase)
assertFalse(
compose.activity.window.decorView.importantForAccessibility ==
View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS
)
}
}
@Test fun inputSessionsRequestNoLearningAndNoKeyboardExtractWindow() {
val attributes = AtomicReference<EditorInfo?>()
compose.runOnIdle {
compose.activity.setContent {
InterceptPlatformTextInput(interceptor = { request, _ ->
val info = EditorInfo()
val connection = request.createInputConnection(info)
attributes.set(info)
try {
awaitCancellation()
} finally {
connection.closeConnection()
}
}) {
ProtectedContent(compose.activity.authentication.presentation) {
MaterialTheme {
OutlinedTextField("", {}, label = { Text("Public input") })
}
}
}
}
}
compose.onNode(hasSetTextAction()).performClick()
compose.waitUntil(10000) { attributes.get() != null }
val flags = attributes.get()!!.imeOptions
assertTrue(flags and EditorInfo.IME_FLAG_NO_PERSONALIZED_LEARNING != 0)
assertTrue(flags and EditorInfo.IME_FLAG_NO_FULLSCREEN != 0)
assertTrue(flags and EditorInfo.IME_FLAG_NO_EXTRACT_UI != 0)
}
@Test fun clipboardOwnershipPreservesIdenticalNewerCopiesAndUserContent() {
compose.runOnIdle {
val manager = compose.activity.getSystemService(ClipboardManager::class.java)
val first = AndroidSecretClipboard(compose.activity).beginCopy()
val second = AndroidSecretClipboard(compose.activity).beginCopy()
first.write("public-same-value".toByteArray())
second.write("public-same-value".toByteArray())
assertNull(first.read())
first.clear()
assertEquals("public-same-value", second.read()!!.toString(Charsets.UTF_8))
manager.setPrimaryClip(ClipData.newPlainText("User", "public-same-value"))
second.clear()
assertEquals("public-same-value", manager.primaryClip!!.getItemAt(0).text.toString())
assertNull(second.read())
first.write("public-restored-value".toByteArray())
val restored = ClipData(manager.primaryClip!!)
android.os.SystemClock.sleep(2)
manager.setPrimaryClip(restored)
assertNull(first.read())
first.clear()
assertEquals(
"public-restored-value",
manager.primaryClip!!.getItemAt(0).text.toString()
)
second.write("owned public value".toByteArray())
second.clear()
assertTrue(
manager.primaryClip == null ||
manager.primaryClip!!.getItemAt(0).text.isNullOrEmpty()
)
}
}
@Suppress("DEPRECATION")
@Test
fun memoryPressureMasksImmediatelyAndRecreationNeverSavesSetupSecrets() {
lateinit var form: SetupFormViewModel
compose.runOnIdle {
form = ViewModelProvider(compose.activity)[SetupFormViewModel::class.java]
compose.activity.setProtectedTestContent {
MaterialTheme { Text("Public security fixture") }
}
}
compose.onNodeWithText("Public security fixture").assertIsDisplayed()
fun populate() {
form.passphrase = "public-passphrase-marker-100"
form.armor = "public-armor-marker-100"
form.token = "public-token-marker-100"
}
fun assertCleared() {
assertEquals("", form.passphrase)
assertEquals("", form.armor)
assertEquals("", form.token)
}
compose.runOnIdle {
populate()
compose.activity.onWindowFocusChanged(false)
assertCleared()
compose.activity.onWindowFocusChanged(true)
}
compose.runOnIdle {
val window = compose.activity.window
populate()
(compose.activity.application as IronStorageApplication).onTrimMemory(
ComponentCallbacks2.TRIM_MEMORY_RUNNING_LOW
)
assertCleared()
assertEquals(
View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS,
window.decorView.importantForAccessibility
)
assertTrue(
window.attributes.flags and WindowManager.LayoutParams.FLAG_SECURE !=
0
)
assertEquals(
View.IMPORTANT_FOR_AUTOFILL_NO_EXCLUDE_DESCENDANTS,
window.decorView.importantForAutofill
)
if (Build.VERSION.SDK_INT >= 30) {
assertEquals(
View.IMPORTANT_FOR_CONTENT_CAPTURE_NO_EXCLUDE_DESCENDANTS,
window.decorView.importantForContentCapture
)
}
}
compose.onNodeWithText("Public security fixture").assertIsDisplayed()
compose.runOnIdle {
populate()
val saved = Bundle()
InstrumentationRegistry.getInstrumentation().callActivityOnSaveInstanceState(
compose.activity,
saved
)
val parcel = Parcel.obtain()
try {
parcel.writeBundle(saved)
val bytes = parcel.marshall()
for (marker in listOf(form.passphrase, form.armor, form.token)) {
assertFalse(bytes.toString(Charsets.UTF_8).contains(marker))
assertFalse(bytes.toString(Charsets.UTF_16LE).contains(marker))
}
} finally {
parcel.recycle()
}
}
compose.activityRule.scenario.recreate()
compose.runOnIdle { assertCleared() }
}
}
@@ -0,0 +1,10 @@
package de.rfc1437.ironstorage
import androidx.activity.compose.setContent
import androidx.compose.runtime.Composable
import de.rfc1437.ironstorage.features.ProtectedContent
/** Keep the production surface and IME protections around substituted test screens. */
internal fun MainActivity.setProtectedTestContent(content: @Composable () -> Unit) {
setContent { ProtectedContent(authentication.presentation, content) }
}
+2 -1
View File
@@ -11,7 +11,8 @@
android:supportsRtl="true"
android:theme="@style/Theme.IronStorage"
android:usesCleartextTraffic="false">
<activity android:name=".MainActivity" android:exported="true">
<activity android:name=".MainActivity" android:exported="true"
android:supportsPictureInPicture="false">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
@@ -2,6 +2,27 @@ package de.rfc1437.ironstorage
import android.app.Application
import dagger.hilt.android.HiltAndroidApp
import de.rfc1437.ironstorage.core.AuthenticationLifecycle
import de.rfc1437.ironstorage.core.AuthenticationRepository
import javax.inject.Inject
@HiltAndroidApp
class IronStorageApplication : Application()
class IronStorageApplication : Application() {
@Inject lateinit var authentication: AuthenticationRepository
override fun onCreate() {
super.onCreate()
authentication.lifecycle(AuthenticationLifecycle.ProcessRecreated)
}
@Suppress("DEPRECATION", "OVERRIDE_DEPRECATION")
override fun onTrimMemory(level: Int) {
authentication.lifecycle(AuthenticationLifecycle.MemoryPressure)
super.onTrimMemory(level)
}
@Suppress("DEPRECATION", "OVERRIDE_DEPRECATION")
override fun onLowMemory() {
authentication.lifecycle(AuthenticationLifecycle.MemoryPressure)
super.onLowMemory()
}
}
@@ -1,6 +1,14 @@
package de.rfc1437.ironstorage
import android.app.KeyguardManager
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.os.Build
import android.os.Bundle
import android.view.Display
import android.view.View
import android.view.WindowManager
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
@@ -8,9 +16,11 @@ import androidx.activity.viewModels
import androidx.compose.material3.MaterialTheme
import androidx.fragment.app.FragmentActivity
import dagger.hilt.android.AndroidEntryPoint
import de.rfc1437.ironstorage.core.AuthenticationLifecycle
import de.rfc1437.ironstorage.core.AuthenticationRepository
import de.rfc1437.ironstorage.core.PasswordRepository
import de.rfc1437.ironstorage.core.OnboardingRepository
import de.rfc1437.ironstorage.core.ShellRepository
import de.rfc1437.ironstorage.features.ProtectedContent
import de.rfc1437.ironstorage.features.SetupFormViewModel
import de.rfc1437.ironstorage.features.ShellViewModel
import de.rfc1437.ironstorage.platform.AndroidKeystoreAdapter
@@ -24,23 +34,69 @@ class MainActivity : FragmentActivity() {
@Inject lateinit var authentication: AuthenticationRepository
@Inject lateinit var passwords: PasswordRepository
@Inject lateinit var onboarding: OnboardingRepository
private val setupForm: SetupFormViewModel by viewModels()
private val viewModel: ShellViewModel by viewModels()
private var cleanup: AutoCloseable? = null
private val protectedState = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
if (intent.action == Intent.ACTION_SCREEN_OFF) {
authentication.lifecycle(AuthenticationLifecycle.DeviceLocked)
}
}
}
override fun onPause() {
authentication.lifecycle(AuthenticationLifecycle.Background)
super.onPause()
}
override fun onUserLeaveHint() {
authentication.lifecycle(AuthenticationLifecycle.Background)
super.onUserLeaveHint()
}
override fun onWindowFocusChanged(hasFocus: Boolean) {
super.onWindowFocusChanged(hasFocus)
if (!hasFocus && !keystore.authenticationPending &&
!authentication.presentation.hasOwnedWindow
) {
authentication.lifecycle(AuthenticationLifecycle.Background)
}
}
override fun onStop() {
passwords.lock()
authentication.lifecycle(AuthenticationLifecycle.Background)
super.onStop()
}
override fun onResume() {
super.onResume()
if (getSystemService(KeyguardManager::class.java).isDeviceLocked) {
authentication.lifecycle(AuthenticationLifecycle.DeviceLocked)
} else {
authentication.lifecycle(AuthenticationLifecycle.Foreground)
}
}
override fun onTopResumedActivityChanged(isTopResumedActivity: Boolean) {
super.onTopResumedActivityChanged(isTopResumedActivity)
if (!isTopResumedActivity && !keystore.authenticationPending) {
authentication.lifecycle(AuthenticationLifecycle.Background)
}
}
override fun onUserInteraction() {
authentication.touch()
super.onUserInteraction()
}
override fun onDestroy() {
cleanup?.close()
unregisterReceiver(protectedState)
keystore.detach(this)
super.onDestroy()
}
@@ -48,11 +104,37 @@ class MainActivity : FragmentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
window.addFlags(WindowManager.LayoutParams.FLAG_SECURE)
if (Build.VERSION.SDK_INT >= 33) setRecentsScreenshotEnabled(false)
window.decorView.importantForAutofill = View.IMPORTANT_FOR_AUTOFILL_NO_EXCLUDE_DESCENDANTS
if (Build.VERSION.SDK_INT >= 30) {
window.decorView.importantForContentCapture =
View.IMPORTANT_FOR_CONTENT_CAPTURE_NO_EXCLUDE_DESCENDANTS
}
cleanup = authentication.presentation.register {
setupForm.clearSecrets()
onboarding.cancel()
}
val filter = IntentFilter(Intent.ACTION_SCREEN_OFF)
if (Build.VERSION.SDK_INT >= 33) {
registerReceiver(protectedState, filter, RECEIVER_NOT_EXPORTED)
} else {
@Suppress("UnspecifiedRegisterReceiverFlag")
registerReceiver(protectedState, filter)
}
@Suppress("DEPRECATION")
val externalDisplay = windowManager.defaultDisplay.displayId != Display.DEFAULT_DISPLAY
if (externalDisplay) {
authentication.lifecycle(AuthenticationLifecycle.Background)
finish()
return
}
keystore.attach(this)
enableEdgeToEdge()
setContent {
MaterialTheme {
VariantContent(viewModel, repository, setupForm)
ProtectedContent(authentication.presentation) {
MaterialTheme {
VariantContent(viewModel, repository, setupForm)
}
}
}
}