fix: User Verified response flag

This commit is contained in:
J-Jamet
2025-11-25 19:15:37 +01:00
parent c17fba8ef7
commit d90d175bd8
4 changed files with 74 additions and 35 deletions

View File

@@ -46,13 +46,15 @@ import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.setActivity
import com.kunzisoft.keepass.credentialprovider.SpecialMode import com.kunzisoft.keepass.credentialprovider.SpecialMode
import com.kunzisoft.keepass.credentialprovider.TypeMode import com.kunzisoft.keepass.credentialprovider.TypeMode
import com.kunzisoft.keepass.credentialprovider.passkey.data.AndroidPrivilegedApp import com.kunzisoft.keepass.credentialprovider.passkey.data.AndroidPrivilegedApp
import com.kunzisoft.keepass.credentialprovider.passkey.data.UserVerificationRequirement
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.ALLOWED_AUTHENTICATORS import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.ALLOWED_AUTHENTICATORS
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.addAppOrigin import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.addAppOrigin
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.addAuthCode import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.addAuthCode
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.addUserVerificationRequired import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.addUserVerification
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.getUserVerificationCondition
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.getUserVerifiedWithAuth
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.isAuthenticatorsAllowed import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.isAuthenticatorsAllowed
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.isUserVerificationRequired import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.removeUserVerification
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.removeUserVerificationRequired
import com.kunzisoft.keepass.credentialprovider.viewmodel.CredentialLauncherViewModel import com.kunzisoft.keepass.credentialprovider.viewmodel.CredentialLauncherViewModel
import com.kunzisoft.keepass.credentialprovider.viewmodel.PasskeyLauncherViewModel import com.kunzisoft.keepass.credentialprovider.viewmodel.PasskeyLauncherViewModel
import com.kunzisoft.keepass.database.ContextualDatabase import com.kunzisoft.keepass.database.ContextualDatabase
@@ -91,16 +93,17 @@ class PasskeyLauncherActivity : DatabaseLockActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
// To manage https://github.com/Kunzisoft/KeePassDX/issues/2283 // To manage https://github.com/Kunzisoft/KeePassDX/issues/2283
if (intent.isUserVerificationRequired()) { val userVerificationCondition = intent.getUserVerificationCondition()
if (userVerificationCondition) {
if (isAuthenticatorsAllowed().not()) { if (isAuthenticatorsAllowed().not()) {
intent.removeUserVerificationRequired() intent.removeUserVerification()
sendBroadcast(Intent(LOCK_ACTION)) sendBroadcast(Intent(LOCK_ACTION))
} }
} }
// super.onCreate must be after UserVerification to allow database lock // super.onCreate must be after UserVerification to allow database lock
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
// Biometric must be after super.onCreate // Biometric must be after super.onCreate
if (intent.isUserVerificationRequired()) { if (userVerificationCondition) {
if (isAuthenticatorsAllowed()) { if (isAuthenticatorsAllowed()) {
BiometricPrompt( BiometricPrompt(
this, ContextCompat.getMainExecutor(this), this, ContextCompat.getMainExecutor(this),
@@ -127,7 +130,7 @@ class PasskeyLauncherActivity : DatabaseLockActivity() {
result: BiometricPrompt.AuthenticationResult result: BiometricPrompt.AuthenticationResult
) { ) {
super.onAuthenticationSucceeded(result) super.onAuthenticationSucceeded(result)
passkeyLauncherViewModel.launchAction(intent, mSpecialMode) passkeyLauncherViewModel.launchAction(userVerified = true, intent, mSpecialMode)
} }
override fun onAuthenticationFailed() { override fun onAuthenticationFailed() {
super.onAuthenticationFailed() super.onAuthenticationFailed()
@@ -146,7 +149,7 @@ class PasskeyLauncherActivity : DatabaseLockActivity() {
lifecycleScope.launch { lifecycleScope.launch {
// Initialize the parameters // Initialize the parameters
passkeyLauncherViewModel.initialize() passkeyLauncherViewModel.initialize(userVerified = intent.getUserVerifiedWithAuth())
// Retrieve the UI // Retrieve the UI
passkeyLauncherViewModel.uiState.collect { uiState -> passkeyLauncherViewModel.uiState.collect { uiState ->
when (uiState) { when (uiState) {
@@ -340,7 +343,8 @@ class PasskeyLauncherActivity : DatabaseLockActivity() {
searchInfo: SearchInfo? = null, searchInfo: SearchInfo? = null,
appOrigin: AppOrigin? = null, appOrigin: AppOrigin? = null,
nodeId: UUID? = null, nodeId: UUID? = null,
userVerificationRequired: Boolean = false userVerification: UserVerificationRequirement = UserVerificationRequirement.PREFERRED,
userVerifiedWithAuth: Boolean = true
): PendingIntent? { ): PendingIntent? {
return PendingIntent.getActivity( return PendingIntent.getActivity(
context, context,
@@ -352,7 +356,7 @@ class PasskeyLauncherActivity : DatabaseLockActivity() {
addAppOrigin(appOrigin) addAppOrigin(appOrigin)
addNodeId(nodeId) addNodeId(nodeId)
addAuthCode(nodeId) addAuthCode(nodeId)
addUserVerificationRequired(userVerificationRequired) addUserVerification(userVerification, userVerifiedWithAuth)
}, },
PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_UPDATE_CURRENT PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
) )

View File

@@ -157,9 +157,9 @@ class PasskeyProviderService : CredentialProviderService() {
val credentialIdList = publicKeyCredentialRequestOptions.allowCredentials val credentialIdList = publicKeyCredentialRequestOptions.allowCredentials
.map { b64Encode(it.id) } .map { b64Encode(it.id) }
val searchInfo = buildPasskeySearchInfo(relyingPartyId, credentialIdList) val searchInfo = buildPasskeySearchInfo(relyingPartyId, credentialIdList)
val userVerificationRequired = publicKeyCredentialRequestOptions //val userVerification = publicKeyCredentialRequestOptions.userVerification
.userVerification == UserVerificationRequirement.REQUIRED val userVerification = UserVerificationRequirement.REQUIRED
Log.d(TAG, "Build passkey search for UV $userVerificationRequired, " + Log.d(TAG, "Build passkey search for UV $userVerification, " +
"RP $relyingPartyId and Credential IDs $credentialIdList") "RP $relyingPartyId and Credential IDs $credentialIdList")
SearchHelper.checkAutoSearchInfo( SearchHelper.checkAutoSearchInfo(
context = this, context = this,
@@ -170,7 +170,7 @@ class PasskeyProviderService : CredentialProviderService() {
passkeyEntries = passkeyEntries, passkeyEntries = passkeyEntries,
searchInfo = searchInfo, searchInfo = searchInfo,
option = option, option = option,
userVerificationRequired = userVerificationRequired userVerification = userVerification
) { ) {
Log.d(TAG, "Add pending intent for passkey selection with found items") Log.d(TAG, "Add pending intent for passkey selection with found items")
for (passkeyEntry in items) { for (passkeyEntry in items) {
@@ -179,7 +179,8 @@ class PasskeyProviderService : CredentialProviderService() {
specialMode = SpecialMode.SELECTION, specialMode = SpecialMode.SELECTION,
nodeId = passkeyEntry.id, nodeId = passkeyEntry.id,
appOrigin = passkeyEntry.appOrigin, appOrigin = passkeyEntry.appOrigin,
userVerificationRequired = userVerificationRequired userVerification = userVerification,
userVerifiedWithAuth = false
)?.let { usagePendingIntent -> )?.let { usagePendingIntent ->
val passkey = passkeyEntry.passkey val passkey = passkeyEntry.passkey
passkeyEntries.add( passkeyEntries.add(
@@ -208,7 +209,7 @@ class PasskeyProviderService : CredentialProviderService() {
passkeyEntries = passkeyEntries, passkeyEntries = passkeyEntries,
searchInfo = searchInfo, searchInfo = searchInfo,
option = option, option = option,
userVerificationRequired = userVerificationRequired userVerification = userVerification,
) { ) {
Log.w(TAG, "No passkey found in the database with this relying party : $relyingPartyId") Log.w(TAG, "No passkey found in the database with this relying party : $relyingPartyId")
if (credentialIdList.isEmpty()) { if (credentialIdList.isEmpty()) {
@@ -217,7 +218,8 @@ class PasskeyProviderService : CredentialProviderService() {
context = applicationContext, context = applicationContext,
specialMode = SpecialMode.SELECTION, specialMode = SpecialMode.SELECTION,
searchInfo = searchInfo, searchInfo = searchInfo,
userVerificationRequired = userVerificationRequired userVerification = userVerification,
userVerifiedWithAuth = false
)?.let { pendingIntent -> )?.let { pendingIntent ->
passkeyEntries.add( passkeyEntries.add(
PublicKeyCredentialEntry( PublicKeyCredentialEntry(
@@ -250,7 +252,8 @@ class PasskeyProviderService : CredentialProviderService() {
PasskeyLauncherActivity.getPendingIntent( PasskeyLauncherActivity.getPendingIntent(
context = applicationContext, context = applicationContext,
specialMode = SpecialMode.SELECTION, specialMode = SpecialMode.SELECTION,
searchInfo = searchInfo searchInfo = searchInfo,
userVerifiedWithAuth = true
)?.let { pendingIntent -> )?.let { pendingIntent ->
passkeyEntries.add( passkeyEntries.add(
PublicKeyCredentialEntry( PublicKeyCredentialEntry(
@@ -277,15 +280,16 @@ class PasskeyProviderService : CredentialProviderService() {
passkeyEntries: MutableList<CredentialEntry>, passkeyEntries: MutableList<CredentialEntry>,
searchInfo: SearchInfo, searchInfo: SearchInfo,
option: BeginGetPublicKeyCredentialOption, option: BeginGetPublicKeyCredentialOption,
userVerificationRequired: Boolean, userVerification: UserVerificationRequirement,
standardAction: () -> Unit standardAction: () -> Unit
) { ) {
if (userVerificationRequired && isAuthenticatorsAllowed().not()) { if (userVerification == UserVerificationRequirement.REQUIRED && isAuthenticatorsAllowed().not()) {
PasskeyLauncherActivity.getPendingIntent( PasskeyLauncherActivity.getPendingIntent(
context = applicationContext, context = applicationContext,
specialMode = SpecialMode.SELECTION, specialMode = SpecialMode.SELECTION,
searchInfo = searchInfo, searchInfo = searchInfo,
userVerificationRequired = true userVerification = userVerification,
userVerifiedWithAuth = true
)?.let { pendingIntent -> )?.let { pendingIntent ->
passkeyEntries.add( passkeyEntries.add(
PublicKeyCredentialEntry( PublicKeyCredentialEntry(

View File

@@ -60,6 +60,7 @@ import com.kunzisoft.keepass.credentialprovider.passkey.data.PublicKeyCredential
import com.kunzisoft.keepass.credentialprovider.passkey.data.PublicKeyCredentialCreationParameters import com.kunzisoft.keepass.credentialprovider.passkey.data.PublicKeyCredentialCreationParameters
import com.kunzisoft.keepass.credentialprovider.passkey.data.PublicKeyCredentialRequestOptions import com.kunzisoft.keepass.credentialprovider.passkey.data.PublicKeyCredentialRequestOptions
import com.kunzisoft.keepass.credentialprovider.passkey.data.PublicKeyCredentialUsageParameters import com.kunzisoft.keepass.credentialprovider.passkey.data.PublicKeyCredentialUsageParameters
import com.kunzisoft.keepass.credentialprovider.passkey.data.UserVerificationRequirement
import com.kunzisoft.keepass.credentialprovider.passkey.util.PrivilegedAllowLists.getOriginFromPrivilegedAllowLists import com.kunzisoft.keepass.credentialprovider.passkey.util.PrivilegedAllowLists.getOriginFromPrivilegedAllowLists
import com.kunzisoft.keepass.model.AndroidOrigin import com.kunzisoft.keepass.model.AndroidOrigin
import com.kunzisoft.keepass.model.AppOrigin import com.kunzisoft.keepass.model.AppOrigin
@@ -67,7 +68,9 @@ import com.kunzisoft.keepass.model.EntryInfo
import com.kunzisoft.keepass.model.Passkey import com.kunzisoft.keepass.model.Passkey
import com.kunzisoft.keepass.utils.AppUtil import com.kunzisoft.keepass.utils.AppUtil
import com.kunzisoft.keepass.utils.StringUtil.toHexString import com.kunzisoft.keepass.utils.StringUtil.toHexString
import com.kunzisoft.keepass.utils.getEnumExtra
import com.kunzisoft.keepass.utils.getParcelableExtraCompat import com.kunzisoft.keepass.utils.getParcelableExtraCompat
import com.kunzisoft.keepass.utils.putEnumExtra
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import java.io.IOException import java.io.IOException
@@ -95,7 +98,8 @@ object PasskeyHelper {
private const val EXTRA_APP_ORIGIN = "com.kunzisoft.keepass.extra.appOrigin" private const val EXTRA_APP_ORIGIN = "com.kunzisoft.keepass.extra.appOrigin"
private const val EXTRA_TIMESTAMP = "com.kunzisoft.keepass.extra.timestamp" private const val EXTRA_TIMESTAMP = "com.kunzisoft.keepass.extra.timestamp"
private const val EXTRA_AUTHENTICATION_CODE = "com.kunzisoft.keepass.extra.authenticationCode" private const val EXTRA_AUTHENTICATION_CODE = "com.kunzisoft.keepass.extra.authenticationCode"
private const val EXTRA_UV_REQUIRED = "com.kunzisoft.keepass.extra.userVerification" private const val EXTRA_USER_VERIFICATION = "com.kunzisoft.keepass.extra.userVerification"
private const val EXTRA_USER_VERIFIED_WITH_AUTH = "com.kunzisoft.keepass.extra.userVerifiedWithAuth"
private const val SEPARATOR = "_" private const val SEPARATOR = "_"
@@ -113,26 +117,46 @@ object PasskeyHelper {
private val internalSecureRandom: SecureRandom = SecureRandom() private val internalSecureRandom: SecureRandom = SecureRandom()
/** /**
* Add the user verification to the intent * Add the User Verification to the intent
*/ */
fun Intent.addUserVerificationRequired(userVerification: Boolean) { fun Intent.addUserVerification(
putExtra(EXTRA_UV_REQUIRED, userVerification) userVerification: UserVerificationRequirement,
userVerifiedWithAuth: Boolean
) {
putEnumExtra(EXTRA_USER_VERIFICATION, userVerification)
putExtra(EXTRA_USER_VERIFIED_WITH_AUTH, userVerifiedWithAuth)
} }
/** /**
* Check if the user verification is required * Get the User Verification from the intent
*/ */
fun Intent.isUserVerificationRequired(): Boolean { fun Intent.getUserVerificationCondition(): Boolean {
return getBooleanExtra(EXTRA_UV_REQUIRED, false) return (getEnumExtra<UserVerificationRequirement>(EXTRA_USER_VERIFICATION)
?: UserVerificationRequirement.PREFERRED) == UserVerificationRequirement.REQUIRED
} }
/** /**
* Remove the user verification from the intent * Define if the User is verified with authentification from the intent
*/ */
fun Intent.removeUserVerificationRequired() { fun Intent.getUserVerifiedWithAuth(): Boolean {
removeExtra(EXTRA_UV_REQUIRED) return getBooleanExtra(EXTRA_USER_VERIFIED_WITH_AUTH, true)
} }
/**
* Remove the User Verification from the intent
*/
fun Intent.removeUserVerification() {
removeExtra(EXTRA_USER_VERIFICATION)
}
/**
* Remove the User verified with auth from the intent
*/
fun Intent.removeUserVerifiedWithAuth() {
removeExtra(EXTRA_USER_VERIFIED_WITH_AUTH)
}
/** /**
* Allowed authenticators for the User Verification * Allowed authenticators for the User Verification
*/ */
@@ -622,6 +646,7 @@ object PasskeyHelper {
requestOptions: PublicKeyCredentialRequestOptions, requestOptions: PublicKeyCredentialRequestOptions,
clientDataResponse: ClientDataResponse, clientDataResponse: ClientDataResponse,
passkey: Passkey, passkey: Passkey,
userVerified: Boolean,
defaultBackupEligibility: Boolean, defaultBackupEligibility: Boolean,
defaultBackupState: Boolean defaultBackupState: Boolean
): PublicKeyCredential { ): PublicKeyCredential {
@@ -630,7 +655,7 @@ object PasskeyHelper {
response = AuthenticatorAssertionResponse( response = AuthenticatorAssertionResponse(
requestOptions = requestOptions, requestOptions = requestOptions,
userPresent = true, userPresent = true,
userVerified = true, userVerified = userVerified,
backupEligibility = passkey.backupEligibility ?: defaultBackupEligibility, backupEligibility = passkey.backupEligibility ?: defaultBackupEligibility,
backupState = passkey.backupState ?: defaultBackupState, backupState = passkey.backupState ?: defaultBackupState,
userHandle = passkey.userHandle, userHandle = passkey.userHandle,

View File

@@ -24,8 +24,8 @@ import com.kunzisoft.keepass.credentialprovider.passkey.data.PublicKeyCredential
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.buildCreatePublicKeyCredentialResponse import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.buildCreatePublicKeyCredentialResponse
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.buildPasskeyPublicKeyCredential import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.buildPasskeyPublicKeyCredential
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.checkSecurity import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.checkSecurity
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.getUserVerificationCondition
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.getVerifiedGETClientDataResponse import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.getVerifiedGETClientDataResponse
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.isUserVerificationRequired
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.removeAppOrigin import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.removeAppOrigin
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.removePasskey import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.removePasskey
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.retrieveAppOrigin import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.retrieveAppOrigin
@@ -65,14 +65,16 @@ class PasskeyLauncherViewModel(application: Application): CredentialLauncherView
private var mPasskey: Passkey? = null private var mPasskey: Passkey? = null
private var mLockDatabaseAfterSelection: Boolean = false private var mLockDatabaseAfterSelection: Boolean = false
private var mUserVerified: Boolean = true
private var mBackupEligibility: Boolean = true private var mBackupEligibility: Boolean = true
private var mBackupState: Boolean = false private var mBackupState: Boolean = false
private val mUiState = MutableStateFlow<UIState>(UIState.Loading) private val mUiState = MutableStateFlow<UIState>(UIState.Loading)
val uiState: StateFlow<UIState> = mUiState val uiState: StateFlow<UIState> = mUiState
fun initialize() { fun initialize(userVerified: Boolean) {
mLockDatabaseAfterSelection = PreferencesUtil.isPasskeyCloseDatabaseEnable(getApplication()) mLockDatabaseAfterSelection = PreferencesUtil.isPasskeyCloseDatabaseEnable(getApplication())
mUserVerified = userVerified
mBackupEligibility = PreferencesUtil.isPasskeyBackupEligibilityEnable(getApplication()) mBackupEligibility = PreferencesUtil.isPasskeyBackupEligibilityEnable(getApplication())
mBackupState = PreferencesUtil.isPasskeyBackupStateEnable(getApplication()) mBackupState = PreferencesUtil.isPasskeyBackupStateEnable(getApplication())
} }
@@ -151,9 +153,11 @@ class PasskeyLauncherViewModel(application: Application): CredentialLauncherView
} }
fun launchAction( fun launchAction(
userVerified: Boolean,
intent: Intent, intent: Intent,
specialMode: SpecialMode, specialMode: SpecialMode,
) { ) {
this.mUserVerified = userVerified
super.launchActionIfNeeded(intent, specialMode, mDatabase) super.launchActionIfNeeded(intent, specialMode, mDatabase)
} }
@@ -162,7 +166,7 @@ class PasskeyLauncherViewModel(application: Application): CredentialLauncherView
specialMode: SpecialMode, specialMode: SpecialMode,
database: ContextualDatabase? database: ContextualDatabase?
) { ) {
if (intent.isUserVerificationRequired()) { if (intent.getUserVerificationCondition()) {
if (database != null) { if (database != null) {
onDatabaseRetrieved(database) onDatabaseRetrieved(database)
} }
@@ -321,6 +325,7 @@ class PasskeyLauncherViewModel(application: Application): CredentialLauncherView
appOrigin = appOrigin appOrigin = appOrigin
), ),
passkey = passkey, passkey = passkey,
userVerified = mUserVerified,
defaultBackupEligibility = mBackupEligibility, defaultBackupEligibility = mBackupEligibility,
defaultBackupState = mBackupState defaultBackupState = mBackupState
) )
@@ -377,6 +382,7 @@ class PasskeyLauncherViewModel(application: Application): CredentialLauncherView
appOrigin = appOrigin appOrigin = appOrigin
), ),
passkey = passkey, passkey = passkey,
userVerified = mUserVerified,
defaultBackupEligibility = mBackupEligibility, defaultBackupEligibility = mBackupEligibility,
defaultBackupState = mBackupState defaultBackupState = mBackupState
) )