Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: Android fix unlock crash #2297

Merged
merged 9 commits into from
Jan 22, 2024
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
Loading