diff --git a/appholder/src/main/java/com/android/mdl/app/authconfirmation/AuthConfirmationFragment.kt b/appholder/src/main/java/com/android/mdl/app/authconfirmation/AuthConfirmationFragment.kt index 7d377f408..9802289d1 100644 --- a/appholder/src/main/java/com/android/mdl/app/authconfirmation/AuthConfirmationFragment.kt +++ b/appholder/src/main/java/com/android/mdl/app/authconfirmation/AuthConfirmationFragment.kt @@ -38,6 +38,7 @@ 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 override fun onCreateView( inflater: LayoutInflater, @@ -73,7 +74,7 @@ class AuthConfirmationFragment : BottomSheetDialogFragment() { viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.CREATED) { passphraseViewModel.authorizationState.collect { value -> if (value is PassphraseAuthResult.Success) { - authenticationSucceeded(value.userPassphrase) + onPassphraseProvided(value.userPassphrase) passphraseViewModel.reset() } } @@ -145,8 +146,6 @@ class AuthConfirmationFragment : BottomSheetDialogFragment() { userAuthRequest.build().authenticate(cryptoObject) } - private var androidKeyUnlockData: AndroidKeystoreSecureArea.KeyUnlockData? = null - private fun getSubtitle(): String { val readerCommonName = arguments.readerCommonName val readerIsTrusted = arguments.readerIsTrusted @@ -161,7 +160,7 @@ class AuthConfirmationFragment : BottomSheetDialogFragment() { } } - private fun authenticationSucceeded(passphrase: String) { + private fun onPassphraseProvided(passphrase: String) { val unlockData = BouncyCastleSecureArea.KeyUnlockData(passphrase) val result = viewModel.sendResponseForSelection(unlockData) onSendResponseResult(result) @@ -181,8 +180,7 @@ class AuthConfirmationFragment : BottomSheetDialogFragment() { private fun onSendResponseResult(result: AddDocumentToResponseResult) { when (result) { is AddDocumentToResponseResult.UserAuthRequired -> { - val keyUnlockData = AndroidKeystoreSecureArea.KeyUnlockData(result.keyAlias) - androidKeyUnlockData = keyUnlockData + androidKeyUnlockData = AndroidKeystoreSecureArea.KeyUnlockData(result.keyAlias) requestUserAuth( result.allowLSKFUnlocking, result.allowBiometricUnlocking @@ -190,7 +188,7 @@ class AuthConfirmationFragment : BottomSheetDialogFragment() { } is AddDocumentToResponseResult.PassphraseRequired -> { - requestPassphrase() + requestPassphrase(result.attemptedWithIncorrectPassword) } is AddDocumentToResponseResult.DocumentAdded -> { @@ -212,9 +210,13 @@ class AuthConfirmationFragment : BottomSheetDialogFragment() { viewModel.closeConnection() } - private fun requestPassphrase() { - val destination = AuthConfirmationFragmentDirections.openPassphrasePrompt() - findNavController().navigate(destination) + private fun requestPassphrase(attemptedWithIncorrectPassword: Boolean) { + val destination = AuthConfirmationFragmentDirections.openPassphrasePrompt( + showIncorrectPassword = attemptedWithIncorrectPassword + ) + val runnable = { findNavController().navigate(destination) } + // The system needs a little time to get back to this screen + Handler(Looper.getMainLooper()).postDelayed(runnable, 500) } private fun toast(message: String, duration: Int = Toast.LENGTH_SHORT) { diff --git a/appholder/src/main/java/com/android/mdl/app/authconfirmation/PassphrasePrompt.kt b/appholder/src/main/java/com/android/mdl/app/authconfirmation/PassphrasePrompt.kt index de35e6fb4..18d774bb2 100644 --- a/appholder/src/main/java/com/android/mdl/app/authconfirmation/PassphrasePrompt.kt +++ b/appholder/src/main/java/com/android/mdl/app/authconfirmation/PassphrasePrompt.kt @@ -8,6 +8,7 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults import androidx.compose.material3.MaterialTheme @@ -23,6 +24,8 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.PasswordVisualTransformation +import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.fragment.app.DialogFragment @@ -30,10 +33,12 @@ import androidx.fragment.app.activityViewModels import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.navArgs import com.android.mdl.app.R +import com.android.mdl.app.composables.PreviewLightDark import com.android.mdl.app.theme.HolderAppTheme class PassphrasePrompt : DialogFragment() { + private val args by navArgs() private val viewModel by activityViewModels() override fun onCreateView( @@ -45,6 +50,7 @@ class PassphrasePrompt : DialogFragment() { setContent { HolderAppTheme { PassphrasePromptUI( + showIncorrectPassword = args.showIncorrectPassword, onDone = { passphrase -> viewModel.authorize(userPassphrase = passphrase) findNavController().navigateUp() @@ -58,6 +64,7 @@ class PassphrasePrompt : DialogFragment() { @Composable private fun PassphrasePromptUI( + showIncorrectPassword: Boolean, onDone: (passphrase: String) -> Unit ) { var value by remember { mutableStateOf("") } @@ -82,8 +89,23 @@ private fun PassphrasePromptUI( modifier = Modifier.fillMaxWidth(), value = value, onValueChange = { value = it }, - textStyle = MaterialTheme.typography.bodyMedium + textStyle = MaterialTheme.typography.bodyMedium, + visualTransformation = PasswordVisualTransformation(), + placeholder = { + Text( + text = stringResource(id = R.string.passphrase_prompt_hint), + style = MaterialTheme.typography.titleSmall, + color = MaterialTheme.colorScheme.onBackground.copy(alpha = .5f) + ) + } ) + if (showIncorrectPassword) { + Text( + text = stringResource(id = R.string.passphrase_prompt_incorrect_passphrase), + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.error + ) + } TextButton( modifier = Modifier .align(Alignment.End), @@ -95,9 +117,23 @@ private fun PassphrasePromptUI( } @Composable -@Preview +@PreviewLightDark private fun PreviewPassphrasePrompt() { HolderAppTheme { - PassphrasePromptUI(onDone = {}) + PassphrasePromptUI( + showIncorrectPassword = false, + onDone = {} + ) + } +} + +@Composable +@PreviewLightDark +private fun PreviewPassphrasePromptWithIncorrectPassword() { + HolderAppTheme { + PassphrasePromptUI( + showIncorrectPassword = true, + onDone = {} + ) } } \ No newline at end of file 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 8b6354ef5..eaa2802f4 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 @@ -14,7 +14,7 @@ data class AddSelfSignedScreenState( val documentName: String = "Driving License", val secureAreaImplementationState: SecureAreaImplementationState = SecureAreaImplementationState.Android, val userAuthentication: Boolean = true, - val userAuthenticationTimeoutSeconds: Int = 10, + val userAuthenticationTimeoutSeconds: Int = 0, val allowLSKFUnlocking: AuthTypeState = AuthTypeState( isEnabled = true, canBeModified = false 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 78573f47f..798c9216b 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 @@ -12,5 +12,7 @@ sealed class AddDocumentToResponseResult { val allowBiometricUnlocking: Boolean ) : AddDocumentToResponseResult() - object PassphraseRequired : AddDocumentToResponseResult() + data class PassphraseRequired( + val attemptedWithIncorrectPassword: Boolean = false + ) : 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 f6441a138..ec30c58b1 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 @@ -8,7 +8,6 @@ import android.graphics.Color.WHITE import android.nfc.cardemulation.HostApduService import android.view.View import android.widget.ImageView -import androidx.biometric.BiometricPrompt import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import com.android.identity.* @@ -40,8 +39,6 @@ import java.util.* class TransferManager private constructor(private val context: Context) { companion object { - private const val LOG_TAG = "TransferManager" - @SuppressLint("StaticFieldLeak") @Volatile private var instance: TransferManager? = null @@ -55,7 +52,6 @@ class TransferManager private constructor(private val context: Context) { private var reversedQrCommunicationSetup: ReverseQrCommunicationSetup? = null private var qrCommunicationSetup: QrCommunicationSetup? = null private var hostApduService: HostApduService? = null - private var session: PresentationSession? = null private var hasStarted = false private lateinit var communication: Communication @@ -63,7 +59,6 @@ class TransferManager private constructor(private val context: Context) { private var transferStatusLd = MutableLiveData() fun setCommunication(session: PresentationSession, communication: Communication) { - this.session = session this.communication = communication } @@ -88,7 +83,6 @@ class TransferManager private constructor(private val context: Context) { reversedQrCommunicationSetup = ReverseQrCommunicationSetup( context = context, onPresentationReady = { session, presentation -> - this.session = session communication.setupPresentation(presentation) }, onNewRequest = { request -> @@ -116,7 +110,6 @@ class TransferManager private constructor(private val context: Context) { onConnecting = { transferStatusLd.value = TransferStatus.CONNECTING }, onQrEngagementReady = { transferStatusLd.value = TransferStatus.QR_ENGAGEMENT_READY }, onDeviceRetrievalHelperReady = { session, deviceRetrievalHelper -> - this.session = session communication.setupPresentation(deviceRetrievalHelper) transferStatusLd.value = TransferStatus.CONNECTED }, @@ -178,76 +171,75 @@ class TransferManager private constructor(private val context: Context) { keyUnlockData: SecureArea.KeyUnlockData? ): AddDocumentToResponseResult { var signingKeyUsageLimitPassed = false - session?.let { - val documentManager = DocumentManager.getInstance(context) - val documentInformation = documentManager.getDocumentInformation(credentialName) - requireValidProperty(documentInformation) { "Document not found!" } + val documentManager = DocumentManager.getInstance(context) + val documentInformation = documentManager.getDocumentInformation(credentialName) + requireValidProperty(documentInformation) { "Document not found!" } - val credential = requireNotNull(documentManager.getCredentialByName(credentialName)) - val dataElements = issuerSignedEntriesToRequest.keys.flatMap { key -> - issuerSignedEntriesToRequest.getOrDefault(key, emptyList()).map { value -> - CredentialRequest.DataElement(key, value, false) - } + val credential = requireNotNull(documentManager.getCredentialByName(credentialName)) + val dataElements = issuerSignedEntriesToRequest.keys.flatMap { key -> + issuerSignedEntriesToRequest.getOrDefault(key, emptyList()).map { value -> + CredentialRequest.DataElement(key, value, false) } + } - val request = CredentialRequest(dataElements) - val authKey = credential.findAuthenticationKey(Timestamp.now()) - ?: throw IllegalStateException("No auth key available") - if (authKey.usageCount >= documentInformation.maxUsagesPerKey) { - logWarning("Using Auth Key previously used ${authKey.usageCount} times, and maxUsagesPerKey is ${documentInformation.maxUsagesPerKey}") - signingKeyUsageLimitPassed = true - } + val request = CredentialRequest(dataElements) + val authKey = credential.findAuthenticationKey(Timestamp.now()) + ?: throw IllegalStateException("No auth key available") + if (authKey.usageCount >= documentInformation.maxUsagesPerKey) { + logWarning("Using Auth Key previously used ${authKey.usageCount} times, and maxUsagesPerKey is ${documentInformation.maxUsagesPerKey}") + signingKeyUsageLimitPassed = true + } - val staticAuthData = StaticAuthDataParser(authKey.issuerProvidedData).parse() - val mergedIssuerNamespaces = MdocUtil.mergeIssuerNamesSpaces( - request, credential.nameSpacedData, staticAuthData - ) + val staticAuthData = StaticAuthDataParser(authKey.issuerProvidedData).parse() + val mergedIssuerNamespaces = MdocUtil.mergeIssuerNamesSpaces( + request, credential.nameSpacedData, staticAuthData + ) - val transcript = communication.getSessionTranscript() ?: byteArrayOf() - val authOption = AddSelfSignedScreenState.MdocAuthStateOption.valueOf(documentInformation.mDocAuthOption) - try { - 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) { - 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 - } + val transcript = communication.getSessionTranscript() ?: byteArrayOf() + val authOption = + AddSelfSignedScreenState.MdocAuthStateOption.valueOf(documentInformation.mDocAuthOption) + try { + 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() + deviceResponseGenerator.addDocument(data) + authKey.increaseUsageCount() + ProvisioningUtil.getInstance(context).trackUsageTimestamp(credential) + } catch (lockedException: SecureArea.KeyLockedException) { + return if (credential.credentialSecureArea is AndroidKeystoreSecureArea) { + val keyInfo = + credential.credentialSecureArea.getKeyInfo(authKey.alias) as AndroidKeystoreSecureArea.KeyInfo + val allowLskf = keyInfo.userAuthenticationType == USER_AUTHENTICATION_TYPE_LSKF + val allowBiometric = + keyInfo.userAuthenticationType == USER_AUTHENTICATION_TYPE_BIOMETRIC + val allowBoth = + keyInfo.userAuthenticationType == USER_AUTHENTICATION_TYPE_LSKF or USER_AUTHENTICATION_TYPE_BIOMETRIC + AddDocumentToResponseResult.UserAuthRequired( + keyAlias = authKey.alias, + allowLSKFUnlocking = allowLskf || allowBoth, + allowBiometricUnlocking = allowBiometric || allowBoth + ) + } else { + AddDocumentToResponseResult.PassphraseRequired( + attemptedWithIncorrectPassword = keyUnlockData != null + ) } } return AddDocumentToResponseResult.DocumentAdded(signingKeyUsageLimitPassed) @@ -274,7 +266,6 @@ class TransferManager private constructor(private val context: Context) { fun destroy() { qrCommunicationSetup = null reversedQrCommunicationSetup = null - session = null hasStarted = false } diff --git a/appholder/src/main/java/com/android/mdl/app/util/ProvisioningUtil.kt b/appholder/src/main/java/com/android/mdl/app/util/ProvisioningUtil.kt index 309f6ec4f..afa5b3c04 100644 --- a/appholder/src/main/java/com/android/mdl/app/util/ProvisioningUtil.kt +++ b/appholder/src/main/java/com/android/mdl/app/util/ProvisioningUtil.kt @@ -268,11 +268,12 @@ class ProvisioningUtil private constructor( ecCurve: Int ): BouncyCastleSecureArea.CreateKeySettings { val keyPurpose = mDocAuthOption.toKeyPurpose() - return BouncyCastleSecureArea.CreateKeySettings.Builder() + val builder = BouncyCastleSecureArea.CreateKeySettings.Builder() .setPassphraseRequired(passphrase != null, passphrase) .setKeyPurposes(keyPurpose) .setEcCurve(ecCurve) - .build() + .setKeyPurposes(SecureArea.KEY_PURPOSE_SIGN or SecureArea.KEY_PURPOSE_AGREE_KEY) + return builder.build() } @KeyPurpose diff --git a/appholder/src/main/res/navigation/navigation_graph.xml b/appholder/src/main/res/navigation/navigation_graph.xml index 3f9974246..3ff2bc163 100644 --- a/appholder/src/main/res/navigation/navigation_graph.xml +++ b/appholder/src/main/res/navigation/navigation_graph.xml @@ -144,5 +144,12 @@ + android:name="com.android.mdl.app.authconfirmation.PassphrasePrompt"> + + + + \ No newline at end of file diff --git a/appholder/src/main/res/values/strings.xml b/appholder/src/main/res/values/strings.xml index 31fa9e792..79c26490c 100644 --- a/appholder/src/main/res/values/strings.xml +++ b/appholder/src/main/res/values/strings.xml @@ -189,6 +189,8 @@ Document Deleted! Passphrase + Type Passphrase + Incorrect Password. Try again! We need your consent to show the document data mdoc ECDSA authentication mdoc MAC authentication