mirror of
https://github.com/Kunzisoft/KeePassDX.git
synced 2025-12-04 15:49:33 +01:00
fix: Behavior when ask device credential #2283
This commit is contained in:
@@ -57,7 +57,7 @@ import com.kunzisoft.keepass.activities.legacy.DatabaseLockActivity
|
||||
import com.kunzisoft.keepass.adapters.TagsAdapter
|
||||
import com.kunzisoft.keepass.credentialprovider.SpecialMode
|
||||
import com.kunzisoft.keepass.credentialprovider.UserVerificationData
|
||||
import com.kunzisoft.keepass.credentialprovider.UserVerificationHelper.Companion.askUserVerification
|
||||
import com.kunzisoft.keepass.credentialprovider.UserVerificationHelper.Companion.checkUserVerification
|
||||
import com.kunzisoft.keepass.credentialprovider.UserVerificationHelper.Companion.isUserVerificationNeeded
|
||||
import com.kunzisoft.keepass.credentialprovider.magikeyboard.MagikeyboardService
|
||||
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||
@@ -331,17 +331,7 @@ class EntryActivity : DatabaseLockActivity() {
|
||||
mUserVerificationViewModel.onUserVerificationReceived()
|
||||
}
|
||||
is UserVerificationViewModel.UIState.OnUserVerificationSucceeded -> {
|
||||
uIState.dataToVerify.database?.let { database ->
|
||||
uIState.dataToVerify.entryId?.let { entryId ->
|
||||
EntryEditActivity.launch(
|
||||
activity = this@EntryActivity,
|
||||
database = database,
|
||||
registrationType = EntryEditActivity.RegistrationType.UPDATE,
|
||||
nodeId = entryId,
|
||||
activityResultLauncher = mEntryActivityResultLauncher
|
||||
)
|
||||
}
|
||||
}
|
||||
editEntry(uIState.dataToVerify.database, uIState.dataToVerify.entryId)
|
||||
mUserVerificationViewModel.onUserVerificationReceived()
|
||||
}
|
||||
}
|
||||
@@ -500,15 +490,31 @@ class EntryActivity : DatabaseLockActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun editEntry(database: ContextualDatabase?, entryId: NodeId<*>?) {
|
||||
database?.let { database ->
|
||||
entryId?.let { entryId ->
|
||||
EntryEditActivity.launch(
|
||||
activity = this@EntryActivity,
|
||||
database = database,
|
||||
registrationType = EntryEditActivity.RegistrationType.UPDATE,
|
||||
nodeId = entryId,
|
||||
activityResultLauncher = mEntryActivityResultLauncher
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
when (item.itemId) {
|
||||
R.id.menu_edit -> {
|
||||
askUserVerification(
|
||||
userVerificationViewModel = mUserVerificationViewModel,
|
||||
userVerificationCondition = mEntryViewModel.entryInfo
|
||||
?.isUserVerificationNeeded() == true,
|
||||
dataToVerify = UserVerificationData(mDatabase, mEntryViewModel.mainEntryId)
|
||||
)
|
||||
if (mEntryViewModel.entryInfo?.isUserVerificationNeeded() == true) {
|
||||
checkUserVerification(
|
||||
userVerificationViewModel = mUserVerificationViewModel,
|
||||
dataToVerify = UserVerificationData(mDatabase, mEntryViewModel.mainEntryId)
|
||||
)
|
||||
} else {
|
||||
editEntry(mDatabase, mEntryViewModel.mainEntryId)
|
||||
}
|
||||
return true
|
||||
}
|
||||
R.id.menu_restore_entry_history -> {
|
||||
|
||||
@@ -76,7 +76,7 @@ import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.retrieveSea
|
||||
import com.kunzisoft.keepass.credentialprovider.SpecialMode
|
||||
import com.kunzisoft.keepass.credentialprovider.TypeMode
|
||||
import com.kunzisoft.keepass.credentialprovider.UserVerificationData
|
||||
import com.kunzisoft.keepass.credentialprovider.UserVerificationHelper.Companion.askUserVerification
|
||||
import com.kunzisoft.keepass.credentialprovider.UserVerificationHelper.Companion.checkUserVerification
|
||||
import com.kunzisoft.keepass.credentialprovider.UserVerificationHelper.Companion.isUserVerificationNeeded
|
||||
import com.kunzisoft.keepass.credentialprovider.magikeyboard.MagikeyboardService
|
||||
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.buildPasskeyResponseAndSetResult
|
||||
@@ -584,17 +584,7 @@ class GroupActivity : DatabaseLockActivity(),
|
||||
mUserVerificationViewModel.onUserVerificationReceived()
|
||||
}
|
||||
is UserVerificationViewModel.UIState.OnUserVerificationSucceeded -> {
|
||||
uIState.dataToVerify.database?.let { database ->
|
||||
uIState.dataToVerify.entryId?.let { entryId ->
|
||||
EntryEditActivity.launch(
|
||||
activity = this@GroupActivity,
|
||||
database = database,
|
||||
registrationType = EntryEditActivity.RegistrationType.UPDATE,
|
||||
nodeId = entryId,
|
||||
activityResultLauncher = mEntryActivityResultLauncher
|
||||
)
|
||||
}
|
||||
}
|
||||
editEntry(uIState.dataToVerify.database, uIState.dataToVerify.entryId)
|
||||
mUserVerificationViewModel.onUserVerificationReceived()
|
||||
}
|
||||
}
|
||||
@@ -1047,6 +1037,20 @@ class GroupActivity : DatabaseLockActivity(),
|
||||
).containsSearchInfo(searchInfo)
|
||||
}
|
||||
|
||||
private fun editEntry(database: ContextualDatabase?, entryId: NodeId<*>?) {
|
||||
database?.let {
|
||||
entryId?.let {
|
||||
EntryEditActivity.launch(
|
||||
activity = this@GroupActivity,
|
||||
database = database,
|
||||
registrationType = EntryEditActivity.RegistrationType.UPDATE,
|
||||
nodeId = entryId,
|
||||
activityResultLauncher = mEntryActivityResultLauncher
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun finishNodeAction() {
|
||||
actionNodeMode?.finish()
|
||||
}
|
||||
@@ -1096,12 +1100,14 @@ class GroupActivity : DatabaseLockActivity(),
|
||||
launchDialogForGroupUpdate(node as Group)
|
||||
}
|
||||
Type.ENTRY -> {
|
||||
askUserVerification(
|
||||
userVerificationViewModel = mUserVerificationViewModel,
|
||||
userVerificationCondition = (node as Entry).getEntryInfo(database)
|
||||
.isUserVerificationNeeded(),
|
||||
dataToVerify = UserVerificationData(database,node.nodeId)
|
||||
)
|
||||
if ((node as Entry).getEntryInfo(database).isUserVerificationNeeded()) {
|
||||
checkUserVerification(
|
||||
userVerificationViewModel = mUserVerificationViewModel,
|
||||
dataToVerify = UserVerificationData(database, node.nodeId)
|
||||
)
|
||||
} else {
|
||||
editEntry(database, node.nodeId)
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
|
||||
@@ -75,9 +75,14 @@ class UserVerificationHelper {
|
||||
/**
|
||||
* Get the User Verification from the intent
|
||||
*/
|
||||
fun Intent.getUserVerificationCondition(): Boolean {
|
||||
return (getEnumExtra<UserVerificationRequirement>(EXTRA_USER_VERIFICATION)
|
||||
?: UserVerificationRequirement.PREFERRED) == UserVerificationRequirement.REQUIRED
|
||||
fun Intent.isUserVerificationNeeded(userVerificationPreferred: Boolean): Boolean {
|
||||
val userVerification: UserVerificationRequirement =
|
||||
getEnumExtra<UserVerificationRequirement>(EXTRA_USER_VERIFICATION)
|
||||
?: UserVerificationRequirement.PREFERRED
|
||||
return (userVerification == UserVerificationRequirement.REQUIRED
|
||||
|| (userVerificationPreferred
|
||||
&& userVerification == UserVerificationRequirement.PREFERRED)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -87,67 +92,66 @@ class UserVerificationHelper {
|
||||
return this.passkey != null
|
||||
}
|
||||
|
||||
/**
|
||||
* Ask the user for verification
|
||||
* Ask for the biometric if defined on the device
|
||||
* Ask for the database credential otherwise
|
||||
*/
|
||||
fun FragmentActivity.askUserVerification(
|
||||
fun FragmentActivity.checkUserVerification(
|
||||
userVerificationViewModel: UserVerificationViewModel,
|
||||
userVerificationCondition: Boolean,
|
||||
dataToVerify: UserVerificationData
|
||||
) {
|
||||
if (isAuthenticatorsAllowed() && userVerificationCondition) {
|
||||
// Important to check the nullable database here
|
||||
dataToVerify.database?.let {
|
||||
BiometricPrompt(
|
||||
this, ContextCompat.getMainExecutor(this),
|
||||
object : BiometricPrompt.AuthenticationCallback() {
|
||||
override fun onAuthenticationError(
|
||||
errorCode: Int,
|
||||
errString: CharSequence
|
||||
) {
|
||||
super.onAuthenticationError(errorCode, errString)
|
||||
when (errorCode) {
|
||||
BiometricPrompt.ERROR_CANCELED,
|
||||
BiometricPrompt.ERROR_NEGATIVE_BUTTON,
|
||||
BiometricPrompt.ERROR_USER_CANCELED -> {
|
||||
// No operation
|
||||
Log.i("UserVerification", "$errString")
|
||||
}
|
||||
|
||||
else -> {
|
||||
toastError(SecurityException("Authentication error: $errString"))
|
||||
}
|
||||
}
|
||||
userVerificationViewModel.onUserVerificationFailed(dataToVerify)
|
||||
}
|
||||
|
||||
override fun onAuthenticationSucceeded(
|
||||
result: BiometricPrompt.AuthenticationResult
|
||||
) {
|
||||
super.onAuthenticationSucceeded(result)
|
||||
userVerificationViewModel.onUserVerificationSucceeded(dataToVerify)
|
||||
}
|
||||
|
||||
override fun onAuthenticationFailed() {
|
||||
super.onAuthenticationFailed()
|
||||
toastError(SecurityException(getString(R.string.device_unlock_not_recognized)))
|
||||
userVerificationViewModel.onUserVerificationFailed(dataToVerify)
|
||||
}
|
||||
}).authenticate(
|
||||
BiometricPrompt.PromptInfo.Builder()
|
||||
.setTitle(getString(R.string.user_verification_required))
|
||||
.setAllowedAuthenticators(ALLOWED_AUTHENTICATORS)
|
||||
.setConfirmationRequired(false)
|
||||
.build()
|
||||
)
|
||||
}
|
||||
if (isAuthenticatorsAllowed()) {
|
||||
showUserVerificationDeviceCredential(userVerificationViewModel, dataToVerify)
|
||||
} else {
|
||||
userVerificationViewModel.onUserVerificationSucceeded(dataToVerify)
|
||||
showUserVerificationMessage {
|
||||
userVerificationViewModel.onUserVerificationFailed()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun FragmentActivity.showUserVerificationDeviceCredential(
|
||||
userVerificationViewModel: UserVerificationViewModel,
|
||||
dataToVerify: UserVerificationData
|
||||
) {
|
||||
BiometricPrompt(
|
||||
this, ContextCompat.getMainExecutor(this),
|
||||
object : BiometricPrompt.AuthenticationCallback() {
|
||||
override fun onAuthenticationError(
|
||||
errorCode: Int,
|
||||
errString: CharSequence
|
||||
) {
|
||||
super.onAuthenticationError(errorCode, errString)
|
||||
when (errorCode) {
|
||||
BiometricPrompt.ERROR_CANCELED,
|
||||
BiometricPrompt.ERROR_NEGATIVE_BUTTON,
|
||||
BiometricPrompt.ERROR_USER_CANCELED -> {
|
||||
// No operation
|
||||
Log.i("UserVerification", "$errString")
|
||||
}
|
||||
else -> {
|
||||
toastError(SecurityException("Authentication error: $errString"))
|
||||
}
|
||||
}
|
||||
userVerificationViewModel.onUserVerificationFailed(dataToVerify)
|
||||
}
|
||||
|
||||
override fun onAuthenticationSucceeded(
|
||||
result: BiometricPrompt.AuthenticationResult
|
||||
) {
|
||||
super.onAuthenticationSucceeded(result)
|
||||
userVerificationViewModel.onUserVerificationSucceeded(dataToVerify)
|
||||
}
|
||||
|
||||
override fun onAuthenticationFailed() {
|
||||
super.onAuthenticationFailed()
|
||||
toastError(SecurityException(getString(R.string.device_unlock_not_recognized)))
|
||||
userVerificationViewModel.onUserVerificationFailed(dataToVerify)
|
||||
}
|
||||
}).authenticate(
|
||||
BiometricPrompt.PromptInfo.Builder()
|
||||
.setTitle(getString(R.string.user_verification_required))
|
||||
.setAllowedAuthenticators(ALLOWED_AUTHENTICATORS)
|
||||
.setConfirmationRequired(false)
|
||||
.build()
|
||||
)
|
||||
}
|
||||
|
||||
fun FragmentActivity.showUserVerificationMessage(
|
||||
onActionPerformed: () -> Unit
|
||||
) {
|
||||
|
||||
@@ -47,11 +47,9 @@ import com.kunzisoft.keepass.credentialprovider.SpecialMode
|
||||
import com.kunzisoft.keepass.credentialprovider.TypeMode
|
||||
import com.kunzisoft.keepass.credentialprovider.UserVerificationData
|
||||
import com.kunzisoft.keepass.credentialprovider.UserVerificationHelper.Companion.addUserVerification
|
||||
import com.kunzisoft.keepass.credentialprovider.UserVerificationHelper.Companion.askUserVerification
|
||||
import com.kunzisoft.keepass.credentialprovider.UserVerificationHelper.Companion.getUserVerificationCondition
|
||||
import com.kunzisoft.keepass.credentialprovider.UserVerificationHelper.Companion.checkUserVerification
|
||||
import com.kunzisoft.keepass.credentialprovider.UserVerificationHelper.Companion.getUserVerifiedWithAuth
|
||||
import com.kunzisoft.keepass.credentialprovider.UserVerificationHelper.Companion.isAuthenticatorsAllowed
|
||||
import com.kunzisoft.keepass.credentialprovider.UserVerificationHelper.Companion.showUserVerificationMessage
|
||||
import com.kunzisoft.keepass.credentialprovider.UserVerificationHelper.Companion.isUserVerificationNeeded
|
||||
import com.kunzisoft.keepass.credentialprovider.passkey.data.AndroidPrivilegedApp
|
||||
import com.kunzisoft.keepass.credentialprovider.passkey.data.UserVerificationRequirement
|
||||
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.addAppOrigin
|
||||
@@ -63,6 +61,7 @@ import com.kunzisoft.keepass.model.AppOrigin
|
||||
import com.kunzisoft.keepass.model.SearchInfo
|
||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CHECK_CREDENTIAL_TASK
|
||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_ENTRY_TASK
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil.isPasskeyUserVerificationPreferred
|
||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||
import com.kunzisoft.keepass.utils.AppUtil.randomRequestCode
|
||||
import com.kunzisoft.keepass.view.toastError
|
||||
@@ -202,16 +201,20 @@ class PasskeyLauncherActivity : DatabaseLockActivity() {
|
||||
override fun onUnknownDatabaseRetrieved(database: ContextualDatabase?) {
|
||||
super.onUnknownDatabaseRetrieved(database)
|
||||
// To manage https://github.com/Kunzisoft/KeePassDX/issues/2283
|
||||
if (isAuthenticatorsAllowed()) {
|
||||
askUserVerification(
|
||||
val userVerificationNeeded = intent.isUserVerificationNeeded(
|
||||
userVerificationPreferred = isPasskeyUserVerificationPreferred(this)
|
||||
) && intent.getUserVerifiedWithAuth().not()
|
||||
if (userVerificationNeeded) {
|
||||
checkUserVerification(
|
||||
userVerificationViewModel = userVerificationViewModel,
|
||||
userVerificationCondition = intent.getUserVerificationCondition(),
|
||||
dataToVerify = UserVerificationData(database)
|
||||
)
|
||||
} else {
|
||||
showUserVerificationMessage {
|
||||
userVerificationViewModel.onUserVerificationFailed()
|
||||
}
|
||||
passkeyLauncherViewModel.launchActionIfNeeded(
|
||||
intent = intent,
|
||||
specialMode = mSpecialMode,
|
||||
database = database
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -150,8 +150,7 @@ class PasskeyProviderService : CredentialProviderService() {
|
||||
val credentialIdList = publicKeyCredentialRequestOptions.allowCredentials
|
||||
.map { b64Encode(it.id) }
|
||||
val searchInfo = buildPasskeySearchInfo(relyingPartyId, credentialIdList)
|
||||
// TODO remove
|
||||
val userVerification = UserVerificationRequirement.REQUIRED//publicKeyCredentialRequestOptions.userVerification
|
||||
val userVerification = publicKeyCredentialRequestOptions.userVerification
|
||||
Log.d(TAG, "Build passkey search for UV $userVerification, " +
|
||||
"RP $relyingPartyId and Credential IDs $credentialIdList")
|
||||
SearchHelper.checkAutoSearchInfo(
|
||||
|
||||
@@ -18,7 +18,6 @@ import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.retrieveNod
|
||||
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.retrieveSearchInfo
|
||||
import com.kunzisoft.keepass.credentialprovider.SpecialMode
|
||||
import com.kunzisoft.keepass.credentialprovider.TypeMode
|
||||
import com.kunzisoft.keepass.credentialprovider.UserVerificationHelper.Companion.getUserVerificationCondition
|
||||
import com.kunzisoft.keepass.credentialprovider.passkey.data.AndroidPrivilegedApp
|
||||
import com.kunzisoft.keepass.credentialprovider.passkey.data.PublicKeyCredentialCreationParameters
|
||||
import com.kunzisoft.keepass.credentialprovider.passkey.data.PublicKeyCredentialUsageParameters
|
||||
@@ -159,7 +158,7 @@ class PasskeyLauncherViewModel(application: Application): CredentialLauncherView
|
||||
database: ContextualDatabase?
|
||||
) {
|
||||
this.mUserVerified = userVerified
|
||||
super.launchActionIfNeeded(intent, specialMode, database)
|
||||
launchActionIfNeeded(intent, specialMode, database)
|
||||
}
|
||||
|
||||
override fun launchActionIfNeeded(
|
||||
@@ -167,15 +166,9 @@ class PasskeyLauncherViewModel(application: Application): CredentialLauncherView
|
||||
specialMode: SpecialMode,
|
||||
database: ContextualDatabase?
|
||||
) {
|
||||
if (intent.getUserVerificationCondition()) {
|
||||
if (database != null) {
|
||||
onDatabaseRetrieved(database)
|
||||
}
|
||||
} else {
|
||||
// Launch with database when a nodeId is present
|
||||
if ((database != null && database.loaded) || intent.retrieveNodeId() == null) {
|
||||
super.launchActionIfNeeded(intent, specialMode, database)
|
||||
}
|
||||
// Launch with database when a nodeId is present
|
||||
if ((database != null && database.loaded) || intent.retrieveNodeId() == null) {
|
||||
super.launchActionIfNeeded(intent, specialMode, database)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -690,6 +690,12 @@ object PreferencesUtil {
|
||||
context.resources.getBoolean(R.bool.passkeys_close_database_default))
|
||||
}
|
||||
|
||||
fun isPasskeyUserVerificationPreferred(context: Context): Boolean {
|
||||
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
return prefs.getBoolean(context.getString(R.string.passkeys_user_verification_preferred_key),
|
||||
context.resources.getBoolean(R.bool.passkeys_user_verification_preferred_default))
|
||||
}
|
||||
|
||||
fun isPasskeyBackupEligibilityEnable(context: Context): Boolean {
|
||||
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
return prefs.getBoolean(context.getString(R.string.passkeys_backup_eligibility_key),
|
||||
|
||||
@@ -139,6 +139,8 @@
|
||||
<string name="passkeys_privileged_apps_key" translatable="false">passkeys_privileged_apps_key</string>
|
||||
<string name="passkeys_auto_select_key" translatable="false">passkeys_auto_select_key</string>
|
||||
<bool name="passkeys_auto_select_default" translatable="false">true</bool>
|
||||
<string name="passkeys_user_verification_preferred_key" translatable="false">passkeys_user_verification_preferred_key</string>
|
||||
<bool name="passkeys_user_verification_preferred_default" translatable="false">false</bool>
|
||||
<string name="passkeys_backup_eligibility_key" translatable="false">passkeys_backup_eligibility_key</string>
|
||||
<bool name="passkeys_backup_eligibility_default" translatable="false">true</bool>
|
||||
<string name="passkeys_backup_state_key" translatable="false">passkeys_backup_state_key</string>
|
||||
|
||||
@@ -434,6 +434,8 @@
|
||||
<string name="passkeys_missing_signature_app_ask_question">Add app signature to passkey entry?</string>
|
||||
<string name="passkeys_auto_select_title">Auto select</string>
|
||||
<string name="passkeys_auto_select_summary">Auto select if only one entry and the database is open, only if the requesting app is compatible</string>
|
||||
<string name="passkeys_user_verification_preferred_title">Preferred User Verification</string>
|
||||
<string name="passkeys_user_verification_preferred_summary">Perform a user verification to access sensitive data when the relying party requests \"preferred\".</string>
|
||||
<string name="passkeys_backup_eligibility_title">Backup Eligibility</string>
|
||||
<string name="passkeys_backup_eligibility_summary">Determine at creation time whether the public key credential source is allowed to be backed up</string>
|
||||
<string name="passkeys_backup_state_title">Backup State</string>
|
||||
|
||||
@@ -37,6 +37,11 @@
|
||||
</PreferenceCategory>
|
||||
<PreferenceCategory
|
||||
android:title="@string/database">
|
||||
<SwitchPreferenceCompat
|
||||
android:key="@string/passkeys_user_verification_preferred_key"
|
||||
android:title="@string/passkeys_user_verification_preferred_title"
|
||||
android:summary="@string/passkeys_user_verification_preferred_summary"
|
||||
android:defaultValue="@bool/passkeys_user_verification_preferred_default"/>
|
||||
<SwitchPreferenceCompat
|
||||
android:key="@string/passkeys_backup_eligibility_key"
|
||||
android:title="@string/passkeys_backup_eligibility_title"
|
||||
|
||||
Reference in New Issue
Block a user