Skip to content

Commit

Permalink
Extract SecureArea-specific UI into an interface
Browse files Browse the repository at this point in the history
  • Loading branch information
mitrejcevski committed Nov 8, 2023
1 parent 60fcadc commit 600f62d
Show file tree
Hide file tree
Showing 49 changed files with 1,514 additions and 1,386 deletions.
24 changes: 24 additions & 0 deletions appholder/src/main/java/com/android/identity/wallet/HolderApp.kt
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
package com.android.identity.wallet

import android.app.Application
import android.content.Context
import com.android.identity.android.securearea.AndroidKeystoreSecureArea
import com.android.identity.android.storage.AndroidStorageEngine
import com.android.identity.android.util.AndroidLogPrinter
import com.android.identity.credential.CredentialStore
import com.android.identity.securearea.SecureAreaRepository
import com.android.identity.securearea.SoftwareSecureArea
import com.android.identity.util.Logger
import com.android.identity.wallet.util.PeriodicKeysRefreshWorkRequest
import com.android.identity.wallet.util.PreferencesHelper
import com.android.identity.wallet.util.ProvisioningUtil
import com.google.android.material.color.DynamicColors
import org.bouncycastle.jce.provider.BouncyCastleProvider
import java.security.Security
Expand All @@ -22,4 +29,21 @@ class HolderApp: Application() {
PreferencesHelper.initialize(this)
PeriodicKeysRefreshWorkRequest(this).schedulePeriodicKeysRefreshing()
}

