diff --git a/appholder/src/main/java/com/android/mdl/app/adapter/DocumentAdapter.kt b/appholder/src/main/java/com/android/mdl/app/adapter/DocumentAdapter.kt index 95fc4a03a..7be24e1ce 100644 --- a/appholder/src/main/java/com/android/mdl/app/adapter/DocumentAdapter.kt +++ b/appholder/src/main/java/com/android/mdl/app/adapter/DocumentAdapter.kt @@ -60,7 +60,7 @@ class DocumentAdapter : ListAdapter requestUserAuth(false) - is AddDocumentToResponseResult.PassphraseRequired -> requestPassphrase() - else -> findNavController().navigateUp() - } + val result = viewModel.sendResponseForSelection() + onSendResponseResult(result) } - private fun requestUserAuth(forceLskf: Boolean) { + private fun requestUserAuth( + allowLskfUnlock: Boolean, + allowBiometricUnlock: Boolean, + forceLskf: Boolean = !allowBiometricUnlock + ) { val userAuthRequest = UserAuthPromptBuilder.requestUserAuth(this) .withTitle(getString(R.string.bio_auth_title)) - .withNegativeButton(getString(R.string.bio_auth_use_pin)) .withSuccessCallback { authenticationSucceeded() } - .withCancelledCallback { retryForcingPinUse() } + .withCancelledCallback { + if (allowLskfUnlock) { + retryForcingPinUse(allowLskfUnlock, allowBiometricUnlock) + } else { + cancelAuthorization() + } + } .withFailureCallback { authenticationFailed() } .setForceLskf(forceLskf) - .build() - val cryptoObject = viewModel.getCryptoObject() - userAuthRequest.authenticate(cryptoObject) + if (allowLskfUnlock) { + userAuthRequest.withNegativeButton(getString(R.string.bio_auth_use_pin)) + } else { + userAuthRequest.withNegativeButton("Cancel") + } + val cryptoObject = androidKeyUnlockData?.getCryptoObjectForSigning(ALGORITHM_ES256) + userAuthRequest.build().authenticate(cryptoObject) } + private var androidKeyUnlockData: AndroidKeystoreSecureArea.KeyUnlockData? = null + private fun getSubtitle(): String { val readerCommonName = arguments.readerCommonName val readerIsTrusted = arguments.readerIsTrusted @@ -147,23 +161,47 @@ class AuthConfirmationFragment : BottomSheetDialogFragment() { } private fun authenticationSucceeded(passphrase: String) { - viewModel.sendResponseForSelection(true, passphrase) + viewModel.sendResponseForSelection() findNavController().navigateUp() } private fun authenticationSucceeded() { try { - viewModel.sendResponseForSelection(true) - findNavController().navigateUp() + val result = viewModel.sendResponseForSelection(keyUnlockData = androidKeyUnlockData) + onSendResponseResult(result) } catch (e: Exception) { val message = "Send response error: ${e.message}" log(message, e) - Toast.makeText(requireContext(), message, Toast.LENGTH_SHORT).show() + toast(message) } } - private fun retryForcingPinUse() { - val runnable = { requestUserAuth(true) } + private fun onSendResponseResult(result: AddDocumentToResponseResult) { + when (result) { + is AddDocumentToResponseResult.UserAuthRequired -> { + val keyUnlockData = AndroidKeystoreSecureArea.KeyUnlockData(result.keyAlias) + androidKeyUnlockData = keyUnlockData + requestUserAuth( + result.allowLSKFUnlocking, + result.allowBiometricUnlocking + ) + } + + is AddDocumentToResponseResult.PassphraseRequired -> { + requestPassphrase() + } + + is AddDocumentToResponseResult.DocumentAdded -> { + if (result.signingKeyUsageLimitPassed) { + toast("Using previously used Auth Key") + } + findNavController().navigateUp() + } + } + } + + private fun retryForcingPinUse(allowLsfk: Boolean, allowBiometric: Boolean) { + val runnable = { requestUserAuth(allowLsfk, allowBiometric, true) } // Without this delay, the prompt won't reshow Handler(Looper.getMainLooper()).postDelayed(runnable, 100) } @@ -177,4 +215,8 @@ class AuthConfirmationFragment : BottomSheetDialogFragment() { .openPassphrasePrompt() findNavController().navigate(destination) } + + private fun toast(message: String, duration: Int = Toast.LENGTH_SHORT) { + Toast.makeText(requireContext(), message, duration).show() + } } \ No newline at end of file diff --git a/appholder/src/main/java/com/android/mdl/app/authconfirmation/RequestedDocumentData.kt b/appholder/src/main/java/com/android/mdl/app/authconfirmation/RequestedDocumentData.kt index 97023fefd..d3d09638f 100644 --- a/appholder/src/main/java/com/android/mdl/app/authconfirmation/RequestedDocumentData.kt +++ b/appholder/src/main/java/com/android/mdl/app/authconfirmation/RequestedDocumentData.kt @@ -5,7 +5,6 @@ import com.android.identity.mdoc.request.DeviceRequestParser data class RequestedDocumentData( val userReadableName: String, val identityCredentialName: String, - val needsAuth: Boolean, val requestedElements: ArrayList, val requestedDocument: DeviceRequestParser.DocumentRequest ) \ No newline at end of file diff --git a/appholder/src/main/java/com/android/mdl/app/authconfirmation/SignedDocumentData.kt b/appholder/src/main/java/com/android/mdl/app/authconfirmation/SignedDocumentData.kt index ca75badce..29f5f824a 100644 --- a/appholder/src/main/java/com/android/mdl/app/authconfirmation/SignedDocumentData.kt +++ b/appholder/src/main/java/com/android/mdl/app/authconfirmation/SignedDocumentData.kt @@ -4,8 +4,6 @@ class SignedDocumentData( private val signedElements: List, val identityCredentialName: String, val documentType: String, - val readerAuth: ByteArray?, - val itemsRequest: ByteArray ) { fun issuerSignedEntries(): MutableMap> { diff --git a/appholder/src/main/java/com/android/mdl/app/authconfirmation/SignedElementsCollection.kt b/appholder/src/main/java/com/android/mdl/app/authconfirmation/SignedElementsCollection.kt index 1991876fb..5eb4fd77c 100644 --- a/appholder/src/main/java/com/android/mdl/app/authconfirmation/SignedElementsCollection.kt +++ b/appholder/src/main/java/com/android/mdl/app/authconfirmation/SignedElementsCollection.kt @@ -19,11 +19,9 @@ class SignedElementsCollection { return requestedDocuments.keys.map { namespace -> val document = requestedDocuments.getValue(namespace) SignedDocumentData( - signedElements, - document.userReadableName, - document.requestedDocument.docType, - document.requestedDocument.readerAuth, - document.requestedDocument.itemsRequest + signedElements = signedElements, + identityCredentialName = document.identityCredentialName, + documentType = document.requestedDocument.docType, ) } } diff --git a/appholder/src/main/java/com/android/mdl/app/composables/Preview.kt b/appholder/src/main/java/com/android/mdl/app/composables/Preview.kt new file mode 100644 index 000000000..1c5933d99 --- /dev/null +++ b/appholder/src/main/java/com/android/mdl/app/composables/Preview.kt @@ -0,0 +1,8 @@ +package com.android.mdl.app.composables + +import android.content.res.Configuration +import androidx.compose.ui.tooling.preview.Preview + +@Preview(name = "Light", uiMode = Configuration.UI_MODE_NIGHT_NO) +@Preview(name = "Dark", uiMode = Configuration.UI_MODE_NIGHT_YES) +annotation class PreviewLightDark \ No newline at end of file diff --git a/appholder/src/main/java/com/android/mdl/app/document/DocumentInformation.kt b/appholder/src/main/java/com/android/mdl/app/document/DocumentInformation.kt index a84dbc493..e71d38858 100644 --- a/appholder/src/main/java/com/android/mdl/app/document/DocumentInformation.kt +++ b/appholder/src/main/java/com/android/mdl/app/document/DocumentInformation.kt @@ -2,11 +2,14 @@ package com.android.mdl.app.document data class DocumentInformation( val userVisibleName: String, + val docName: String, val docType: String, val dateProvisioned: String, val selfSigned: Boolean, val documentColor: Int, val maxUsagesPerKey: Int, + val lastTimeUsed: String, + val mDocAuthOption: String, val authKeys: List ) { @@ -15,7 +18,10 @@ data class DocumentInformation( val validFrom: String, val validUntil: String, val issuerDataBytesCount: Int, - val usagesCount: Int + val usagesCount: Int, + val keyPurposes: Int, + val ecCurve: Int, + val isHardwareBacked: Boolean ) } diff --git a/appholder/src/main/java/com/android/mdl/app/document/DocumentManager.kt b/appholder/src/main/java/com/android/mdl/app/document/DocumentManager.kt index 19cd4dcce..fd150fa52 100644 --- a/appholder/src/main/java/com/android/mdl/app/document/DocumentManager.kt +++ b/appholder/src/main/java/com/android/mdl/app/document/DocumentManager.kt @@ -11,8 +11,8 @@ import com.android.identity.* import com.android.identity.android.legacy.* import com.android.identity.credential.Credential import com.android.identity.credential.NameSpacedData -import com.android.mdl.app.keystore.CredentialUtil -import com.android.mdl.app.keystore.CredentialUtil.Companion.toDocumentInformation +import com.android.mdl.app.util.ProvisioningUtil +import com.android.mdl.app.util.ProvisioningUtil.Companion.toDocumentInformation import com.android.mdl.app.selfsigned.SelfSignedDocumentData import com.android.mdl.app.util.DocumentData import com.android.mdl.app.util.DocumentData.EU_PID_DOCTYPE @@ -40,7 +40,7 @@ class DocumentManager private constructor(private val context: Context) { } fun getDocumentInformation(documentName: String): DocumentInformation? { - val credentialStore = CredentialUtil.getInstance(context).credentialStore + val credentialStore = ProvisioningUtil.getInstance(context).credentialStore val credential = credentialStore.lookupCredential(documentName) return credential.toDocumentInformation() } @@ -48,14 +48,14 @@ class DocumentManager private constructor(private val context: Context) { fun getCredentialByName(documentName: String): Credential? { val documentInfo = getDocumentInformation(documentName) documentInfo?.let { - val credentialStore = CredentialUtil.getInstance(context).credentialStore + val credentialStore = ProvisioningUtil.getInstance(context).credentialStore return credentialStore.lookupCredential(documentName) } return null } fun getDocuments(): List { - val credentialStore = CredentialUtil.getInstance(context).credentialStore + val credentialStore = ProvisioningUtil.getInstance(context).credentialStore return credentialStore.listCredentials().mapNotNull { documentName -> val credential = credentialStore.lookupCredential(documentName) credential.toDocumentInformation() @@ -65,7 +65,7 @@ class DocumentManager private constructor(private val context: Context) { fun deleteCredentialByName(documentName: String) { val document = getDocumentInformation(documentName) document?.let { - val credentialStore = CredentialUtil.getInstance(context).credentialStore + val credentialStore = ProvisioningUtil.getInstance(context).credentialStore credentialStore.deleteCredential(documentName) } } @@ -95,7 +95,7 @@ class DocumentManager private constructor(private val context: Context) { docName: String = documentData.provisionInfo.docName, count: Int = 1 ): String { - val store = CredentialUtil.getInstance(context).credentialStore + val store = ProvisioningUtil.getInstance(context).credentialStore store.listCredentials().forEach { name -> if (name == docName) { return getUniqueDocumentName(documentData, "$docName ($count)", count + 1) @@ -246,7 +246,7 @@ class DocumentManager private constructor(private val context: Context) { ) .build() - CredentialUtil.getInstance(context) + ProvisioningUtil.getInstance(context) .provisionSelfSigned(nameSpacedData, documentData.provisionInfo) } @@ -304,7 +304,7 @@ class DocumentManager private constructor(private val context: Context) { .putEntryString(MVR_NAMESPACE, "vin", "1M8GDM9AXKP042788") .build() - CredentialUtil.getInstance(context) + ProvisioningUtil.getInstance(context) .provisionSelfSigned(nameSpacedData, documentData.provisionInfo) } @@ -468,7 +468,7 @@ class DocumentManager private constructor(private val context: Context) { ) .build() - CredentialUtil.getInstance(context) + ProvisioningUtil.getInstance(context) .provisionSelfSigned(nameSpacedData, documentData.provisionInfo) } @@ -630,7 +630,7 @@ class DocumentManager private constructor(private val context: Context) { ) .build() - CredentialUtil.getInstance(context) + ProvisioningUtil.getInstance(context) .provisionSelfSigned(nameSpacedData, documentData.provisionInfo) } @@ -643,6 +643,6 @@ class DocumentManager private constructor(private val context: Context) { fun refreshAuthKeys(documentName: String) { val documentInformation = requireNotNull(getDocumentInformation(documentName)) val credential = requireNotNull(getCredentialByName(documentName)) - CredentialUtil.getInstance(context).refreshAuthKeys(credential, documentInformation) + ProvisioningUtil.getInstance(context).refreshAuthKeys(credential, documentInformation) } } diff --git a/appholder/src/main/java/com/android/mdl/app/documentinfo/DocumentInfoScreen.kt b/appholder/src/main/java/com/android/mdl/app/documentinfo/DocumentInfoScreen.kt index 1f54ccb8c..0719b05e6 100644 --- a/appholder/src/main/java/com/android/mdl/app/documentinfo/DocumentInfoScreen.kt +++ b/appholder/src/main/java/com/android/mdl/app/documentinfo/DocumentInfoScreen.kt @@ -14,7 +14,6 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.wrapContentWidth import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.PagerState import androidx.compose.foundation.pager.rememberPagerState @@ -49,6 +48,15 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.withStyle import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import com.android.identity.securearea.SecureArea.EC_CURVE_ED25519 +import com.android.identity.securearea.SecureArea.EC_CURVE_ED448 +import com.android.identity.securearea.SecureArea.EC_CURVE_P256 +import com.android.identity.securearea.SecureArea.EC_CURVE_P384 +import com.android.identity.securearea.SecureArea.EC_CURVE_P521 +import com.android.identity.securearea.SecureArea.EC_CURVE_X25519 +import com.android.identity.securearea.SecureArea.EC_CURVE_X448 +import com.android.identity.securearea.SecureArea.KEY_PURPOSE_AGREE_KEY +import com.android.identity.securearea.SecureArea.KEY_PURPOSE_SIGN import com.android.mdl.app.R import com.android.mdl.app.composables.LoadingIndicator import com.android.mdl.app.composables.ShowToast @@ -60,7 +68,7 @@ import com.android.mdl.app.theme.HolderAppTheme fun DocumentInfoScreen( viewModel: DocumentInfoViewModel, onNavigateUp: () -> Unit, - onNavigateToDocumentDetails: (documentName: String) -> Unit + onNavigateToDocumentDetails: () -> Unit ) { val state by viewModel.screenState.collectAsState() if (state.isDeleted) { @@ -70,10 +78,10 @@ fun DocumentInfoScreen( DocumentInfoScreenContent( screenState = state, - onRefreshAuthKeys = { viewModel.refreshAuthKeys(state.documentName) }, - onShowDocumentElements = { onNavigateToDocumentDetails(state.documentName) }, + onRefreshAuthKeys = viewModel::refreshAuthKeys, + onShowDocumentElements = { onNavigateToDocumentDetails() }, onDeleteDocument = { viewModel.promptDocumentDelete() }, - onConfirmDocumentDelete = { viewModel.confirmDocumentDelete(state.documentName) }, + onConfirmDocumentDelete = viewModel::confirmDocumentDelete, onCancelDocumentDelete = viewModel::cancelDocumentDelete ) } @@ -86,7 +94,7 @@ private fun DocumentInfoScreenContent( onRefreshAuthKeys: () -> Unit, onShowDocumentElements: () -> Unit, onDeleteDocument: () -> Unit, - onConfirmDocumentDelete: (documentName: String) -> Unit, + onConfirmDocumentDelete: () -> Unit, onCancelDocumentDelete: () -> Unit, ) { Scaffold( @@ -95,7 +103,7 @@ private fun DocumentInfoScreenContent( Column( modifier = Modifier.padding(paddingValues), verticalArrangement = Arrangement.spacedBy(12.dp), - horizontalAlignment = Alignment.CenterHorizontally + horizontalAlignment = CenterHorizontally ) { if (screenState.isLoading) { LoadingIndicator( @@ -118,6 +126,10 @@ private fun DocumentInfoScreenContent( modifier = Modifier.padding(12.dp), verticalArrangement = Arrangement.spacedBy(8.dp) ) { + LabeledValue( + label = stringResource(id = R.string.label_credential_shape), + value = "mdoc" + ) LabeledValue( label = stringResource(id = R.string.label_document_name), value = screenState.documentName @@ -131,8 +143,12 @@ private fun DocumentInfoScreenContent( value = screenState.provisioningDate ) LabeledValue( - label = stringResource(id = R.string.label_self_signed), - value = if (screenState.isSelfSigned) "Yes" else "No" + label = stringResource(id = R.string.label_last_time_used), + value = screenState.lastTimeUsedDate.ifBlank { "N/A" } + ) + LabeledValue( + label = stringResource(id = R.string.label_issuer), + value = if (screenState.isSelfSigned) "Self-Signed on Device" else "N/A" ) LabeledValue( label = stringResource(id = R.string.txt_keystore_implementation), @@ -150,8 +166,10 @@ private fun DocumentInfoScreenContent( val key = screenState.authKeys[page] AuthenticationKeyInfo( modifier = Modifier - .wrapContentWidth() - .padding(16.dp), + .fillMaxWidth() + .padding(horizontal = 16.dp) + .clip(RoundedCornerShape(12.dp)) + .background(MaterialTheme.colorScheme.secondaryContainer), authKeyInfo = key ) } @@ -175,11 +193,11 @@ private fun DocumentInfoScreenContent( .clip(RoundedCornerShape(8.dp)) .weight(1f) .clickable { onRefreshAuthKeys() }, - horizontalAlignment = Alignment.CenterHorizontally + horizontalAlignment = CenterHorizontally ) { Column( modifier = Modifier.padding(12.dp), - horizontalAlignment = Alignment.CenterHorizontally + horizontalAlignment = CenterHorizontally ) { Icon( imageVector = Icons.Default.Refresh, @@ -197,11 +215,11 @@ private fun DocumentInfoScreenContent( .clip(RoundedCornerShape(8.dp)) .weight(1f) .clickable { onShowDocumentElements() }, - horizontalAlignment = Alignment.CenterHorizontally + horizontalAlignment = CenterHorizontally ) { Column( modifier = Modifier.padding(12.dp), - horizontalAlignment = Alignment.CenterHorizontally + horizontalAlignment = CenterHorizontally ) { Icon( imageVector = Icons.Default.RemoveRedEye, @@ -241,7 +259,7 @@ private fun DocumentInfoScreenContent( } if (screenState.isDeletingPromptShown) { DeleteDocumentPrompt( - onConfirm = { onConfirmDocumentDelete(screenState.documentName) }, + onConfirm = onConfirmDocumentDelete, onCancel = onCancelDocumentDelete ) } @@ -286,24 +304,21 @@ private fun AuthenticationKeyInfo( authKeyInfo: DocumentInfoScreenState.KeyInformation ) { Row( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp) - .clip(RoundedCornerShape(12.dp)) - .background(MaterialTheme.colorScheme.secondaryContainer), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceEvenly + modifier = modifier, + verticalAlignment = Alignment.CenterVertically ) { Icon( modifier = Modifier - .size(64.dp) + .size(48.dp) .padding(horizontal = 8.dp), imageVector = Icons.Default.Key, contentDescription = authKeyInfo.alias, - tint = MaterialTheme.colorScheme.primary + tint = MaterialTheme.colorScheme.primary.copy(alpha = .5f) ) Column( - modifier = modifier, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 8.dp, vertical = 12.dp), verticalArrangement = Arrangement.spacedBy(8.dp), ) { LabeledValue( @@ -320,16 +335,50 @@ private fun AuthenticationKeyInfo( ) LabeledValue( label = stringResource(id = R.string.document_info_issuer_data), - value = "${authKeyInfo.issuerDataBytesCount}" + value = stringResource( + id = R.string.document_info_issuer_data_bytes, + authKeyInfo.issuerDataBytesCount + ) ) LabeledValue( label = stringResource(id = R.string.document_info_usage_count), value = "${authKeyInfo.usagesCount}" ) + LabeledValue( + label = stringResource(id = R.string.document_info_key_purposes), + value = authKeyInfo.keyPurposes.keyPurposesReadableValue() + ) + LabeledValue( + label = stringResource(id = R.string.document_info_ec_curve), + value = authKeyInfo.ecCurve.ecCurveReadableValue() + ) } } } +@Composable +private fun Int.keyPurposesReadableValue() : String { + return when (this) { + KEY_PURPOSE_SIGN -> "KEY_PURPOSE_SIGN" + KEY_PURPOSE_AGREE_KEY -> "KEY_PURPOSE_AGREE" + else -> this.toString() + } +} + +@Composable +private fun Int.ecCurveReadableValue(): String { + return when (this) { + EC_CURVE_P256 -> "P-256" + EC_CURVE_P384 -> "P-384" + EC_CURVE_P521 -> "P-512" + EC_CURVE_ED25519 -> "Ed25519" + EC_CURVE_X25519 -> "X25519" + EC_CURVE_ED448 -> "ED448" + EC_CURVE_X448 -> "X448" + else -> this.toString() + } +} + @Composable private fun DeleteDocumentPrompt( onConfirm: () -> Unit, @@ -411,14 +460,20 @@ private fun PreviewDocumentInfoScreen() { validFrom = "16-07-2023", validUntil = "23-07-2023", usagesCount = 1, - issuerDataBytesCount = "Issuer 1".toByteArray().count() + issuerDataBytesCount = "Issuer 1".toByteArray().count(), + keyPurposes = KEY_PURPOSE_AGREE_KEY, + ecCurve = EC_CURVE_P256, + isHardwareBacked = false ), DocumentInfoScreenState.KeyInformation( alias = "Key Alias 2", validFrom = "16-07-2023", validUntil = "23-07-2023", usagesCount = 0, - issuerDataBytesCount = "Issuer 2".toByteArray().count() + issuerDataBytesCount = "Issuer 2".toByteArray().count(), + keyPurposes = KEY_PURPOSE_SIGN, + ecCurve = EC_CURVE_ED25519, + isHardwareBacked = true ) ) ), diff --git a/appholder/src/main/java/com/android/mdl/app/documentinfo/DocumentInfoScreenState.kt b/appholder/src/main/java/com/android/mdl/app/documentinfo/DocumentInfoScreenState.kt index cfd2cec0f..34fd09d8b 100644 --- a/appholder/src/main/java/com/android/mdl/app/documentinfo/DocumentInfoScreenState.kt +++ b/appholder/src/main/java/com/android/mdl/app/documentinfo/DocumentInfoScreenState.kt @@ -1,14 +1,20 @@ package com.android.mdl.app.documentinfo +import androidx.compose.runtime.Immutable +import androidx.compose.runtime.Stable +import com.android.identity.securearea.SecureArea.EcCurve +import com.android.identity.securearea.SecureArea.KeyPurpose import com.android.mdl.app.document.DocumentColor import com.android.mdl.app.document.SecureAreaImplementationState +@Immutable data class DocumentInfoScreenState( val isLoading: Boolean = false, val documentName: String = "", val documentType: String = "", val documentColor: DocumentColor = DocumentColor.Green, val provisioningDate: String = "", + val lastTimeUsedDate: String = "", val isSelfSigned: Boolean = false, val secureAreaImplementationState: SecureAreaImplementationState = SecureAreaImplementationState.Android, val authKeys: List = emptyList(), @@ -16,11 +22,15 @@ data class DocumentInfoScreenState( val isDeleted: Boolean = false ) { + @Immutable data class KeyInformation( val alias: String, val validFrom: String, val validUntil: String, val issuerDataBytesCount: Int, - val usagesCount: Int + val usagesCount: Int, + @KeyPurpose val keyPurposes: Int, + @EcCurve val ecCurve: Int, + val isHardwareBacked: Boolean ) } \ No newline at end of file diff --git a/appholder/src/main/java/com/android/mdl/app/documentinfo/DocumentInfoViewModel.kt b/appholder/src/main/java/com/android/mdl/app/documentinfo/DocumentInfoViewModel.kt index 1fe92486b..2621e9f57 100644 --- a/appholder/src/main/java/com/android/mdl/app/documentinfo/DocumentInfoViewModel.kt +++ b/appholder/src/main/java/com/android/mdl/app/documentinfo/DocumentInfoViewModel.kt @@ -1,13 +1,16 @@ package com.android.mdl.app.documentinfo +import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.createSavedStateHandle import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewmodel.initializer import androidx.lifecycle.viewmodel.viewModelFactory import com.android.mdl.app.composables.toCardArt import com.android.mdl.app.document.DocumentInformation import com.android.mdl.app.document.DocumentManager +import com.android.mdl.app.fragment.DocumentDetailFragmentArgs import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -18,8 +21,10 @@ import kotlinx.coroutines.withContext class DocumentInfoViewModel( private val documentManager: DocumentManager, + savedStateHandle: SavedStateHandle ) : ViewModel() { + private val args = DocumentDetailFragmentArgs.fromSavedStateHandle(savedStateHandle) private val _state = MutableStateFlow(DocumentInfoScreenState()) val screenState: StateFlow = _state.asStateFlow() @@ -37,8 +42,8 @@ class DocumentInfoViewModel( _state.update { it.copy(isDeletingPromptShown = true) } } - fun confirmDocumentDelete(documentName: String) { - documentManager.deleteCredentialByName(documentName) + fun confirmDocumentDelete() { + documentManager.deleteCredentialByName(args.documentName) _state.update { it.copy(isDeleted = true, isDeletingPromptShown = false) } } @@ -46,13 +51,13 @@ class DocumentInfoViewModel( _state.update { it.copy(isDeletingPromptShown = false) } } - fun refreshAuthKeys(documentName: String) { + fun refreshAuthKeys() { _state.update { it.copy(isLoading = true) } viewModelScope.launch { withContext(Dispatchers.IO) { - documentManager.refreshAuthKeys(documentName) + documentManager.refreshAuthKeys(args.documentName) } - onAuthKeysRefreshed() + loadDocument(args.documentName) } } @@ -66,6 +71,7 @@ class DocumentInfoViewModel( documentColor = documentInformation.documentColor.toCardArt(), provisioningDate = documentInformation.dateProvisioned, isSelfSigned = documentInformation.selfSigned, + lastTimeUsedDate = documentInformation.lastTimeUsed, authKeys = documentInformation.authKeys.asScreenStateKeys() ) } @@ -79,20 +85,19 @@ class DocumentInfoViewModel( validFrom = keyData.validFrom, validUntil = keyData.validUntil, issuerDataBytesCount = keyData.issuerDataBytesCount, - usagesCount = keyData.usagesCount + usagesCount = keyData.usagesCount, + keyPurposes = keyData.keyPurposes, + ecCurve = keyData.ecCurve, + isHardwareBacked = keyData.isHardwareBacked ) } } - private fun onAuthKeysRefreshed() { - _state.update { it.copy(isLoading = false) } - } - companion object { fun Factory(documentManager: DocumentManager): ViewModelProvider.Factory = viewModelFactory { initializer { - DocumentInfoViewModel(documentManager) + DocumentInfoViewModel(documentManager, createSavedStateHandle()) } } } diff --git a/appholder/src/main/java/com/android/mdl/app/fragment/DocumentDetailFragment.kt b/appholder/src/main/java/com/android/mdl/app/fragment/DocumentDetailFragment.kt index ed7a38911..0bec296ef 100644 --- a/appholder/src/main/java/com/android/mdl/app/fragment/DocumentDetailFragment.kt +++ b/appholder/src/main/java/com/android/mdl/app/fragment/DocumentDetailFragment.kt @@ -32,7 +32,7 @@ class DocumentDetailFragment : Fragment() { DocumentInfoScreen( viewModel = viewModel, onNavigateUp = { findNavController().navigateUp() }, - onNavigateToDocumentDetails = { documentName -> onShowData(documentName) } + onNavigateToDocumentDetails = { onShowData(args.documentName) } ) } } diff --git a/appholder/src/main/java/com/android/mdl/app/selfsigned/AddSelfSignedDocumentScreen.kt b/appholder/src/main/java/com/android/mdl/app/selfsigned/AddSelfSignedDocumentScreen.kt index 5660ae2d3..be2750fe7 100644 --- a/appholder/src/main/java/com/android/mdl/app/selfsigned/AddSelfSignedDocumentScreen.kt +++ b/appholder/src/main/java/com/android/mdl/app/selfsigned/AddSelfSignedDocumentScreen.kt @@ -31,6 +31,7 @@ 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 @@ -40,6 +41,7 @@ import androidx.compose.material3.Scaffold import androidx.compose.material3.Switch import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -47,21 +49,25 @@ 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.tooling.preview.Preview import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import com.android.mdl.app.R +import com.android.mdl.app.composables.PreviewLightDark import com.android.mdl.app.composables.gradientFor import com.android.mdl.app.composables.keystoreNameFor import com.android.mdl.app.document.DocumentColor import com.android.mdl.app.document.DocumentType import com.android.mdl.app.document.SecureAreaImplementationState +import com.android.mdl.app.selfsigned.AddSelfSignedScreenState.AndroidAuthKeyCurveOption +import com.android.mdl.app.selfsigned.AddSelfSignedScreenState.MdocAuthStateOption import com.android.mdl.app.theme.HolderAppTheme @Composable @@ -69,7 +75,11 @@ fun AddSelfSignedDocumentScreen( viewModel: AddSelfSignedViewModel, onNext: () -> Unit ) { + val context = LocalContext.current val screenState by viewModel.screenState.collectAsState() + LaunchedEffect(Unit) { + viewModel.loadConfiguration(context) + } AddSelfSignedDocumentScreenContent( modifier = Modifier.fillMaxSize(), @@ -80,9 +90,16 @@ fun AddSelfSignedDocumentScreen( onKeystoreImplementationChanged = viewModel::updateKeystoreImplementation, onUserAuthenticationChanged = viewModel::updateUserAuthentication, onAuthTimeoutChanged = viewModel::updateUserAuthenticationTimeoutSeconds, + onLskfAuthChanged = viewModel::updateLskfUnlocking, + onBiometricAuthChanged = viewModel::updateBiometricUnlocking, + onMdocAuthOptionChange = viewModel::updateMdocAuthOption, + onAndroidAuthKeyCurveChanged = viewModel::updateAndroidAuthKeyCurve, + onStrongBoxChanged = viewModel::updateStrongBox, onPassphraseChanged = viewModel::updatePassphrase, onNumberOfMsoChanged = viewModel::updateNumberOfMso, onMaxUseOfMsoChanged = viewModel::updateMaxUseOfMso, + onValidityInDaysChanged = viewModel::updateValidityInDays, + onMinValidityInDaysChanged = viewModel::updateMinValidityInDays, onNext = onNext ) } @@ -97,9 +114,16 @@ private fun AddSelfSignedDocumentScreenContent( 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, onPassphraseChanged: (newValue: String) -> Unit, onNumberOfMsoChanged: (newValue: Int) -> Unit, onMaxUseOfMsoChanged: (newValue: Int) -> Unit, + onValidityInDaysChanged: (newValue: Int) -> Unit, + onMinValidityInDaysChanged: (newValue: Int) -> Unit, onNext: () -> Unit ) { Scaffold(modifier = modifier) { paddingValues -> @@ -140,22 +164,43 @@ private fun AddSelfSignedDocumentScreenContent( onKeystoreImplementationChanged = onKeystoreImplementationChanged ) if (screenState.isAndroidKeystoreSelected) { - UserAuthenticationToggle( + 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 + onAuthTimeoutChanged = onAuthTimeoutChanged, + onLskfAuthChanged = onLskfAuthChanged, + onBiometricAuthChanged = onBiometricAuthChanged, + onStrongBoxChanged = onStrongBoxChanged, + ) + MdocAuthenticationAndroid( + 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 { - BouncyCastlePassphraseInput( + BouncyCastleSetupContainer( modifier = Modifier .fillMaxWidth() .padding(horizontal = 16.dp), - value = screenState.passphrase, - onValueChanged = onPassphraseChanged + state = screenState, + onPassphraseChanged = onPassphraseChanged ) } CounterInput( @@ -174,6 +219,22 @@ private fun AddSelfSignedDocumentScreenContent( value = screenState.maxUseOfMso, onValueChange = onMaxUseOfMsoChanged ) + CounterInput( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + label = stringResource(id = R.string.validity_in_days), + value = screenState.validityInDays, + onValueChange = onValidityInDaysChanged + ) + CounterInput( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + label = stringResource(id = R.string.minimum_validity_in_days), + value = screenState.minValidityInDays, + onValueChange = onMinValidityInDaysChanged + ) Button( modifier = Modifier .fillMaxWidth() @@ -341,6 +402,23 @@ 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, @@ -419,12 +497,18 @@ private fun KeystoreImplementationChooser( } @Composable -private fun UserAuthenticationToggle( +private fun AndroidSetupContainer( modifier: Modifier = Modifier, isOn: Boolean, timeoutSeconds: Int, + lskfAuthTypeState: AddSelfSignedScreenState.AuthTypeState, + biometricAuthTypeState: AddSelfSignedScreenState.AuthTypeState, + useStrongBox: AddSelfSignedScreenState.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()) { @@ -448,25 +532,189 @@ private fun UserAuthenticationToggle( modifier = Modifier.fillMaxWidth(), visible = isOn ) { - 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 - ) + 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 MdocAuthenticationAndroid( + modifier: Modifier = Modifier, + state: AddSelfSignedScreenState.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: AddSelfSignedScreenState.AndroidAuthKeyCurveState, + mDocAuthState: AddSelfSignedScreenState.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) + ) + 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), + onSelected = { + onAndroidAuthKeyCurveChanged(AndroidAuthKeyCurveOption.P_256) + keyCurveDropDownExpanded = false + } + ) + TextDropDownRow( + label = curveLabelFor(curveOption = ecCurveOption), + onSelected = { + onAndroidAuthKeyCurveChanged(ecCurveOption) + keyCurveDropDownExpanded = false + } + ) + } + } +} + @Composable private fun CounterInput( modifier: Modifier = Modifier, @@ -659,6 +907,35 @@ private fun NumberChanger( } } +@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) + } +} + +@Composable +private fun curveLabelFor( + curveOption: AndroidAuthKeyCurveOption +): String { + return when (curveOption) { + AndroidAuthKeyCurveOption.P_256 -> + stringResource(id = R.string.curve_p_256) + + AndroidAuthKeyCurveOption.Ed25519 -> + stringResource(id = R.string.curve_ed25519) + + AndroidAuthKeyCurveOption.X25519 -> + stringResource(id = R.string.curve_x25519) + } +} + @StringRes private fun documentNameFor(documentType: DocumentType): Int { return when (documentType) { @@ -680,7 +957,7 @@ private fun colorNameFor(cardArt: DocumentColor): Int { } @Composable -@Preview +@PreviewLightDark private fun PreviewAddSelfSignedDocumentScreenAndroidKeystore() { HolderAppTheme { AddSelfSignedDocumentScreenContent( @@ -692,16 +969,61 @@ private fun PreviewAddSelfSignedDocumentScreenAndroidKeystore() { onKeystoreImplementationChanged = {}, onUserAuthenticationChanged = {}, onAuthTimeoutChanged = {}, + onLskfAuthChanged = {}, + onBiometricAuthChanged = {}, + onStrongBoxChanged = {}, + onMdocAuthOptionChange = {}, + onAndroidAuthKeyCurveChanged = {}, + onPassphraseChanged = {}, + onNumberOfMsoChanged = {}, + onMaxUseOfMsoChanged = {}, + onValidityInDaysChanged = {}, + onMinValidityInDaysChanged = {}, + onNext = {} + ) + } +} + +@Composable +@PreviewLightDark +private fun PreviewAddSelfSignedDocumentScreenAndroidKeystoreAuthOn() { + HolderAppTheme { + AddSelfSignedDocumentScreenContent( + modifier = Modifier.fillMaxSize(), + screenState = AddSelfSignedScreenState( + userAuthentication = true, + allowLSKFUnlocking = AddSelfSignedScreenState.AuthTypeState( + isEnabled = true, + canBeModified = true + ), + allowBiometricUnlocking = AddSelfSignedScreenState.AuthTypeState( + isEnabled = true, + canBeModified = false + ), + ), + onDocumentTypeChanged = {}, + onCardArtSelected = {}, + onDocumentNameChanged = {}, + onKeystoreImplementationChanged = {}, + onUserAuthenticationChanged = {}, + onAuthTimeoutChanged = {}, + onLskfAuthChanged = {}, + onBiometricAuthChanged = {}, + onStrongBoxChanged = {}, + onMdocAuthOptionChange = {}, + onAndroidAuthKeyCurveChanged = {}, onPassphraseChanged = {}, onNumberOfMsoChanged = {}, onMaxUseOfMsoChanged = {}, + onValidityInDaysChanged = {}, + onMinValidityInDaysChanged = {}, onNext = {} ) } } @Composable -@Preview +@PreviewLightDark private fun PreviewAddSelfSignedDocumentScreenBouncyCastleKeystore() { HolderAppTheme { AddSelfSignedDocumentScreenContent( @@ -715,9 +1037,16 @@ private fun PreviewAddSelfSignedDocumentScreenBouncyCastleKeystore() { onKeystoreImplementationChanged = {}, onUserAuthenticationChanged = {}, onAuthTimeoutChanged = {}, + onLskfAuthChanged = {}, + onBiometricAuthChanged = {}, + onStrongBoxChanged = {}, + onMdocAuthOptionChange = {}, + onAndroidAuthKeyCurveChanged = {}, onPassphraseChanged = {}, onNumberOfMsoChanged = {}, onMaxUseOfMsoChanged = {}, + onValidityInDaysChanged = {}, + onMinValidityInDaysChanged = {}, onNext = {} ) } diff --git a/appholder/src/main/java/com/android/mdl/app/selfsigned/AddSelfSignedFragment.kt b/appholder/src/main/java/com/android/mdl/app/selfsigned/AddSelfSignedFragment.kt index dde2e934e..d613add69 100644 --- a/appholder/src/main/java/com/android/mdl/app/selfsigned/AddSelfSignedFragment.kt +++ b/appholder/src/main/java/com/android/mdl/app/selfsigned/AddSelfSignedFragment.kt @@ -40,6 +40,13 @@ class AddSelfSignedFragment : Fragment() { 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, + androidAuthKeyCurveOption = state.androidAuthKeyCurveState.authCurve, + 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/mdl/app/selfsigned/AddSelfSignedScreenState.kt b/appholder/src/main/java/com/android/mdl/app/selfsigned/AddSelfSignedScreenState.kt index 6a0b972f7..40d788303 100644 --- a/appholder/src/main/java/com/android/mdl/app/selfsigned/AddSelfSignedScreenState.kt +++ b/appholder/src/main/java/com/android/mdl/app/selfsigned/AddSelfSignedScreenState.kt @@ -1,6 +1,7 @@ package com.android.mdl.app.selfsigned import android.os.Parcelable +import com.android.identity.android.securearea.AndroidKeystoreSecureArea import com.android.mdl.app.document.DocumentColor import com.android.mdl.app.document.DocumentType import com.android.mdl.app.document.SecureAreaImplementationState @@ -12,13 +13,65 @@ data class AddSelfSignedScreenState( val cardArt: DocumentColor = DocumentColor.Green, val documentName: String = "Driving License", val secureAreaImplementationState: SecureAreaImplementationState = SecureAreaImplementationState.Android, - val userAuthentication: Boolean = false, + val userAuthentication: Boolean = true, val userAuthenticationTimeoutSeconds: Int = 10, + 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 passphrase: String = "", val numberOfMso: Int = 10, - val maxUseOfMso: Int = 1 + val maxUseOfMso: Int = 1, + val validityInDays: Int = 30, + val minValidityInDays: Int = 10 ) : Parcelable { val isAndroidKeystoreSelected: Boolean get() = secureAreaImplementationState == SecureAreaImplementationState.Android + + @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 + enum class MdocAuthStateOption : Parcelable { + ECDSA, MAC + } + + @Parcelize + enum class AndroidAuthKeyCurveOption : Parcelable { + P_256, Ed25519, X25519; + + fun toEcCurve(): Int { + return when (this) { + P_256 -> AndroidKeystoreSecureArea.EC_CURVE_P256 + Ed25519 -> AndroidKeystoreSecureArea.EC_CURVE_ED25519 + X25519 -> AndroidKeystoreSecureArea.EC_CURVE_X25519 + } + } + } } diff --git a/appholder/src/main/java/com/android/mdl/app/selfsigned/AddSelfSignedViewModel.kt b/appholder/src/main/java/com/android/mdl/app/selfsigned/AddSelfSignedViewModel.kt index bcec7717b..cc033707f 100644 --- a/appholder/src/main/java/com/android/mdl/app/selfsigned/AddSelfSignedViewModel.kt +++ b/appholder/src/main/java/com/android/mdl/app/selfsigned/AddSelfSignedViewModel.kt @@ -1,20 +1,48 @@ package com.android.mdl.app.selfsigned +import android.content.Context import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel +import com.android.identity.android.securearea.KeystoreUtil import com.android.mdl.app.document.DocumentColor import com.android.mdl.app.document.DocumentType import com.android.mdl.app.document.SecureAreaImplementationState +import com.android.mdl.app.selfsigned.AddSelfSignedScreenState.AndroidAuthKeyCurveOption +import com.android.mdl.app.selfsigned.AddSelfSignedScreenState.AndroidAuthKeyCurveState +import com.android.mdl.app.selfsigned.AddSelfSignedScreenState.AuthTypeState +import com.android.mdl.app.selfsigned.AddSelfSignedScreenState.MdocAuthOptionState +import com.android.mdl.app.selfsigned.AddSelfSignedScreenState.MdocAuthStateOption import com.android.mdl.app.util.getState import com.android.mdl.app.util.updateState import kotlinx.coroutines.flow.StateFlow +import java.lang.Integer.max class AddSelfSignedViewModel( private val savedStateHandle: SavedStateHandle ) : ViewModel() { - val screenState: StateFlow = - savedStateHandle.getState(AddSelfSignedScreenState()) + private var capabilities = KeystoreUtil.DeviceCapabilities() + + val screenState: StateFlow = savedStateHandle.getState( + AddSelfSignedScreenState() + ) + + fun loadConfiguration(context: Context) { + 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 + ) + ) + } + } fun updateDocumentType(newValue: DocumentType) { savedStateHandle.updateState { @@ -47,11 +75,71 @@ class AddSelfSignedViewModel( } 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) + ) + } + } + + fun updateAndroidAuthKeyCurve(newValue: AndroidAuthKeyCurveOption) { + savedStateHandle.updateState { + it.copy(androidAuthKeyCurveState = it.androidAuthKeyCurveState.copy(authCurve = newValue)) + } + } + + fun updateValidityInDays(newValue: Int) { + val state = savedStateHandle.getState(AddSelfSignedScreenState()) + if (newValue < state.value.minValidityInDays) return + savedStateHandle.updateState { + it.copy(validityInDays = newValue) + } + } + + fun updateMinValidityInDays(newValue: Int) { + if (newValue <= 0) return + savedStateHandle.updateState { + val validityDays = max(newValue, it.validityInDays) + it.copy(minValidityInDays = newValue, validityInDays = validityDays) + } + } + fun updatePassphrase(newValue: String) { savedStateHandle.updateState { it.copy(passphrase = newValue) diff --git a/appholder/src/main/java/com/android/mdl/app/selfsigned/SelfSignedDocumentData.kt b/appholder/src/main/java/com/android/mdl/app/selfsigned/SelfSignedDocumentData.kt index 565cc30d1..898f5bbe2 100644 --- a/appholder/src/main/java/com/android/mdl/app/selfsigned/SelfSignedDocumentData.kt +++ b/appholder/src/main/java/com/android/mdl/app/selfsigned/SelfSignedDocumentData.kt @@ -3,6 +3,8 @@ package com.android.mdl.app.selfsigned import android.graphics.Bitmap import android.os.Parcelable import com.android.mdl.app.document.SecureAreaImplementationState +import com.android.mdl.app.selfsigned.AddSelfSignedScreenState.AndroidAuthKeyCurveOption +import com.android.mdl.app.selfsigned.AddSelfSignedScreenState.MdocAuthStateOption import com.android.mdl.app.util.Field import kotlinx.parcelize.Parcelize @@ -39,6 +41,13 @@ data class ProvisionInfo( val secureAreaImplementationStateType: SecureAreaImplementationState, val userAuthentication: Boolean, val userAuthenticationTimeoutSeconds: Int, + val allowLskfUnlocking: Boolean, + val allowBiometricUnlocking: Boolean, + val useStrongBox: Boolean, + val mDocAuthenticationOption: MdocAuthStateOption, + val androidAuthKeyCurveOption: AndroidAuthKeyCurveOption, + val validityInDays: Int, + val minValidityInDays: Int, val passphrase: String?, val numberMso: Int, val maxUseMso: Int diff --git a/appholder/src/main/java/com/android/mdl/app/transfer/AddDocumentToResponseResult.kt b/appholder/src/main/java/com/android/mdl/app/transfer/AddDocumentToResponseResult.kt index e63207d5e..78573f47f 100644 --- a/appholder/src/main/java/com/android/mdl/app/transfer/AddDocumentToResponseResult.kt +++ b/appholder/src/main/java/com/android/mdl/app/transfer/AddDocumentToResponseResult.kt @@ -2,9 +2,15 @@ package com.android.mdl.app.transfer sealed class AddDocumentToResponseResult { - object DocumentAdded : AddDocumentToResponseResult() + data class DocumentAdded( + val signingKeyUsageLimitPassed: Boolean + ) : AddDocumentToResponseResult() - object UserAuthRequired : AddDocumentToResponseResult() + data class UserAuthRequired( + val keyAlias: String, + val allowLSKFUnlocking: Boolean, + val allowBiometricUnlocking: Boolean + ) : AddDocumentToResponseResult() object PassphraseRequired : AddDocumentToResponseResult() } \ No newline at end of file diff --git a/appholder/src/main/java/com/android/mdl/app/transfer/TransferManager.kt b/appholder/src/main/java/com/android/mdl/app/transfer/TransferManager.kt index 700780c96..7174ac887 100644 --- a/appholder/src/main/java/com/android/mdl/app/transfer/TransferManager.kt +++ b/appholder/src/main/java/com/android/mdl/app/transfer/TransferManager.kt @@ -14,6 +14,8 @@ import androidx.lifecycle.MutableLiveData import com.android.identity.* import com.android.identity.android.legacy.* import com.android.identity.android.securearea.AndroidKeystoreSecureArea +import com.android.identity.android.securearea.AndroidKeystoreSecureArea.USER_AUTHENTICATION_TYPE_BIOMETRIC +import com.android.identity.android.securearea.AndroidKeystoreSecureArea.USER_AUTHENTICATION_TYPE_LSKF import com.android.identity.credential.CredentialRequest import com.android.identity.credential.NameSpacedData import com.android.identity.mdoc.mso.StaticAuthDataParser @@ -27,6 +29,7 @@ import com.android.identity.util.Timestamp import com.android.mdl.app.document.DocumentManager import com.android.mdl.app.documentdata.DocumentDataReader import com.android.mdl.app.documentdata.DocumentElements +import com.android.mdl.app.selfsigned.AddSelfSignedScreenState import com.android.mdl.app.util.* import com.google.zxing.BarcodeFormat import com.google.zxing.MultiFormatWriter @@ -172,7 +175,9 @@ class TransferManager private constructor(private val context: Context) { docType: String, issuerSignedEntriesToRequest: MutableMap>, deviceResponseGenerator: DeviceResponseGenerator, + keyUnlockData: SecureArea.KeyUnlockData? ): AddDocumentToResponseResult { + var signingKeyUsageLimitPassed = false session?.let { val documentManager = DocumentManager.getInstance(context) val documentInformation = documentManager.getDocumentInformation(credentialName) @@ -189,7 +194,8 @@ class TransferManager private constructor(private val context: Context) { val authKey = credential.findAuthenticationKey(Timestamp.now()) ?: throw IllegalStateException("No auth key available") if (authKey.usageCount >= documentInformation.maxUsagesPerKey) { - throw IllegalStateException("No auth key available") + logWarning("Using Auth Key previously used ${authKey.usageCount} times, and maxUsagesPerKey is ${documentInformation.maxUsagesPerKey}") + signingKeyUsageLimitPassed = true } val staticAuthData = StaticAuthDataParser(authKey.issuerProvidedData).parse() @@ -198,28 +204,53 @@ class TransferManager private constructor(private val context: Context) { ) val transcript = communication.getSessionTranscript() ?: byteArrayOf() + val authOption = AddSelfSignedScreenState.MdocAuthStateOption.valueOf(documentInformation.mDocAuthOption) try { - deviceResponseGenerator.addDocument( - DocumentGenerator(docType, staticAuthData.issuerAuth, transcript) - .setIssuerNamespaces(mergedIssuerNamespaces) - .setDeviceNamespacesSignature( - NameSpacedData.Builder().build(), - authKey.secureArea, - authKey.alias, - null, - SecureArea.ALGORITHM_ES256 - ).generate() - ) + val generator = DocumentGenerator(docType, staticAuthData.issuerAuth, transcript) + .setIssuerNamespaces(mergedIssuerNamespaces) + if (authOption == AddSelfSignedScreenState.MdocAuthStateOption.ECDSA) { + generator.setDeviceNamespacesSignature(NameSpacedData.Builder().build(), + authKey.secureArea, + authKey.alias, + keyUnlockData, + SecureArea.ALGORITHM_ES256 + ) + } else { + generator.setDeviceNamespacesMac(NameSpacedData.Builder().build(), + authKey.secureArea, + authKey.alias, + keyUnlockData, + authKey.attestation.first().publicKey + ) + } + val data = generator.generate() + keyUnlockData?.let { + if (authOption == AddSelfSignedScreenState.MdocAuthStateOption.ECDSA) { + credential.credentialSecureArea.sign(authKey.alias, SecureArea.ALGORITHM_ES256, data, it) + } else { + credential.credentialSecureArea.keyAgreement(authKey.alias, authKey.attestation.first().publicKey, it) + } + } + deviceResponseGenerator.addDocument(data) authKey.increaseUsageCount() + ProvisioningUtil.getInstance(context).trackUsageTimestamp(credential) } catch (lockedException: SecureArea.KeyLockedException) { return if (credential.credentialSecureArea is AndroidKeystoreSecureArea) { - AddDocumentToResponseResult.UserAuthRequired + val keyInfo = credential.credentialSecureArea.getKeyInfo(authKey.alias) as AndroidKeystoreSecureArea.KeyInfo + val allowLskf = keyInfo.userAuthenticationType == USER_AUTHENTICATION_TYPE_LSKF + val allowBiometric = keyInfo.userAuthenticationType == USER_AUTHENTICATION_TYPE_BIOMETRIC + val allowBoth = keyInfo.userAuthenticationType == USER_AUTHENTICATION_TYPE_LSKF or USER_AUTHENTICATION_TYPE_BIOMETRIC + AddDocumentToResponseResult.UserAuthRequired( + keyAlias = authKey.alias, + allowLSKFUnlocking = allowLskf || allowBoth, + allowBiometricUnlocking = allowBiometric || allowBoth + ) } else { AddDocumentToResponseResult.PassphraseRequired } } } - return AddDocumentToResponseResult.DocumentAdded + return AddDocumentToResponseResult.DocumentAdded(signingKeyUsageLimitPassed) } fun stopPresentation( diff --git a/appholder/src/main/java/com/android/mdl/app/keystore/CredentialUtil.kt b/appholder/src/main/java/com/android/mdl/app/util/ProvisioningUtil.kt similarity index 61% rename from appholder/src/main/java/com/android/mdl/app/keystore/CredentialUtil.kt rename to appholder/src/main/java/com/android/mdl/app/util/ProvisioningUtil.kt index 42d44ee45..861eb6122 100644 --- a/appholder/src/main/java/com/android/mdl/app/keystore/CredentialUtil.kt +++ b/appholder/src/main/java/com/android/mdl/app/util/ProvisioningUtil.kt @@ -1,4 +1,4 @@ -package com.android.mdl.app.keystore +package com.android.mdl.app.util import android.annotation.SuppressLint import android.content.Context @@ -19,29 +19,19 @@ import com.android.identity.util.Timestamp import com.android.mdl.app.document.DocumentInformation import com.android.mdl.app.document.KeysAndCertificates import com.android.mdl.app.document.SecureAreaImplementationState +import com.android.mdl.app.selfsigned.AddSelfSignedScreenState import com.android.mdl.app.selfsigned.ProvisionInfo import com.android.mdl.app.util.DocumentData.MICOV_DOCTYPE import com.android.mdl.app.util.DocumentData.MVR_DOCTYPE -import com.android.mdl.app.util.PreferencesHelper -import org.bouncycastle.asn1.x500.X500Name -import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter -import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder -import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder import java.io.File -import java.math.BigInteger -import java.security.KeyPair -import java.security.KeyPairGenerator import java.security.cert.X509Certificate -import java.security.spec.ECGenParameterSpec import java.time.Instant import java.time.ZoneId import java.time.ZonedDateTime import java.time.format.DateTimeFormatter -import java.util.ArrayList -import java.util.Date import java.util.Random -class CredentialUtil private constructor( +class ProvisioningUtil private constructor( private val context: Context ) { @@ -70,25 +60,28 @@ class CredentialUtil private constructor( ) { val settings = when (provisionInfo.secureAreaImplementationStateType) { SecureAreaImplementationState.Android -> createAndroidKeystoreSettings( - provisioningChallenge = CHALLENGE, userAuthenticationRequired = provisionInfo.userAuthentication, - authTimeoutSeconds = provisionInfo.userAuthenticationTimeoutSeconds.toLong() + mDocAuthOption = provisionInfo.mDocAuthenticationOption, + authTimeoutMillis = provisionInfo.userAuthenticationTimeoutSeconds * 1000L, + userAuthenticationType = provisionInfo.userAuthType(), + useStrongBox = provisionInfo.useStrongBox, + ecCurve = provisionInfo.androidAuthKeyCurveOption.toEcCurve(), + validUntil = provisionInfo.validityInDays.toTimestampFromNow() ) SecureAreaImplementationState.BouncyCastle -> createBouncyCastleKeystoreSettings( passphrase = provisionInfo.passphrase ) } - if (provisionInfo.userAuthentication) { - createAndroidKey(provisionInfo.userAuthenticationTimeoutSeconds.toLong()) - } else if (provisionInfo.passphrase != null) { - createBouncyCastleKey(provisionInfo.passphrase) - } - val credential = credentialStore.createCredential(provisionInfo.docName, settings) + val credential = credentialStore.createCredential(provisionInfo.credentialName(), settings) credential.nameSpacedData = nameSpacedData - provisionAuthKeys(credential, provisionInfo.docType, settings, provisionInfo.numberMso) + repeat(provisionInfo.numberMso) { + val pendingKey = credential.createPendingAuthenticationKey(settings, null) + pendingKey.applicationData.setBoolean(AUTH_KEY_DOMAIN, true) + } + provisionAuthKeys(credential, provisionInfo.docType, provisionInfo.validityInDays) credential.applicationData.setString(USER_VISIBLE_NAME, provisionInfo.docName) credential.applicationData.setString(DOCUMENT_TYPE, provisionInfo.docType) @@ -96,40 +89,52 @@ class CredentialUtil private constructor( credential.applicationData.setNumber(CARD_ART, provisionInfo.docColor.toLong()) credential.applicationData.setBoolean(IS_SELF_SIGNED, true) 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.setNumber(LAST_TIME_USED, -1) } - fun refreshAuthKeys( - credential: Credential, - documentInformation: DocumentInformation - ) { - val keySettings = when (credential.credentialSecureArea) { - is AndroidKeystoreSecureArea -> { - createAndroidKeystoreSettings(CHALLENGE, false) - } + private fun Int.toTimestampFromNow(): Timestamp { + val now = Timestamp.now().toEpochMilli() + val validityDuration = this * 24 * 60 * 60 * 1000L + return Timestamp.ofEpochMilli(now + validityDuration) + } - is BouncyCastleSecureArea -> { - createBouncyCastleKeystoreSettings() - } + private fun ProvisionInfo.credentialName(): String { + val regex = Regex("[^A-Za-z0-9 ]") + return regex.replace(docName, "").replace(" ", "_").lowercase() + } - else -> throw IllegalStateException("Unknown keystore secure area implementation") + 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 } - val pendingKeysCount = manageKeysFor(documentInformation, credential) + return userAuthenticationType + } + + fun trackUsageTimestamp(credential: Credential) { + val now = Timestamp.now() + credential.applicationData.setNumber(LAST_TIME_USED, now.toEpochMilli()) + } + + fun refreshAuthKeys(credential: Credential, documentInformation: DocumentInformation) { + val pendingKeysCount = manageKeysFor(credential) if (pendingKeysCount <= 0) return - provisionAuthKeys(credential, documentInformation.docType, keySettings, pendingKeysCount) + val minValidityInDays = credential.applicationData.getNumber(MIN_VALIDITY_IN_DAYS).toInt() + provisionAuthKeys(credential, documentInformation.docType, minValidityInDays) } - private fun provisionAuthKeys( - credential: Credential, - documentType: String, - keySettings: SecureArea.CreateKeySettings, - msoCount: Int - ) { + private fun provisionAuthKeys(credential: Credential, documentType: String, validityInDays: Int) { val nowMillis = Timestamp.now().toEpochMilli() val timeSigned = Timestamp.now() val timeValidityBegin = Timestamp.ofEpochMilli(nowMillis) - val timeValidityEnd = Timestamp.ofEpochMilli(nowMillis + 10 * 86400 * 1000) - repeat(msoCount) { - val pendingAuthKey = credential.createPendingAuthenticationKey(keySettings, null) + val timeValidityEnd = validityInDays.toTimestampFromNow() + for (pendingAuthKey in credential.pendingAuthenticationKeys) { val msoGenerator = MobileSecurityObjectGenerator( "SHA-256", documentType, @@ -193,46 +198,20 @@ class CredentialUtil private constructor( } } - private fun generateIssuingAuthorityKeyPair(): KeyPair { - val keyPairGenerator = KeyPairGenerator.getInstance("EC") - val ecSpec = ECGenParameterSpec("secp256r1") - keyPairGenerator.initialize(ecSpec) - return keyPairGenerator.generateKeyPair() - } - - private fun getSelfSignedIssuerAuthorityCertificate( - issuerAuthorityKeyPair: KeyPair - ): X509Certificate { - val issuer = X500Name("CN=State Of Utopia") - val subject = X500Name("CN=State Of Utopia Issuing Authority Signing Key") - - val now = Date() - val milliSecsInOneYear = 365L * 24 * 60 * 60 * 1000 - val expirationDate = Date(now.time + 5 * milliSecsInOneYear) - val serial = BigInteger("42") - val builder = JcaX509v3CertificateBuilder( - issuer, - serial, - now, - expirationDate, - subject, - issuerAuthorityKeyPair.public - ) - - val signer = JcaContentSignerBuilder("SHA256withECDSA") - .build(issuerAuthorityKeyPair.private) - - val certHolder = builder.build(signer) - return JcaX509CertificateConverter().getCertificate(certHolder) - } - - fun manageKeysFor( - documentInformation: DocumentInformation?, - credential: Credential - ): Int { + private fun manageKeysFor(credential: Credential): Int { val settings = when (credential.credentialSecureArea) { is AndroidKeystoreSecureArea -> { - createAndroidKeystoreSettings(CHALLENGE, false) + val mDocAuthOption = credential.applicationData.getString(MDOC_AUTHENTICATION) + val keyInfo = credential.credentialSecureArea.getKeyInfo(credential.credentialKeyAlias) as AndroidKeystoreSecureArea.KeyInfo + createAndroidKeystoreSettings( + keyInfo.isUserAuthenticationRequired, + AddSelfSignedScreenState.MdocAuthStateOption.valueOf(mDocAuthOption), + keyInfo.userAuthenticationTimeoutMillis, + keyInfo.userAuthenticationType, + keyInfo.isStrongBoxBacked, + keyInfo.ecCurve, + keyInfo.validUntil ?: Timestamp.now() + ) } is BouncyCastleSecureArea -> { @@ -241,30 +220,44 @@ class CredentialUtil private constructor( else -> throw IllegalStateException("Unknown keystore secure area implementation") } + val minValidTimeDays = credential.applicationData.getNumber(MIN_VALIDITY_IN_DAYS) + val maxUsagesPerKey = credential.applicationData.getNumber(MAX_USAGES_PER_KEY) return CredentialUtil.managedAuthenticationKeyHelper( credential, settings, - "some_hardcoded_string", + AUTH_KEY_DOMAIN, Timestamp.now(), credential.authenticationKeys.size, - documentInformation?.maxUsagesPerKey ?: 1, - 1000 + maxUsagesPerKey.toInt(), + minValidTimeDays * 24 * 60 * 60 * 1000L ) } private fun createAndroidKeystoreSettings( - provisioningChallenge: ByteArray, userAuthenticationRequired: Boolean, - authTimeoutSeconds: Long = 10 + mDocAuthOption: AddSelfSignedScreenState.MdocAuthStateOption, + authTimeoutMillis: Long, + userAuthenticationType: Int, + useStrongBox: Boolean, + ecCurve: Int, + validUntil: Timestamp ): AndroidKeystoreSecureArea.CreateKeySettings { - - return AndroidKeystoreSecureArea.CreateKeySettings.Builder(provisioningChallenge) - .setKeyPurposes(SecureArea.KEY_PURPOSE_SIGN or SecureArea.KEY_PURPOSE_AGREE_KEY) + val keyPurpose = if (mDocAuthOption == AddSelfSignedScreenState.MdocAuthStateOption.ECDSA) { + SecureArea.KEY_PURPOSE_SIGN + } else { + SecureArea.KEY_PURPOSE_AGREE_KEY + } + return AndroidKeystoreSecureArea.CreateKeySettings.Builder(CHALLENGE) + .setKeyPurposes(keyPurpose) + .setUseStrongBox(useStrongBox) + .setEcCurve(ecCurve) + .setValidityPeriod(Timestamp.now(), validUntil) .setUserAuthenticationRequired( userAuthenticationRequired, - authTimeoutSeconds * 1000, - AndroidKeystoreSecureArea.USER_AUTHENTICATION_TYPE_BIOMETRIC - ).build() + authTimeoutMillis, + userAuthenticationType + ) + .build() } private fun createBouncyCastleKeystoreSettings( @@ -276,68 +269,63 @@ class CredentialUtil private constructor( .build() } - private fun createAndroidKey(timeoutSeconds: Long) { - androidKeystoreSecureArea.createKey( - ANDROID_KEY_BIOMETRIC, - AndroidKeystoreSecureArea.CreateKeySettings.Builder(CHALLENGE) - .setUserAuthenticationRequired( - true, - timeoutSeconds * 1000, - AndroidKeystoreSecureArea.USER_AUTHENTICATION_TYPE_BIOMETRIC - ).build() - ) - } - - private fun createBouncyCastleKey(passphrase: String) { - bouncyCastleSecureArea.createKey( - BOUNCY_CASTLE_KEY_PASSPHRASE, - BouncyCastleSecureArea.CreateKeySettings.Builder() - .setPassphraseRequired(true, passphrase) - .build() - ) - } - companion object { + private const val AUTH_KEY_DOMAIN = "some_hardcoded_string" private const val USER_VISIBLE_NAME = "userVisibleName" private const val DOCUMENT_TYPE = "documentType" private const val DATE_PROVISIONED = "dateProvisioned" private const val CARD_ART = "cardArt" private const val IS_SELF_SIGNED = "isSelfSigned" private const val MAX_USAGES_PER_KEY = "maxUsagesPerKey" + private const val VALIDITY_IN_DAYS = "validityInDays" + private const val MIN_VALIDITY_IN_DAYS = "minValidityInDays" + private const val MDOC_AUTHENTICATION = "mDocAuthentication" + private const val LAST_TIME_USED = "lastTimeUsed" - private const val ANDROID_KEY_BIOMETRIC = "user_auth_key_with_timeout" - private const val BOUNCY_CASTLE_KEY_PASSPHRASE = "bouncy_castle_auth_key" private val CHALLENGE = "challenge".toByteArray() - private val dateTimeFormatter = DateTimeFormatter.ofPattern("dd/MM/yyyy") + private val dateTimeFormatter = DateTimeFormatter.ISO_LOCAL_DATE_TIME @SuppressLint("StaticFieldLeak") @Volatile - private var instance: com.android.mdl.app.keystore.CredentialUtil? = null + private var instance: ProvisioningUtil? = null fun getInstance(context: Context) = instance ?: synchronized(this) { - instance ?: CredentialUtil(context).also { instance = it } + instance ?: ProvisioningUtil(context).also { instance = it } } fun Credential?.toDocumentInformation(): DocumentInformation? { return this?.let { val authKeys = authenticationKeys.map { key -> + val info = credentialSecureArea.getKeyInfo(key.alias) DocumentInformation.KeyData( alias = key.alias, validFrom = key.validFrom.formatted(), validUntil = key.validUntil.formatted(), issuerDataBytesCount = key.issuerProvidedData.size, - usagesCount = key.usageCount + usagesCount = key.usageCount, + keyPurposes = info.keyPurposes, + ecCurve = info.ecCurve, + isHardwareBacked = info.isHardwareBacked ) } + val lastTimeUsedMillis = it.applicationData.getNumber(LAST_TIME_USED) + val lastTimeUsed = if (lastTimeUsedMillis == -1L) { + "" + } else { + Timestamp.ofEpochMilli(lastTimeUsedMillis).formatted() + } DocumentInformation( userVisibleName = it.applicationData.getString(USER_VISIBLE_NAME), + docName = name, docType = it.applicationData.getString(DOCUMENT_TYPE), dateProvisioned = it.applicationData.getString(DATE_PROVISIONED), documentColor = it.applicationData.getNumber(CARD_ART).toInt(), selfSigned = it.applicationData.getBoolean(IS_SELF_SIGNED), maxUsagesPerKey = it.applicationData.getNumber(MAX_USAGES_PER_KEY).toInt(), + mDocAuthOption = it.applicationData.getString(MDOC_AUTHENTICATION), + lastTimeUsed = lastTimeUsed, authKeys = authKeys ) } diff --git a/appholder/src/main/java/com/android/mdl/app/viewmodel/TransferDocumentViewModel.kt b/appholder/src/main/java/com/android/mdl/app/viewmodel/TransferDocumentViewModel.kt index e1bad6594..486ee97c8 100644 --- a/appholder/src/main/java/com/android/mdl/app/viewmodel/TransferDocumentViewModel.kt +++ b/appholder/src/main/java/com/android/mdl/app/viewmodel/TransferDocumentViewModel.kt @@ -3,7 +3,6 @@ package com.android.mdl.app.viewmodel import android.app.Application import android.view.View import android.widget.Toast -import androidx.biometric.BiometricPrompt import androidx.databinding.ObservableField import androidx.databinding.ObservableInt import androidx.lifecycle.AndroidViewModel @@ -13,6 +12,7 @@ import com.android.identity.util.Constants.DEVICE_RESPONSE_STATUS_OK import com.android.identity.android.legacy.CredentialInvalidatedException import com.android.identity.mdoc.response.DeviceResponseGenerator import com.android.identity.mdoc.request.DeviceRequestParser +import com.android.identity.securearea.SecureArea import com.android.mdl.app.R import com.android.mdl.app.authconfirmation.RequestedDocumentData import com.android.mdl.app.authconfirmation.RequestedElement @@ -86,11 +86,10 @@ class TransferDocumentViewModel(val app: Application) : AndroidViewModel(app) { val issuerSignedEntriesToRequest = requestedElementsFrom(requestedDocument) result.add( RequestedDocumentData( - ownDocument.userVisibleName, - ownDocument.docType, - false, //TODO - issuerSignedEntriesToRequest, - requestedDocument + userReadableName = ownDocument.userVisibleName, + identityCredentialName = ownDocument.docName, + requestedElements = issuerSignedEntriesToRequest, + requestedDocument = requestedDocument ) ) } catch (e: NoSuchElementException) { @@ -101,11 +100,11 @@ class TransferDocumentViewModel(val app: Application) : AndroidViewModel(app) { } fun sendResponseForSelection( - didUserAuthorize: Boolean = false, - passphrase: String? = null + keyUnlockData: SecureArea.KeyUnlockData? = null ): AddDocumentToResponseResult { val elementsToSend = signedElements.collect() val responseGenerator = DeviceResponseGenerator(DEVICE_RESPONSE_STATUS_OK) + var signingKeyOverused = false elementsToSend.forEach { signedDocument -> try { val issuerSignedEntries = signedDocument.issuerSignedEntries() @@ -114,9 +113,12 @@ class TransferDocumentViewModel(val app: Application) : AndroidViewModel(app) { signedDocument.documentType, issuerSignedEntries, responseGenerator, + keyUnlockData ) - if (result != AddDocumentToResponseResult.DocumentAdded) { + if (result !is AddDocumentToResponseResult.DocumentAdded) { return result + } else { + signingKeyOverused = result.signingKeyUsageLimitPassed } } catch (e: CredentialInvalidatedException) { logWarning("Credential '${signedDocument.identityCredentialName}' is invalid. Deleting.") @@ -133,7 +135,7 @@ class TransferDocumentViewModel(val app: Application) : AndroidViewModel(app) { val documentsCount = elementsToSend.count() documentsSent.set(app.getString(R.string.txt_documents_sent, documentsCount)) cleanUp() - return AddDocumentToResponseResult.DocumentAdded + return AddDocumentToResponseResult.DocumentAdded(signingKeyOverused) } fun cancelPresentation( diff --git a/appholder/src/main/res/values/strings.xml b/appholder/src/main/res/values/strings.xml index b622f058e..539b642a0 100644 --- a/appholder/src/main/res/values/strings.xml +++ b/appholder/src/main/res/values/strings.xml @@ -28,11 +28,13 @@ User authentication has failed Biometric Authentication Error Fingerprint was removed from the device so the credential cannot be shared anymore. + Credential Shape Name - Document type + DocType Check for Update Delete Date Provisioned + Last Time Used MSO Usage Count Last Update Check Tap back button again to exit @@ -113,11 +115,11 @@ Document type Document name Storage implementation - Keystore implementation + Secure Area Use user authentication - Number of MSOs - Max use of MSO - Self Signed + Number of Authentication Keys + Max use per Authentication Key + Issuer User Auth Hardware Backed Show Data Elements @@ -162,23 +164,38 @@ Red Android Keystore - BouncyCastle Keystore + Bouncy Castle Keystore Timeout seconds Passphrase (optional) - User authentication on - User authentication off + Require User Authentication + No User Authentication + Allow LSKF unlocking + Allow biometric unlocking + Use StrongBox + mdoc authentication + Authentication Key Curve Alias Valid from Valid until Issuer data + %1$d bytes Usage count + Key Purposes + EcCurve Delete Confirmation Are you sure you want to delete the document? Document Deleted! Passphrase We need your consent to show the document data + mdoc ECDSA authentication + mdoc MAC authentication + P-256 + Ed25519 + X25519 + Validity time in days + Minimum validity in days \ No newline at end of file diff --git a/appholder/src/test/java/com/android/mdl/app/selfsigned/SelfSignedScreenStateTest.kt b/appholder/src/test/java/com/android/mdl/app/selfsigned/SelfSignedScreenStateTest.kt index bf2c75040..54cf3fbcb 100644 --- a/appholder/src/test/java/com/android/mdl/app/selfsigned/SelfSignedScreenStateTest.kt +++ b/appholder/src/test/java/com/android/mdl/app/selfsigned/SelfSignedScreenStateTest.kt @@ -4,6 +4,9 @@ import androidx.lifecycle.SavedStateHandle import com.android.mdl.app.document.DocumentColor import com.android.mdl.app.document.DocumentType import com.android.mdl.app.document.SecureAreaImplementationState +import com.android.mdl.app.selfsigned.AddSelfSignedScreenState.AndroidAuthKeyCurveState +import com.android.mdl.app.selfsigned.AddSelfSignedScreenState.AuthTypeState +import com.android.mdl.app.selfsigned.AddSelfSignedScreenState.MdocAuthOptionState import com.google.common.truth.Truth.assertThat import org.junit.jupiter.api.Test @@ -103,6 +106,140 @@ class SelfSignedScreenStateTest { .isEqualTo(AddSelfSignedScreenState(userAuthenticationTimeoutSeconds = newValue)) } + @Test + fun updateUserAuthenticationTimeoutSecondsInvalidValue() { + val viewModel = AddSelfSignedViewModel(savedStateHandle) + + viewModel.updateUserAuthenticationTimeoutSeconds(1) + viewModel.updateUserAuthenticationTimeoutSeconds(0) + viewModel.updateUserAuthenticationTimeoutSeconds(-1) + + assertThat(viewModel.screenState.value) + .isEqualTo(AddSelfSignedScreenState(userAuthenticationTimeoutSeconds = 0)) + } + + @Test + fun updateAllowedLskfUnlocking() { + val viewModel = AddSelfSignedViewModel(savedStateHandle) + + viewModel.updateLskfUnlocking(false) + + assertThat(viewModel.screenState.value.allowLSKFUnlocking) + .isEqualTo(AuthTypeState(isEnabled = false, canBeModified = false)) + } + + @Test + fun updateAllowedLskfUnlockingWhenBiometricIsOff() { + val viewModel = AddSelfSignedViewModel(savedStateHandle) + viewModel.updateBiometricUnlocking(false) + + viewModel.updateLskfUnlocking(false) + + assertThat(viewModel.screenState.value.allowLSKFUnlocking) + .isEqualTo(AuthTypeState(isEnabled = true, canBeModified = false)) + } + + @Test + fun updateAllowedBiometricUnlocking() { + val viewModel = AddSelfSignedViewModel(savedStateHandle) + + viewModel.updateBiometricUnlocking(false) + + assertThat(viewModel.screenState.value.allowBiometricUnlocking) + .isEqualTo(AuthTypeState(isEnabled = false, canBeModified = false)) + } + + @Test + fun updateAllowedBiometricUnlockingWhenLskfIsOff() { + val viewModel = AddSelfSignedViewModel(savedStateHandle) + viewModel.updateLskfUnlocking(false) + + viewModel.updateBiometricUnlocking(false) + + assertThat(viewModel.screenState.value.allowBiometricUnlocking) + .isEqualTo(AuthTypeState(isEnabled = true, canBeModified = false)) + } + + @Test + fun updateStrongBox() { + val enabled = true + val viewModel = AddSelfSignedViewModel(savedStateHandle) + + viewModel.updateStrongBox(enabled) + + assertThat(viewModel.screenState.value.useStrongBox) + .isEqualTo(AuthTypeState(isEnabled = enabled, canBeModified = false)) + } + + @Test + fun updateMdocAuthOption() { + val authOption = AddSelfSignedScreenState.MdocAuthStateOption.MAC + val viewModel = AddSelfSignedViewModel(savedStateHandle) + + viewModel.updateMdocAuthOption(authOption) + + assertThat(viewModel.screenState.value.androidMdocAuthState) + .isEqualTo(MdocAuthOptionState(isEnabled = true, mDocAuthentication = authOption)) + } + + @Test + fun updateAndroidAuthKeyCurve() { + val x25519 = AddSelfSignedScreenState.AndroidAuthKeyCurveOption.X25519 + val viewModel = AddSelfSignedViewModel(savedStateHandle) + + viewModel.updateAndroidAuthKeyCurve(x25519) + + assertThat(viewModel.screenState.value.androidAuthKeyCurveState) + .isEqualTo(AndroidAuthKeyCurveState(isEnabled = true, authCurve = x25519)) + } + + @Test + fun updateValidityInDays() { + val newValue = 15 + val viewModel = AddSelfSignedViewModel(savedStateHandle) + + viewModel.updateValidityInDays(newValue) + + assertThat(viewModel.screenState.value.validityInDays) + .isEqualTo(newValue) + } + + @Test + fun updateValidityInDaysBelowMinValidityDays() { + val defaultMinValidity = 10 + val belowMinValidity = 9 + val viewModel = AddSelfSignedViewModel(savedStateHandle) + + viewModel.updateValidityInDays(defaultMinValidity) + viewModel.updateValidityInDays(belowMinValidity) + + assertThat(viewModel.screenState.value.validityInDays) + .isEqualTo(defaultMinValidity) + } + + @Test + fun updateMinValidityInDays() { + val newMinValidity = 15 + val viewModel = AddSelfSignedViewModel(savedStateHandle) + + viewModel.updateMinValidityInDays(newMinValidity) + + assertThat(viewModel.screenState.value.minValidityInDays) + .isEqualTo(newMinValidity) + } + + @Test + fun updateMinValidityInDaysAboveValidityInDays() { + val defaultValidityInDays = 30 + val minValidityInDays = defaultValidityInDays + 5 + val viewModel = AddSelfSignedViewModel(savedStateHandle) + + viewModel.updateMinValidityInDays(minValidityInDays) + + assertThat(viewModel.screenState.value.validityInDays) + .isEqualTo(minValidityInDays) + } + @Test fun updateBouncyCastlePassphrase() { val newPassphrase = ":irrelevant:" diff --git a/identity-android/src/main/java/com/android/identity/android/securearea/KeystoreUtil.kt b/identity-android/src/main/java/com/android/identity/android/securearea/KeystoreUtil.kt new file mode 100644 index 000000000..3673a5862 --- /dev/null +++ b/identity-android/src/main/java/com/android/identity/android/securearea/KeystoreUtil.kt @@ -0,0 +1,32 @@ +package com.android.identity.android.securearea + +import android.content.Context +import android.os.Build + +class KeystoreUtil( + private val context: Context +) { + + fun getDeviceCapabilities(): DeviceCapabilities { + val systemAvailableFeatures = context.packageManager.systemAvailableFeatures + //TODO use the system available features to find out device capabilities + val isApiLevelOver30 = isApiLevelOver30() + return DeviceCapabilities(configureUserAuthenticationType = isApiLevelOver30) + } + + private fun isApiLevelOver30(): Boolean { + return Build.VERSION.SDK_INT >= Build.VERSION_CODES.R + } + + data class DeviceCapabilities( + val attestKey: Boolean = true, + val secureLockScreen: Boolean = true, + val ecdh: Boolean = true, + val curve25519: Boolean = true, + val strongBox: Boolean = true, + val strongBoxEcdh: Boolean = true, + val strongBox25519: Boolean = true, + val strongBoxAttestKey: Boolean = true, + val configureUserAuthenticationType: Boolean = true + ) +}