Skip to content

Commit

Permalink
fix: Android fix unlock crash (#2297)
Browse files Browse the repository at this point in the history
* work in progress - catching errors when storage created to pass it to UI
fixes #2272

* seed storage error handling implemented in seed storage

* passing seed storage errors to the UI and show error

* fixed feature toggle, migrated logs to timber

* added error state for global nav state

* removed outdated log
  • Loading branch information
Dmitry-Borodin authored Jan 22, 2024
1 parent f7393f5 commit b042fa8
Show file tree
Hide file tree
Showing 14 changed files with 199 additions and 78 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ import androidx.fragment.app.FragmentActivity
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import io.parity.signer.dependencygraph.ServiceLocator
import io.parity.signer.domain.backend.OperationResult
import io.parity.signer.domain.usecases.ResetUseCase
import io.parity.signer.screens.error.ErrorStateDestinationState
import io.parity.signer.uniffi.*
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
Expand All @@ -27,17 +29,16 @@ class MainFlowViewModel() : ViewModel() {
val activity: FragmentActivity
get() = ServiceLocator.activityScope!!.activity

fun onUnlockClicked() {
viewModelScope.launch {
when (authentication.authenticate(activity)) {
suspend fun onUnlockClicked(): OperationResult<Unit, ErrorStateDestinationState> {
return when (authentication.authenticate(activity)) {
AuthResult.AuthSuccess -> resetUseCase.totalRefresh()
AuthResult.AuthError,
AuthResult.AuthFailed,
AuthResult.AuthUnavailable -> {
Timber.e("Signer", "Auth failed, not unlocked")
OperationResult.Ok(Unit)
}
}
}
}

val authenticated: StateFlow<Boolean> = authentication.auth
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,12 @@ import android.content.Intent
import android.content.IntentFilter
import android.net.wifi.WifiManager
import android.provider.Settings
import android.util.Log
import io.parity.signer.domain.backend.UniffiInteractor
import io.parity.signer.uniffi.historyAcknowledgeWarnings
import io.parity.signer.uniffi.historyGetWarnings
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import timber.log.Timber


class NetworkExposedStateKeeper(
Expand Down Expand Up @@ -89,7 +89,6 @@ class NetworkExposedStateKeeper(
val intentFilter = IntentFilter("android.hardware.usb.action.USB_STATE")
val receiver: BroadcastReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
Log.e("TAGG", "usb broadcast")
reactOnUsb(intent)
}
}
Expand Down Expand Up @@ -134,7 +133,7 @@ class NetworkExposedStateKeeper(

private fun reactOnUsb(usbIntent: Intent) {
if (FeatureFlags.isEnabled(FeatureOption.SKIP_USB_CHECK)) {
_usbDisconnected.value = false
_usbDisconnected.value = true
updateGeneralAirgapState()
return
}
Expand All @@ -149,7 +148,7 @@ class NetworkExposedStateKeeper(
updateGeneralAirgapState()
}
null -> {
Log.d("USB", "usb action intent doesn't have connection state")
Timber.d("USB", "usb action intent doesn't have connection state")
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,19 @@ import android.security.keystore.UserNotAuthenticatedException
import timber.log.Timber
import androidx.security.crypto.EncryptedSharedPreferences
import androidx.security.crypto.MasterKey
import io.parity.signer.R
import io.parity.signer.domain.FeatureFlags
import io.parity.signer.domain.FeatureOption
import io.parity.signer.domain.backend.OperationResult
import io.parity.signer.screens.error.ErrorStateDestinationState
import io.parity.signer.uniffi.ErrorDisplayed
import io.parity.signer.uniffi.historySeedWasShown
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import java.security.UnrecoverableKeyException
import javax.crypto.AEADBadTagException


/**
Expand Down Expand Up @@ -42,7 +48,7 @@ class SeedStorage {
/**
* @throws UserNotAuthenticatedException
*/
fun init(appContext: Context) {
fun init(appContext: Context): OperationResult<Unit, ErrorStateDestinationState> {
hasStrongbox = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
appContext
.packageManager
Expand Down Expand Up @@ -76,21 +82,26 @@ class SeedStorage {

Timber.e("ENCRY", "$appContext $KEYSTORE_NAME $masterKey")
//we need to be authenticated for this
sharedPreferences =
if (FeatureFlags.isEnabled(FeatureOption.SKIP_UNLOCK_FOR_DEVELOPMENT)) {
appContext.getSharedPreferences(
"FeatureOption.SKIP_UNLOCK_FOR_DEVELOPMENT",
Context.MODE_PRIVATE
)
} else {
EncryptedSharedPreferences(
appContext,
KEYSTORE_NAME,
masterKey,
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)
}
try {
sharedPreferences =
if (FeatureFlags.isEnabled(FeatureOption.SKIP_UNLOCK_FOR_DEVELOPMENT)) {
appContext.getSharedPreferences(
"FeatureOption.SKIP_UNLOCK_FOR_DEVELOPMENT",
Context.MODE_PRIVATE
)
} else {
EncryptedSharedPreferences(
appContext,
KEYSTORE_NAME,
masterKey,
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)
}
} catch (e: Exception) {
return OperationResult.Err(consumeStorageAuthError(e, appContext))
}
return OperationResult.Ok(Unit)
}


Expand Down Expand Up @@ -174,6 +185,38 @@ class SeedStorage {
fun wipe() {
sharedPreferences.edit().clear().commit() // No, not apply(), do it now!
}
}


private fun consumeStorageAuthError(e: Exception, context: Context): ErrorStateDestinationState {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
when (e) {
is AEADBadTagException,
is android.security.KeyStoreException,
is UnrecoverableKeyException -> {
return ErrorStateDestinationState(
argHeader = context.getString(R.string.error_secure_storage_title),
argDescription = context.getString(R.string.error_secure_storage_description),
argVerbose = e.stackTraceToString()
)
}
else -> throw e
}
} else {
when (e) {
is AEADBadTagException,
is UnrecoverableKeyException -> {
return ErrorStateDestinationState(
argHeader = context.getString(R.string.error_secure_storage_title),
argDescription = context.getString(R.string.error_secure_storage_description),
argVerbose = e.stackTraceToString()
)
}
else -> throw e
}
}
}





Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
package io.parity.signer.domain.usecases

import android.widget.Toast
import androidx.fragment.app.FragmentActivity
import io.parity.signer.R
import io.parity.signer.dependencygraph.ServiceLocator
import io.parity.signer.domain.AuthResult
import io.parity.signer.domain.Callback
import io.parity.signer.domain.backend.OperationResult
import io.parity.signer.domain.getDbNameFromContext
import io.parity.signer.domain.isDbCreatedAndOnboardingPassed
import io.parity.signer.domain.storage.DatabaseAssetsInteractor
import io.parity.signer.screens.error.ErrorStateDestinationState
import io.parity.signer.uniffi.historyInitHistoryNoCert
import io.parity.signer.uniffi.historyInitHistoryWithCert
import io.parity.signer.uniffi.initNavigation
Expand All @@ -22,26 +27,52 @@ class ResetUseCase {
private val activity: FragmentActivity
get() = ServiceLocator.activityScope!!.activity

fun wipeToFactoryWithAuth(onAfterWide: Callback) {
suspend fun wipeToFactoryWithAuth(onAfterWipe: Callback): OperationResult<Unit, ErrorStateDestinationState> {
val authentication = ServiceLocator.authentication
authentication.authenticate(activity) {
databaseAssetsInteractor.wipe()
totalRefresh()
onAfterWide()
return when (authentication.authenticate(activity)) {
AuthResult.AuthError,
AuthResult.AuthFailed ,
AuthResult.AuthUnavailable -> {
Toast.makeText(
activity.baseContext,
activity.baseContext.getString(R.string.auth_failed_message),
Toast.LENGTH_SHORT
).show()
OperationResult.Ok(Unit)
}
AuthResult.AuthSuccess -> {
databaseAssetsInteractor.wipe()
val result = totalRefresh()
onAfterWipe()
return result
}
}
}

/**
* Auth user and wipe Vault to state without general verifier certificate
*/
fun wipeNoGeneralCertWithAuth(onAfterWide: Callback) {
suspend fun wipeNoGeneralCertWithAuth(onAfterWide: Callback): OperationResult<Unit, ErrorStateDestinationState> {
val authentication = ServiceLocator.authentication
authentication.authenticate(activity) {
databaseAssetsInteractor.wipe()
databaseAssetsInteractor.copyAsset("")
totalRefresh()
historyInitHistoryNoCert()
onAfterWide()
return when (authentication.authenticate(activity)) {
AuthResult.AuthError,
AuthResult.AuthFailed,
AuthResult.AuthUnavailable -> {
Toast.makeText(
activity.baseContext,
activity.baseContext.getString(R.string.auth_failed_message),
Toast.LENGTH_SHORT
).show()
OperationResult.Ok(Unit)
}
AuthResult.AuthSuccess -> {
databaseAssetsInteractor.wipe()
databaseAssetsInteractor.copyAsset("")
val result = totalRefresh()
historyInitHistoryNoCert()
onAfterWide()
return result
}
}
}

Expand All @@ -67,14 +98,18 @@ class ResetUseCase {
* This returns the app into starting state;
* Do not forget to reset navigation UI state!
*/
fun totalRefresh() {
fun totalRefresh(): OperationResult<Unit, ErrorStateDestinationState> {
if (!seedStorage.isInitialized()) {
seedStorage.init(appContext)
val result = seedStorage.init(appContext)
if (result is OperationResult.Err) {
return result
}
}
if (!appContext.isDbCreatedAndOnboardingPassed()) {
initAssetsAndTotalRefresh()
} else {
totalRefreshDbExist()
}
return OperationResult.Ok(Unit)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,11 @@ inline fun <reified T> UniffiResult<T>.handleErrorAppState(coreNavController: Na
return this.toOperationResult().handleErrorAppState(coreNavController)
}

data class ErrorStateDestinationState(
val argHeader: String,
val argDescription: String,
val argVerbose: String,
)

inline fun <reified T, E> OperationResult<T, E>.handleErrorAppState(
coreNavController: NavController
Expand All @@ -75,6 +80,13 @@ inline fun <reified T, E> OperationResult<T, E>.handleErrorAppState(
is OperationResult.Err -> {
coreNavController.navigate(
when (error) {
is ErrorStateDestinationState -> {
CoreUnlockedNavSubgraph.ErrorScreenGeneral.destination(
argHeader = error.argHeader,
argDescription = error.argDescription,
argVerbose = error.argVerbose,
)
}
is NavigationError -> {
CoreUnlockedNavSubgraph.ErrorScreenGeneral.destination(
argHeader = "Operation navigation error trying to get ${T::class.java}",
Expand All @@ -88,6 +100,7 @@ inline fun <reified T, E> OperationResult<T, E>.handleErrorAppState(
is ErrorDisplayed.DbSchemaMismatch -> {
CoreUnlockedNavSubgraph.errorWrongDbVersionUpdate
}

else -> {
CoreUnlockedNavSubgraph.ErrorScreenGeneral.destination(
argHeader = "Operation error to get ${T::class.java}",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ import io.parity.signer.ui.mainnavigation.CoreUnlockedNavSubgraph
* all subsequent interactions should be in modals or drop-down menus
*/
fun NavGraphBuilder.settingsFullSubgraph(
navController: NavController,
coreNavController: NavController,
) {
navigation(
route = CoreUnlockedNavSubgraph.settings,
Expand All @@ -40,32 +40,32 @@ fun NavGraphBuilder.settingsFullSubgraph(
enterTransition = { fadeIn(animationSpec = tween(700)) },
exitTransition = { fadeOut(animationSpec = tween(700)) },
) {
SettingsGeneralNavSubgraph(parentNavController = navController)
SettingsGeneralNavSubgraph(coreNavController = coreNavController)
}
composable(SettingsNavSubgraph.terms) {
Box(modifier = Modifier.statusBarsPadding()) {
TosScreen(onBack = {
navController.popBackStack(SettingsNavSubgraph.home, false)
coreNavController.popBackStack(SettingsNavSubgraph.home, false)
})
}
}
composable(SettingsNavSubgraph.privacyPolicy) {
Box(modifier = Modifier.statusBarsPadding()) {
PpScreen(onBack = {
navController.popBackStack(SettingsNavSubgraph.home, false)
coreNavController.popBackStack(SettingsNavSubgraph.home, false)
})
}
}
composable(SettingsNavSubgraph.backup) {
SeedBackupIntegratedScreen(navController) {
navController.popBackStack(SettingsNavSubgraph.home, false)
SeedBackupIntegratedScreen(coreNavController) {
coreNavController.popBackStack(SettingsNavSubgraph.home, false)
}
}
logsNavigationSubgraph(
navController = navController,
navController = coreNavController,
)
networkListDestination(navController)
verifierSettingsDestination(navController)
networkListDestination(coreNavController)
verifierSettingsDestination(coreNavController)
composable(
route = SettingsNavSubgraph.NetworkDetails.route,
arguments = listOf(
Expand All @@ -78,10 +78,10 @@ fun NavGraphBuilder.settingsFullSubgraph(
it.arguments?.getString(SettingsNavSubgraph.NetworkDetails.networkKey)!!
NetworkDetailsSubgraph(
networkKey,
navController,
coreNavController,
)
}
signSpecsDestination(navController)
signSpecsDestination(coreNavController)
}
}

Expand Down
Loading

0 comments on commit b042fa8

Please sign in to comment.