diff --git a/android/app/src/main/kotlin/com/flagship/abox/manager/ui/ABoxManagerApp.kt b/android/app/src/main/kotlin/com/flagship/abox/manager/ui/ABoxManagerApp.kt index 5ced03b..5f41001 100644 --- a/android/app/src/main/kotlin/com/flagship/abox/manager/ui/ABoxManagerApp.kt +++ b/android/app/src/main/kotlin/com/flagship/abox/manager/ui/ABoxManagerApp.kt @@ -16,6 +16,7 @@ import com.flagship.abox.manager.designsystem.ABoxSurface import com.flagship.abox.manager.designsystem.ABoxTheme import com.flagship.abox.manager.ui.authenticated.AuthenticatedPlaceholderScreen import com.flagship.abox.manager.ui.login.LoginRoute +import com.flagship.abox.manager.ui.login.LoginViewModel import com.flagship.abox.manager.ui.provider.ProviderSelectionRoute import com.flagship.abox.manager.ui.session.AppSessionUiState import com.flagship.abox.manager.ui.session.AppSessionViewModel @@ -25,12 +26,16 @@ import com.flagship.abox.manager.ui.session.SessionActionsViewModel fun ABoxManagerApp( appSessionViewModel: AppSessionViewModel = hiltViewModel(), sessionActionsViewModel: SessionActionsViewModel = hiltViewModel(), + loginViewModel: LoginViewModel = hiltViewModel(), ) { val actionState = sessionActionsViewModel.uiState.collectAsStateWithLifecycle().value val sessionState = appSessionViewModel.uiState.collectAsStateWithLifecycle().value val screenKey = sessionState.screenKey() LaunchedEffect(screenKey) { sessionActionsViewModel.dismissError() + if (sessionState !is AppSessionUiState.Login) { + loginViewModel.clearSensitiveState() + } } ABoxTheme { ABoxSurface(modifier = Modifier.fillMaxSize()) { @@ -50,6 +55,7 @@ fun ABoxManagerApp( actionError = actionState.errorMessage, actionInProgress = actionState.inProgress, onSwitchProvider = sessionActionsViewModel::switchProvider, + viewModel = loginViewModel, ) is AppSessionUiState.Authenticated -> AuthenticatedPlaceholderScreen( profile = state.profile, diff --git a/android/app/src/main/kotlin/com/flagship/abox/manager/ui/login/LoginScreen.kt b/android/app/src/main/kotlin/com/flagship/abox/manager/ui/login/LoginScreen.kt index 704bdbd..c0adf70 100644 --- a/android/app/src/main/kotlin/com/flagship/abox/manager/ui/login/LoginScreen.kt +++ b/android/app/src/main/kotlin/com/flagship/abox/manager/ui/login/LoginScreen.kt @@ -21,7 +21,6 @@ import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable -import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -51,9 +50,6 @@ fun LoginRoute( viewModel: LoginViewModel = hiltViewModel(), ) { LaunchedEffect(profile.id) { viewModel.loadProfile(profile) } - DisposableEffect(profile.id) { - onDispose(viewModel::clearSensitiveState) - } val state = viewModel.uiState.collectAsStateWithLifecycle().value val errorText = state.errorMessage?.let { stringResource(it.stringResource()) } ?: actionError diff --git a/android/app/src/main/kotlin/com/flagship/abox/manager/ui/session/AppSessionViewModel.kt b/android/app/src/main/kotlin/com/flagship/abox/manager/ui/session/AppSessionViewModel.kt index 34da97a..323c1c3 100644 --- a/android/app/src/main/kotlin/com/flagship/abox/manager/ui/session/AppSessionViewModel.kt +++ b/android/app/src/main/kotlin/com/flagship/abox/manager/ui/session/AppSessionViewModel.kt @@ -98,7 +98,7 @@ class AppSessionViewModel @Inject constructor( ): AppSessionUiState = when { profile == null -> AppSessionUiState.ProviderSelection availability.forType(profile.type) is Availability.Disabled -> AppSessionUiState.ProviderSelection - session != null -> AppSessionUiState.Authenticated(profile) + session?.profileId == profile.id -> AppSessionUiState.Authenticated(profile) else -> AppSessionUiState.Login(profile, bootstrapError) } } diff --git a/android/core/session/src/main/kotlin/com/flagship/abox/manager/session/credential/AndroidKeystoreCredentialCipher.kt b/android/core/session/src/main/kotlin/com/flagship/abox/manager/session/credential/AndroidKeystoreCredentialCipher.kt index 13b4f7c..bcb7234 100644 --- a/android/core/session/src/main/kotlin/com/flagship/abox/manager/session/credential/AndroidKeystoreCredentialCipher.kt +++ b/android/core/session/src/main/kotlin/com/flagship/abox/manager/session/credential/AndroidKeystoreCredentialCipher.kt @@ -40,6 +40,11 @@ class AndroidKeystoreCredentialCipher( return cipher.doFinal(ciphertext) } + override fun resetPermanentlyInvalidatedKey() { + val keyStore = KeyStore.getInstance(ANDROID_KEY_STORE).apply { load(null) } + if (keyStore.containsAlias(keyAlias)) keyStore.deleteEntry(keyAlias) + } + private fun secretKey(): SecretKey { val keyStore = KeyStore.getInstance(ANDROID_KEY_STORE).apply { load(null) } (keyStore.getKey(keyAlias, null) as? SecretKey)?.let { return it } diff --git a/android/core/session/src/main/kotlin/com/flagship/abox/manager/session/credential/CredentialCipher.kt b/android/core/session/src/main/kotlin/com/flagship/abox/manager/session/credential/CredentialCipher.kt index ea3fbab..5101278 100644 --- a/android/core/session/src/main/kotlin/com/flagship/abox/manager/session/credential/CredentialCipher.kt +++ b/android/core/session/src/main/kotlin/com/flagship/abox/manager/session/credential/CredentialCipher.kt @@ -6,4 +6,6 @@ interface CredentialCipher { fun encrypt(profileId: ProviderProfileId, plaintext: ByteArray): ByteArray fun decrypt(profileId: ProviderProfileId, encrypted: ByteArray): ByteArray + + fun resetPermanentlyInvalidatedKey() } diff --git a/android/core/session/src/main/kotlin/com/flagship/abox/manager/session/credential/KeystoreCredentialStore.kt b/android/core/session/src/main/kotlin/com/flagship/abox/manager/session/credential/KeystoreCredentialStore.kt index a989ad0..7f6366c 100644 --- a/android/core/session/src/main/kotlin/com/flagship/abox/manager/session/credential/KeystoreCredentialStore.kt +++ b/android/core/session/src/main/kotlin/com/flagship/abox/manager/session/credential/KeystoreCredentialStore.kt @@ -46,6 +46,9 @@ class KeystoreCredentialStore( } catch (error: Exception) { if (error.isPermanentCredentialFailure()) { try { + if (error.hasPermanentlyInvalidatedKey()) { + cipher.resetPermanentlyInvalidatedKey() + } dataStore.edit { it.remove(credentialKey(profileId)) } DomainResult.Success(null) } catch (cleanupError: CancellationException) { @@ -122,6 +125,9 @@ private fun DataInputStream.readByteArray(maxSize: Int): ByteArray { return ByteArray(length).also(::readFully) } +private fun Throwable.hasPermanentlyInvalidatedKey(): Boolean = + this is KeyPermanentlyInvalidatedException || cause?.hasPermanentlyInvalidatedKey() == true + private fun Throwable.isPermanentCredentialFailure(): Boolean = when (this) { is AEADBadTagException, is KeyPermanentlyInvalidatedException, diff --git a/android/core/session/src/test/kotlin/com/flagship/abox/manager/session/CredentialCleanupFailureTest.kt b/android/core/session/src/test/kotlin/com/flagship/abox/manager/session/CredentialCleanupFailureTest.kt index 6373da6..50554ca 100644 --- a/android/core/session/src/test/kotlin/com/flagship/abox/manager/session/CredentialCleanupFailureTest.kt +++ b/android/core/session/src/test/kotlin/com/flagship/abox/manager/session/CredentialCleanupFailureTest.kt @@ -39,5 +39,7 @@ class CredentialCleanupFailureTest { override fun encrypt(profileId: ProviderProfileId, plaintext: ByteArray): ByteArray = plaintext override fun decrypt(profileId: ProviderProfileId, encrypted: ByteArray): ByteArray = encrypted + + override fun resetPermanentlyInvalidatedKey() = Unit } } diff --git a/android/core/session/src/test/kotlin/com/flagship/abox/manager/session/CredentialStoreTest.kt b/android/core/session/src/test/kotlin/com/flagship/abox/manager/session/CredentialStoreTest.kt index c34e1c4..b912d27 100644 --- a/android/core/session/src/test/kotlin/com/flagship/abox/manager/session/CredentialStoreTest.kt +++ b/android/core/session/src/test/kotlin/com/flagship/abox/manager/session/CredentialStoreTest.kt @@ -69,6 +69,8 @@ class CredentialStoreTest { override fun decrypt(profileId: ProviderProfileId, encrypted: ByteArray): ByteArray { throw IllegalArgumentException("Credential payload is truncated") } + + override fun resetPermanentlyInvalidatedKey() = Unit } private class ReversingCipher : CredentialCipher { @@ -81,5 +83,7 @@ class CredentialStoreTest { require(decoded.copyOfRange(0, delimiter).decodeToString() == profileId.value) return decoded.copyOfRange(delimiter + 1, decoded.size) } + + override fun resetPermanentlyInvalidatedKey() = Unit } }