Enforce Android lifecycle and presentation security (#100)
This commit is contained in:
1 parent
c0ba6d43d8
commit
a06f39af23
37 files changed
+1129
-110
No files matched your search
+24
-4
@@ -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")
|
||||
|
||||
+17
-4
@@ -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")
|
||||
|
||||
+224
@@ -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) }
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user