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 Oct 29, 2023
1 parent 4ad39f6 commit d512ad9
Show file tree
Hide file tree
Showing 42 changed files with 1,321 additions and 1,212 deletions.
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,30 @@ 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.AndroidKeystoreSecureAreaSupport
import com.android.identity.wallet.support.AndroidSecureAreaKeyUnlockArgs
import com.android.identity.wallet.support.SoftwareKeystoreSecureAreaSupport
import com.android.identity.wallet.support.SoftwareSecureAreaKeyUnlockArgs
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

private val capabilities by lazy { AndroidKeystoreSecureArea.Capabilities(requireContext()) }
private val androidKeystoreSecureAreaSupport by lazy { AndroidKeystoreSecureAreaSupport(capabilities) }
private val softwareKeystoreSecureAreaSupport by lazy { SoftwareKeystoreSecureAreaSupport() }

override fun onCreateView(
inflater: LayoutInflater,
Expand Down Expand Up @@ -69,19 +66,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 +104,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 +118,32 @@ 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
)
with(androidKeystoreSecureAreaSupport) {
val keyUnlockArgs = AndroidSecureAreaKeyUnlockArgs(
alias = result.keyAlias,
allowLskfUnlock = result.allowLSKFUnlocking,
allowBiometricUnlock = result.allowBiometricUnlocking
)
unlockKey(viewModel, keyUnlockArgs) { keyUnlockData ->
val responseResult = viewModel.sendResponseForSelection(keyUnlockData)
onSendResponseResult(responseResult)
}
}
}

is AddDocumentToResponseResult.PassphraseRequired -> {
requestPassphrase(result.attemptedWithIncorrectPassword)
with(softwareKeystoreSecureAreaSupport) {
val keyUnlockArgs = SoftwareSecureAreaKeyUnlockArgs(
result.attemptedWithIncorrectPassword
)
unlockKey(passphraseViewModel, keyUnlockArgs) { keyUnlockData ->
val responseResult = viewModel.sendResponseForSelection(keyUnlockData)
onSendResponseResult(responseResult)
}
}
}

is AddDocumentToResponseResult.DocumentAdded -> {
Expand All @@ -200,25 +155,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 d512ad9

Please sign in to comment.