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 39680f63c..57a491c0e 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 @@ -19,13 +19,13 @@ 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.BouncyCastleSecureArea -import com.android.identity.securearea.SecureArea.ALGORITHM_ES256 +import com.android.identity.android.securearea.KeystoreUtil import com.android.identity.wallet.R import com.android.identity.wallet.authprompt.UserAuthPromptBuilder import com.android.identity.wallet.theme.HolderAppTheme import com.android.identity.wallet.transfer.AddDocumentToResponseResult +import com.android.identity.wallet.support.AndroidSecureAreaSupport +import com.android.identity.wallet.support.BouncyCastleSecureAreaSupport import com.android.identity.wallet.util.DocumentData import com.android.identity.wallet.util.log import com.android.identity.wallet.viewmodel.TransferDocumentViewModel @@ -38,7 +38,10 @@ class AuthConfirmationFragment : BottomSheetDialogFragment() { private val passphraseViewModel: PassphrasePromptViewModel by activityViewModels() private val arguments by navArgs() private var isSendingInProgress = mutableStateOf(false) - private var androidKeyUnlockData: AndroidKeystoreSecureArea.KeyUnlockData? = null + + private var capabilities = KeystoreUtil(requireContext()).getDeviceCapabilities() + private val androidSecureAreaSupport = AndroidSecureAreaSupport(capabilities) + private val bouncyCastleSecureAreaSupport = BouncyCastleSecureAreaSupport() override fun onCreateView( inflater: LayoutInflater, @@ -142,7 +145,7 @@ class AuthConfirmationFragment : BottomSheetDialogFragment() { } else { userAuthRequest.withNegativeButton("Cancel") } - val cryptoObject = androidKeyUnlockData?.getCryptoObjectForSigning(ALGORITHM_ES256) + val cryptoObject = androidSecureAreaSupport.getCryptoObjectForSigning() userAuthRequest.build().authenticate(cryptoObject) } @@ -161,14 +164,15 @@ class AuthConfirmationFragment : BottomSheetDialogFragment() { } private fun onPassphraseProvided(passphrase: String) { - val unlockData = BouncyCastleSecureArea.KeyUnlockData(passphrase) + val unlockData = bouncyCastleSecureAreaSupport.createKeyUnlockData(passphrase) val result = viewModel.sendResponseForSelection(unlockData) onSendResponseResult(result) } private fun authenticationSucceeded() { try { - val result = viewModel.sendResponseForSelection(keyUnlockData = androidKeyUnlockData) + val result = + viewModel.sendResponseForSelection(keyUnlockData = androidSecureAreaSupport.lastKeyUnlockData()) onSendResponseResult(result) } catch (e: Exception) { val message = "Send response error: ${e.message}" @@ -180,7 +184,7 @@ class AuthConfirmationFragment : BottomSheetDialogFragment() { private fun onSendResponseResult(result: AddDocumentToResponseResult) { when (result) { is AddDocumentToResponseResult.UserAuthRequired -> { - androidKeyUnlockData = AndroidKeystoreSecureArea.KeyUnlockData(result.keyAlias) + androidSecureAreaSupport.createKeyUnlockData(result.keyAlias) requestUserAuth( result.allowLSKFUnlocking, result.allowBiometricUnlocking 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..caf317eda --- /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.AndroidAuthKeyCurveOption +import com.android.identity.wallet.composables.state.MdocAuthOption +import com.android.identity.wallet.composables.state.MdocAuthStateOption +import com.android.identity.wallet.composables.state.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/AuthenticationKeyCurveBouncyCastle.kt b/appholder/src/main/java/com/android/identity/wallet/composables/AuthenticationKeyCurveBouncyCastle.kt new file mode 100644 index 000000000..385219ff1 --- /dev/null +++ b/appholder/src/main/java/com/android/identity/wallet/composables/AuthenticationKeyCurveBouncyCastle.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.composables.state.BouncyCastleAuthKeyCurveOption +import com.android.identity.wallet.composables.state.BouncyCastleAuthKeyCurveState +import com.android.identity.wallet.composables.state.MdocAuthOption +import com.android.identity.wallet.composables.state.MdocAuthStateOption + +@Composable +fun AuthenticationKeyCurveBouncyCastle( + modifier: Modifier = Modifier, + state: BouncyCastleAuthKeyCurveState, + mDocAuthState: MdocAuthOption, + 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 + } + ) + } + } + } +} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/composables/BouncyCastlePassphraseInput.kt b/appholder/src/main/java/com/android/identity/wallet/composables/BouncyCastlePassphraseInput.kt new file mode 100644 index 000000000..db4432481 --- /dev/null +++ b/appholder/src/main/java/com/android/identity/wallet/composables/BouncyCastlePassphraseInput.kt @@ -0,0 +1,46 @@ +package com.android.identity.wallet.composables + +import androidx.compose.foundation.layout.Box +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 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) + ), + ) + } + } + } +} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/composables/BouncyCastleSetupContainer.kt b/appholder/src/main/java/com/android/identity/wallet/composables/BouncyCastleSetupContainer.kt new file mode 100644 index 000000000..274256984 --- /dev/null +++ b/appholder/src/main/java/com/android/identity/wallet/composables/BouncyCastleSetupContainer.kt @@ -0,0 +1,24 @@ +package com.android.identity.wallet.composables + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp + +@Composable +fun BouncyCastleSetupContainer( + modifier: Modifier = Modifier, + passphrase: String, + onPassphraseChanged: (newValue: String) -> Unit +) { + Column( + modifier = modifier, + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + BouncyCastlePassphraseInput( + value = passphrase, + onValueChanged = onPassphraseChanged + ) + } +} \ 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/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/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/AndroidAuthKeyCurveOption.kt b/appholder/src/main/java/com/android/identity/wallet/composables/state/AndroidAuthKeyCurveOption.kt new file mode 100644 index 000000000..c1ef37e40 --- /dev/null +++ b/appholder/src/main/java/com/android/identity/wallet/composables/state/AndroidAuthKeyCurveOption.kt @@ -0,0 +1,18 @@ +package com.android.identity.wallet.composables.state + +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/composables/state/AndroidAuthKeyCurveState.kt b/appholder/src/main/java/com/android/identity/wallet/composables/state/AndroidAuthKeyCurveState.kt new file mode 100644 index 000000000..e4c9237ee --- /dev/null +++ b/appholder/src/main/java/com/android/identity/wallet/composables/state/AndroidAuthKeyCurveState.kt @@ -0,0 +1,11 @@ +package com.android.identity.wallet.composables.state + +import android.os.Parcelable +import com.android.identity.wallet.composables.state.AndroidAuthKeyCurveOption +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/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/BouncyCastleAuthKeyCurveOption.kt b/appholder/src/main/java/com/android/identity/wallet/composables/state/BouncyCastleAuthKeyCurveOption.kt new file mode 100644 index 000000000..b63ee55fa --- /dev/null +++ b/appholder/src/main/java/com/android/identity/wallet/composables/state/BouncyCastleAuthKeyCurveOption.kt @@ -0,0 +1,36 @@ +package com.android.identity.wallet.composables.state + +import android.os.Parcelable +import com.android.identity.securearea.SecureArea +import kotlinx.parcelize.Parcelize + +@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 + } + } +} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/composables/state/BouncyCastleAuthKeyCurveState.kt b/appholder/src/main/java/com/android/identity/wallet/composables/state/BouncyCastleAuthKeyCurveState.kt new file mode 100644 index 000000000..c02a27c54 --- /dev/null +++ b/appholder/src/main/java/com/android/identity/wallet/composables/state/BouncyCastleAuthKeyCurveState.kt @@ -0,0 +1,10 @@ +package com.android.identity.wallet.composables.state + +import android.os.Parcelable +import kotlinx.parcelize.Parcelize + +@Parcelize +data class BouncyCastleAuthKeyCurveState( + val isEnabled: Boolean = true, + val authCurve: BouncyCastleAuthKeyCurveOption = BouncyCastleAuthKeyCurveOption.P256 +) : 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/selfsigned/AddSelfSignedDocumentScreen.kt b/appholder/src/main/java/com/android/identity/wallet/selfsigned/AddSelfSignedDocumentScreen.kt index 7881e6197..607044dce 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,19 +19,11 @@ 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 @@ -49,25 +34,27 @@ 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.CounterInput +import com.android.identity.wallet.composables.DropDownIndicator +import com.android.identity.wallet.composables.LabeledUserInput +import com.android.identity.wallet.composables.NumberChanger +import com.android.identity.wallet.composables.OutlinedContainerHorizontal import com.android.identity.wallet.composables.PreviewLightDark -import com.android.identity.wallet.composables.curveLabelFor +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 @Composable @@ -88,15 +75,6 @@ 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, onNumberOfMsoChanged = viewModel::updateNumberOfMso, onMaxUseOfMsoChanged = viewModel::updateMaxUseOfMso, onValidityInDaysChanged = viewModel::updateValidityInDays, @@ -113,15 +91,6 @@ private fun AddSelfSignedDocumentScreenContent( 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, onNumberOfMsoChanged: (newValue: Int) -> Unit, onMaxUseOfMsoChanged: (newValue: Int) -> Unit, onValidityInDaysChanged: (newValue: Int) -> Unit, @@ -165,61 +134,7 @@ private fun AddSelfSignedDocumentScreenContent( currentImplementation = screenState.secureAreaImplementationState, 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 - ) - } + screenState.secureAreaImplementationSupport.SecureAreaAuthUi() CounterInput( modifier = Modifier .fillMaxWidth() @@ -419,54 +334,6 @@ 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, @@ -513,315 +380,6 @@ private fun KeystoreImplementationChooser( } } -@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 - } - ) - } - } -} - -@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 +406,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 +431,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) { @@ -1022,54 +462,6 @@ private fun PreviewAddSelfSignedDocumentScreenAndroidKeystore() { 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 = {}, @@ -1092,15 +484,6 @@ private fun PreviewAddSelfSignedDocumentScreenBouncyCastleKeystore() { onCardArtSelected = {}, onDocumentNameChanged = {}, onKeystoreImplementationChanged = {}, - onUserAuthenticationChanged = {}, - onAuthTimeoutChanged = {}, - onLskfAuthChanged = {}, - onBiometricAuthChanged = {}, - onStrongBoxChanged = {}, - onMdocAuthOptionChange = {}, - onAndroidAuthKeyCurveChanged = {}, - onBouncyCastleAuthKeyCurveChanged = {}, - onPassphraseChanged = {}, onNumberOfMsoChanged = {}, onMaxUseOfMsoChanged = {}, onValidityInDaysChanged = {}, 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..aec86ca60 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 = state.secureAreaImplementationSupport.getUiState() 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, + 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..786b25dc1 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,14 @@ package com.android.identity.wallet.selfsigned import android.os.Parcelable -import com.android.identity.securearea.SecureArea +import com.android.identity.android.securearea.KeystoreUtil 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.AndroidSecureAreaSupport +import com.android.identity.wallet.support.BouncyCastleSecureAreaSupport +import com.android.identity.wallet.support.SecureAreaSupport +import kotlinx.parcelize.IgnoredOnParcel import kotlinx.parcelize.Parcelize @Parcelize @@ -13,110 +17,16 @@ data class AddSelfSignedScreenState( 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 numberOfMso: Int = 10, val maxUseOfMso: Int = 1, val validityInDays: Int = 30, - val minValidityInDays: Int = 10 + val minValidityInDays: Int = 10, + @IgnoredOnParcel val deviceCapabilities: KeystoreUtil.DeviceCapabilities = KeystoreUtil.DeviceCapabilities() ) : 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 secureAreaImplementationSupport: SecureAreaSupport + get() = when (secureAreaImplementationState) { + is SecureAreaImplementationState.Android -> AndroidSecureAreaSupport(deviceCapabilities) + is SecureAreaImplementationState.BouncyCastle -> BouncyCastleSecureAreaSupport() } - } } 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 3830842c4..76c5251b2 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 @@ -7,12 +7,6 @@ import com.android.identity.android.securearea.KeystoreUtil 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.util.getState import com.android.identity.wallet.util.updateState import kotlinx.coroutines.flow.StateFlow @@ -22,32 +16,14 @@ class AddSelfSignedViewModel( private val savedStateHandle: SavedStateHandle ) : ViewModel() { - private var capabilities = KeystoreUtil.DeviceCapabilities() - val screenState: StateFlow = savedStateHandle.getState( AddSelfSignedScreenState() ) fun loadConfiguration(context: Context) { - capabilities = KeystoreUtil(context).getDeviceCapabilities() + val capabilities = KeystoreUtil(context).getDeviceCapabilities() savedStateHandle.updateState { - it.copy( - allowLSKFUnlocking = AuthTypeState( - true, - capabilities.configureUserAuthenticationType - ), - allowBiometricUnlocking = AuthTypeState( - true, - capabilities.configureUserAuthenticationType - ), - useStrongBox = AuthTypeState(false, capabilities.strongBox), - androidMdocAuthState = MdocAuthOptionState( - isEnabled = if (it.useStrongBox.isEnabled) capabilities.strongBoxEcdh else capabilities.ecdh - ), - androidAuthKeyCurveState = AndroidAuthKeyCurveState( - isEnabled = if (it.useStrongBox.isEnabled) capabilities.strongBox25519 else capabilities.curve25519 - ) - ) + it.copy(deviceCapabilities = capabilities) } } @@ -75,69 +51,6 @@ class AddSelfSignedViewModel( } } - 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 (newValue) capabilities.strongBoxEcdh else capabilities.ecdh - ), - androidAuthKeyCurveState = AndroidAuthKeyCurveState( - isEnabled = if (newValue) capabilities.strongBox25519 else capabilities.curve25519 - ) - ) - } - } - - fun updateMdocAuthOption(newValue: MdocAuthStateOption) { - 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) - ) - } - } - - fun updateAndroidAuthKeyCurve(newValue: AndroidAuthKeyCurveOption) { - savedStateHandle.updateState { - it.copy(androidAuthKeyCurveState = it.androidAuthKeyCurveState.copy(authCurve = newValue)) - } - } - - fun updateBouncyCastleAuthKeyCurve(newValue: BouncyCastleAuthKeyCurveOption) { - savedStateHandle.updateState { - it.copy(bouncyCastleAuthKeyCurveState = it.bouncyCastleAuthKeyCurveState.copy(authCurve = newValue)) - } - } - fun updateValidityInDays(newValue: Int) { val state = savedStateHandle.getState(AddSelfSignedScreenState()) if (newValue < state.value.minValidityInDays) return @@ -154,11 +67,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..58ce9afbb 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 @@ -3,8 +3,8 @@ 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.util.Field +import com.android.identity.wallet.support.SecureAreaSupportState import kotlinx.parcelize.Parcelize data class SelfSignedDocumentData( @@ -38,16 +38,9 @@ data class ProvisionInfo( 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 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/AndroidSecureAreaSupport.kt b/appholder/src/main/java/com/android/identity/wallet/support/AndroidSecureAreaSupport.kt new file mode 100644 index 000000000..52c1011ec --- /dev/null +++ b/appholder/src/main/java/com/android/identity/wallet/support/AndroidSecureAreaSupport.kt @@ -0,0 +1,135 @@ +package com.android.identity.wallet.support + +import androidx.biometric.BiometricPrompt +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +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.unit.dp +import com.android.identity.android.securearea.AndroidKeystoreSecureArea +import com.android.identity.android.securearea.KeystoreUtil +import com.android.identity.securearea.SecureArea +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.composables.state.AndroidAuthKeyCurveOption +import com.android.identity.wallet.composables.state.AndroidAuthKeyCurveState +import com.android.identity.wallet.composables.state.AuthTypeState +import com.android.identity.wallet.composables.state.MdocAuthOption + +class AndroidSecureAreaSupport( + private val capabilities: KeystoreUtil.DeviceCapabilities +) : SecureAreaSupport { + + private var unlockData: AndroidKeystoreSecureArea.KeyUnlockData? = null + + private var screenState = AndroidSecureAreaSupportState( + allowLSKFUnlocking = AuthTypeState(true, capabilities.configureUserAuthenticationType), + allowBiometricUnlocking = AuthTypeState(true, capabilities.configureUserAuthenticationType), + useStrongBox = AuthTypeState(false, capabilities.strongBox), + mDocAuthOption = MdocAuthOption(isEnabled = capabilities.ecdh), + authKeyCurveState = AndroidAuthKeyCurveState(isEnabled = capabilities.curve25519) + ) + + override fun createKeyUnlockData(value: String): SecureArea.KeyUnlockData { + return AndroidKeystoreSecureArea.KeyUnlockData(value).also { + unlockData = it + } + } + + @Composable + override fun SecureAreaAuthUi() { + var compositionState by remember { mutableStateOf(screenState) } + 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) + screenState = compositionState + }, + onAuthTimeoutChanged = { seconds -> + if (seconds < 0) return@AndroidSetupContainer + compositionState = compositionState.copy(userAuthenticationTimeoutSeconds = seconds) + screenState = compositionState + }, + onLskfAuthChanged = { + val allowLskfUnlock = + if (compositionState.allowBiometricUnlocking.isEnabled) it else true + val newValue = compositionState.allowLSKFUnlocking.copy(isEnabled = allowLskfUnlock) + compositionState = compositionState.copy(allowLSKFUnlocking = newValue) + screenState = compositionState + }, + onBiometricAuthChanged = { + val allowBiometricUnlock = + if (compositionState.allowLSKFUnlocking.isEnabled) it else true + val newValue = + compositionState.allowBiometricUnlocking.copy(isEnabled = allowBiometricUnlock) + compositionState = compositionState.copy(allowBiometricUnlocking = newValue) + screenState = compositionState + }, + onStrongBoxChanged = { newValue -> + val update = compositionState.copy( + useStrongBox = compositionState.useStrongBox.copy(isEnabled = newValue), + mDocAuthOption = MdocAuthOption( + isEnabled = if (newValue) capabilities.strongBoxEcdh else capabilities.ecdh + ), + authKeyCurveState = AndroidAuthKeyCurveState( + isEnabled = if (newValue) capabilities.strongBox25519 else capabilities.curve25519 + ) + ) + compositionState = update + screenState = compositionState + } + ) + 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 + ) + ) + screenState = compositionState + } + ) + 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) + screenState = compositionState + } + ) + } + + override fun getUiState(): SecureAreaSupportState { + return screenState + } + + fun lastKeyUnlockData(): SecureArea.KeyUnlockData { + return unlockData ?: throw IllegalStateException("No unlock data has been created") + } + + fun getCryptoObjectForSigning(): BiometricPrompt.CryptoObject? { + return unlockData?.getCryptoObjectForSigning(SecureArea.ALGORITHM_ES256) + } +} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/support/AndroidSecureAreaSupportState.kt b/appholder/src/main/java/com/android/identity/wallet/support/AndroidSecureAreaSupportState.kt new file mode 100644 index 000000000..4e2544cfe --- /dev/null +++ b/appholder/src/main/java/com/android/identity/wallet/support/AndroidSecureAreaSupportState.kt @@ -0,0 +1,67 @@ +package com.android.identity.wallet.support + +import com.android.identity.android.securearea.AndroidKeystoreSecureArea +import com.android.identity.securearea.SecureArea +import com.android.identity.util.Timestamp +import com.android.identity.wallet.composables.state.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 AndroidSecureAreaSupportState( + 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() + } + + @SecureArea.KeyPurpose + private fun MdocAuthStateOption.toKeyPurpose(): Int { + return if (this == MdocAuthStateOption.ECDSA) { + SecureArea.KEY_PURPOSE_SIGN + } else { + SecureArea.KEY_PURPOSE_AGREE_KEY + } + } + + 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/BouncyCastleSecureAreaSupport.kt b/appholder/src/main/java/com/android/identity/wallet/support/BouncyCastleSecureAreaSupport.kt new file mode 100644 index 000000000..d98df9eea --- /dev/null +++ b/appholder/src/main/java/com/android/identity/wallet/support/BouncyCastleSecureAreaSupport.kt @@ -0,0 +1,73 @@ +package com.android.identity.wallet.support + +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +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.unit.dp +import com.android.identity.securearea.BouncyCastleSecureArea +import com.android.identity.securearea.SecureArea +import com.android.identity.wallet.composables.AuthenticationKeyCurveBouncyCastle +import com.android.identity.wallet.composables.BouncyCastleSetupContainer +import com.android.identity.wallet.composables.MdocAuthentication +import com.android.identity.wallet.composables.state.BouncyCastleAuthKeyCurveOption + +class BouncyCastleSecureAreaSupport : SecureAreaSupport { + + private var screenState = BouncyCastleSecureAreaSupportState() + + override fun createKeyUnlockData(value: String): SecureArea.KeyUnlockData { + return BouncyCastleSecureArea.KeyUnlockData(value) + } + + @Composable + override fun SecureAreaAuthUi() { + var compositionState by remember { mutableStateOf(screenState) } + BouncyCastleSetupContainer( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + passphrase = compositionState.passphrase, + onPassphraseChanged = { + compositionState = compositionState.copy(passphrase = it) + screenState = compositionState + } + ) + MdocAuthentication( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + state = compositionState.mDocAuthOption, + onMdocAuthOptionChange = { + val newValue = compositionState.mDocAuthOption.copy(mDocAuthentication = it) + compositionState = compositionState.copy( + mDocAuthOption = newValue, + bouncyCastleAuthKeyCurveState = compositionState.bouncyCastleAuthKeyCurveState.copy( + authCurve = BouncyCastleAuthKeyCurveOption.P256 + ) + ) + screenState = compositionState + } + ) + AuthenticationKeyCurveBouncyCastle( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + state = compositionState.bouncyCastleAuthKeyCurveState, + mDocAuthState = compositionState.mDocAuthOption, + onBouncyCastleAuthKeyCurveChanged = { + val newValue = compositionState.authKeyCurve.copy(authCurve = it) + compositionState = compositionState.copy(bouncyCastleAuthKeyCurveState = newValue) + screenState = compositionState + } + ) + } + + override fun getUiState(): SecureAreaSupportState { + return screenState + } +} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/support/BouncyCastleSecureAreaSupportState.kt b/appholder/src/main/java/com/android/identity/wallet/support/BouncyCastleSecureAreaSupportState.kt new file mode 100644 index 000000000..6ec38253b --- /dev/null +++ b/appholder/src/main/java/com/android/identity/wallet/support/BouncyCastleSecureAreaSupportState.kt @@ -0,0 +1,25 @@ +package com.android.identity.wallet.support + +import com.android.identity.securearea.BouncyCastleSecureArea +import com.android.identity.securearea.SecureArea +import com.android.identity.wallet.composables.state.BouncyCastleAuthKeyCurveState +import com.android.identity.wallet.composables.state.MdocAuthOption +import kotlinx.parcelize.Parcelize + +@Parcelize +data class BouncyCastleSecureAreaSupportState( + override val mDocAuthOption: MdocAuthOption = MdocAuthOption(), + val bouncyCastleAuthKeyCurveState: BouncyCastleAuthKeyCurveState = BouncyCastleAuthKeyCurveState(), + val passphrase: String = "", + val authKeyCurve: BouncyCastleAuthKeyCurveState = BouncyCastleAuthKeyCurveState(), +) : SecureAreaSupportState { + + override fun createKeystoreSettings(validityInDays: Int): SecureArea.CreateKeySettings { + val passphraseValue = passphrase.ifBlank { null } + val builder = BouncyCastleSecureArea.CreateKeySettings.Builder() + .setPassphraseRequired(passphraseValue != null, passphraseValue) + .setEcCurve(authKeyCurve.authCurve.toEcCurve()) + .setKeyPurposes(SecureArea.KEY_PURPOSE_SIGN or SecureArea.KEY_PURPOSE_AGREE_KEY) + return builder.build() + } +} \ 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..2e4d9de1a --- /dev/null +++ b/appholder/src/main/java/com/android/identity/wallet/support/SecureAreaSupport.kt @@ -0,0 +1,14 @@ +package com.android.identity.wallet.support + +import androidx.compose.runtime.Composable +import com.android.identity.securearea.SecureArea + +interface SecureAreaSupport { + + fun createKeyUnlockData(value: String): SecureArea.KeyUnlockData + + fun getUiState(): SecureAreaSupportState + + @Composable + fun SecureAreaAuthUi() +} \ No newline at end of file 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..68ef54b4e --- /dev/null +++ b/appholder/src/main/java/com/android/identity/wallet/support/SecureAreaSupportState.kt @@ -0,0 +1,12 @@ +package com.android.identity.wallet.support + +import android.os.Parcelable +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 +} \ 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..02a16ba48 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,7 +5,6 @@ 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 @@ -25,10 +24,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 +193,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, 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 12d295457..4e0245175 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 @@ -17,10 +17,10 @@ import com.android.identity.securearea.SecureArea import com.android.identity.securearea.SecureArea.KeyPurpose import com.android.identity.securearea.SecureAreaRepository import com.android.identity.util.Timestamp +import com.android.identity.wallet.composables.state.MdocAuthStateOption 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.util.DocumentData.MICOV_DOCTYPE import com.android.identity.wallet.util.DocumentData.MVR_DOCTYPE @@ -59,24 +59,9 @@ class ProvisioningUtil private constructor( nameSpacedData: NameSpacedData, provisionInfo: ProvisionInfo, ) { - val settings = when (provisionInfo.secureAreaImplementationStateType) { - SecureAreaImplementationState.Android -> 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 -> createBouncyCastleKeystoreSettings( - passphrase = provisionInfo.passphrase, - mDocAuthOption = provisionInfo.mDocAuthenticationOption, - ecCurve = provisionInfo.authKeyCurve - ) - } + val settings = provisionInfo.secureAreaSupportState + .createKeystoreSettings(provisionInfo.validityInDays) val credential = credentialStore.createCredential(provisionInfo.credentialName(), settings) credential.nameSpacedData = nameSpacedData @@ -94,32 +79,15 @@ 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) - } - private fun ProvisionInfo.credentialName(): String { val regex = Regex("[^A-Za-z0-9 ]") 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()) @@ -132,7 +100,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) @@ -205,10 +177,11 @@ class ProvisioningUtil private constructor( val mDocAuthOption = credential.applicationData.getString(MDOC_AUTHENTICATION) val settings = when (credential.credentialSecureArea) { is AndroidKeystoreSecureArea -> { - val keyInfo = credential.credentialSecureArea.getKeyInfo(credential.credentialKeyAlias) as AndroidKeystoreSecureArea.KeyInfo + val keyInfo = + credential.credentialSecureArea.getKeyInfo(credential.credentialKeyAlias) as AndroidKeystoreSecureArea.KeyInfo createAndroidKeystoreSettings( keyInfo.isUserAuthenticationRequired, - AddSelfSignedScreenState.MdocAuthStateOption.valueOf(mDocAuthOption), + MdocAuthStateOption.valueOf(mDocAuthOption), keyInfo.userAuthenticationTimeoutMillis, keyInfo.userAuthenticationType, keyInfo.isStrongBoxBacked, @@ -218,9 +191,10 @@ class ProvisioningUtil private constructor( } is BouncyCastleSecureArea -> { - val keyInfo = credential.credentialSecureArea.getKeyInfo(credential.credentialKeyAlias) as BouncyCastleSecureArea.KeyInfo + val keyInfo = + credential.credentialSecureArea.getKeyInfo(credential.credentialKeyAlias) as BouncyCastleSecureArea.KeyInfo createBouncyCastleKeystoreSettings( - mDocAuthOption = AddSelfSignedScreenState.MdocAuthStateOption.valueOf(mDocAuthOption), + mDocAuthOption = MdocAuthStateOption.valueOf(mDocAuthOption), ecCurve = keyInfo.ecCurve ) } @@ -242,7 +216,7 @@ class ProvisioningUtil private constructor( private fun createAndroidKeystoreSettings( userAuthenticationRequired: Boolean, - mDocAuthOption: AddSelfSignedScreenState.MdocAuthStateOption, + mDocAuthOption: MdocAuthStateOption, authTimeoutMillis: Long, userAuthenticationType: Int, useStrongBox: Boolean, @@ -264,7 +238,7 @@ class ProvisioningUtil private constructor( private fun createBouncyCastleKeystoreSettings( passphrase: String? = null, - mDocAuthOption: AddSelfSignedScreenState.MdocAuthStateOption, + mDocAuthOption: MdocAuthStateOption, ecCurve: Int ): BouncyCastleSecureArea.CreateKeySettings { val keyPurpose = mDocAuthOption.toKeyPurpose() @@ -277,8 +251,8 @@ class ProvisioningUtil private constructor( } @KeyPurpose - private fun AddSelfSignedScreenState.MdocAuthStateOption.toKeyPurpose(): Int { - return if (this == AddSelfSignedScreenState.MdocAuthStateOption.ECDSA) { + private fun MdocAuthStateOption.toKeyPurpose(): Int { + return if (this == MdocAuthStateOption.ECDSA) { SecureArea.KEY_PURPOSE_SIGN } else { SecureArea.KEY_PURPOSE_AGREE_KEY 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/test/java/com/android/identity/wallet/selfsigned/SelfSignedScreenStateTest.kt b/appholder/src/test/java/com/android/identity/wallet/selfsigned/SelfSignedScreenStateTest.kt index 1878eb23d..fad2d6b02 100644 --- a/appholder/src/test/java/com/android/identity/wallet/selfsigned/SelfSignedScreenStateTest.kt +++ b/appholder/src/test/java/com/android/identity/wallet/selfsigned/SelfSignedScreenStateTest.kt @@ -1,12 +1,14 @@ package com.android.identity.wallet.selfsigned import androidx.lifecycle.SavedStateHandle +import com.android.identity.wallet.composables.state.AndroidAuthKeyCurveOption +import com.android.identity.wallet.composables.state.AndroidAuthKeyCurveState +import com.android.identity.wallet.composables.state.MdocAuthStateOption 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.composables.state.AuthTypeState +import com.android.identity.wallet.composables.state.MdocAuthOption import com.google.common.truth.Truth.assertThat import org.junit.jupiter.api.Test @@ -173,18 +175,18 @@ class SelfSignedScreenStateTest { @Test fun updateMdocAuthOption() { - val authOption = AddSelfSignedScreenState.MdocAuthStateOption.MAC + val authOption = MdocAuthStateOption.MAC val viewModel = AddSelfSignedViewModel(savedStateHandle) viewModel.updateMdocAuthOption(authOption) assertThat(viewModel.screenState.value.androidMdocAuthState) - .isEqualTo(MdocAuthOptionState(isEnabled = true, mDocAuthentication = authOption)) + .isEqualTo(MdocAuthOption(isEnabled = true, mDocAuthentication = authOption)) } @Test fun updateAndroidAuthKeyCurve() { - val x25519 = AddSelfSignedScreenState.AndroidAuthKeyCurveOption.X25519 + val x25519 = AndroidAuthKeyCurveOption.X25519 val viewModel = AddSelfSignedViewModel(savedStateHandle) viewModel.updateAndroidAuthKeyCurve(x25519)