companion object {
fun createCredentialStore(
context: Context,
keystoreEngineRepository: SecureAreaRepository
): CredentialStore {
val storageDir = PreferencesHelper.getKeystoreBackedStorageLocation(context)
val storageEngine = AndroidStorageEngine.Builder(context, storageDir).build()

val androidKeystoreSecureArea = AndroidKeystoreSecureArea(context, storageEngine)
val softwareSecureArea = SoftwareSecureArea(storageEngine)

keystoreEngineRepository.addImplementation(androidKeystoreSecureArea)
keystoreEngineRepository.addImplementation(softwareSecureArea)
return CredentialStore(storageEngine, keystoreEngineRepository)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import androidx.navigation.findNavController
import androidx.navigation.ui.NavigationUI
import androidx.navigation.ui.NavigationUI.setupActionBarWithNavController
import androidx.navigation.ui.setupWithNavController
import androidx.preference.PreferenceManager
import com.android.identity.mdoc.origininfo.OriginInfo
import com.android.identity.mdoc.origininfo.OriginInfoReferrerUrl
import com.android.identity.util.Logger
Expand All @@ -25,8 +24,6 @@ import com.android.identity.wallet.util.logInfo
import com.android.identity.wallet.util.logWarning
import com.android.identity.wallet.viewmodel.ShareDocumentViewModel
import com.google.android.material.elevation.SurfaceColors
import java.security.Security
import org.bouncycastle.jce.provider.BouncyCastleProvider

class MainActivity : AppCompatActivity() {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@ package com.android.identity.wallet.authconfirmation

import android.content.DialogInterface
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
Expand All @@ -14,31 +12,21 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs
import com.android.identity.android.securearea.AndroidKeystoreSecureArea
import com.android.identity.securearea.SoftwareSecureArea
import com.android.identity.securearea.SecureArea.ALGORITHM_ES256
import com.android.identity.wallet.R
import com.android.identity.wallet.authprompt.UserAuthPromptBuilder
import com.android.identity.wallet.support.SecureAreaSupport
import com.android.identity.wallet.theme.HolderAppTheme
import com.android.identity.wallet.transfer.AddDocumentToResponseResult
import com.android.identity.wallet.util.DocumentData
import com.android.identity.wallet.util.log
import com.android.identity.wallet.viewmodel.TransferDocumentViewModel
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import kotlinx.coroutines.launch

class AuthConfirmationFragment : BottomSheetDialogFragment() {

private val viewModel: TransferDocumentViewModel by activityViewModels()
private val passphraseViewModel: PassphrasePromptViewModel by activityViewModels()
private val arguments by navArgs<AuthConfirmationFragmentArgs>()
private var isSendingInProgress = mutableStateOf(false)
private var androidKeyUnlockData: AndroidKeystoreSecureArea.KeyUnlockData? = null

override fun onCreateView(
inflater: LayoutInflater,
Expand Down Expand Up @@ -69,19 +57,6 @@ class AuthConfirmationFragment : BottomSheetDialogFragment() {
}
}

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.CREATED) {
passphraseViewModel.authorizationState.collect { value ->
if (value is PassphraseAuthResult.Success) {
onPassphraseProvided(value.userPassphrase)
passphraseViewModel.reset()
}
}
}
}
}

override fun onCancel(dialog: DialogInterface) {
cancelAuthorization()
}
Expand Down Expand Up @@ -120,32 +95,6 @@ class AuthConfirmationFragment : BottomSheetDialogFragment() {
onSendResponseResult(result)
}

private fun requestUserAuth(
allowLskfUnlock: Boolean,
allowBiometricUnlock: Boolean,
forceLskf: Boolean = !allowBiometricUnlock
) {
val userAuthRequest = UserAuthPromptBuilder.requestUserAuth(this)
.withTitle(getString(R.string.bio_auth_title))
.withSuccessCallback { authenticationSucceeded() }
.withCancelledCallback {
if (allowLskfUnlock) {
retryForcingPinUse(allowLskfUnlock, allowBiometricUnlock)
} else {
cancelAuthorization()
}
}
.withFailureCallback { authenticationFailed() }
.setForceLskf(forceLskf)
if (allowLskfUnlock) {
userAuthRequest.withNegativeButton(getString(R.string.bio_auth_use_pin))
} else {
userAuthRequest.withNegativeButton("Cancel")
}
val cryptoObject = androidKeyUnlockData?.getCryptoObjectForSigning(ALGORITHM_ES256)
userAuthRequest.build().authenticate(cryptoObject)
}

private fun getSubtitle(): String {
val readerCommonName = arguments.readerCommonName
val readerIsTrusted = arguments.readerIsTrusted
Expand All @@ -160,35 +109,29 @@ class AuthConfirmationFragment : BottomSheetDialogFragment() {
}
}

private fun onPassphraseProvided(passphrase: String) {
val unlockData = SoftwareSecureArea.KeyUnlockData(passphrase)
val result = viewModel.sendResponseForSelection(unlockData)
onSendResponseResult(result)
}

private fun authenticationSucceeded() {
try {
val result = viewModel.sendResponseForSelection(keyUnlockData = androidKeyUnlockData)
onSendResponseResult(result)
} catch (e: Exception) {
val message = "Send response error: ${e.message}"
log(message, e)
toast(message)
}
}

private fun onSendResponseResult(result: AddDocumentToResponseResult) {
when (result) {
is AddDocumentToResponseResult.UserAuthRequired -> {
androidKeyUnlockData = AndroidKeystoreSecureArea.KeyUnlockData(result.keyAlias)
requestUserAuth(
result.allowLSKFUnlocking,
result.allowBiometricUnlocking
is AddDocumentToResponseResult.DocumentLocked -> {
val secureAreaSupport = SecureAreaSupport.getInstance(
requireContext(),
result.credential
)
}

is AddDocumentToResponseResult.PassphraseRequired -> {
requestPassphrase(result.attemptedWithIncorrectPassword)
with(secureAreaSupport) {
unlockKey(
credential = result.credential,
onKeyUnlocked = { keyUnlockData ->
val responseResult = viewModel.sendResponseForSelection(keyUnlockData)
onSendResponseResult(responseResult)
},
onUnlockFailure = { wasCancelled ->
if (wasCancelled) {
cancelAuthorization()
} else {
viewModel.closeConnection()
}
}
)
}
}

is AddDocumentToResponseResult.DocumentAdded -> {
Expand All @@ -200,25 +143,6 @@ class AuthConfirmationFragment : BottomSheetDialogFragment() {
}
}

private fun retryForcingPinUse(allowLsfk: Boolean, allowBiometric: Boolean) {
val runnable = { requestUserAuth(allowLsfk, allowBiometric, true) }
// Without this delay, the prompt won't reshow
Handler(Looper.getMainLooper()).postDelayed(runnable, 100)
}

private fun authenticationFailed() {
viewModel.closeConnection()
}

private fun requestPassphrase(attemptedWithIncorrectPassword: Boolean) {
val destination = AuthConfirmationFragmentDirections.openPassphrasePrompt(
showIncorrectPassword = attemptedWithIncorrectPassword
)
val runnable = { findNavController().navigate(destination) }
// The system needs a little time to get back to this screen
Handler(Looper.getMainLooper()).postDelayed(runnable, 500)
}

private fun toast(message: String, duration: Int = Toast.LENGTH_SHORT) {
Toast.makeText(requireContext(), message, duration).show()
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
package com.android.identity.wallet.composables

import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Checkbox
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Switch
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.android.identity.wallet.R
import com.android.identity.wallet.composables.state.AuthTypeState
import com.android.identity.wallet.selfsigned.OutlinedContainerVertical

@Composable
fun AndroidSetupContainer(
modifier: Modifier = Modifier,
isOn: Boolean,
timeoutSeconds: Int,
lskfAuthTypeState: AuthTypeState,
biometricAuthTypeState: AuthTypeState,
useStrongBox: AuthTypeState,
onUserAuthenticationChanged: (isOn: Boolean) -> Unit,
onAuthTimeoutChanged: (authTimeout: Int) -> Unit,
onLskfAuthChanged: (isOn: Boolean) -> Unit,
onBiometricAuthChanged: (isOn: Boolean) -> Unit,
onStrongBoxChanged: (isOn: Boolean) -> Unit
) {
Column(modifier = modifier) {
OutlinedContainerVertical(modifier = Modifier.fillMaxWidth()) {
val labelOn = stringResource(id = R.string.user_authentication_on)
val labelOff = stringResource(id = R.string.user_authentication_off)
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
) {
ValueLabel(
modifier = Modifier.weight(1f),
label = if (isOn) labelOn else labelOff,
)
Switch(
modifier = Modifier.padding(start = 8.dp),
checked = isOn,
onCheckedChange = onUserAuthenticationChanged
)
}
AnimatedVisibility(
modifier = Modifier.fillMaxWidth(),
visible = isOn
) {
Column(modifier = Modifier.fillMaxWidth()) {
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
) {
ValueLabel(
modifier = Modifier.weight(1f),
label = stringResource(id = R.string.keystore_android_user_auth_timeout)
)
NumberChanger(
number = timeoutSeconds,
onNumberChanged = onAuthTimeoutChanged,
counterTextStyle = MaterialTheme.typography.titleLarge
)
}
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
) {
val alpha = if (lskfAuthTypeState.canBeModified) 1f else .5f
ValueLabel(
modifier = Modifier
.weight(1f)
.alpha(alpha),
label = stringResource(id = R.string.user_auth_type_allow_lskf)
)
Checkbox(
checked = lskfAuthTypeState.isEnabled,
onCheckedChange = onLskfAuthChanged,
enabled = lskfAuthTypeState.canBeModified
)
}
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
) {
val alpha = if (biometricAuthTypeState.canBeModified) 1f else .5f
ValueLabel(
modifier = Modifier
.weight(1f)
.alpha(alpha),
label = stringResource(id = R.string.user_auth_type_allow_biometric)
)
Checkbox(
checked = biometricAuthTypeState.isEnabled,
onCheckedChange = onBiometricAuthChanged,
enabled = biometricAuthTypeState.canBeModified
)
}
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
) {
val alpha = if (useStrongBox.canBeModified) 1f else .5f
ValueLabel(
modifier = Modifier
.weight(1f)
.alpha(alpha),
label = stringResource(id = R.string.user_auth_use_strong_box)
)
Checkbox(
checked = useStrongBox.isEnabled,
onCheckedChange = onStrongBoxChanged,
enabled = useStrongBox.canBeModified
)
}
}
}
}
}
}

Loading

0 comments on commit 600f62d

Please sign in to comment.