From 0d59f8375f6bca2fe0e4d4594f595193b6e7598e Mon Sep 17 00:00:00 2001 From: Jov Mit Date: Thu, 19 Oct 2023 16:47:53 +0200 Subject: [PATCH] Extract SecureArea-specific UI into an interface --- .../selfsigned/SelfSignedScreenStateTest.kt | 166 +--- .../com/android/identity/wallet/HolderApp.kt | 24 + .../android/identity/wallet/MainActivity.kt | 3 - .../AuthConfirmationFragment.kt | 118 +-- .../composables/AndroidSetupContainer.kt | 128 ++++ .../AuthenticationKeyCurveAndroid.kt | 76 ++ .../AuthenticationKeyCurveSoftware.kt | 74 ++ .../wallet/composables/CounterInput.kt | 24 + .../wallet/composables/DocumentValues.kt | 11 - .../wallet/composables/DropDownIndicator.kt | 20 + .../wallet/composables/LabeledUserInput.kt | 26 + .../wallet/composables/MdocAuthentication.kt | 68 ++ .../wallet/composables/NumberChanger.kt | 58 ++ .../OutlinedContainerHorizontal.kt | 43 ++ .../composables/SoftwareSetupContainer.kt | 53 ++ .../wallet/composables/TextDropDownRow.kt | 24 + .../identity/wallet/composables/ValueLabel.kt | 19 + .../composables/mdocAuthOptionLabelFor.kt | 19 + .../wallet/composables/state/AuthTypeState.kt | 10 + .../composables/state/MdocAuthOption.kt | 10 + .../composables/state/MdocAuthStateOption.kt | 9 + .../wallet/document/DocumentInformation.kt | 4 +- .../document/SecureAreaImplementationState.kt | 16 - .../wallet/documentinfo/DocumentInfoScreen.kt | 3 +- .../documentinfo/DocumentInfoScreenState.kt | 7 +- .../documentinfo/DocumentInfoViewModel.kt | 2 +- .../selfsigned/AddSelfSignedDocumentScreen.kt | 725 +----------------- .../selfsigned/AddSelfSignedFragment.kt | 12 +- .../selfsigned/AddSelfSignedScreenState.kt | 115 +-- .../selfsigned/AddSelfSignedViewModel.kt | 120 +-- .../selfsigned/SelfSignedDocumentData.kt | 15 +- .../AndroidKeystoreSecureAreaSupport.kt | 168 ++++ .../AndroidKeystoreSecureAreaSupportState.kt | 78 ++ .../wallet/support/CurrentSecureArea.kt | 13 + .../wallet/support/MdocAuthStateExtensions.kt | 13 + .../wallet/support/SecureAreaSupport.kt | 86 +++ .../wallet/support/SecureAreaSupportNull.kt | 51 ++ .../wallet/support/SecureAreaSupportState.kt | 18 + .../support/SecureAreaSupportStateNull.kt | 24 + .../SoftwareKeystoreSecureAreaSupport.kt | 113 +++ .../SoftwareKeystoreSecureAreaSupportState.kt | 92 +++ .../AndroidAuthKeyCurveOption.kt | 18 + .../AndroidAuthKeyCurveState.kt | 10 + .../SoftwareAuthKeyCurveOption.kt | 36 + .../SoftwareAuthKeyCurveState.kt | 10 + .../transfer/AddDocumentToResponseResult.kt | 12 +- .../wallet/transfer/TransferManager.kt | 29 +- .../identity/wallet/util/ProvisioningUtil.kt | 201 +---- .../android/identity/wallet/util/Timestamp.kt | 9 + appholder/src/main/res/values/strings.xml | 5 +- gradle/libs.versions.toml | 2 +- .../identity/securearea/SecureArea.java | 1 + 52 files changed, 1579 insertions(+), 1412 deletions(-) rename appholder/src/{test/java/com/android/identity/wallet => androidTest/java/com/android/mdl/app}/selfsigned/SelfSignedScreenStateTest.kt (50%) create mode 100644 appholder/src/main/java/com/android/identity/wallet/composables/AndroidSetupContainer.kt create mode 100644 appholder/src/main/java/com/android/identity/wallet/composables/AuthenticationKeyCurveAndroid.kt create mode 100644 appholder/src/main/java/com/android/identity/wallet/composables/AuthenticationKeyCurveSoftware.kt create mode 100644 appholder/src/main/java/com/android/identity/wallet/composables/CounterInput.kt create mode 100644 appholder/src/main/java/com/android/identity/wallet/composables/DropDownIndicator.kt create mode 100644 appholder/src/main/java/com/android/identity/wallet/composables/LabeledUserInput.kt create mode 100644 appholder/src/main/java/com/android/identity/wallet/composables/MdocAuthentication.kt create mode 100644 appholder/src/main/java/com/android/identity/wallet/composables/NumberChanger.kt create mode 100644 appholder/src/main/java/com/android/identity/wallet/composables/OutlinedContainerHorizontal.kt create mode 100644 appholder/src/main/java/com/android/identity/wallet/composables/SoftwareSetupContainer.kt create mode 100644 appholder/src/main/java/com/android/identity/wallet/composables/TextDropDownRow.kt create mode 100644 appholder/src/main/java/com/android/identity/wallet/composables/ValueLabel.kt create mode 100644 appholder/src/main/java/com/android/identity/wallet/composables/mdocAuthOptionLabelFor.kt create mode 100644 appholder/src/main/java/com/android/identity/wallet/composables/state/AuthTypeState.kt create mode 100644 appholder/src/main/java/com/android/identity/wallet/composables/state/MdocAuthOption.kt create mode 100644 appholder/src/main/java/com/android/identity/wallet/composables/state/MdocAuthStateOption.kt delete mode 100644 appholder/src/main/java/com/android/identity/wallet/document/SecureAreaImplementationState.kt create mode 100644 appholder/src/main/java/com/android/identity/wallet/support/AndroidKeystoreSecureAreaSupport.kt create mode 100644 appholder/src/main/java/com/android/identity/wallet/support/AndroidKeystoreSecureAreaSupportState.kt create mode 100644 appholder/src/main/java/com/android/identity/wallet/support/CurrentSecureArea.kt create mode 100644 appholder/src/main/java/com/android/identity/wallet/support/MdocAuthStateExtensions.kt create mode 100644 appholder/src/main/java/com/android/identity/wallet/support/SecureAreaSupport.kt create mode 100644 appholder/src/main/java/com/android/identity/wallet/support/SecureAreaSupportNull.kt create mode 100644 appholder/src/main/java/com/android/identity/wallet/support/SecureAreaSupportState.kt create mode 100644 appholder/src/main/java/com/android/identity/wallet/support/SecureAreaSupportStateNull.kt create mode 100644 appholder/src/main/java/com/android/identity/wallet/support/SoftwareKeystoreSecureAreaSupport.kt create mode 100644 appholder/src/main/java/com/android/identity/wallet/support/SoftwareKeystoreSecureAreaSupportState.kt create mode 100644 appholder/src/main/java/com/android/identity/wallet/support/androidkeystore/AndroidAuthKeyCurveOption.kt create mode 100644 appholder/src/main/java/com/android/identity/wallet/support/androidkeystore/AndroidAuthKeyCurveState.kt create mode 100644 appholder/src/main/java/com/android/identity/wallet/support/softwarekeystore/SoftwareAuthKeyCurveOption.kt create mode 100644 appholder/src/main/java/com/android/identity/wallet/support/softwarekeystore/SoftwareAuthKeyCurveState.kt create mode 100644 appholder/src/main/java/com/android/identity/wallet/util/Timestamp.kt diff --git a/appholder/src/test/java/com/android/identity/wallet/selfsigned/SelfSignedScreenStateTest.kt b/appholder/src/androidTest/java/com/android/mdl/app/selfsigned/SelfSignedScreenStateTest.kt similarity index 50% rename from appholder/src/test/java/com/android/identity/wallet/selfsigned/SelfSignedScreenStateTest.kt rename to appholder/src/androidTest/java/com/android/mdl/app/selfsigned/SelfSignedScreenStateTest.kt index 1878eb23d..aa772e46b 100644 --- a/appholder/src/test/java/com/android/identity/wallet/selfsigned/SelfSignedScreenStateTest.kt +++ b/appholder/src/androidTest/java/com/android/mdl/app/selfsigned/SelfSignedScreenStateTest.kt @@ -1,18 +1,40 @@ -package com.android.identity.wallet.selfsigned +package com.android.mdl.app.selfsigned import androidx.lifecycle.SavedStateHandle +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import com.android.identity.android.securearea.AndroidKeystoreSecureArea +import com.android.identity.android.storage.AndroidStorageEngine +import com.android.identity.securearea.SecureAreaRepository +import com.android.identity.securearea.SoftwareSecureArea import com.android.identity.wallet.document.DocumentColor import com.android.identity.wallet.document.DocumentType -import com.android.identity.wallet.document.SecureAreaImplementationState -import com.android.identity.wallet.selfsigned.AddSelfSignedScreenState.AndroidAuthKeyCurveState -import com.android.identity.wallet.selfsigned.AddSelfSignedScreenState.AuthTypeState -import com.android.identity.wallet.selfsigned.AddSelfSignedScreenState.MdocAuthOptionState +import com.android.identity.wallet.selfsigned.AddSelfSignedScreenState +import com.android.identity.wallet.selfsigned.AddSelfSignedViewModel +import com.android.identity.wallet.util.PreferencesHelper +import com.android.identity.wallet.util.ProvisioningUtil import com.google.common.truth.Truth.assertThat -import org.junit.jupiter.api.Test +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +@RunWith(AndroidJUnit4::class) class SelfSignedScreenStateTest { private val savedStateHandle = SavedStateHandle() + private lateinit var repository: SecureAreaRepository + + @Before + fun setUp() { + val context = InstrumentationRegistry.getInstrumentation().targetContext + repository = ProvisioningUtil.getInstance(context).secureAreaRepository + val storageDir = PreferencesHelper.getKeystoreBackedStorageLocation(context) + val storageEngine = AndroidStorageEngine.Builder(context, storageDir).build() + val androidKeystoreSecureArea = AndroidKeystoreSecureArea(context, storageEngine) + val softwareSecureArea = SoftwareSecureArea(storageEngine) + repository.addImplementation(androidKeystoreSecureArea) + repository.addImplementation(softwareSecureArea) + } @Test fun defaultScreenState() { @@ -72,127 +94,6 @@ class SelfSignedScreenStateTest { .isEqualTo(AddSelfSignedScreenState(cardArt = blue)) } - @Test - fun updateKeystoreImplementation() { - val bouncyCastle = SecureAreaImplementationState.BouncyCastle - val viewModel = AddSelfSignedViewModel(savedStateHandle) - - viewModel.updateKeystoreImplementation(bouncyCastle) - - assertThat(viewModel.screenState.value).isEqualTo( - AddSelfSignedScreenState(secureAreaImplementationState = bouncyCastle) - ) - } - - @Test - fun updateUserAuthentication() { - val authenticationOn = true - val viewModel = AddSelfSignedViewModel(savedStateHandle) - - viewModel.updateUserAuthentication(authenticationOn) - - assertThat(viewModel.screenState.value) - .isEqualTo(AddSelfSignedScreenState(userAuthentication = authenticationOn)) - } - - @Test - fun updateUserAuthenticationTimeoutSeconds() { - val newValue = 12 - val viewModel = AddSelfSignedViewModel(savedStateHandle) - - viewModel.updateUserAuthenticationTimeoutSeconds(newValue) - - assertThat(viewModel.screenState.value) - .isEqualTo(AddSelfSignedScreenState(userAuthenticationTimeoutSeconds = newValue)) - } - - @Test - fun updateUserAuthenticationTimeoutSecondsInvalidValue() { - val viewModel = AddSelfSignedViewModel(savedStateHandle) - - viewModel.updateUserAuthenticationTimeoutSeconds(1) - viewModel.updateUserAuthenticationTimeoutSeconds(0) - viewModel.updateUserAuthenticationTimeoutSeconds(-1) - - assertThat(viewModel.screenState.value) - .isEqualTo(AddSelfSignedScreenState(userAuthenticationTimeoutSeconds = 0)) - } - - @Test - fun updateAllowedLskfUnlocking() { - val viewModel = AddSelfSignedViewModel(savedStateHandle) - - viewModel.updateLskfUnlocking(false) - - assertThat(viewModel.screenState.value.allowLSKFUnlocking) - .isEqualTo(AuthTypeState(isEnabled = false, canBeModified = false)) - } - - @Test - fun updateAllowedLskfUnlockingWhenBiometricIsOff() { - val viewModel = AddSelfSignedViewModel(savedStateHandle) - viewModel.updateBiometricUnlocking(false) - - viewModel.updateLskfUnlocking(false) - - assertThat(viewModel.screenState.value.allowLSKFUnlocking) - .isEqualTo(AuthTypeState(isEnabled = true, canBeModified = false)) - } - - @Test - fun updateAllowedBiometricUnlocking() { - val viewModel = AddSelfSignedViewModel(savedStateHandle) - - viewModel.updateBiometricUnlocking(false) - - assertThat(viewModel.screenState.value.allowBiometricUnlocking) - .isEqualTo(AuthTypeState(isEnabled = false, canBeModified = false)) - } - - @Test - fun updateAllowedBiometricUnlockingWhenLskfIsOff() { - val viewModel = AddSelfSignedViewModel(savedStateHandle) - viewModel.updateLskfUnlocking(false) - - viewModel.updateBiometricUnlocking(false) - - assertThat(viewModel.screenState.value.allowBiometricUnlocking) - .isEqualTo(AuthTypeState(isEnabled = true, canBeModified = false)) - } - - @Test - fun updateStrongBox() { - val enabled = true - val viewModel = AddSelfSignedViewModel(savedStateHandle) - - viewModel.updateStrongBox(enabled) - - assertThat(viewModel.screenState.value.useStrongBox) - .isEqualTo(AuthTypeState(isEnabled = enabled, canBeModified = false)) - } - - @Test - fun updateMdocAuthOption() { - val authOption = AddSelfSignedScreenState.MdocAuthStateOption.MAC - val viewModel = AddSelfSignedViewModel(savedStateHandle) - - viewModel.updateMdocAuthOption(authOption) - - assertThat(viewModel.screenState.value.androidMdocAuthState) - .isEqualTo(MdocAuthOptionState(isEnabled = true, mDocAuthentication = authOption)) - } - - @Test - fun updateAndroidAuthKeyCurve() { - val x25519 = AddSelfSignedScreenState.AndroidAuthKeyCurveOption.X25519 - val viewModel = AddSelfSignedViewModel(savedStateHandle) - - viewModel.updateAndroidAuthKeyCurve(x25519) - - assertThat(viewModel.screenState.value.androidAuthKeyCurveState) - .isEqualTo(AndroidAuthKeyCurveState(isEnabled = true, authCurve = x25519)) - } - @Test fun updateValidityInDays() { val newValue = 15 @@ -240,17 +141,6 @@ class SelfSignedScreenStateTest { .isEqualTo(minValidityInDays) } - @Test - fun updateBouncyCastlePassphrase() { - val newPassphrase = ":irrelevant:" - val viewModel = AddSelfSignedViewModel(savedStateHandle) - - viewModel.updatePassphrase(newPassphrase) - - assertThat(viewModel.screenState.value) - .isEqualTo(AddSelfSignedScreenState(passphrase = newPassphrase)) - } - @Test fun updateNumberOfMso() { val msoCount = 2 diff --git a/appholder/src/main/java/com/android/identity/wallet/HolderApp.kt b/appholder/src/main/java/com/android/identity/wallet/HolderApp.kt index b2ab88269..f13643590 100644 --- a/appholder/src/main/java/com/android/identity/wallet/HolderApp.kt +++ b/appholder/src/main/java/com/android/identity/wallet/HolderApp.kt @@ -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 @@ -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) + } + } } \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/MainActivity.kt b/appholder/src/main/java/com/android/identity/wallet/MainActivity.kt index adbbff931..b59e3668d 100644 --- a/appholder/src/main/java/com/android/identity/wallet/MainActivity.kt +++ b/appholder/src/main/java/com/android/identity/wallet/MainActivity.kt @@ -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 @@ -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() { diff --git a/appholder/src/main/java/com/android/identity/wallet/authconfirmation/AuthConfirmationFragment.kt b/appholder/src/main/java/com/android/identity/wallet/authconfirmation/AuthConfirmationFragment.kt index 0df5c4433..8f4758b83 100644 --- a/appholder/src/main/java/com/android/identity/wallet/authconfirmation/AuthConfirmationFragment.kt +++ b/appholder/src/main/java/com/android/identity/wallet/authconfirmation/AuthConfirmationFragment.kt @@ -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 @@ -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() private var isSendingInProgress = mutableStateOf(false) - private var androidKeyUnlockData: AndroidKeystoreSecureArea.KeyUnlockData? = null override fun onCreateView( inflater: LayoutInflater, @@ -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() } @@ -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 @@ -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 -> { @@ -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() } diff --git a/appholder/src/main/java/com/android/identity/wallet/composables/AndroidSetupContainer.kt b/appholder/src/main/java/com/android/identity/wallet/composables/AndroidSetupContainer.kt new file mode 100644 index 000000000..45473e5af --- /dev/null +++ b/appholder/src/main/java/com/android/identity/wallet/composables/AndroidSetupContainer.kt @@ -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 + ) + } + } + } + } + } +} + diff --git a/appholder/src/main/java/com/android/identity/wallet/composables/AuthenticationKeyCurveAndroid.kt b/appholder/src/main/java/com/android/identity/wallet/composables/AuthenticationKeyCurveAndroid.kt new file mode 100644 index 000000000..0bed3a7c6 --- /dev/null +++ b/appholder/src/main/java/com/android/identity/wallet/composables/AuthenticationKeyCurveAndroid.kt @@ -0,0 +1,76 @@ +package com.android.identity.wallet.composables + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.material3.DropdownMenu +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.res.stringResource +import com.android.identity.wallet.R +import com.android.identity.wallet.composables.state.MdocAuthOption +import com.android.identity.wallet.composables.state.MdocAuthStateOption +import com.android.identity.wallet.support.androidkeystore.AndroidAuthKeyCurveOption +import com.android.identity.wallet.support.androidkeystore.AndroidAuthKeyCurveState + +@Composable +fun AuthenticationKeyCurveAndroid( + modifier: Modifier = Modifier, + state: AndroidAuthKeyCurveState, + mDocAuthState: MdocAuthOption, + onAndroidAuthKeyCurveChanged: (newValue: AndroidAuthKeyCurveOption) -> Unit +) { + LabeledUserInput( + modifier = modifier, + label = stringResource(id = R.string.authentication_key_curve_label) + ) { + var keyCurveDropDownExpanded by remember { mutableStateOf(false) } + val clickModifier = if (state.isEnabled) { + Modifier.clickable { keyCurveDropDownExpanded = true } + } else { + Modifier + } + val alpha = if (state.isEnabled) 1f else .5f + OutlinedContainerHorizontal( + modifier = Modifier + .fillMaxWidth() + .alpha(alpha) + .then(clickModifier) + ) { + ValueLabel( + modifier = Modifier.weight(1f), + label = curveLabelFor(state.authCurve.toEcCurve()) + ) + DropDownIndicator() + } + DropdownMenu( + expanded = keyCurveDropDownExpanded, + onDismissRequest = { keyCurveDropDownExpanded = false } + ) { + val ecCurveOption = + if (mDocAuthState.mDocAuthentication == MdocAuthStateOption.ECDSA) { + AndroidAuthKeyCurveOption.Ed25519 + } else { + AndroidAuthKeyCurveOption.X25519 + } + TextDropDownRow( + label = curveLabelFor(curveOption = AndroidAuthKeyCurveOption.P_256.toEcCurve()), + onSelected = { + onAndroidAuthKeyCurveChanged(AndroidAuthKeyCurveOption.P_256) + keyCurveDropDownExpanded = false + } + ) + TextDropDownRow( + label = curveLabelFor(curveOption = ecCurveOption.toEcCurve()), + onSelected = { + onAndroidAuthKeyCurveChanged(ecCurveOption) + keyCurveDropDownExpanded = false + } + ) + } + } +} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/composables/AuthenticationKeyCurveSoftware.kt b/appholder/src/main/java/com/android/identity/wallet/composables/AuthenticationKeyCurveSoftware.kt new file mode 100644 index 000000000..5f9f94be5 --- /dev/null +++ b/appholder/src/main/java/com/android/identity/wallet/composables/AuthenticationKeyCurveSoftware.kt @@ -0,0 +1,74 @@ +package com.android.identity.wallet.composables + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.material3.DropdownMenu +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.res.stringResource +import com.android.identity.wallet.R +import com.android.identity.wallet.support.softwarekeystore.SoftwareAuthKeyCurveOption +import com.android.identity.wallet.support.softwarekeystore.SoftwareAuthKeyCurveState +import com.android.identity.wallet.composables.state.MdocAuthOption +import com.android.identity.wallet.composables.state.MdocAuthStateOption + +@Composable +fun AuthenticationKeyCurveSoftware( + modifier: Modifier = Modifier, + state: SoftwareAuthKeyCurveState, + mDocAuthState: MdocAuthOption, + onSoftwareAuthKeyCurveChanged: (newValue: SoftwareAuthKeyCurveOption) -> Unit +) { + LabeledUserInput( + modifier = modifier, + label = stringResource(id = R.string.authentication_key_curve_label) + ) { + var keyCurveDropDownExpanded by remember { mutableStateOf(false) } + val clickModifier = if (state.isEnabled) { + Modifier.clickable { keyCurveDropDownExpanded = true } + } else { + Modifier + } + val alpha = if (state.isEnabled) 1f else .5f + OutlinedContainerHorizontal( + modifier = Modifier + .fillMaxWidth() + .alpha(alpha) + .then(clickModifier) + ) { + ValueLabel( + modifier = Modifier.weight(1f), + label = curveLabelFor(state.authCurve.toEcCurve()) + ) + DropDownIndicator() + } + val entries = + SoftwareAuthKeyCurveOption.values().toMutableList() + if (mDocAuthState.mDocAuthentication == MdocAuthStateOption.ECDSA) { + entries.remove(SoftwareAuthKeyCurveOption.X448) + entries.remove(SoftwareAuthKeyCurveOption.X25519) + } else { + entries.remove(SoftwareAuthKeyCurveOption.Ed448) + entries.remove(SoftwareAuthKeyCurveOption.Ed25519) + } + DropdownMenu( + expanded = keyCurveDropDownExpanded, + onDismissRequest = { keyCurveDropDownExpanded = false } + ) { + for (entry in entries) { + TextDropDownRow( + label = curveLabelFor(curveOption = entry.toEcCurve()), + onSelected = { + onSoftwareAuthKeyCurveChanged(entry) + keyCurveDropDownExpanded = false + } + ) + } + } + } +} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/composables/CounterInput.kt b/appholder/src/main/java/com/android/identity/wallet/composables/CounterInput.kt new file mode 100644 index 000000000..4b5e48695 --- /dev/null +++ b/appholder/src/main/java/com/android/identity/wallet/composables/CounterInput.kt @@ -0,0 +1,24 @@ +package com.android.identity.wallet.composables + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier + +@Composable +fun CounterInput( + modifier: Modifier = Modifier, + label: String, + value: Int, + onValueChange: (newValue: Int) -> Unit +) { + Column(modifier = modifier) { + OutlinedContainerHorizontal(modifier = Modifier.fillMaxWidth()) { + ValueLabel( + modifier = Modifier.weight(1f), + label = label + ) + NumberChanger(number = value, onNumberChanged = onValueChange) + } + } +} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/composables/DocumentValues.kt b/appholder/src/main/java/com/android/identity/wallet/composables/DocumentValues.kt index df35a6905..82066891a 100644 --- a/appholder/src/main/java/com/android/identity/wallet/composables/DocumentValues.kt +++ b/appholder/src/main/java/com/android/identity/wallet/composables/DocumentValues.kt @@ -1,23 +1,12 @@ package com.android.identity.wallet.composables -import androidx.annotation.StringRes import androidx.compose.ui.graphics.Brush -import com.android.identity.wallet.R import com.android.identity.wallet.document.DocumentColor -import com.android.identity.wallet.document.SecureAreaImplementationState import com.android.identity.wallet.theme.BlueGradient import com.android.identity.wallet.theme.GreenGradient import com.android.identity.wallet.theme.RedGradient import com.android.identity.wallet.theme.YellowGradient -@StringRes -fun keystoreNameFor(implementation: SecureAreaImplementationState): Int { - return when (implementation) { - is SecureAreaImplementationState.Android -> R.string.keystore_android - is SecureAreaImplementationState.BouncyCastle -> R.string.keystore_bouncy_castle - } -} - fun Int.toCardArt(): DocumentColor { return when (this) { 1 -> DocumentColor.Yellow diff --git a/appholder/src/main/java/com/android/identity/wallet/composables/DropDownIndicator.kt b/appholder/src/main/java/com/android/identity/wallet/composables/DropDownIndicator.kt new file mode 100644 index 000000000..06f09b8fe --- /dev/null +++ b/appholder/src/main/java/com/android/identity/wallet/composables/DropDownIndicator.kt @@ -0,0 +1,20 @@ +package com.android.identity.wallet.composables + +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ArrowDropDown +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier + +@Composable +fun DropDownIndicator( + modifier: Modifier = Modifier +) { + Icon( + modifier = modifier, + imageVector = Icons.Default.ArrowDropDown, + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurface + ) +} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/composables/LabeledUserInput.kt b/appholder/src/main/java/com/android/identity/wallet/composables/LabeledUserInput.kt new file mode 100644 index 000000000..b642101d7 --- /dev/null +++ b/appholder/src/main/java/com/android/identity/wallet/composables/LabeledUserInput.kt @@ -0,0 +1,26 @@ +package com.android.identity.wallet.composables + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp + +@Composable +fun LabeledUserInput( + modifier: Modifier = Modifier, + label: String, + content: @Composable () -> Unit +) { + Column( + modifier = modifier, + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + Row(modifier = Modifier.fillMaxWidth()) { + ValueLabel(label = label) + } + content() + } +} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/composables/MdocAuthentication.kt b/appholder/src/main/java/com/android/identity/wallet/composables/MdocAuthentication.kt new file mode 100644 index 000000000..b52cd286a --- /dev/null +++ b/appholder/src/main/java/com/android/identity/wallet/composables/MdocAuthentication.kt @@ -0,0 +1,68 @@ +package com.android.identity.wallet.composables + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.material3.DropdownMenu +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.res.stringResource +import com.android.identity.wallet.R +import com.android.identity.wallet.composables.state.MdocAuthOption +import com.android.identity.wallet.composables.state.MdocAuthStateOption + +@Composable +fun MdocAuthentication( + modifier: Modifier = Modifier, + state: MdocAuthOption, + onMdocAuthOptionChange: (newValue: MdocAuthStateOption) -> Unit +) { + LabeledUserInput( + modifier = modifier, + label = stringResource(id = R.string.mdoc_authentication_label) + ) { + var expanded by remember { mutableStateOf(false) } + val alpha = if (state.isEnabled) 1f else .5f + val clickModifier = if (state.isEnabled) { + Modifier.clickable { expanded = true } + } else { + Modifier + } + OutlinedContainerHorizontal( + modifier = Modifier + .fillMaxWidth() + .alpha(alpha) + .then(clickModifier) + ) { + ValueLabel( + modifier = Modifier.weight(1f), + label = mdocAuthOptionLabelFor(state.mDocAuthentication) + ) + DropDownIndicator() + } + DropdownMenu( + modifier = Modifier.fillMaxWidth(0.8f), + expanded = expanded, + onDismissRequest = { expanded = false } + ) { + TextDropDownRow( + label = stringResource(id = R.string.mdoc_auth_ecdsa), + onSelected = { + onMdocAuthOptionChange(MdocAuthStateOption.ECDSA) + expanded = false + } + ) + TextDropDownRow( + label = stringResource(id = R.string.mdoc_auth_mac), + onSelected = { + onMdocAuthOptionChange(MdocAuthStateOption.MAC) + expanded = false + } + ) + } + } +} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/composables/NumberChanger.kt b/appholder/src/main/java/com/android/identity/wallet/composables/NumberChanger.kt new file mode 100644 index 000000000..e9e4ea904 --- /dev/null +++ b/appholder/src/main/java/com/android/identity/wallet/composables/NumberChanger.kt @@ -0,0 +1,58 @@ +package com.android.identity.wallet.composables + +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.ExperimentalAnimationApi +import androidx.compose.animation.slideInVertically +import androidx.compose.animation.slideOutVertically +import androidx.compose.animation.with +import androidx.compose.foundation.layout.Row +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Add +import androidx.compose.material.icons.filled.Remove +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.style.TextAlign + +@OptIn(ExperimentalAnimationApi::class) +@Composable +fun NumberChanger( + modifier: Modifier = Modifier, + number: Int, + onNumberChanged: (newValue: Int) -> Unit, + counterTextStyle: TextStyle = MaterialTheme.typography.bodyLarge +) { + Row( + modifier = modifier, + verticalAlignment = Alignment.CenterVertically + ) { + IconButton(onClick = { onNumberChanged(number - 1) }) { + Icon(imageVector = Icons.Default.Remove, contentDescription = null) + } + AnimatedContent( + targetState = number, + label = "", + transitionSpec = { + if (targetState > initialState) { + slideInVertically { -it } with slideOutVertically { it } + } else { + slideInVertically { it } with slideOutVertically { -it } + } + } + ) { count -> + Text( + text = "$count", + textAlign = TextAlign.Center, + style = counterTextStyle + ) + } + IconButton(onClick = { onNumberChanged(number + 1) }) { + Icon(imageVector = Icons.Default.Add, contentDescription = null) + } + } +} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/composables/OutlinedContainerHorizontal.kt b/appholder/src/main/java/com/android/identity/wallet/composables/OutlinedContainerHorizontal.kt new file mode 100644 index 000000000..ceae2ab58 --- /dev/null +++ b/appholder/src/main/java/com/android/identity/wallet/composables/OutlinedContainerHorizontal.kt @@ -0,0 +1,43 @@ +package com.android.identity.wallet.composables + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp + +@Composable +fun OutlinedContainerHorizontal( + modifier: Modifier = Modifier, + outlineBorderWidth: Dp = 2.dp, + outlineBrush: Brush? = null, + content: @Composable RowScope.() -> Unit +) { + val brush = outlineBrush ?: SolidColor(MaterialTheme.colorScheme.outline) + Row( + modifier = modifier + .heightIn(48.dp) + .clip(RoundedCornerShape(12.dp)) + .border(outlineBorderWidth, brush, RoundedCornerShape(12.dp)) + .background(MaterialTheme.colorScheme.inverseOnSurface), + verticalAlignment = Alignment.CenterVertically + ) { + Row( + modifier = Modifier.padding(horizontal = 12.dp), + verticalAlignment = Alignment.CenterVertically + ) { + content() + } + } +} diff --git a/appholder/src/main/java/com/android/identity/wallet/composables/SoftwareSetupContainer.kt b/appholder/src/main/java/com/android/identity/wallet/composables/SoftwareSetupContainer.kt new file mode 100644 index 000000000..914da5c1e --- /dev/null +++ b/appholder/src/main/java/com/android/identity/wallet/composables/SoftwareSetupContainer.kt @@ -0,0 +1,53 @@ +package com.android.identity.wallet.composables + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.text.BasicTextField +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import com.android.identity.wallet.R + +@Composable +fun SoftwareSetupContainer( + modifier: Modifier = Modifier, + passphrase: String, + onPassphraseChanged: (newValue: String) -> Unit +) { + Column( + modifier = modifier, + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + OutlinedContainerHorizontal(modifier = Modifier.fillMaxWidth()) { + Box(contentAlignment = Alignment.CenterStart) { + BasicTextField( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 10.dp), + textStyle = MaterialTheme.typography.labelMedium.copy( + color = MaterialTheme.colorScheme.onSurface, + ), + value = passphrase, + onValueChange = onPassphraseChanged, + cursorBrush = SolidColor(MaterialTheme.colorScheme.onSurface) + ) + if (passphrase.isEmpty()) { + Text( + text = stringResource(id = R.string.keystore_software_passphrase_hint), + style = MaterialTheme.typography.labelMedium.copy( + color = MaterialTheme.colorScheme.onSurface.copy(alpha = .5f) + ), + ) + } + } + } + } +} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/composables/TextDropDownRow.kt b/appholder/src/main/java/com/android/identity/wallet/composables/TextDropDownRow.kt new file mode 100644 index 000000000..e9c9dc1d3 --- /dev/null +++ b/appholder/src/main/java/com/android/identity/wallet/composables/TextDropDownRow.kt @@ -0,0 +1,24 @@ +package com.android.identity.wallet.composables + +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier + +@Composable +fun TextDropDownRow( + modifier: Modifier = Modifier, + label: String, + onSelected: () -> Unit +) { + DropdownMenuItem( + modifier = modifier, + text = { + ValueLabel( + modifier = Modifier.fillMaxWidth(), + label = label + ) + }, + onClick = onSelected + ) +} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/composables/ValueLabel.kt b/appholder/src/main/java/com/android/identity/wallet/composables/ValueLabel.kt new file mode 100644 index 000000000..2d1ff3c65 --- /dev/null +++ b/appholder/src/main/java/com/android/identity/wallet/composables/ValueLabel.kt @@ -0,0 +1,19 @@ +package com.android.identity.wallet.composables + +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier + +@Composable +fun ValueLabel( + modifier: Modifier = Modifier, + label: String +) { + Text( + modifier = modifier, + text = label, + color = MaterialTheme.colorScheme.onSurface, + style = MaterialTheme.typography.labelMedium + ) +} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/composables/mdocAuthOptionLabelFor.kt b/appholder/src/main/java/com/android/identity/wallet/composables/mdocAuthOptionLabelFor.kt new file mode 100644 index 000000000..f86eaec29 --- /dev/null +++ b/appholder/src/main/java/com/android/identity/wallet/composables/mdocAuthOptionLabelFor.kt @@ -0,0 +1,19 @@ +package com.android.identity.wallet.composables + +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import com.android.identity.wallet.R +import com.android.identity.wallet.composables.state.MdocAuthStateOption + +@Composable +fun mdocAuthOptionLabelFor( + state: MdocAuthStateOption +): String { + return when (state) { + MdocAuthStateOption.ECDSA -> + stringResource(id = R.string.mdoc_auth_ecdsa) + + MdocAuthStateOption.MAC -> + stringResource(id = R.string.mdoc_auth_mac) + } +} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/composables/state/AuthTypeState.kt b/appholder/src/main/java/com/android/identity/wallet/composables/state/AuthTypeState.kt new file mode 100644 index 000000000..67236282b --- /dev/null +++ b/appholder/src/main/java/com/android/identity/wallet/composables/state/AuthTypeState.kt @@ -0,0 +1,10 @@ +package com.android.identity.wallet.composables.state + +import android.os.Parcelable +import kotlinx.parcelize.Parcelize + +@Parcelize +data class AuthTypeState( + val isEnabled: Boolean = true, + val canBeModified: Boolean = false +) : Parcelable \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/composables/state/MdocAuthOption.kt b/appholder/src/main/java/com/android/identity/wallet/composables/state/MdocAuthOption.kt new file mode 100644 index 000000000..06c424f66 --- /dev/null +++ b/appholder/src/main/java/com/android/identity/wallet/composables/state/MdocAuthOption.kt @@ -0,0 +1,10 @@ +package com.android.identity.wallet.composables.state + +import android.os.Parcelable +import kotlinx.parcelize.Parcelize + +@Parcelize +data class MdocAuthOption( + val isEnabled: Boolean = true, + val mDocAuthentication: MdocAuthStateOption = MdocAuthStateOption.ECDSA +) : Parcelable \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/composables/state/MdocAuthStateOption.kt b/appholder/src/main/java/com/android/identity/wallet/composables/state/MdocAuthStateOption.kt new file mode 100644 index 000000000..eb6a7923b --- /dev/null +++ b/appholder/src/main/java/com/android/identity/wallet/composables/state/MdocAuthStateOption.kt @@ -0,0 +1,9 @@ +package com.android.identity.wallet.composables.state + +import android.os.Parcelable +import kotlinx.parcelize.Parcelize + +@Parcelize +enum class MdocAuthStateOption : Parcelable { + ECDSA, MAC +} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/document/DocumentInformation.kt b/appholder/src/main/java/com/android/identity/wallet/document/DocumentInformation.kt index 1d80f23ac..4158722c3 100644 --- a/appholder/src/main/java/com/android/identity/wallet/document/DocumentInformation.kt +++ b/appholder/src/main/java/com/android/identity/wallet/document/DocumentInformation.kt @@ -1,5 +1,7 @@ package com.android.identity.wallet.document +import com.android.identity.wallet.support.CurrentSecureArea + data class DocumentInformation( val userVisibleName: String, val docName: String, @@ -10,7 +12,7 @@ data class DocumentInformation( val maxUsagesPerKey: Int, val lastTimeUsed: String, val mDocAuthOption: String, - val secureAreaImplementationState: SecureAreaImplementationState, + val currentSecureArea: CurrentSecureArea, val authKeys: List ) { diff --git a/appholder/src/main/java/com/android/identity/wallet/document/SecureAreaImplementationState.kt b/appholder/src/main/java/com/android/identity/wallet/document/SecureAreaImplementationState.kt deleted file mode 100644 index dc91dd814..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/document/SecureAreaImplementationState.kt +++ /dev/null @@ -1,16 +0,0 @@ -package com.android.identity.wallet.document - -import android.os.Parcelable -import kotlinx.parcelize.Parcelize -import kotlinx.serialization.Serializable - -@Parcelize -@Serializable -sealed class SecureAreaImplementationState : Parcelable { - - @Serializable - object Android : SecureAreaImplementationState() - - @Serializable - object BouncyCastle : SecureAreaImplementationState() -} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/documentinfo/DocumentInfoScreen.kt b/appholder/src/main/java/com/android/identity/wallet/documentinfo/DocumentInfoScreen.kt index 73d3b2328..10a441c4f 100644 --- a/appholder/src/main/java/com/android/identity/wallet/documentinfo/DocumentInfoScreen.kt +++ b/appholder/src/main/java/com/android/identity/wallet/documentinfo/DocumentInfoScreen.kt @@ -61,7 +61,6 @@ import com.android.identity.wallet.R import com.android.identity.wallet.composables.LoadingIndicator import com.android.identity.wallet.composables.ShowToast import com.android.identity.wallet.composables.gradientFor -import com.android.identity.wallet.composables.keystoreNameFor import com.android.identity.wallet.theme.HolderAppTheme @Composable @@ -152,7 +151,7 @@ private fun DocumentInfoScreenContent( ) LabeledValue( label = stringResource(id = R.string.txt_keystore_implementation), - value = stringResource(id = keystoreNameFor(screenState.secureAreaImplementationState)) + value = screenState.currentSecureArea.displayName ) } } diff --git a/appholder/src/main/java/com/android/identity/wallet/documentinfo/DocumentInfoScreenState.kt b/appholder/src/main/java/com/android/identity/wallet/documentinfo/DocumentInfoScreenState.kt index d92445143..65be434bd 100644 --- a/appholder/src/main/java/com/android/identity/wallet/documentinfo/DocumentInfoScreenState.kt +++ b/appholder/src/main/java/com/android/identity/wallet/documentinfo/DocumentInfoScreenState.kt @@ -1,11 +1,12 @@ package com.android.identity.wallet.documentinfo import androidx.compose.runtime.Immutable -import androidx.compose.runtime.Stable import com.android.identity.securearea.SecureArea.EcCurve import com.android.identity.securearea.SecureArea.KeyPurpose import com.android.identity.wallet.document.DocumentColor -import com.android.identity.wallet.document.SecureAreaImplementationState +import com.android.identity.wallet.support.CurrentSecureArea +import com.android.identity.wallet.support.toSecureAreaState +import com.android.identity.wallet.util.ProvisioningUtil @Immutable data class DocumentInfoScreenState( @@ -16,7 +17,7 @@ data class DocumentInfoScreenState( val provisioningDate: String = "", val lastTimeUsedDate: String = "", val isSelfSigned: Boolean = false, - val secureAreaImplementationState: SecureAreaImplementationState = SecureAreaImplementationState.Android, + val currentSecureArea: CurrentSecureArea = ProvisioningUtil.defaultSecureArea.toSecureAreaState(), val authKeys: List = emptyList(), val isDeletingPromptShown: Boolean = false, val isDeleted: Boolean = false diff --git a/appholder/src/main/java/com/android/identity/wallet/documentinfo/DocumentInfoViewModel.kt b/appholder/src/main/java/com/android/identity/wallet/documentinfo/DocumentInfoViewModel.kt index 6215e3b9f..a18f5a383 100644 --- a/appholder/src/main/java/com/android/identity/wallet/documentinfo/DocumentInfoViewModel.kt +++ b/appholder/src/main/java/com/android/identity/wallet/documentinfo/DocumentInfoViewModel.kt @@ -70,7 +70,7 @@ class DocumentInfoViewModel( documentType = documentInformation.docType, documentColor = documentInformation.documentColor.toCardArt(), provisioningDate = documentInformation.dateProvisioned, - secureAreaImplementationState = documentInformation.secureAreaImplementationState, + currentSecureArea = documentInformation.currentSecureArea, isSelfSigned = documentInformation.selfSigned, lastTimeUsedDate = documentInformation.lastTimeUsed, authKeys = documentInformation.authKeys.asScreenStateKeys() diff --git a/appholder/src/main/java/com/android/identity/wallet/selfsigned/AddSelfSignedDocumentScreen.kt b/appholder/src/main/java/com/android/identity/wallet/selfsigned/AddSelfSignedDocumentScreen.kt index 7881e6197..0f3b68bee 100644 --- a/appholder/src/main/java/com/android/identity/wallet/selfsigned/AddSelfSignedDocumentScreen.kt +++ b/appholder/src/main/java/com/android/identity/wallet/selfsigned/AddSelfSignedDocumentScreen.kt @@ -1,12 +1,6 @@ package com.android.identity.wallet.selfsigned import androidx.annotation.StringRes -import androidx.compose.animation.AnimatedContent -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.ExperimentalAnimationApi -import androidx.compose.animation.slideInVertically -import androidx.compose.animation.slideOutVertically -import androidx.compose.animation.with import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.clickable @@ -15,7 +9,6 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth @@ -26,22 +19,13 @@ import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.BasicTextField import androidx.compose.foundation.verticalScroll -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Add -import androidx.compose.material.icons.filled.ArrowDropDown -import androidx.compose.material.icons.filled.Remove import androidx.compose.material3.Button -import androidx.compose.material3.Checkbox import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenuItem -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold -import androidx.compose.material3.Switch import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -49,37 +33,35 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import com.android.identity.wallet.R -import com.android.identity.wallet.composables.PreviewLightDark -import com.android.identity.wallet.composables.curveLabelFor +import com.android.identity.wallet.composables.CounterInput +import com.android.identity.wallet.composables.DropDownIndicator +import com.android.identity.wallet.composables.LabeledUserInput +import com.android.identity.wallet.composables.OutlinedContainerHorizontal +import com.android.identity.wallet.composables.TextDropDownRow +import com.android.identity.wallet.composables.ValueLabel import com.android.identity.wallet.composables.gradientFor -import com.android.identity.wallet.composables.keystoreNameFor import com.android.identity.wallet.document.DocumentColor import com.android.identity.wallet.document.DocumentType -import com.android.identity.wallet.document.SecureAreaImplementationState -import com.android.identity.wallet.selfsigned.AddSelfSignedScreenState.* -import com.android.identity.wallet.theme.HolderAppTheme +import com.android.identity.wallet.support.CurrentSecureArea +import com.android.identity.wallet.support.SecureAreaSupport +import com.android.identity.wallet.support.SecureAreaSupportState +import com.android.identity.wallet.support.toSecureAreaState +import com.android.identity.wallet.util.ProvisioningUtil @Composable fun AddSelfSignedDocumentScreen( viewModel: AddSelfSignedViewModel, onNext: () -> Unit ) { - val context = LocalContext.current val screenState by viewModel.screenState.collectAsState() - LaunchedEffect(Unit) { - viewModel.loadConfiguration(context) - } AddSelfSignedDocumentScreenContent( modifier = Modifier.fillMaxSize(), @@ -88,15 +70,7 @@ fun AddSelfSignedDocumentScreen( onCardArtSelected = viewModel::updateCardArt, onDocumentNameChanged = viewModel::updateDocumentName, onKeystoreImplementationChanged = viewModel::updateKeystoreImplementation, - onUserAuthenticationChanged = viewModel::updateUserAuthentication, - onAuthTimeoutChanged = viewModel::updateUserAuthenticationTimeoutSeconds, - onLskfAuthChanged = viewModel::updateLskfUnlocking, - onBiometricAuthChanged = viewModel::updateBiometricUnlocking, - onMdocAuthOptionChange = viewModel::updateMdocAuthOption, - onAndroidAuthKeyCurveChanged = viewModel::updateAndroidAuthKeyCurve, - onBouncyCastleAuthKeyCurveChanged = viewModel::updateBouncyCastleAuthKeyCurve, - onStrongBoxChanged = viewModel::updateStrongBox, - onPassphraseChanged = viewModel::updatePassphrase, + onSecureAreaSupportStateUpdated = viewModel::updateSecureAreaSupportState, onNumberOfMsoChanged = viewModel::updateNumberOfMso, onMaxUseOfMsoChanged = viewModel::updateMaxUseOfMso, onValidityInDaysChanged = viewModel::updateValidityInDays, @@ -112,16 +86,8 @@ private fun AddSelfSignedDocumentScreenContent( onDocumentTypeChanged: (newType: DocumentType) -> Unit, onCardArtSelected: (newCardArt: DocumentColor) -> Unit, onDocumentNameChanged: (newValue: String) -> Unit, - onKeystoreImplementationChanged: (newImplementation: SecureAreaImplementationState) -> Unit, - onUserAuthenticationChanged: (isOn: Boolean) -> Unit, - onAuthTimeoutChanged: (newValue: Int) -> Unit, - onLskfAuthChanged: (newValue: Boolean) -> Unit, - onStrongBoxChanged: (newValue: Boolean) -> Unit, - onBiometricAuthChanged: (newValue: Boolean) -> Unit, - onMdocAuthOptionChange: (newValue: MdocAuthStateOption) -> Unit, - onAndroidAuthKeyCurveChanged: (newValue: AndroidAuthKeyCurveOption) -> Unit, - onBouncyCastleAuthKeyCurveChanged: (newValue: BouncyCastleAuthKeyCurveOption) -> Unit, - onPassphraseChanged: (newValue: String) -> Unit, + onKeystoreImplementationChanged: (newImplementation: CurrentSecureArea) -> Unit, + onSecureAreaSupportStateUpdated: (newState: SecureAreaSupportState) -> Unit, onNumberOfMsoChanged: (newValue: Int) -> Unit, onMaxUseOfMsoChanged: (newValue: Int) -> Unit, onValidityInDaysChanged: (newValue: Int) -> Unit, @@ -162,64 +128,13 @@ private fun AddSelfSignedDocumentScreenContent( modifier = Modifier .fillMaxWidth() .padding(horizontal = 16.dp), - currentImplementation = screenState.secureAreaImplementationState, + currentImplementation = screenState.currentSecureArea, onKeystoreImplementationChanged = onKeystoreImplementationChanged ) - if (screenState.isAndroidKeystoreSelected) { - AndroidSetupContainer( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp), - isOn = screenState.userAuthentication, - timeoutSeconds = screenState.userAuthenticationTimeoutSeconds, - lskfAuthTypeState = screenState.allowLSKFUnlocking, - biometricAuthTypeState = screenState.allowBiometricUnlocking, - useStrongBox = screenState.useStrongBox, - onUserAuthenticationChanged = onUserAuthenticationChanged, - onAuthTimeoutChanged = onAuthTimeoutChanged, - onLskfAuthChanged = onLskfAuthChanged, - onBiometricAuthChanged = onBiometricAuthChanged, - onStrongBoxChanged = onStrongBoxChanged, - ) - MdocAuthentication( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp), - state = screenState.androidMdocAuthState, - onMdocAuthOptionChange = onMdocAuthOptionChange - ) - AuthenticationKeyCurveAndroid( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp), - state = screenState.androidAuthKeyCurveState, - mDocAuthState = screenState.androidMdocAuthState, - onAndroidAuthKeyCurveChanged = onAndroidAuthKeyCurveChanged - ) - } else { - BouncyCastleSetupContainer( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp), - state = screenState, - onPassphraseChanged = onPassphraseChanged - ) - MdocAuthentication( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp), - state = screenState.androidMdocAuthState, - onMdocAuthOptionChange = onMdocAuthOptionChange - ) - AuthenticationKeyCurveBouncyCastle( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp), - state = screenState.bouncyCastleAuthKeyCurveState, - mDocAuthState = screenState.androidMdocAuthState, - onBouncyCastleAuthKeyCurveChanged = onBouncyCastleAuthKeyCurveChanged - ) - } + SecureAreaSupport.getInstance( + LocalContext.current, + screenState.currentSecureArea + ).SecureAreaAuthUi(onUiStateUpdated = onSecureAreaSupportStateUpdated) CounterInput( modifier = Modifier .fillMaxWidth() @@ -419,59 +334,11 @@ private fun DocumentNameInput( } } -@Composable -private fun BouncyCastleSetupContainer( - modifier: Modifier = Modifier, - state: AddSelfSignedScreenState, - onPassphraseChanged: (newValue: String) -> Unit -) { - Column( - modifier = modifier, - verticalArrangement = Arrangement.spacedBy(12.dp) - ) { - BouncyCastlePassphraseInput( - value = state.passphrase, - onValueChanged = onPassphraseChanged - ) - } -} - -@Composable -private fun BouncyCastlePassphraseInput( - modifier: Modifier = Modifier, - value: String, - onValueChanged: (newValue: String) -> Unit -) { - OutlinedContainerHorizontal(modifier = modifier) { - Box(contentAlignment = Alignment.CenterStart) { - BasicTextField( - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 10.dp), - textStyle = MaterialTheme.typography.labelMedium.copy( - color = MaterialTheme.colorScheme.onSurface, - ), - value = value, - onValueChange = onValueChanged, - cursorBrush = SolidColor(MaterialTheme.colorScheme.onSurface) - ) - if (value.isEmpty()) { - Text( - text = stringResource(id = R.string.keystore_bouncy_castle_passphrase_hint), - style = MaterialTheme.typography.labelMedium.copy( - color = MaterialTheme.colorScheme.onSurface.copy(alpha = .5f) - ), - ) - } - } - } -} - @Composable private fun KeystoreImplementationChooser( modifier: Modifier = Modifier, - currentImplementation: SecureAreaImplementationState, - onKeystoreImplementationChanged: (newImplementation: SecureAreaImplementationState) -> Unit + currentImplementation: CurrentSecureArea, + onKeystoreImplementationChanged: (newImplementation: CurrentSecureArea) -> Unit ) { LabeledUserInput( modifier = modifier, @@ -485,7 +352,7 @@ private fun KeystoreImplementationChooser( ) { ValueLabel( modifier = Modifier.weight(1f), - label = stringResource(id = keystoreNameFor(currentImplementation)) + label = currentImplementation.displayName ) DropDownIndicator() } @@ -495,333 +362,20 @@ private fun KeystoreImplementationChooser( expanded = expanded, onDismissRequest = { expanded = false } ) { - TextDropDownRow( - label = stringResource(id = keystoreNameFor(SecureAreaImplementationState.Android)), - onSelected = { - onKeystoreImplementationChanged(SecureAreaImplementationState.Android) - expanded = false - } - ) - TextDropDownRow( - label = stringResource(id = keystoreNameFor(SecureAreaImplementationState.BouncyCastle)), - onSelected = { - onKeystoreImplementationChanged(SecureAreaImplementationState.BouncyCastle) - expanded = false - } - ) - } - } -} - -@Composable -private 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 - ) - } - } - } - } - } -} - - -@Composable -private fun MdocAuthentication( - modifier: Modifier = Modifier, - state: MdocAuthOptionState, - onMdocAuthOptionChange: (newValue: MdocAuthStateOption) -> Unit -) { - LabeledUserInput( - modifier = modifier, - label = stringResource(id = R.string.mdoc_authentication_label) - ) { - var expanded by remember { mutableStateOf(false) } - val alpha = if (state.isEnabled) 1f else .5f - val clickModifier = if (state.isEnabled) { - Modifier.clickable { expanded = true } - } else { - Modifier - } - OutlinedContainerHorizontal( - modifier = Modifier - .fillMaxWidth() - .alpha(alpha) - .then(clickModifier) - ) { - ValueLabel( - modifier = Modifier.weight(1f), - label = mdocAuthOptionLabelFor(state.mDocAuthentication) - ) - DropDownIndicator() - } - DropdownMenu( - modifier = Modifier.fillMaxWidth(0.8f), - expanded = expanded, - onDismissRequest = { expanded = false } - ) { - TextDropDownRow( - label = stringResource(id = R.string.mdoc_auth_ecdsa), - onSelected = { - onMdocAuthOptionChange(MdocAuthStateOption.ECDSA) - expanded = false - } - ) - TextDropDownRow( - label = stringResource(id = R.string.mdoc_auth_mac), - onSelected = { - onMdocAuthOptionChange(MdocAuthStateOption.MAC) - expanded = false + ProvisioningUtil.getInstance(LocalContext.current) + .secureAreaRepository.implementations.forEach { implementation -> + TextDropDownRow( + label = implementation.displayName, + onSelected = { + onKeystoreImplementationChanged(implementation.toSecureAreaState()) + expanded = false + } + ) } - ) } } } -@Composable -private fun AuthenticationKeyCurveAndroid( - modifier: Modifier = Modifier, - state: AndroidAuthKeyCurveState, - mDocAuthState: MdocAuthOptionState, - onAndroidAuthKeyCurveChanged: (newValue: AndroidAuthKeyCurveOption) -> Unit -) { - LabeledUserInput( - modifier = modifier, - label = stringResource(id = R.string.authentication_key_curve_label) - ) { - var keyCurveDropDownExpanded by remember { mutableStateOf(false) } - val clickModifier = if (state.isEnabled) { - Modifier.clickable { keyCurveDropDownExpanded = true } - } else { - Modifier - } - val alpha = if (state.isEnabled) 1f else .5f - OutlinedContainerHorizontal( - modifier = Modifier - .fillMaxWidth() - .alpha(alpha) - .then(clickModifier) - ) { - ValueLabel( - modifier = Modifier.weight(1f), - label = curveLabelFor(state.authCurve.toEcCurve()) - ) - DropDownIndicator() - } - DropdownMenu( - expanded = keyCurveDropDownExpanded, - onDismissRequest = { keyCurveDropDownExpanded = false } - ) { - val ecCurveOption = if (mDocAuthState.mDocAuthentication == MdocAuthStateOption.ECDSA) { - AndroidAuthKeyCurveOption.Ed25519 - } else { - AndroidAuthKeyCurveOption.X25519 - } - TextDropDownRow( - label = curveLabelFor(curveOption = AndroidAuthKeyCurveOption.P_256.toEcCurve()), - onSelected = { - onAndroidAuthKeyCurveChanged(AndroidAuthKeyCurveOption.P_256) - keyCurveDropDownExpanded = false - } - ) - TextDropDownRow( - label = curveLabelFor(curveOption = ecCurveOption.toEcCurve()), - onSelected = { - onAndroidAuthKeyCurveChanged(ecCurveOption) - keyCurveDropDownExpanded = false - } - ) - } - } -} - -@Composable -private fun AuthenticationKeyCurveBouncyCastle( - modifier: Modifier = Modifier, - state: BouncyCastleAuthKeyCurveState, - mDocAuthState: MdocAuthOptionState, - onBouncyCastleAuthKeyCurveChanged: (newValue: BouncyCastleAuthKeyCurveOption) -> Unit -) { - LabeledUserInput( - modifier = modifier, - label = stringResource(id = R.string.authentication_key_curve_label) - ) { - var keyCurveDropDownExpanded by remember { mutableStateOf(false) } - val clickModifier = if (state.isEnabled) { - Modifier.clickable { keyCurveDropDownExpanded = true } - } else { - Modifier - } - val alpha = if (state.isEnabled) 1f else .5f - OutlinedContainerHorizontal( - modifier = Modifier - .fillMaxWidth() - .alpha(alpha) - .then(clickModifier) - ) { - ValueLabel( - modifier = Modifier.weight(1f), - label = curveLabelFor(state.authCurve.toEcCurve()) - ) - DropDownIndicator() - } - val entries = BouncyCastleAuthKeyCurveOption.values().toMutableList() - if (mDocAuthState.mDocAuthentication == MdocAuthStateOption.ECDSA) { - entries.remove(BouncyCastleAuthKeyCurveOption.X448) - entries.remove(BouncyCastleAuthKeyCurveOption.X25519) - } else { - entries.remove(BouncyCastleAuthKeyCurveOption.Ed448) - entries.remove(BouncyCastleAuthKeyCurveOption.Ed25519) - } - DropdownMenu( - expanded = keyCurveDropDownExpanded, - onDismissRequest = { keyCurveDropDownExpanded = false } - ) { - for (entry in entries) { - TextDropDownRow( - label = curveLabelFor(curveOption = entry.toEcCurve()), - onSelected = { - onBouncyCastleAuthKeyCurveChanged(entry) - keyCurveDropDownExpanded = false - } - ) - } - } - } -} - -@Composable -private fun CounterInput( - modifier: Modifier = Modifier, - label: String, - value: Int, - onValueChange: (newValue: Int) -> Unit -) { - Column(modifier = modifier) { - OutlinedContainerHorizontal(modifier = Modifier.fillMaxWidth()) { - ValueLabel( - modifier = Modifier.weight(1f), - label = label - ) - NumberChanger(number = value, onNumberChanged = onValueChange) - } - } -} - -@Composable -private fun TextDropDownRow( - modifier: Modifier = Modifier, - label: String, - onSelected: () -> Unit -) { - DropdownMenuItem( - modifier = modifier, - text = { - ValueLabel( - modifier = Modifier.fillMaxWidth(), - label = label - ) - }, - onClick = onSelected - ) -} - @Composable private fun CardArtDropDownRow( modifier: Modifier = Modifier, @@ -848,48 +402,6 @@ private fun CardArtDropDownRow( ) } -@Composable -private fun LabeledUserInput( - modifier: Modifier = Modifier, - label: String, - content: @Composable () -> Unit -) { - Column( - modifier = modifier, - verticalArrangement = Arrangement.spacedBy(8.dp) - ) { - Row(modifier = Modifier.fillMaxWidth()) { - ValueLabel(label = label) - } - content() - } -} - -@Composable -fun OutlinedContainerHorizontal( - modifier: Modifier = Modifier, - outlineBorderWidth: Dp = 2.dp, - outlineBrush: Brush? = null, - content: @Composable RowScope.() -> Unit -) { - val brush = outlineBrush ?: SolidColor(MaterialTheme.colorScheme.outline) - Row( - modifier = modifier - .heightIn(48.dp) - .clip(RoundedCornerShape(12.dp)) - .border(outlineBorderWidth, brush, RoundedCornerShape(12.dp)) - .background(MaterialTheme.colorScheme.inverseOnSurface), - verticalAlignment = Alignment.CenterVertically - ) { - Row( - modifier = Modifier.padding(horizontal = 12.dp), - verticalAlignment = Alignment.CenterVertically - ) { - content() - } - } -} - @Composable fun OutlinedContainerVertical( modifier: Modifier = Modifier, @@ -915,82 +427,6 @@ fun OutlinedContainerVertical( } } -@Composable -private fun ValueLabel( - modifier: Modifier = Modifier, - label: String -) { - Text( - modifier = modifier, - text = label, - color = MaterialTheme.colorScheme.onSurface, - style = MaterialTheme.typography.labelMedium - ) -} - -@Composable -private fun DropDownIndicator( - modifier: Modifier = Modifier -) { - Icon( - modifier = modifier, - imageVector = Icons.Default.ArrowDropDown, - contentDescription = null, - tint = MaterialTheme.colorScheme.onSurface - ) -} - -@OptIn(ExperimentalAnimationApi::class) -@Composable -private fun NumberChanger( - modifier: Modifier = Modifier, - number: Int, - onNumberChanged: (newValue: Int) -> Unit, - counterTextStyle: TextStyle = MaterialTheme.typography.bodyLarge -) { - Row( - modifier = modifier, - verticalAlignment = Alignment.CenterVertically - ) { - IconButton(onClick = { onNumberChanged(number - 1) }) { - Icon(imageVector = Icons.Default.Remove, contentDescription = null) - } - AnimatedContent( - targetState = number, - label = "", - transitionSpec = { - if (targetState > initialState) { - slideInVertically { -it } with slideOutVertically { it } - } else { - slideInVertically { it } with slideOutVertically { -it } - } - } - ) { count -> - Text( - text = "$count", - textAlign = TextAlign.Center, - style = counterTextStyle - ) - } - IconButton(onClick = { onNumberChanged(number + 1) }) { - Icon(imageVector = Icons.Default.Add, contentDescription = null) - } - } -} - -@Composable -private fun mdocAuthOptionLabelFor( - state: MdocAuthStateOption -): String { - return when (state) { - MdocAuthStateOption.ECDSA -> - stringResource(id = R.string.mdoc_auth_ecdsa) - - MdocAuthStateOption.MAC -> - stringResource(id = R.string.mdoc_auth_mac) - } -} - @StringRes private fun documentNameFor(documentType: DocumentType): Int { return when (documentType) { @@ -1009,103 +445,4 @@ private fun colorNameFor(cardArt: DocumentColor): Int { is DocumentColor.Blue -> R.string.document_color_blue is DocumentColor.Red -> R.string.document_color_red } -} - -@Composable -@PreviewLightDark -private fun PreviewAddSelfSignedDocumentScreenAndroidKeystore() { - HolderAppTheme { - AddSelfSignedDocumentScreenContent( - modifier = Modifier.fillMaxSize(), - screenState = AddSelfSignedScreenState(), - onDocumentTypeChanged = {}, - onCardArtSelected = {}, - onDocumentNameChanged = {}, - onKeystoreImplementationChanged = {}, - onUserAuthenticationChanged = {}, - onAuthTimeoutChanged = {}, - onLskfAuthChanged = {}, - onBiometricAuthChanged = {}, - onStrongBoxChanged = {}, - onMdocAuthOptionChange = {}, - onAndroidAuthKeyCurveChanged = {}, - onBouncyCastleAuthKeyCurveChanged = {}, - onPassphraseChanged = {}, - onNumberOfMsoChanged = {}, - onMaxUseOfMsoChanged = {}, - onValidityInDaysChanged = {}, - onMinValidityInDaysChanged = {}, - onNext = {} - ) - } -} - -@Composable -@PreviewLightDark -private fun PreviewAddSelfSignedDocumentScreenAndroidKeystoreAuthOn() { - HolderAppTheme { - AddSelfSignedDocumentScreenContent( - modifier = Modifier.fillMaxSize(), - screenState = AddSelfSignedScreenState( - userAuthentication = true, - allowLSKFUnlocking = AuthTypeState( - isEnabled = true, - canBeModified = true - ), - allowBiometricUnlocking = AuthTypeState( - isEnabled = true, - canBeModified = false - ), - ), - onDocumentTypeChanged = {}, - onCardArtSelected = {}, - onDocumentNameChanged = {}, - onKeystoreImplementationChanged = {}, - onUserAuthenticationChanged = {}, - onAuthTimeoutChanged = {}, - onLskfAuthChanged = {}, - onBiometricAuthChanged = {}, - onStrongBoxChanged = {}, - onMdocAuthOptionChange = {}, - onAndroidAuthKeyCurveChanged = {}, - onBouncyCastleAuthKeyCurveChanged = {}, - onPassphraseChanged = {}, - onNumberOfMsoChanged = {}, - onMaxUseOfMsoChanged = {}, - onValidityInDaysChanged = {}, - onMinValidityInDaysChanged = {}, - onNext = {} - ) - } -} - -@Composable -@PreviewLightDark -private fun PreviewAddSelfSignedDocumentScreenBouncyCastleKeystore() { - HolderAppTheme { - AddSelfSignedDocumentScreenContent( - modifier = Modifier.fillMaxSize(), - screenState = AddSelfSignedScreenState( - secureAreaImplementationState = SecureAreaImplementationState.BouncyCastle - ), - onDocumentTypeChanged = {}, - onCardArtSelected = {}, - onDocumentNameChanged = {}, - onKeystoreImplementationChanged = {}, - onUserAuthenticationChanged = {}, - onAuthTimeoutChanged = {}, - onLskfAuthChanged = {}, - onBiometricAuthChanged = {}, - onStrongBoxChanged = {}, - onMdocAuthOptionChange = {}, - onAndroidAuthKeyCurveChanged = {}, - onBouncyCastleAuthKeyCurveChanged = {}, - onPassphraseChanged = {}, - onNumberOfMsoChanged = {}, - onMaxUseOfMsoChanged = {}, - onValidityInDaysChanged = {}, - onMinValidityInDaysChanged = {}, - onNext = {} - ) - } } \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/selfsigned/AddSelfSignedFragment.kt b/appholder/src/main/java/com/android/identity/wallet/selfsigned/AddSelfSignedFragment.kt index dbca20cf2..df15a1e5d 100644 --- a/appholder/src/main/java/com/android/identity/wallet/selfsigned/AddSelfSignedFragment.kt +++ b/appholder/src/main/java/com/android/identity/wallet/selfsigned/AddSelfSignedFragment.kt @@ -33,21 +33,15 @@ class AddSelfSignedFragment : Fragment() { private fun onNext() { val state = viewModel.screenState.value + val secureAreaScreenState = requireNotNull(state.secureAreaSupportState) val provisionInfo = ProvisionInfo( docType = state.documentType.value, docName = state.documentName, docColor = state.cardArt.value, - secureAreaImplementationStateType = state.secureAreaImplementationState, - userAuthentication = state.userAuthentication, - userAuthenticationTimeoutSeconds = state.userAuthenticationTimeoutSeconds, - allowLskfUnlocking = state.allowLSKFUnlocking.isEnabled, - allowBiometricUnlocking = state.allowBiometricUnlocking.isEnabled, - useStrongBox = state.useStrongBox.isEnabled, - mDocAuthenticationOption = state.androidMdocAuthState.mDocAuthentication, - authKeyCurve = state.ecCurve, + currentSecureArea = state.currentSecureArea, + secureAreaSupportState = secureAreaScreenState, validityInDays = state.validityInDays, minValidityInDays = state.minValidityInDays, - passphrase = state.passphrase.ifBlank { null }, numberMso = state.numberOfMso, maxUseMso = state.maxUseOfMso ) diff --git a/appholder/src/main/java/com/android/identity/wallet/selfsigned/AddSelfSignedScreenState.kt b/appholder/src/main/java/com/android/identity/wallet/selfsigned/AddSelfSignedScreenState.kt index 184ec58f0..a49ca20da 100644 --- a/appholder/src/main/java/com/android/identity/wallet/selfsigned/AddSelfSignedScreenState.kt +++ b/appholder/src/main/java/com/android/identity/wallet/selfsigned/AddSelfSignedScreenState.kt @@ -1,10 +1,12 @@ package com.android.identity.wallet.selfsigned import android.os.Parcelable -import com.android.identity.securearea.SecureArea import com.android.identity.wallet.document.DocumentColor import com.android.identity.wallet.document.DocumentType -import com.android.identity.wallet.document.SecureAreaImplementationState +import com.android.identity.wallet.support.CurrentSecureArea +import com.android.identity.wallet.support.SecureAreaSupportState +import com.android.identity.wallet.support.toSecureAreaState +import com.android.identity.wallet.util.ProvisioningUtil import kotlinx.parcelize.Parcelize @Parcelize @@ -12,111 +14,10 @@ data class AddSelfSignedScreenState( val documentType: DocumentType = DocumentType.MDL, val cardArt: DocumentColor = DocumentColor.Green, val documentName: String = "Driving License", - val secureAreaImplementationState: SecureAreaImplementationState = SecureAreaImplementationState.Android, - val userAuthentication: Boolean = true, - val userAuthenticationTimeoutSeconds: Int = 0, - val allowLSKFUnlocking: AuthTypeState = AuthTypeState( - isEnabled = true, - canBeModified = false - ), - val allowBiometricUnlocking: AuthTypeState = AuthTypeState( - isEnabled = true, - canBeModified = false - ), - val useStrongBox: AuthTypeState = AuthTypeState( - isEnabled = false, - canBeModified = false - ), - val androidMdocAuthState: MdocAuthOptionState = MdocAuthOptionState(), - val androidAuthKeyCurveState: AndroidAuthKeyCurveState = AndroidAuthKeyCurveState(), - val bouncyCastleAuthKeyCurveState: BouncyCastleAuthKeyCurveState = BouncyCastleAuthKeyCurveState(), - val passphrase: String = "", + val currentSecureArea: CurrentSecureArea = ProvisioningUtil.defaultSecureArea.toSecureAreaState(), val numberOfMso: Int = 10, val maxUseOfMso: Int = 1, val validityInDays: Int = 30, - val minValidityInDays: Int = 10 -) : Parcelable { - - val isAndroidKeystoreSelected: Boolean - get() = secureAreaImplementationState == SecureAreaImplementationState.Android - - val ecCurve: Int - get() = if (secureAreaImplementationState == SecureAreaImplementationState.Android) { - androidAuthKeyCurveState.authCurve.toEcCurve() - } else { - bouncyCastleAuthKeyCurveState.authCurve.toEcCurve() - } - - @Parcelize - data class AuthTypeState( - val isEnabled: Boolean = true, - val canBeModified: Boolean = false - ) : Parcelable - - @Parcelize - data class MdocAuthOptionState( - val isEnabled: Boolean = true, - val mDocAuthentication: MdocAuthStateOption = MdocAuthStateOption.ECDSA - ) : Parcelable - - @Parcelize - data class AndroidAuthKeyCurveState( - val isEnabled: Boolean = true, - val authCurve: AndroidAuthKeyCurveOption = AndroidAuthKeyCurveOption.P_256 - ) : Parcelable - - @Parcelize - data class BouncyCastleAuthKeyCurveState( - val isEnabled: Boolean = true, - val authCurve: BouncyCastleAuthKeyCurveOption = BouncyCastleAuthKeyCurveOption.P256 - ) : Parcelable - - @Parcelize - enum class MdocAuthStateOption : Parcelable { - ECDSA, MAC - } - - @Parcelize - enum class AndroidAuthKeyCurveOption : Parcelable { - P_256, Ed25519, X25519; - - fun toEcCurve(): Int { - return when (this) { - P_256 -> SecureArea.EC_CURVE_P256 - Ed25519 -> SecureArea.EC_CURVE_ED25519 - X25519 -> SecureArea.EC_CURVE_X25519 - } - } - } - - @Parcelize - enum class BouncyCastleAuthKeyCurveOption : Parcelable { - P256, - P384, - P521, - BrainPoolP256R1, - BrainPoolP320R1, - BrainPoolP384R1, - BrainPoolP512R1, - Ed25519, - Ed448, - X25519, - X448; - - fun toEcCurve(): Int { - return when (this) { - P256 -> SecureArea.EC_CURVE_P256 - P384 -> SecureArea.EC_CURVE_P384 - P521 -> SecureArea.EC_CURVE_P521 - BrainPoolP256R1 -> SecureArea.EC_CURVE_BRAINPOOLP256R1 - BrainPoolP320R1 -> SecureArea.EC_CURVE_BRAINPOOLP320R1 - BrainPoolP384R1 -> SecureArea.EC_CURVE_BRAINPOOLP384R1 - BrainPoolP512R1 -> SecureArea.EC_CURVE_BRAINPOOLP512R1 - Ed25519 -> SecureArea.EC_CURVE_ED25519 - Ed448 -> SecureArea.EC_CURVE_ED448 - X25519 -> SecureArea.EC_CURVE_X25519 - X448 -> SecureArea.EC_CURVE_X448 - } - } - } -} + val minValidityInDays: Int = 10, + val secureAreaSupportState: SecureAreaSupportState? = null, +) : Parcelable diff --git a/appholder/src/main/java/com/android/identity/wallet/selfsigned/AddSelfSignedViewModel.kt b/appholder/src/main/java/com/android/identity/wallet/selfsigned/AddSelfSignedViewModel.kt index d84187ad7..713846ce2 100644 --- a/appholder/src/main/java/com/android/identity/wallet/selfsigned/AddSelfSignedViewModel.kt +++ b/appholder/src/main/java/com/android/identity/wallet/selfsigned/AddSelfSignedViewModel.kt @@ -1,18 +1,11 @@ package com.android.identity.wallet.selfsigned -import android.content.Context import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel -import com.android.identity.android.securearea.AndroidKeystoreSecureArea import com.android.identity.wallet.document.DocumentColor import com.android.identity.wallet.document.DocumentType -import com.android.identity.wallet.document.SecureAreaImplementationState -import com.android.identity.wallet.selfsigned.AddSelfSignedScreenState.AndroidAuthKeyCurveOption -import com.android.identity.wallet.selfsigned.AddSelfSignedScreenState.AndroidAuthKeyCurveState -import com.android.identity.wallet.selfsigned.AddSelfSignedScreenState.AuthTypeState -import com.android.identity.wallet.selfsigned.AddSelfSignedScreenState.BouncyCastleAuthKeyCurveOption -import com.android.identity.wallet.selfsigned.AddSelfSignedScreenState.MdocAuthOptionState -import com.android.identity.wallet.selfsigned.AddSelfSignedScreenState.MdocAuthStateOption +import com.android.identity.wallet.support.CurrentSecureArea +import com.android.identity.wallet.support.SecureAreaSupportState import com.android.identity.wallet.util.getState import com.android.identity.wallet.util.updateState import kotlinx.coroutines.flow.StateFlow @@ -22,35 +15,10 @@ class AddSelfSignedViewModel( private val savedStateHandle: SavedStateHandle ) : ViewModel() { - private var capabilities: AndroidKeystoreSecureArea.Capabilities? = null - val screenState: StateFlow = savedStateHandle.getState( AddSelfSignedScreenState() ) - fun loadConfiguration(context: Context) { - capabilities = AndroidKeystoreSecureArea.Capabilities(context) - savedStateHandle.updateState { - it.copy( - allowLSKFUnlocking = AuthTypeState( - true, - capabilities!!.multipleAuthenticationTypesSupported - ), - allowBiometricUnlocking = AuthTypeState( - true, - capabilities!!.multipleAuthenticationTypesSupported - ), - useStrongBox = AuthTypeState(false, capabilities!!.strongBoxSupported), - androidMdocAuthState = MdocAuthOptionState( - isEnabled = if (it.useStrongBox.isEnabled) capabilities!!.strongBoxKeyAgreementSupported else capabilities!!.keyAgreementSupported - ), - androidAuthKeyCurveState = AndroidAuthKeyCurveState( - isEnabled = if (it.useStrongBox.isEnabled) capabilities!!.strongBoxCurve25519Supported else capabilities!!.curve25519Supported - ) - ) - } - } - fun updateDocumentType(newValue: DocumentType) { savedStateHandle.updateState { it.copy(documentType = newValue, documentName = documentNameFor(newValue)) @@ -69,86 +37,15 @@ class AddSelfSignedViewModel( } } - fun updateKeystoreImplementation(newValue: SecureAreaImplementationState) { - savedStateHandle.updateState { - it.copy(secureAreaImplementationState = newValue) - } - } - - fun updateUserAuthentication(newValue: Boolean) { - savedStateHandle.updateState { - it.copy(userAuthentication = newValue) - } - } - - fun updateUserAuthenticationTimeoutSeconds(seconds: Int) { - if (seconds < 0) return - savedStateHandle.updateState { - it.copy(userAuthenticationTimeoutSeconds = seconds) - } - } - - fun updateLskfUnlocking(newValue: Boolean) { - savedStateHandle.updateState { - val allowLskfUnlock = if (it.allowBiometricUnlocking.isEnabled) newValue else true - it.copy(allowLSKFUnlocking = it.allowLSKFUnlocking.copy(isEnabled = allowLskfUnlock)) - } - } - - fun updateBiometricUnlocking(newValue: Boolean) { - savedStateHandle.updateState { - val allowBiometricUnlock = if (it.allowLSKFUnlocking.isEnabled) newValue else true - it.copy(allowBiometricUnlocking = it.allowBiometricUnlocking.copy(isEnabled = allowBiometricUnlock)) - } - } - - fun updateStrongBox(newValue: Boolean) { - savedStateHandle.updateState { - it.copy( - useStrongBox = it.useStrongBox.copy(isEnabled = newValue), - androidMdocAuthState = MdocAuthOptionState( - isEnabled = if (capabilities != null) { - if (newValue) - capabilities!!.strongBoxKeyAgreementSupported - else - capabilities!!.keyAgreementSupported - } else { - false - } - ), - androidAuthKeyCurveState = AndroidAuthKeyCurveState( - isEnabled = if (capabilities != null) { - if (newValue) - capabilities!!.strongBoxCurve25519Supported - else - capabilities!!.curve25519Supported - } else { - false - } - ) - ) - } - } - - fun updateMdocAuthOption(newValue: MdocAuthStateOption) { + fun updateKeystoreImplementation(newValue: CurrentSecureArea) { savedStateHandle.updateState { - it.copy( - androidMdocAuthState = it.androidMdocAuthState.copy(mDocAuthentication = newValue), - androidAuthKeyCurveState = it.androidAuthKeyCurveState.copy(authCurve = AndroidAuthKeyCurveOption.P_256), - bouncyCastleAuthKeyCurveState = it.bouncyCastleAuthKeyCurveState.copy(authCurve = BouncyCastleAuthKeyCurveOption.P256) - ) + it.copy(currentSecureArea = newValue) } } - fun updateAndroidAuthKeyCurve(newValue: AndroidAuthKeyCurveOption) { + fun updateSecureAreaSupportState(newValue: SecureAreaSupportState) { savedStateHandle.updateState { - it.copy(androidAuthKeyCurveState = it.androidAuthKeyCurveState.copy(authCurve = newValue)) - } - } - - fun updateBouncyCastleAuthKeyCurve(newValue: BouncyCastleAuthKeyCurveOption) { - savedStateHandle.updateState { - it.copy(bouncyCastleAuthKeyCurveState = it.bouncyCastleAuthKeyCurveState.copy(authCurve = newValue)) + it.copy(secureAreaSupportState = newValue) } } @@ -168,11 +65,6 @@ class AddSelfSignedViewModel( } } - fun updatePassphrase(newValue: String) { - savedStateHandle.updateState { - it.copy(passphrase = newValue) - } - } fun updateNumberOfMso(newValue: Int) { if (newValue <= 0) return diff --git a/appholder/src/main/java/com/android/identity/wallet/selfsigned/SelfSignedDocumentData.kt b/appholder/src/main/java/com/android/identity/wallet/selfsigned/SelfSignedDocumentData.kt index f258318d8..12894e813 100644 --- a/appholder/src/main/java/com/android/identity/wallet/selfsigned/SelfSignedDocumentData.kt +++ b/appholder/src/main/java/com/android/identity/wallet/selfsigned/SelfSignedDocumentData.kt @@ -2,9 +2,9 @@ package com.android.identity.wallet.selfsigned import android.graphics.Bitmap import android.os.Parcelable -import com.android.identity.wallet.document.SecureAreaImplementationState -import com.android.identity.wallet.selfsigned.AddSelfSignedScreenState.MdocAuthStateOption +import com.android.identity.wallet.support.CurrentSecureArea import com.android.identity.wallet.util.Field +import com.android.identity.wallet.support.SecureAreaSupportState import kotlinx.parcelize.Parcelize data class SelfSignedDocumentData( @@ -37,17 +37,10 @@ data class ProvisionInfo( val docType: String, var docName: String, var docColor: Int, - val secureAreaImplementationStateType: SecureAreaImplementationState, - val userAuthentication: Boolean, - val userAuthenticationTimeoutSeconds: Int, - val allowLskfUnlocking: Boolean, - val allowBiometricUnlocking: Boolean, - val useStrongBox: Boolean, - val mDocAuthenticationOption: MdocAuthStateOption, - val authKeyCurve: Int, + val currentSecureArea: CurrentSecureArea, + val secureAreaSupportState: SecureAreaSupportState, val validityInDays: Int, val minValidityInDays: Int, - val passphrase: String?, val numberMso: Int, val maxUseMso: Int ) : Parcelable \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/support/AndroidKeystoreSecureAreaSupport.kt b/appholder/src/main/java/com/android/identity/wallet/support/AndroidKeystoreSecureAreaSupport.kt new file mode 100644 index 000000000..006268631 --- /dev/null +++ b/appholder/src/main/java/com/android/identity/wallet/support/AndroidKeystoreSecureAreaSupport.kt @@ -0,0 +1,168 @@ +package com.android.identity.wallet.support + +import android.os.Handler +import android.os.Looper +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.fragment.app.Fragment +import com.android.identity.android.securearea.AndroidKeystoreSecureArea +import com.android.identity.android.securearea.AndroidKeystoreSecureArea.USER_AUTHENTICATION_TYPE_BIOMETRIC +import com.android.identity.android.securearea.AndroidKeystoreSecureArea.USER_AUTHENTICATION_TYPE_LSKF +import com.android.identity.credential.Credential +import com.android.identity.securearea.SecureArea +import com.android.identity.util.Timestamp +import com.android.identity.wallet.R +import com.android.identity.wallet.authprompt.UserAuthPromptBuilder +import com.android.identity.wallet.composables.AndroidSetupContainer +import com.android.identity.wallet.composables.AuthenticationKeyCurveAndroid +import com.android.identity.wallet.composables.MdocAuthentication +import com.android.identity.wallet.support.androidkeystore.AndroidAuthKeyCurveOption +import com.android.identity.wallet.support.androidkeystore.AndroidAuthKeyCurveState +import com.android.identity.wallet.composables.state.AuthTypeState +import com.android.identity.wallet.composables.state.MdocAuthOption + +class AndroidKeystoreSecureAreaSupport( + private val capabilities: AndroidKeystoreSecureArea.Capabilities +) : SecureAreaSupport { + + private val screenState = AndroidKeystoreSecureAreaSupportState( + allowLSKFUnlocking = AuthTypeState(true, capabilities.multipleAuthenticationTypesSupported), + allowBiometricUnlocking = AuthTypeState(true, capabilities.multipleAuthenticationTypesSupported), + useStrongBox = AuthTypeState(false, capabilities.strongBoxSupported), + mDocAuthOption = MdocAuthOption(isEnabled = capabilities.keyAgreementSupported), + authKeyCurveState = AndroidAuthKeyCurveState(isEnabled = capabilities.curve25519Supported) + ) + + override fun Fragment.unlockKey( + credential: Credential, + onKeyUnlocked: (unlockData: SecureArea.KeyUnlockData?) -> Unit, + onUnlockFailure: (wasCancelled: Boolean) -> Unit + ) { + val authKey = credential.findAuthenticationKey(Timestamp.now()) ?: throw IllegalStateException("No auth key available") + val keyInfo = credential.credentialSecureArea.getKeyInfo(authKey.alias) as AndroidKeystoreSecureArea.KeyInfo + val unlockData = AndroidKeystoreSecureArea.KeyUnlockData(authKey.alias) + + val allowLskf = keyInfo.userAuthenticationType == USER_AUTHENTICATION_TYPE_LSKF + val allowBiometric = keyInfo.userAuthenticationType == USER_AUTHENTICATION_TYPE_BIOMETRIC + val allowBoth = keyInfo.userAuthenticationType == USER_AUTHENTICATION_TYPE_LSKF or USER_AUTHENTICATION_TYPE_BIOMETRIC + val allowLskfUnlock = allowLskf || allowBoth + val allowBiometricUnlock = allowBiometric || allowBoth + val forceLskf: Boolean = !allowBiometricUnlock + + val userAuthRequest = UserAuthPromptBuilder.requestUserAuth(this) + .withTitle(getString(R.string.bio_auth_title)) + .withSuccessCallback { + onKeyUnlocked(unlockData) + } + .withCancelledCallback { + if (allowLskfUnlock) { + val runnable = { + unlockKey(credential, onKeyUnlocked, onUnlockFailure) + } + // Without this delay, the prompt won't reshow + Handler(Looper.getMainLooper()).postDelayed(runnable, 100) + } else { + onUnlockFailure(true) + } + } + .withFailureCallback { onUnlockFailure(false) } + .setForceLskf(forceLskf) + if (allowLskfUnlock) { + userAuthRequest.withNegativeButton(getString(R.string.bio_auth_use_pin)) + } else { + userAuthRequest.withNegativeButton("Cancel") + } + val cryptoObject = unlockData.getCryptoObjectForSigning(SecureArea.ALGORITHM_ES256) + userAuthRequest.build().authenticate(cryptoObject) + } + + @Composable + override fun SecureAreaAuthUi( + onUiStateUpdated: (newState: SecureAreaSupportState) -> Unit + ) { + var compositionState by remember { mutableStateOf(screenState) } + LaunchedEffect(key1 = compositionState) { + onUiStateUpdated(compositionState) + } + AndroidSetupContainer( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + isOn = compositionState.userAuthentication, + timeoutSeconds = compositionState.userAuthenticationTimeoutSeconds, + lskfAuthTypeState = compositionState.allowLSKFUnlocking, + biometricAuthTypeState = compositionState.allowBiometricUnlocking, + useStrongBox = compositionState.useStrongBox, + onUserAuthenticationChanged = { + compositionState = compositionState.copy(userAuthentication = it) + }, + onAuthTimeoutChanged = { seconds -> + if (seconds < 0) return@AndroidSetupContainer + compositionState = compositionState.copy(userAuthenticationTimeoutSeconds = seconds) + }, + onLskfAuthChanged = { + val allowLskfUnlock = + if (compositionState.allowBiometricUnlocking.isEnabled) it else true + val newValue = compositionState.allowLSKFUnlocking.copy(isEnabled = allowLskfUnlock) + compositionState = compositionState.copy(allowLSKFUnlocking = newValue) + }, + onBiometricAuthChanged = { + val allowBiometricUnlock = + if (compositionState.allowLSKFUnlocking.isEnabled) it else true + val newValue = + compositionState.allowBiometricUnlocking.copy(isEnabled = allowBiometricUnlock) + compositionState = compositionState.copy(allowBiometricUnlocking = newValue) + }, + onStrongBoxChanged = { newValue -> + val update = compositionState.copy( + useStrongBox = compositionState.useStrongBox.copy(isEnabled = newValue), + mDocAuthOption = MdocAuthOption( + isEnabled = if (newValue) capabilities.strongBoxKeyAgreementSupported else capabilities.keyAgreementSupported + ), + authKeyCurveState = AndroidAuthKeyCurveState( + isEnabled = if (newValue) capabilities.strongBoxCurve25519Supported else capabilities.curve25519Supported + ) + ) + compositionState = update + } + ) + MdocAuthentication( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + state = compositionState.mDocAuthOption, + onMdocAuthOptionChange = { newValue -> + val authState = compositionState.mDocAuthOption.copy(mDocAuthentication = newValue) + compositionState = compositionState.copy( + mDocAuthOption = authState, + authKeyCurveState = compositionState.authKeyCurveState.copy( + authCurve = AndroidAuthKeyCurveOption.P_256 + ) + ) + } + ) + AuthenticationKeyCurveAndroid( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + state = compositionState.authKeyCurveState, + mDocAuthState = compositionState.mDocAuthOption, + onAndroidAuthKeyCurveChanged = { + val newValue = compositionState.authKeyCurveState.copy(authCurve = it) + compositionState = compositionState.copy(authKeyCurveState = newValue) + } + ) + } + + override fun getSecureAreaSupportState(): SecureAreaSupportState { + return screenState + } +} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/support/AndroidKeystoreSecureAreaSupportState.kt b/appholder/src/main/java/com/android/identity/wallet/support/AndroidKeystoreSecureAreaSupportState.kt new file mode 100644 index 000000000..8ee513c12 --- /dev/null +++ b/appholder/src/main/java/com/android/identity/wallet/support/AndroidKeystoreSecureAreaSupportState.kt @@ -0,0 +1,78 @@ +package com.android.identity.wallet.support + +import com.android.identity.android.securearea.AndroidKeystoreSecureArea +import com.android.identity.credential.Credential +import com.android.identity.securearea.SecureArea +import com.android.identity.util.Timestamp +import com.android.identity.wallet.support.androidkeystore.AndroidAuthKeyCurveState +import com.android.identity.wallet.composables.state.AuthTypeState +import com.android.identity.wallet.composables.state.MdocAuthOption +import com.android.identity.wallet.composables.state.MdocAuthStateOption +import com.android.identity.wallet.util.toTimestampFromNow +import kotlinx.parcelize.Parcelize + +@Parcelize +data class AndroidKeystoreSecureAreaSupportState( + override val mDocAuthOption: MdocAuthOption = MdocAuthOption(), + val userAuthentication: Boolean = true, + val userAuthenticationTimeoutSeconds: Int = 0, + val allowLSKFUnlocking: AuthTypeState = AuthTypeState( + isEnabled = true, + canBeModified = false + ), + val allowBiometricUnlocking: AuthTypeState = AuthTypeState( + isEnabled = true, + canBeModified = false + ), + val useStrongBox: AuthTypeState = AuthTypeState( + isEnabled = false, + canBeModified = false + ), + val authKeyCurveState: AndroidAuthKeyCurveState = AndroidAuthKeyCurveState(), +) : SecureAreaSupportState { + + override fun createKeystoreSettings(validityInDays: Int): SecureArea.CreateKeySettings { + return AndroidKeystoreSecureArea.CreateKeySettings.Builder("challenge".toByteArray()) + .setKeyPurposes(mDocAuthOption.mDocAuthentication.toKeyPurpose()) + .setUseStrongBox(useStrongBox.isEnabled) + .setEcCurve(authKeyCurveState.authCurve.toEcCurve()) + .setValidityPeriod(Timestamp.now(), validityInDays.toTimestampFromNow()) + .setUserAuthenticationRequired( + userAuthentication, + userAuthenticationTimeoutSeconds * 1000L, + userAuthType() + ).build() + } + + override fun createKeystoreSettingForCredential( + mDocAuthOption: String, + credential: Credential + ): SecureArea.CreateKeySettings { + val keyInfo = credential.credentialSecureArea + .getKeyInfo(credential.credentialKeyAlias) as AndroidKeystoreSecureArea.KeyInfo + val builder = AndroidKeystoreSecureArea.CreateKeySettings.Builder("challenge".toByteArray()) + .setKeyPurposes(MdocAuthStateOption.valueOf(mDocAuthOption).toKeyPurpose()) + .setUseStrongBox(keyInfo.isStrongBoxBacked) + .setEcCurve(keyInfo.ecCurve) + .setValidityPeriod(Timestamp.now(), keyInfo.validUntil ?: Timestamp.now()) + .setUserAuthenticationRequired( + keyInfo.isUserAuthenticationRequired, + keyInfo.userAuthenticationTimeoutMillis, + keyInfo.userAuthenticationType + ) + return builder.build() + } + + private fun userAuthType(): Int { + var userAuthenticationType = 0 + if (allowLSKFUnlocking.isEnabled) { + userAuthenticationType = + userAuthenticationType or AndroidKeystoreSecureArea.USER_AUTHENTICATION_TYPE_LSKF + } + if (allowBiometricUnlocking.isEnabled) { + userAuthenticationType = + userAuthenticationType or AndroidKeystoreSecureArea.USER_AUTHENTICATION_TYPE_BIOMETRIC + } + return userAuthenticationType + } +} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/support/CurrentSecureArea.kt b/appholder/src/main/java/com/android/identity/wallet/support/CurrentSecureArea.kt new file mode 100644 index 000000000..7508e8571 --- /dev/null +++ b/appholder/src/main/java/com/android/identity/wallet/support/CurrentSecureArea.kt @@ -0,0 +1,13 @@ +package com.android.identity.wallet.support + +import android.os.Parcelable +import com.android.identity.securearea.SecureArea +import kotlinx.parcelize.IgnoredOnParcel +import kotlinx.parcelize.Parcelize + +@Parcelize +data class CurrentSecureArea( + @IgnoredOnParcel val secureArea: SecureArea, + val identifier: String = secureArea.identifier, + val displayName: String = secureArea.displayName +) : Parcelable \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/support/MdocAuthStateExtensions.kt b/appholder/src/main/java/com/android/identity/wallet/support/MdocAuthStateExtensions.kt new file mode 100644 index 000000000..450670065 --- /dev/null +++ b/appholder/src/main/java/com/android/identity/wallet/support/MdocAuthStateExtensions.kt @@ -0,0 +1,13 @@ +package com.android.identity.wallet.support + +import com.android.identity.securearea.SecureArea +import com.android.identity.wallet.composables.state.MdocAuthStateOption + +@SecureArea.KeyPurpose +fun MdocAuthStateOption.toKeyPurpose(): Int { + return if (this == MdocAuthStateOption.ECDSA) { + SecureArea.KEY_PURPOSE_SIGN + } else { + SecureArea.KEY_PURPOSE_AGREE_KEY + } +} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/support/SecureAreaSupport.kt b/appholder/src/main/java/com/android/identity/wallet/support/SecureAreaSupport.kt new file mode 100644 index 000000000..74d123b17 --- /dev/null +++ b/appholder/src/main/java/com/android/identity/wallet/support/SecureAreaSupport.kt @@ -0,0 +1,86 @@ +package com.android.identity.wallet.support + +import android.content.Context +import androidx.compose.runtime.Composable +import androidx.fragment.app.Fragment +import com.android.identity.android.securearea.AndroidKeystoreSecureArea +import com.android.identity.credential.Credential +import com.android.identity.securearea.SecureArea +import com.android.identity.securearea.SoftwareSecureArea +import com.android.identity.wallet.util.ProvisioningUtil + +interface SecureAreaSupport { + + /** + * This function should create a composable that will render the portion of the UI + * for the specific [SecureArea] setup inside the [AddSelfSignedDocumentScreen]. + * + * The composable should hold and manage its state internally, and expose it through + * the [onUiStateUpdated] lambda. The state must be an implementation of [SecureAreaSupportState] + */ + @Composable + fun SecureAreaAuthUi(onUiStateUpdated: (newState: SecureAreaSupportState) -> Unit) + + /** + * This function should create [SecureArea.KeyUnlockData] based on the incoming [Credential]. + * It's implementation should decide on the mechanism that will do the unlocking (i.e. present + * a biometric prompts or other sort of UI), and the way the [SecureArea.KeyUnlockData] is created. + * + * The function is an extension on a [Fragment] due to the nature of Android and the navigation, + * so in case of rendering a UI specific for unlocking (like a Biometric Prompt, or a Dialog), + * there is a provided way to navigate using the [findNavController] function. + */ + fun Fragment.unlockKey( + credential: Credential, + onKeyUnlocked: (unlockData: SecureArea.KeyUnlockData?) -> Unit, + onUnlockFailure: (wasCancelled: Boolean) -> Unit + ) + + /** + * Should return the current [SecureAreaSupportState] which is used by the composition + * when rendering the composable UI. + */ + fun getSecureAreaSupportState(): SecureAreaSupportState + + companion object { + fun getInstance( + context: Context, + currentSecureArea: CurrentSecureArea + ): SecureAreaSupport { + return when (currentSecureArea.secureArea) { + is AndroidKeystoreSecureArea -> { + val capabilities = AndroidKeystoreSecureArea.Capabilities(context) + AndroidKeystoreSecureAreaSupport(capabilities) + } + + is SoftwareSecureArea -> SoftwareKeystoreSecureAreaSupport() + + else -> SecureAreaSupportNull() + } + } + + fun getInstance( + context: Context, + credential: Credential + ): SecureAreaSupport { + return when (credential.credentialSecureArea) { + is AndroidKeystoreSecureArea -> { + val capabilities = AndroidKeystoreSecureArea.Capabilities(context) + AndroidKeystoreSecureAreaSupport(capabilities) + } + + is SoftwareSecureArea -> SoftwareKeystoreSecureAreaSupport() + + else -> SecureAreaSupportNull() + } + } + } +} + +/** + * Utility function to convert a [SecureArea] implementation into a corresponding state + * used by the Jetpack Compose composition when rendering the UI. + */ +fun SecureArea.toSecureAreaState(): CurrentSecureArea { + return CurrentSecureArea(this) +} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/support/SecureAreaSupportNull.kt b/appholder/src/main/java/com/android/identity/wallet/support/SecureAreaSupportNull.kt new file mode 100644 index 000000000..003399bf1 --- /dev/null +++ b/appholder/src/main/java/com/android/identity/wallet/support/SecureAreaSupportNull.kt @@ -0,0 +1,51 @@ +package com.android.identity.wallet.support + +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.fragment.app.Fragment +import com.android.identity.credential.Credential +import com.android.identity.securearea.SecureArea +import com.android.identity.wallet.selfsigned.OutlinedContainerVertical + +class SecureAreaSupportNull : SecureAreaSupport { + + private val state = SecureAreaSupportStateNull() + + @Composable + override fun SecureAreaAuthUi(onUiStateUpdated: (newState: SecureAreaSupportState) -> Unit) { + val compositionState by remember { mutableStateOf(state) } + LaunchedEffect(key1 = compositionState) { + onUiStateUpdated(compositionState) + } + OutlinedContainerVertical( + modifier = Modifier.padding(horizontal = 16.dp) + ) { + Text( + modifier = Modifier.padding(horizontal = 4.dp, vertical = 8.dp), + text = "The selected Secure Area lacks dedicated support so no additional options are available. " + + "Please add a SecureAreaSupport-derived class.", + style = MaterialTheme.typography.bodySmall + ) + } + } + + override fun Fragment.unlockKey( + credential: Credential, + onKeyUnlocked: (unlockData: SecureArea.KeyUnlockData?) -> Unit, + onUnlockFailure: (wasCancelled: Boolean) -> Unit + ) { + throw IllegalStateException("No implementation") + } + + override fun getSecureAreaSupportState(): SecureAreaSupportState { + return state + } +} diff --git a/appholder/src/main/java/com/android/identity/wallet/support/SecureAreaSupportState.kt b/appholder/src/main/java/com/android/identity/wallet/support/SecureAreaSupportState.kt new file mode 100644 index 000000000..d6427f69d --- /dev/null +++ b/appholder/src/main/java/com/android/identity/wallet/support/SecureAreaSupportState.kt @@ -0,0 +1,18 @@ +package com.android.identity.wallet.support + +import android.os.Parcelable +import com.android.identity.credential.Credential +import com.android.identity.securearea.SecureArea +import com.android.identity.wallet.composables.state.MdocAuthOption + +interface SecureAreaSupportState : Parcelable { + + val mDocAuthOption: MdocAuthOption + + fun createKeystoreSettings(validityInDays: Int): SecureArea.CreateKeySettings + + fun createKeystoreSettingForCredential( + mDocAuthOption: String, + credential: Credential + ): SecureArea.CreateKeySettings +} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/support/SecureAreaSupportStateNull.kt b/appholder/src/main/java/com/android/identity/wallet/support/SecureAreaSupportStateNull.kt new file mode 100644 index 000000000..f182acd4b --- /dev/null +++ b/appholder/src/main/java/com/android/identity/wallet/support/SecureAreaSupportStateNull.kt @@ -0,0 +1,24 @@ +package com.android.identity.wallet.support + +import com.android.identity.credential.Credential +import com.android.identity.securearea.SecureArea +import com.android.identity.wallet.composables.state.MdocAuthOption +import kotlinx.parcelize.Parcelize + +@Parcelize +class SecureAreaSupportStateNull : SecureAreaSupportState { + + override val mDocAuthOption: MdocAuthOption + get() = MdocAuthOption() + + override fun createKeystoreSettings(validityInDays: Int): SecureArea.CreateKeySettings { + return SecureArea.CreateKeySettings("challenge".toByteArray()) + } + + override fun createKeystoreSettingForCredential( + mDocAuthOption: String, + credential: Credential + ): SecureArea.CreateKeySettings { + return SecureArea.CreateKeySettings("challenge".toByteArray()) + } +} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/support/SoftwareKeystoreSecureAreaSupport.kt b/appholder/src/main/java/com/android/identity/wallet/support/SoftwareKeystoreSecureAreaSupport.kt new file mode 100644 index 000000000..07c3d14b8 --- /dev/null +++ b/appholder/src/main/java/com/android/identity/wallet/support/SoftwareKeystoreSecureAreaSupport.kt @@ -0,0 +1,113 @@ +package com.android.identity.wallet.support + +import android.os.Handler +import android.os.Looper +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.fragment.app.Fragment +import androidx.fragment.app.activityViewModels +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle +import androidx.navigation.fragment.findNavController +import com.android.identity.credential.Credential +import com.android.identity.securearea.SecureArea +import com.android.identity.securearea.SoftwareSecureArea +import com.android.identity.wallet.authconfirmation.AuthConfirmationFragmentDirections +import com.android.identity.wallet.authconfirmation.PassphraseAuthResult +import com.android.identity.wallet.authconfirmation.PassphrasePromptViewModel +import com.android.identity.wallet.composables.AuthenticationKeyCurveSoftware +import com.android.identity.wallet.composables.MdocAuthentication +import com.android.identity.wallet.composables.SoftwareSetupContainer +import com.android.identity.wallet.support.softwarekeystore.SoftwareAuthKeyCurveOption +import kotlinx.coroutines.launch + +class SoftwareKeystoreSecureAreaSupport : SecureAreaSupport { + + private val screenState = SoftwareKeystoreSecureAreaSupportState() + + override fun Fragment.unlockKey( + credential: Credential, + onKeyUnlocked: (unlockData: SecureArea.KeyUnlockData?) -> Unit, + onUnlockFailure: (wasCancelled: Boolean) -> Unit + ) { + val viewModel: PassphrasePromptViewModel by activityViewModels() + var didAttemptToUnlock = false + + viewLifecycleOwner.lifecycleScope.launch { + viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.CREATED) { + viewModel.authorizationState.collect { value -> + if (value is PassphraseAuthResult.Success) { + val keyUnlockData = SoftwareSecureArea.KeyUnlockData(value.userPassphrase) + didAttemptToUnlock = true + onKeyUnlocked(keyUnlockData) + viewModel.reset() + } + } + } + } + val destination = AuthConfirmationFragmentDirections.openPassphrasePrompt( + showIncorrectPassword = didAttemptToUnlock + ) + val runnable = { findNavController().navigate(destination) } + // The system needs a little time to get back to this screen + Handler(Looper.getMainLooper()).postDelayed(runnable, 500) + } + + @Composable + override fun SecureAreaAuthUi( + onUiStateUpdated: (newState: SecureAreaSupportState) -> Unit + ) { + var compositionState by remember { mutableStateOf(screenState) } + LaunchedEffect(key1 = compositionState) { + onUiStateUpdated(compositionState) + } + SoftwareSetupContainer( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + passphrase = compositionState.passphrase, + onPassphraseChanged = { + compositionState = compositionState.copy(passphrase = it) + } + ) + MdocAuthentication( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + state = compositionState.mDocAuthOption, + onMdocAuthOptionChange = { + val newValue = compositionState.mDocAuthOption.copy(mDocAuthentication = it) + compositionState = compositionState.copy( + mDocAuthOption = newValue, + softwareAuthKeyCurveState = compositionState.softwareAuthKeyCurveState.copy( + authCurve = SoftwareAuthKeyCurveOption.P256 + ) + ) + } + ) + AuthenticationKeyCurveSoftware( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + state = compositionState.softwareAuthKeyCurveState, + mDocAuthState = compositionState.mDocAuthOption, + onSoftwareAuthKeyCurveChanged = { + val newValue = compositionState.authKeyCurve.copy(authCurve = it) + compositionState = compositionState.copy(softwareAuthKeyCurveState = newValue) + } + ) + } + + override fun getSecureAreaSupportState(): SecureAreaSupportState { + return screenState + } +} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/support/SoftwareKeystoreSecureAreaSupportState.kt b/appholder/src/main/java/com/android/identity/wallet/support/SoftwareKeystoreSecureAreaSupportState.kt new file mode 100644 index 000000000..2824af7ec --- /dev/null +++ b/appholder/src/main/java/com/android/identity/wallet/support/SoftwareKeystoreSecureAreaSupportState.kt @@ -0,0 +1,92 @@ +package com.android.identity.wallet.support + +import com.android.identity.credential.Credential +import com.android.identity.securearea.SecureArea +import com.android.identity.securearea.SoftwareSecureArea +import com.android.identity.storage.EphemeralStorageEngine +import com.android.identity.util.Timestamp +import com.android.identity.wallet.support.softwarekeystore.SoftwareAuthKeyCurveState +import com.android.identity.wallet.composables.state.MdocAuthOption +import com.android.identity.wallet.composables.state.MdocAuthStateOption +import com.android.identity.wallet.util.ProvisioningUtil +import kotlinx.parcelize.IgnoredOnParcel +import kotlinx.parcelize.Parcelize +import java.security.PrivateKey +import java.security.cert.X509Certificate + +@Parcelize +data class SoftwareKeystoreSecureAreaSupportState( + override val mDocAuthOption: MdocAuthOption = MdocAuthOption(), + val softwareAuthKeyCurveState: SoftwareAuthKeyCurveState = SoftwareAuthKeyCurveState(), + val passphrase: String = "", + val authKeyCurve: SoftwareAuthKeyCurveState = SoftwareAuthKeyCurveState(), +) : SecureAreaSupportState { + + @IgnoredOnParcel + private lateinit var softwareAttestationKey: PrivateKey + @IgnoredOnParcel + private lateinit var softwareAttestationKeySignatureAlgorithm: String + @IgnoredOnParcel + private lateinit var softwareAttestationKeyCertification: List + + override fun createKeystoreSettings(validityInDays: Int): SecureArea.CreateKeySettings { + val passphraseValue = passphrase.ifBlank { null } + val keyPurposes = mDocAuthOption.mDocAuthentication.toKeyPurpose() + if (!this::softwareAttestationKey.isInitialized) { + initSoftwareAttestationKey() + } + val builder = SoftwareSecureArea.CreateKeySettings.Builder("challenge".toByteArray()) + .setAttestationKey( + softwareAttestationKey, + softwareAttestationKeySignatureAlgorithm, + softwareAttestationKeyCertification + ) + .setPassphraseRequired(passphraseValue != null, passphraseValue) + .setEcCurve(authKeyCurve.authCurve.toEcCurve()) + .setKeyPurposes(keyPurposes) + return builder.build() + } + + override fun createKeystoreSettingForCredential( + mDocAuthOption: String, + credential: Credential + ): SecureArea.CreateKeySettings { + val keyInfo = credential.credentialSecureArea + .getKeyInfo(credential.credentialKeyAlias) as SoftwareSecureArea.KeyInfo + if (!this::softwareAttestationKey.isInitialized) { + initSoftwareAttestationKey() + } + val keyPurpose = MdocAuthStateOption.valueOf(mDocAuthOption).toKeyPurpose() + val passphrase = credential.applicationData.getString(ProvisioningUtil.PASSPHRASE) + val builder = SoftwareSecureArea.CreateKeySettings.Builder("challenge".toByteArray()) + .setAttestationKey( + softwareAttestationKey, + softwareAttestationKeySignatureAlgorithm, + softwareAttestationKeyCertification + ) + .setPassphraseRequired(passphrase.isNotBlank(), passphrase) + .setKeyPurposes(keyPurpose) + .setEcCurve(keyInfo.ecCurve) + return builder.build() + } + + private fun initSoftwareAttestationKey() { + val secureArea = SoftwareSecureArea(EphemeralStorageEngine()) + val now = Timestamp.now() + secureArea.createKey( + "SoftwareAttestationRoot", + SoftwareSecureArea.CreateKeySettings.Builder("".toByteArray()) + .setEcCurve(SecureArea.EC_CURVE_P256) + .setKeyPurposes(SecureArea.KEY_PURPOSE_SIGN) + .setSubject("CN=Software Attestation Root") + .setValidityPeriod( + now, + Timestamp.ofEpochMilli(now.toEpochMilli() + 10L * 86400 * 365 * 1000) + ) + .build() + ) + softwareAttestationKey = secureArea.getPrivateKey("SoftwareAttestationRoot", null) + softwareAttestationKeySignatureAlgorithm = "SHA256withECDSA" + softwareAttestationKeyCertification = secureArea.getKeyInfo("SoftwareAttestationRoot").attestation + } +} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/support/androidkeystore/AndroidAuthKeyCurveOption.kt b/appholder/src/main/java/com/android/identity/wallet/support/androidkeystore/AndroidAuthKeyCurveOption.kt new file mode 100644 index 000000000..e9c0876ff --- /dev/null +++ b/appholder/src/main/java/com/android/identity/wallet/support/androidkeystore/AndroidAuthKeyCurveOption.kt @@ -0,0 +1,18 @@ +package com.android.identity.wallet.support.androidkeystore + +import android.os.Parcelable +import com.android.identity.securearea.SecureArea +import kotlinx.parcelize.Parcelize + +@Parcelize +enum class AndroidAuthKeyCurveOption : Parcelable { + P_256, Ed25519, X25519; + + fun toEcCurve(): Int { + return when (this) { + P_256 -> SecureArea.EC_CURVE_P256 + Ed25519 -> SecureArea.EC_CURVE_ED25519 + X25519 -> SecureArea.EC_CURVE_X25519 + } + } +} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/support/androidkeystore/AndroidAuthKeyCurveState.kt b/appholder/src/main/java/com/android/identity/wallet/support/androidkeystore/AndroidAuthKeyCurveState.kt new file mode 100644 index 000000000..80d8cbde8 --- /dev/null +++ b/appholder/src/main/java/com/android/identity/wallet/support/androidkeystore/AndroidAuthKeyCurveState.kt @@ -0,0 +1,10 @@ +package com.android.identity.wallet.support.androidkeystore + +import android.os.Parcelable +import kotlinx.parcelize.Parcelize + +@Parcelize +data class AndroidAuthKeyCurveState( + val isEnabled: Boolean = true, + val authCurve: AndroidAuthKeyCurveOption = AndroidAuthKeyCurveOption.P_256 +) : Parcelable \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/support/softwarekeystore/SoftwareAuthKeyCurveOption.kt b/appholder/src/main/java/com/android/identity/wallet/support/softwarekeystore/SoftwareAuthKeyCurveOption.kt new file mode 100644 index 000000000..df46334a8 --- /dev/null +++ b/appholder/src/main/java/com/android/identity/wallet/support/softwarekeystore/SoftwareAuthKeyCurveOption.kt @@ -0,0 +1,36 @@ +package com.android.identity.wallet.support.softwarekeystore + +import android.os.Parcelable +import com.android.identity.securearea.SecureArea +import kotlinx.parcelize.Parcelize + +@Parcelize +enum class SoftwareAuthKeyCurveOption : Parcelable { + P256, + P384, + P521, + BrainPoolP256R1, + BrainPoolP320R1, + BrainPoolP384R1, + BrainPoolP512R1, + Ed25519, + Ed448, + X25519, + X448; + + fun toEcCurve(): Int { + return when (this) { + P256 -> SecureArea.EC_CURVE_P256 + P384 -> SecureArea.EC_CURVE_P384 + P521 -> SecureArea.EC_CURVE_P521 + BrainPoolP256R1 -> SecureArea.EC_CURVE_BRAINPOOLP256R1 + BrainPoolP320R1 -> SecureArea.EC_CURVE_BRAINPOOLP320R1 + BrainPoolP384R1 -> SecureArea.EC_CURVE_BRAINPOOLP384R1 + BrainPoolP512R1 -> SecureArea.EC_CURVE_BRAINPOOLP512R1 + Ed25519 -> SecureArea.EC_CURVE_ED25519 + Ed448 -> SecureArea.EC_CURVE_ED448 + X25519 -> SecureArea.EC_CURVE_X25519 + X448 -> SecureArea.EC_CURVE_X448 + } + } +} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/support/softwarekeystore/SoftwareAuthKeyCurveState.kt b/appholder/src/main/java/com/android/identity/wallet/support/softwarekeystore/SoftwareAuthKeyCurveState.kt new file mode 100644 index 000000000..f7004ee44 --- /dev/null +++ b/appholder/src/main/java/com/android/identity/wallet/support/softwarekeystore/SoftwareAuthKeyCurveState.kt @@ -0,0 +1,10 @@ +package com.android.identity.wallet.support.softwarekeystore + +import android.os.Parcelable +import kotlinx.parcelize.Parcelize + +@Parcelize +data class SoftwareAuthKeyCurveState( + val isEnabled: Boolean = true, + val authCurve: SoftwareAuthKeyCurveOption = SoftwareAuthKeyCurveOption.P256 +) : Parcelable \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/transfer/AddDocumentToResponseResult.kt b/appholder/src/main/java/com/android/identity/wallet/transfer/AddDocumentToResponseResult.kt index 212a84616..d3acba4ed 100644 --- a/appholder/src/main/java/com/android/identity/wallet/transfer/AddDocumentToResponseResult.kt +++ b/appholder/src/main/java/com/android/identity/wallet/transfer/AddDocumentToResponseResult.kt @@ -1,18 +1,14 @@ package com.android.identity.wallet.transfer +import com.android.identity.credential.Credential + sealed class AddDocumentToResponseResult { data class DocumentAdded( val signingKeyUsageLimitPassed: Boolean ) : AddDocumentToResponseResult() - data class UserAuthRequired( - val keyAlias: String, - val allowLSKFUnlocking: Boolean, - val allowBiometricUnlocking: Boolean - ) : AddDocumentToResponseResult() - - data class PassphraseRequired( - val attemptedWithIncorrectPassword: Boolean = false + data class DocumentLocked( + val credential: Credential, ) : AddDocumentToResponseResult() } \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/transfer/TransferManager.kt b/appholder/src/main/java/com/android/identity/wallet/transfer/TransferManager.kt index a4927771b..eccce7216 100644 --- a/appholder/src/main/java/com/android/identity/wallet/transfer/TransferManager.kt +++ b/appholder/src/main/java/com/android/identity/wallet/transfer/TransferManager.kt @@ -5,16 +5,12 @@ import android.content.Context import android.graphics.Bitmap import android.graphics.Color.BLACK import android.graphics.Color.WHITE -import android.nfc.cardemulation.HostApduService import android.view.View import android.widget.ImageView import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import com.android.identity.* import com.android.identity.android.legacy.* -import com.android.identity.android.securearea.AndroidKeystoreSecureArea -import com.android.identity.android.securearea.AndroidKeystoreSecureArea.USER_AUTHENTICATION_TYPE_BIOMETRIC -import com.android.identity.android.securearea.AndroidKeystoreSecureArea.USER_AUTHENTICATION_TYPE_LSKF import com.android.identity.credential.CredentialRequest import com.android.identity.credential.NameSpacedData import com.android.identity.mdoc.mso.StaticAuthDataParser @@ -25,10 +21,10 @@ import com.android.identity.mdoc.response.DocumentGenerator import com.android.identity.mdoc.util.MdocUtil import com.android.identity.securearea.SecureArea import com.android.identity.util.Timestamp +import com.android.identity.wallet.composables.state.MdocAuthStateOption import com.android.identity.wallet.document.DocumentManager import com.android.identity.wallet.documentdata.DocumentDataReader import com.android.identity.wallet.documentdata.DocumentElements -import com.android.identity.wallet.selfsigned.AddSelfSignedScreenState import com.android.identity.wallet.util.* import com.google.zxing.BarcodeFormat import com.google.zxing.MultiFormatWriter @@ -194,11 +190,11 @@ class TransferManager private constructor(private val context: Context) { val transcript = communication.getSessionTranscript() ?: byteArrayOf() val authOption = - AddSelfSignedScreenState.MdocAuthStateOption.valueOf(documentInformation.mDocAuthOption) + MdocAuthStateOption.valueOf(documentInformation.mDocAuthOption) try { val generator = DocumentGenerator(docType, staticAuthData.issuerAuth, transcript) .setIssuerNamespaces(mergedIssuerNamespaces) - if (authOption == AddSelfSignedScreenState.MdocAuthStateOption.ECDSA) { + if (authOption == MdocAuthStateOption.ECDSA) { generator.setDeviceNamespacesSignature( NameSpacedData.Builder().build(), authKey.secureArea, @@ -220,24 +216,7 @@ class TransferManager private constructor(private val context: Context) { authKey.increaseUsageCount() ProvisioningUtil.getInstance(context).trackUsageTimestamp(credential) } catch (lockedException: SecureArea.KeyLockedException) { - return if (credential.credentialSecureArea is AndroidKeystoreSecureArea) { - val keyInfo = - credential.credentialSecureArea.getKeyInfo(authKey.alias) as AndroidKeystoreSecureArea.KeyInfo - val allowLskf = keyInfo.userAuthenticationType == USER_AUTHENTICATION_TYPE_LSKF - val allowBiometric = - keyInfo.userAuthenticationType == USER_AUTHENTICATION_TYPE_BIOMETRIC - val allowBoth = - keyInfo.userAuthenticationType == USER_AUTHENTICATION_TYPE_LSKF or USER_AUTHENTICATION_TYPE_BIOMETRIC - AddDocumentToResponseResult.UserAuthRequired( - keyAlias = authKey.alias, - allowLSKFUnlocking = allowLskf || allowBoth, - allowBiometricUnlocking = allowBiometric || allowBoth - ) - } else { - AddDocumentToResponseResult.PassphraseRequired( - attemptedWithIncorrectPassword = keyUnlockData != null - ) - } + return AddDocumentToResponseResult.DocumentLocked(credential) } return AddDocumentToResponseResult.DocumentAdded(signingKeyUsageLimitPassed) } diff --git a/appholder/src/main/java/com/android/identity/wallet/util/ProvisioningUtil.kt b/appholder/src/main/java/com/android/identity/wallet/util/ProvisioningUtil.kt index 62d82e245..b5d59a9a4 100644 --- a/appholder/src/main/java/com/android/identity/wallet/util/ProvisioningUtil.kt +++ b/appholder/src/main/java/com/android/identity/wallet/util/ProvisioningUtil.kt @@ -9,9 +9,7 @@ import android.graphics.Color import android.graphics.Paint import android.graphics.Rect import com.android.identity.android.securearea.AndroidKeystoreSecureArea -import com.android.identity.android.storage.AndroidStorageEngine import com.android.identity.credential.Credential -import com.android.identity.credential.CredentialStore import com.android.identity.credential.CredentialUtil import com.android.identity.credential.NameSpacedData import com.android.identity.internal.Util @@ -19,21 +17,19 @@ import com.android.identity.mdoc.mso.MobileSecurityObjectGenerator import com.android.identity.mdoc.mso.StaticAuthDataGenerator import com.android.identity.mdoc.util.MdocUtil import com.android.identity.securearea.SecureArea -import com.android.identity.securearea.SecureArea.KeyPurpose import com.android.identity.securearea.SecureAreaRepository import com.android.identity.securearea.SoftwareSecureArea -import com.android.identity.storage.EphemeralStorageEngine import com.android.identity.util.Timestamp +import com.android.identity.wallet.HolderApp import com.android.identity.wallet.document.DocumentInformation import com.android.identity.wallet.document.KeysAndCertificates -import com.android.identity.wallet.document.SecureAreaImplementationState -import com.android.identity.wallet.selfsigned.AddSelfSignedScreenState import com.android.identity.wallet.selfsigned.ProvisionInfo +import com.android.identity.wallet.support.SecureAreaSupport +import com.android.identity.wallet.support.SoftwareKeystoreSecureAreaSupportState +import com.android.identity.wallet.support.toSecureAreaState import com.android.identity.wallet.util.DocumentData.MICOV_DOCTYPE import com.android.identity.wallet.util.DocumentData.MVR_DOCTYPE import java.io.ByteArrayOutputStream -import java.io.File -import java.security.PrivateKey import java.security.cert.X509Certificate import java.time.Instant import java.time.ZoneId @@ -45,74 +41,19 @@ class ProvisioningUtil private constructor( private val context: Context ) { - private val storageDir: File - get() = PreferencesHelper.getKeystoreBackedStorageLocation(context) - - private val storageEngine: AndroidStorageEngine - get() = AndroidStorageEngine.Builder(context, storageDir).build() - - private val androidKeystoreSecureArea: SecureArea - get() = AndroidKeystoreSecureArea(context, storageEngine) - - private val softwareSecureArea: SecureArea - get() = SoftwareSecureArea(storageEngine) - + val secureAreaRepository = SecureAreaRepository() val credentialStore by lazy(LazyThreadSafetyMode.SYNCHRONIZED) { - val keystoreEngineRepository = SecureAreaRepository() - keystoreEngineRepository.addImplementation(androidKeystoreSecureArea) - keystoreEngineRepository.addImplementation(softwareSecureArea) - CredentialStore(storageEngine, keystoreEngineRepository) - } - - private lateinit var softwareAttestationKey: PrivateKey - private lateinit var softwareAttestationKeySignatureAlgorithm: String - private lateinit var softwareAttestationKeyCertification: List - - private fun initSoftwareAttestationKey() { - val secureArea = SoftwareSecureArea(EphemeralStorageEngine()) - val now = Timestamp.now() - secureArea.createKey( - "SoftwareAttestationRoot", - SoftwareSecureArea.CreateKeySettings.Builder("".toByteArray()) - .setEcCurve(SecureArea.EC_CURVE_P256) - .setKeyPurposes(SecureArea.KEY_PURPOSE_SIGN) - .setSubject("CN=Software Attestation Root") - .setValidityPeriod( - now, - Timestamp.ofEpochMilli(now.toEpochMilli() + 10L * 86400 * 365 * 1000) - ) - .build() - ) - softwareAttestationKey = secureArea.getPrivateKey("SoftwareAttestationRoot", null) - softwareAttestationKeySignatureAlgorithm = "SHA256withECDSA" - softwareAttestationKeyCertification = secureArea.getKeyInfo("SoftwareAttestationRoot").attestation + HolderApp.createCredentialStore(context, secureAreaRepository) } fun provisionSelfSigned( nameSpacedData: NameSpacedData, provisionInfo: ProvisionInfo, ) { - val (secureArea, settings) = when (provisionInfo.secureAreaImplementationStateType) { - SecureAreaImplementationState.Android -> Pair( - androidKeystoreSecureArea, - createAndroidKeystoreSettings( - userAuthenticationRequired = provisionInfo.userAuthentication, - mDocAuthOption = provisionInfo.mDocAuthenticationOption, - authTimeoutMillis = provisionInfo.userAuthenticationTimeoutSeconds * 1000L, - userAuthenticationType = provisionInfo.userAuthType(), - useStrongBox = provisionInfo.useStrongBox, - ecCurve = provisionInfo.authKeyCurve, - validUntil = provisionInfo.validityInDays.toTimestampFromNow() - )) - - SecureAreaImplementationState.BouncyCastle -> Pair( - softwareSecureArea, - createBouncyCastleKeystoreSettings( - passphrase = provisionInfo.passphrase, - mDocAuthOption = provisionInfo.mDocAuthenticationOption, - ecCurve = provisionInfo.authKeyCurve - )) - } + + val secureArea: SecureArea = provisionInfo.currentSecureArea.secureArea + val settings = provisionInfo.secureAreaSupportState + .createKeystoreSettings(provisionInfo.validityInDays) val credential = credentialStore.createCredential(provisionInfo.credentialName(), secureArea, settings) credential.nameSpacedData = nameSpacedData @@ -131,14 +72,11 @@ class ProvisioningUtil private constructor( credential.applicationData.setNumber(MAX_USAGES_PER_KEY, provisionInfo.maxUseMso.toLong()) credential.applicationData.setNumber(VALIDITY_IN_DAYS, provisionInfo.validityInDays.toLong()) credential.applicationData.setNumber(MIN_VALIDITY_IN_DAYS, provisionInfo.minValidityInDays.toLong()) - credential.applicationData.setString(MDOC_AUTHENTICATION, provisionInfo.mDocAuthenticationOption.name) + credential.applicationData.setString(MDOC_AUTHENTICATION, provisionInfo.secureAreaSupportState.mDocAuthOption.mDocAuthentication.name) credential.applicationData.setNumber(LAST_TIME_USED, -1) - } - - private fun Int.toTimestampFromNow(): Timestamp { - val now = Timestamp.now().toEpochMilli() - val validityDuration = this * 24 * 60 * 60 * 1000L - return Timestamp.ofEpochMilli(now + validityDuration) + if (provisionInfo.secureAreaSupportState is SoftwareKeystoreSecureAreaSupportState) { + credential.applicationData.setString(PASSPHRASE, provisionInfo.secureAreaSupportState.passphrase) + } } private fun ProvisionInfo.credentialName(): String { @@ -146,17 +84,6 @@ class ProvisioningUtil private constructor( return regex.replace(docName, "").replace(" ", "_").lowercase() } - private fun ProvisionInfo.userAuthType(): Int { - var userAuthenticationType = 0 - if (allowLskfUnlocking) { - userAuthenticationType = userAuthenticationType or AndroidKeystoreSecureArea.USER_AUTHENTICATION_TYPE_LSKF - } - if (allowBiometricUnlocking) { - userAuthenticationType = userAuthenticationType or AndroidKeystoreSecureArea.USER_AUTHENTICATION_TYPE_BIOMETRIC - } - return userAuthenticationType - } - fun trackUsageTimestamp(credential: Credential) { val now = Timestamp.now() credential.applicationData.setNumber(LAST_TIME_USED, now.toEpochMilli()) @@ -169,7 +96,11 @@ class ProvisioningUtil private constructor( provisionAuthKeys(credential, documentInformation.docType, minValidityInDays) } - private fun provisionAuthKeys(credential: Credential, documentType: String, validityInDays: Int) { + private fun provisionAuthKeys( + credential: Credential, + documentType: String, + validityInDays: Int + ) { val nowMillis = Timestamp.now().toEpochMilli() val timeSigned = Timestamp.now() val timeValidityBegin = Timestamp.ofEpochMilli(nowMillis) @@ -288,34 +219,22 @@ class ProvisioningUtil private constructor( private fun manageKeysFor(credential: Credential): Int { val mDocAuthOption = credential.applicationData.getString(MDOC_AUTHENTICATION) - val (secureArea, settings) = when (credential.credentialSecureArea) { + val secureArea = when (credential.credentialSecureArea) { is AndroidKeystoreSecureArea -> { - val keyInfo = credential.credentialSecureArea.getKeyInfo(credential.credentialKeyAlias) as AndroidKeystoreSecureArea.KeyInfo - Pair( - androidKeystoreSecureArea, - createAndroidKeystoreSettings( - keyInfo.isUserAuthenticationRequired, - AddSelfSignedScreenState.MdocAuthStateOption.valueOf(mDocAuthOption), - keyInfo.userAuthenticationTimeoutMillis, - keyInfo.userAuthenticationType, - keyInfo.isStrongBoxBacked, - keyInfo.ecCurve, - keyInfo.validUntil ?: Timestamp.now() - )) + secureAreaRepository.implementations.first { it is AndroidKeystoreSecureArea } } is SoftwareSecureArea -> { - val keyInfo = credential.credentialSecureArea.getKeyInfo(credential.credentialKeyAlias) as SoftwareSecureArea.KeyInfo - Pair( - softwareSecureArea, - createBouncyCastleKeystoreSettings( - mDocAuthOption = AddSelfSignedScreenState.MdocAuthStateOption.valueOf(mDocAuthOption), - ecCurve = keyInfo.ecCurve - )) + secureAreaRepository.implementations.first { it is SoftwareSecureArea } } else -> throw IllegalStateException("Unknown keystore secure area implementation") } + + val settings = SecureAreaSupport.getInstance(context, credential) + .getSecureAreaSupportState() + .createKeystoreSettingForCredential(mDocAuthOption, credential) + val minValidTimeDays = credential.applicationData.getNumber(MIN_VALIDITY_IN_DAYS) val maxUsagesPerKey = credential.applicationData.getNumber(MAX_USAGES_PER_KEY) return CredentialUtil.managedAuthenticationKeyHelper( @@ -330,56 +249,6 @@ class ProvisioningUtil private constructor( ) } - private fun createAndroidKeystoreSettings( - userAuthenticationRequired: Boolean, - mDocAuthOption: AddSelfSignedScreenState.MdocAuthStateOption, - authTimeoutMillis: Long, - userAuthenticationType: Int, - useStrongBox: Boolean, - ecCurve: Int, - validUntil: Timestamp - ): AndroidKeystoreSecureArea.CreateKeySettings { - return AndroidKeystoreSecureArea.CreateKeySettings.Builder(CHALLENGE) - .setKeyPurposes(mDocAuthOption.toKeyPurpose()) - .setUseStrongBox(useStrongBox) - .setEcCurve(ecCurve) - .setValidityPeriod(Timestamp.now(), validUntil) - .setUserAuthenticationRequired( - userAuthenticationRequired, - authTimeoutMillis, - userAuthenticationType - ) - .build() - } - - private fun createBouncyCastleKeystoreSettings( - passphrase: String? = null, - mDocAuthOption: AddSelfSignedScreenState.MdocAuthStateOption, - ecCurve: Int - ): SoftwareSecureArea.CreateKeySettings { - if (!this::softwareAttestationKey.isInitialized) { - initSoftwareAttestationKey() - } - val keyPurpose = mDocAuthOption.toKeyPurpose() - val builder = SoftwareSecureArea.CreateKeySettings.Builder("DoNotCare".toByteArray()) - .setAttestationKey(softwareAttestationKey, - softwareAttestationKeySignatureAlgorithm, softwareAttestationKeyCertification) - .setPassphraseRequired(passphrase != null, passphrase) - .setKeyPurposes(keyPurpose) - .setEcCurve(ecCurve) - .setKeyPurposes(mDocAuthOption.toKeyPurpose()) - return builder.build() - } - - @KeyPurpose - private fun AddSelfSignedScreenState.MdocAuthStateOption.toKeyPurpose(): Int { - return if (this == AddSelfSignedScreenState.MdocAuthStateOption.ECDSA) { - SecureArea.KEY_PURPOSE_SIGN - } else { - SecureArea.KEY_PURPOSE_AGREE_KEY - } - } - companion object { private const val AUTH_KEY_DOMAIN = "some_hardcoded_string" @@ -393,8 +262,7 @@ class ProvisioningUtil private constructor( private const val MIN_VALIDITY_IN_DAYS = "minValidityInDays" private const val MDOC_AUTHENTICATION = "mDocAuthentication" private const val LAST_TIME_USED = "lastTimeUsed" - - private val CHALLENGE = "challenge".toByteArray() + const val PASSPHRASE = "passphrase" private val dateTimeFormatter = DateTimeFormatter.ISO_LOCAL_DATE_TIME @@ -406,6 +274,9 @@ class ProvisioningUtil private constructor( instance ?: ProvisioningUtil(context).also { instance = it } } + val defaultSecureArea: SecureArea + get() = requireNotNull(instance?.secureAreaRepository?.implementations?.first()) + fun Credential?.toDocumentInformation(): DocumentInformation? { return this?.let { val authKeys = authenticationKeys.map { key -> @@ -436,21 +307,13 @@ class ProvisioningUtil private constructor( selfSigned = it.applicationData.getBoolean(IS_SELF_SIGNED), maxUsagesPerKey = it.applicationData.getNumber(MAX_USAGES_PER_KEY).toInt(), mDocAuthOption = it.applicationData.getString(MDOC_AUTHENTICATION), - secureAreaImplementationState = it.credentialSecureArea.toSecureAreaState(), + currentSecureArea = it.credentialSecureArea.toSecureAreaState(), lastTimeUsed = lastTimeUsed, authKeys = authKeys ) } } - private fun SecureArea.toSecureAreaState(): SecureAreaImplementationState { - return when (this) { - is AndroidKeystoreSecureArea -> SecureAreaImplementationState.Android - is SoftwareSecureArea -> SecureAreaImplementationState.BouncyCastle - else -> throw IllegalStateException("Unknown Secure Area Implementation") - } - } - private fun Timestamp.formatted(): String { val instant = Instant.ofEpochMilli(this.toEpochMilli()) val dateTime = ZonedDateTime.ofInstant(instant, ZoneId.systemDefault()) diff --git a/appholder/src/main/java/com/android/identity/wallet/util/Timestamp.kt b/appholder/src/main/java/com/android/identity/wallet/util/Timestamp.kt new file mode 100644 index 000000000..38007c4b5 --- /dev/null +++ b/appholder/src/main/java/com/android/identity/wallet/util/Timestamp.kt @@ -0,0 +1,9 @@ +package com.android.identity.wallet.util + +import com.android.identity.util.Timestamp + +fun Int.toTimestampFromNow(): Timestamp { + val now = Timestamp.now().toEpochMilli() + val validityDuration = this * 24 * 60 * 60 * 1000L + return Timestamp.ofEpochMilli(now + validityDuration) +} \ No newline at end of file diff --git a/appholder/src/main/res/values/strings.xml b/appholder/src/main/res/values/strings.xml index feb7ebc9d..a2bd4a24c 100644 --- a/appholder/src/main/res/values/strings.xml +++ b/appholder/src/main/res/values/strings.xml @@ -163,9 +163,10 @@ Red Android Keystore - Bouncy Castle Keystore + Software Keystore + Secure Area Not Implemented Timeout seconds - Passphrase (optional) + Passphrase (optional) Require User Authentication No User Authentication diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index a9813dfd9..ad369345f 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -94,7 +94,7 @@ compose = ["compose-ui", "compose-foundation", "compose-material", "compose-ui-tooling", "compose-preview", "compose-icons"] androidx-crypto = ["androidx-biometrics", "androidx-zxing-core"] bouncy-castle = ["bouncy-castle-bcprov", "bouncy-castle-bcpkix"] - ui-testing = ["androidx-test-ext-junit", "androidx-test-espresso"] + ui-testing = ["androidx-test-ext-junit", "androidx-test-espresso", "truth"] unit-testing = ["jupiter", "kotlinx-coroutine-test", "truth"] [plugins] diff --git a/identity/src/main/java/com/android/identity/securearea/SecureArea.java b/identity/src/main/java/com/android/identity/securearea/SecureArea.java index 85ca420d8..a06353ce1 100644 --- a/identity/src/main/java/com/android/identity/securearea/SecureArea.java +++ b/identity/src/main/java/com/android/identity/securearea/SecureArea.java @@ -19,6 +19,7 @@ import androidx.annotation.IntDef; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.annotation.StringRes; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy;