Skip to content

Commit

Permalink
Default auth timeout to 0sec, fix presentation bug with keys with 0 t…
Browse files Browse the repository at this point in the history
…imeout
  • Loading branch information
mitrejcevski committed Oct 3, 2023
1 parent 0c91366 commit 86c542f
Show file tree
Hide file tree
Showing 7 changed files with 129 additions and 89 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ class AuthConfirmationFragment : BottomSheetDialogFragment() {
private val passphraseViewModel: PassphrasePromptViewModel by activityViewModels()
private val arguments by navArgs<AuthConfirmationFragmentArgs>()
private var isSendingInProgress = mutableStateOf(false)
private var androidKeyUnlockData: AndroidKeystoreSecureArea.KeyUnlockData? = null

override fun onCreateView(
inflater: LayoutInflater,
Expand Down Expand Up @@ -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()
}
}
Expand Down Expand Up @@ -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
Expand All @@ -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)
Expand All @@ -181,16 +180,15 @@ 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
)
}

is AddDocumentToResponseResult.PassphraseRequired -> {
requestPassphrase()
requestPassphrase(result.attemptedWithIncorrectPassword)
}

is AddDocumentToResponseResult.DocumentAdded -> {
Expand All @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -23,17 +24,21 @@ 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
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<PassphrasePromptArgs>()
private val viewModel by activityViewModels<PassphrasePromptViewModel>()

override fun onCreateView(
Expand All @@ -45,6 +50,7 @@ class PassphrasePrompt : DialogFragment() {
setContent {
HolderAppTheme {
PassphrasePromptUI(
showIncorrectPassword = args.showIncorrectPassword,
onDone = { passphrase ->
viewModel.authorize(userPassphrase = passphrase)
findNavController().navigateUp()
Expand All @@ -58,6 +64,7 @@ class PassphrasePrompt : DialogFragment() {

@Composable
private fun PassphrasePromptUI(
showIncorrectPassword: Boolean,
onDone: (passphrase: String) -> Unit
) {
var value by remember { mutableStateOf("") }
Expand All @@ -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),
Expand All @@ -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 = {}
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,7 @@ sealed class AddDocumentToResponseResult {
val allowBiometricUnlocking: Boolean
) : AddDocumentToResponseResult()

object PassphraseRequired : AddDocumentToResponseResult()
data class PassphraseRequired(
val attemptedWithIncorrectPassword: Boolean = false
) : AddDocumentToResponseResult()
}
Original file line number Diff line number Diff line change
Expand Up @@ -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.*
Expand Down Expand Up @@ -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
Expand All @@ -55,15 +52,13 @@ 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

private var transferStatusLd = MutableLiveData<TransferStatus>()

fun setCommunication(session: PresentationSession, communication: Communication) {
this.session = session
this.communication = communication
}

Expand All @@ -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 ->
Expand Down Expand Up @@ -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
},
Expand Down Expand Up @@ -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)
Expand All @@ -274,7 +266,6 @@ class TransferManager private constructor(private val context: Context) {
fun destroy() {
qrCommunicationSetup = null
reversedQrCommunicationSetup = null
session = null
hasStarted = false
}

Expand Down
Loading

0 comments on commit 86c542f

Please sign in to comment.