diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/GroupActivity.kt b/app/src/main/java/com/kunzisoft/keepass/activities/GroupActivity.kt index 94ad52ef6..5adbdcd31 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/GroupActivity.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/GroupActivity.kt @@ -52,6 +52,7 @@ import androidx.core.view.WindowInsetsCompat import androidx.core.view.WindowInsetsControllerCompat import androidx.core.view.isVisible import androidx.drawerlayout.widget.DrawerLayout +import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.RecyclerView import com.google.android.material.datepicker.MaterialDatePicker import com.google.android.material.timepicker.MaterialTimePicker @@ -75,7 +76,6 @@ import com.kunzisoft.keepass.credentialprovider.TypeMode import com.kunzisoft.keepass.credentialprovider.magikeyboard.MagikeyboardService import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.buildPasskeyResponseAndSetResult import com.kunzisoft.keepass.database.ContextualDatabase -import com.kunzisoft.keepass.database.MainCredential import com.kunzisoft.keepass.database.element.DateInstant import com.kunzisoft.keepass.database.element.Entry import com.kunzisoft.keepass.database.element.Group @@ -121,6 +121,8 @@ import com.kunzisoft.keepass.view.toastError import com.kunzisoft.keepass.view.updateLockPaddingStart import com.kunzisoft.keepass.viewmodels.GroupEditViewModel import com.kunzisoft.keepass.viewmodels.GroupViewModel +import com.kunzisoft.keepass.viewmodels.MainCredentialViewModel +import kotlinx.coroutines.launch import org.joda.time.LocalDateTime import java.util.EnumSet @@ -130,8 +132,7 @@ class GroupActivity : DatabaseLockActivity(), GroupFragment.NodesActionMenuListener, GroupFragment.OnScrollListener, GroupFragment.GroupRefreshedListener, - SortDialogFragment.SortSelectionListener, - MainCredentialDialogFragment.AskMainCredentialDialogListener { + SortDialogFragment.SortSelectionListener { // Views private var header: ViewGroup? = null @@ -157,6 +158,8 @@ class GroupActivity : DatabaseLockActivity(), private val mGroupViewModel: GroupViewModel by viewModels() private val mGroupEditViewModel: GroupEditViewModel by viewModels() + private val mMainCredentialViewModel: MainCredentialViewModel by viewModels() + private val mGroupActivityEducation = GroupActivityEducation(this) private var mBreadcrumbAdapter: BreadcrumbAdapter? = null @@ -548,6 +551,21 @@ class GroupActivity : DatabaseLockActivity(), } } } + + lifecycleScope.launch { + // Initialize the parameters + mMainCredentialViewModel.uiState.collect { uiState -> + when (uiState) { + is MainCredentialViewModel.UIState.Loading -> {} + is MainCredentialViewModel.UIState.OnMainCredentialValidated -> { + mergeDatabaseFrom(uiState.databaseUri, uiState.mainCredential) + } + is MainCredentialViewModel.UIState.OnMainCredentialCanceled -> { + // Noting here + } + } + } + } } override fun viewToInvalidateTimeout(): View? { @@ -1133,20 +1151,6 @@ class GroupActivity : DatabaseLockActivity(), return true } - override fun onAskMainCredentialDialogPositiveClick( - databaseUri: Uri?, - mainCredential: MainCredential - ) { - databaseUri?.let { - mergeDatabaseFrom(it, mainCredential) - } - } - - override fun onAskMainCredentialDialogNegativeClick( - databaseUri: Uri?, - mainCredential: MainCredential - ) { } - override fun onResume() { super.onResume() diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/MainCredentialDialogFragment.kt b/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/MainCredentialDialogFragment.kt index e00d39563..ffbca28a1 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/MainCredentialDialogFragment.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/MainCredentialDialogFragment.kt @@ -20,45 +20,26 @@ package com.kunzisoft.keepass.activities.dialogs import android.app.Dialog -import android.content.Context import android.net.Uri import android.os.Bundle import android.widget.TextView import androidx.appcompat.app.AlertDialog +import androidx.fragment.app.activityViewModels import com.kunzisoft.keepass.R import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper import com.kunzisoft.keepass.database.MainCredential import com.kunzisoft.keepass.utils.UriUtil.getDocumentFile import com.kunzisoft.keepass.utils.getParcelableCompat import com.kunzisoft.keepass.view.MainCredentialView +import com.kunzisoft.keepass.viewmodels.MainCredentialViewModel class MainCredentialDialogFragment : DatabaseDialogFragment() { private var mainCredentialView: MainCredentialView? = null - private var mListener: AskMainCredentialDialogListener? = null - private var mExternalFileHelper: ExternalFileHelper? = null - interface AskMainCredentialDialogListener { - fun onAskMainCredentialDialogPositiveClick(databaseUri: Uri?, mainCredential: MainCredential) - fun onAskMainCredentialDialogNegativeClick(databaseUri: Uri?, mainCredential: MainCredential) - } - - override fun onAttach(activity: Context) { - super.onAttach(activity) - try { - mListener = activity as AskMainCredentialDialogListener - } catch (e: ClassCastException) { - throw ClassCastException(activity.toString() - + " must implement " + AskMainCredentialDialogListener::class.java.name) - } - } - - override fun onDetach() { - mListener = null - super.onDetach() - } + private val mMainCredentialViewModel: MainCredentialViewModel by activityViewModels() override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { activity?.let { activity -> @@ -76,22 +57,21 @@ class MainCredentialDialogFragment : DatabaseDialogFragment() { databaseUri?.let { root.findViewById(R.id.title_database)?.text = it.getDocumentFile(requireContext())?.name - } - builder.setView(root) + + builder.setView(root) // Add action buttons .setPositiveButton(android.R.string.ok) { _, _ -> - mListener?.onAskMainCredentialDialogPositiveClick( - databaseUri, - retrieveMainCredential() + mMainCredentialViewModel.validateMainCredential( + databaseUri = databaseUri, + mainCredential = retrieveMainCredential() ) } .setNegativeButton(android.R.string.cancel) { _, _ -> - mListener?.onAskMainCredentialDialogNegativeClick( - databaseUri, - retrieveMainCredential() + mMainCredentialViewModel.cancelMainCredential( + databaseUri = databaseUri ) } - + } mExternalFileHelper = ExternalFileHelper(this) mExternalFileHelper?.buildOpenDocument { uri -> diff --git a/app/src/main/java/com/kunzisoft/keepass/credentialprovider/UserVerification.kt b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/UserVerification.kt new file mode 100644 index 000000000..1461feef6 --- /dev/null +++ b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/UserVerification.kt @@ -0,0 +1,142 @@ +package com.kunzisoft.keepass.credentialprovider + +import android.content.Context +import android.content.Intent +import android.util.Log +import androidx.biometric.BiometricManager +import androidx.biometric.BiometricManager.Authenticators.BIOMETRIC_WEAK +import androidx.biometric.BiometricManager.Authenticators.DEVICE_CREDENTIAL +import androidx.biometric.BiometricManager.BIOMETRIC_SUCCESS +import androidx.biometric.BiometricPrompt +import androidx.core.content.ContextCompat +import androidx.fragment.app.FragmentActivity +import com.kunzisoft.keepass.R +import com.kunzisoft.keepass.activities.dialogs.MainCredentialDialogFragment +import com.kunzisoft.keepass.credentialprovider.passkey.data.UserVerificationRequirement +import com.kunzisoft.keepass.database.ContextualDatabase +import com.kunzisoft.keepass.utils.getEnumExtra +import com.kunzisoft.keepass.utils.putEnumExtra +import com.kunzisoft.keepass.view.toastError + +class UserVerification { + + companion object { + + private const val EXTRA_USER_VERIFICATION = "com.kunzisoft.keepass.extra.userVerification" + private const val EXTRA_USER_VERIFIED_WITH_AUTH = "com.kunzisoft.keepass.extra.userVerifiedWithAuth" + + /** + * Allowed authenticators for the User Verification + */ + const val ALLOWED_AUTHENTICATORS = BIOMETRIC_WEAK or DEVICE_CREDENTIAL + + /** + * Check if the device supports the biometric prompt for User Verification + */ + fun Context.isAuthenticatorsAllowed(): Boolean { + return BiometricManager.from(this) + .canAuthenticate(ALLOWED_AUTHENTICATORS) == BIOMETRIC_SUCCESS + } + + /** + * Add the User Verification to the intent + */ + fun Intent.addUserVerification( + userVerification: UserVerificationRequirement, + userVerifiedWithAuth: Boolean + ) { + putEnumExtra(EXTRA_USER_VERIFICATION, userVerification) + putExtra(EXTRA_USER_VERIFIED_WITH_AUTH, userVerifiedWithAuth) + } + + /** + * Define if the User is verified with authentification from the intent + */ + fun Intent.getUserVerifiedWithAuth(): Boolean { + 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) + } + + /** + * Get the User Verification from the intent + */ + fun Intent.getUserVerificationCondition(): Boolean { + return (getEnumExtra(EXTRA_USER_VERIFICATION) + ?: UserVerificationRequirement.PREFERRED) == UserVerificationRequirement.REQUIRED + } + + /** + * Ask the user for verification + * Ask for the biometric if defined on the device + * Ask for the database credential otherwise + */ + fun FragmentActivity.askUserVerification( + database: ContextualDatabase, + onVerificationSucceeded: () -> Unit, + onVerificationFailed: () -> Unit + ) { + if (this.intent.getUserVerificationCondition()) { + if (isAuthenticatorsAllowed()) { + 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")) + } + } + onVerificationFailed() + } + override fun onAuthenticationSucceeded( + result: BiometricPrompt.AuthenticationResult + ) { + super.onAuthenticationSucceeded(result) + onVerificationSucceeded() + } + override fun onAuthenticationFailed() { + super.onAuthenticationFailed() + toastError(SecurityException(getString(R.string.device_unlock_not_recognized))) + onVerificationFailed() + } + }).authenticate( + BiometricPrompt.PromptInfo.Builder() + .setTitle(getString(R.string.user_verification_required)) + .setAllowedAuthenticators(ALLOWED_AUTHENTICATORS) + .setConfirmationRequired(false) + .build() + ) + } else { + MainCredentialDialogFragment.getInstance(database.fileUri) + .show( + supportFragmentManager, + MainCredentialDialogFragment.TAG_ASK_MAIN_CREDENTIAL + ) + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/kunzisoft/keepass/credentialprovider/activity/PasskeyLauncherActivity.kt b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/activity/PasskeyLauncherActivity.kt index c2881d2f8..e539f5a51 100644 --- a/app/src/main/java/com/kunzisoft/keepass/credentialprovider/activity/PasskeyLauncherActivity.kt +++ b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/activity/PasskeyLauncherActivity.kt @@ -31,8 +31,6 @@ import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.viewModels import androidx.annotation.RequiresApi import androidx.appcompat.app.AlertDialog -import androidx.biometric.BiometricPrompt -import androidx.core.content.ContextCompat import androidx.lifecycle.lifecycleScope import com.kunzisoft.keepass.R import com.kunzisoft.keepass.activities.FileDatabaseSelectActivity @@ -45,16 +43,13 @@ import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.addTypeMode import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.setActivityResult import com.kunzisoft.keepass.credentialprovider.SpecialMode import com.kunzisoft.keepass.credentialprovider.TypeMode +import com.kunzisoft.keepass.credentialprovider.UserVerification.Companion.addUserVerification +import com.kunzisoft.keepass.credentialprovider.UserVerification.Companion.askUserVerification +import com.kunzisoft.keepass.credentialprovider.UserVerification.Companion.getUserVerifiedWithAuth 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.addAppOrigin import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.addAuthCode -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.removeUserVerification import com.kunzisoft.keepass.credentialprovider.viewmodel.CredentialLauncherViewModel import com.kunzisoft.keepass.credentialprovider.viewmodel.PasskeyLauncherViewModel import com.kunzisoft.keepass.database.ContextualDatabase @@ -63,8 +58,8 @@ import com.kunzisoft.keepass.model.SearchInfo import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_ENTRY_TASK import com.kunzisoft.keepass.tasks.ActionRunnable import com.kunzisoft.keepass.utils.AppUtil.randomRequestCode -import com.kunzisoft.keepass.utils.LOCK_ACTION import com.kunzisoft.keepass.view.toastError +import com.kunzisoft.keepass.viewmodels.MainCredentialViewModel import kotlinx.coroutines.launch import java.util.UUID @@ -72,6 +67,7 @@ import java.util.UUID class PasskeyLauncherActivity : DatabaseLockActivity() { private val passkeyLauncherViewModel: PasskeyLauncherViewModel by viewModels() + private val mainCredentialViewModel: MainCredentialViewModel by viewModels() private var mPasskeySelectionActivityResultLauncher: ActivityResultLauncher? = this.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { @@ -92,60 +88,7 @@ class PasskeyLauncherActivity : DatabaseLockActivity() { } override fun onCreate(savedInstanceState: Bundle?) { - // To manage https://github.com/Kunzisoft/KeePassDX/issues/2283 - val userVerificationCondition = intent.getUserVerificationCondition() - if (userVerificationCondition) { - if (isAuthenticatorsAllowed().not()) { - intent.removeUserVerification() - sendBroadcast(Intent(LOCK_ACTION)) - } - } - // super.onCreate must be after UserVerification to allow database lock super.onCreate(savedInstanceState) - // Biometric must be after super.onCreate - if (userVerificationCondition) { - if (isAuthenticatorsAllowed()) { - 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(TAG, "$errString") - } - else -> { - toastError(SecurityException("Authentication error: $errString")) - } - } - passkeyLauncherViewModel.cancelResult() - } - override fun onAuthenticationSucceeded( - result: BiometricPrompt.AuthenticationResult - ) { - super.onAuthenticationSucceeded(result) - passkeyLauncherViewModel.launchAction(userVerified = true, intent, mSpecialMode) - } - override fun onAuthenticationFailed() { - super.onAuthenticationFailed() - toastError(SecurityException(getString(R.string.device_unlock_not_recognized))) - passkeyLauncherViewModel.cancelResult() - } - }).authenticate( - BiometricPrompt.PromptInfo.Builder() - .setTitle(getString(R.string.user_verification_required)) - .setAllowedAuthenticators(ALLOWED_AUTHENTICATORS) - .setConfirmationRequired(false) - .build() - ) - } - } lifecycleScope.launch { // Initialize the parameters @@ -225,10 +168,38 @@ class PasskeyLauncherActivity : DatabaseLockActivity() { } } } + lifecycleScope.launch { + mainCredentialViewModel.uiState.collect { uiState -> + when (uiState) { + is MainCredentialViewModel.UIState.Loading -> {} + is MainCredentialViewModel.UIState.OnMainCredentialValidated -> { + // TODO Pass through UserVerification View Model + passkeyLauncherViewModel.launchAction(userVerified = true, intent, mSpecialMode) + } + is MainCredentialViewModel.UIState.OnMainCredentialCanceled -> { + passkeyLauncherViewModel.cancelResult() + } + } + } + } } override fun onUnknownDatabaseRetrieved(database: ContextualDatabase?) { super.onUnknownDatabaseRetrieved(database) + + // To manage https://github.com/Kunzisoft/KeePassDX/issues/2283 + database?.let { + askUserVerification( + database = it, + onVerificationSucceeded = { + passkeyLauncherViewModel.launchAction(userVerified = true, intent, mSpecialMode) + }, + onVerificationFailed = { + passkeyLauncherViewModel.cancelResult() + } + ) + } + passkeyLauncherViewModel.launchActionIfNeeded(intent, mSpecialMode, database) } diff --git a/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/PasskeyProviderService.kt b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/PasskeyProviderService.kt index d1e7f5871..5346fd013 100644 --- a/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/PasskeyProviderService.kt +++ b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/PasskeyProviderService.kt @@ -50,7 +50,6 @@ import com.kunzisoft.keepass.credentialprovider.activity.PasskeyLauncherActivity import com.kunzisoft.keepass.credentialprovider.passkey.data.PublicKeyCredentialCreationOptions import com.kunzisoft.keepass.credentialprovider.passkey.data.PublicKeyCredentialRequestOptions import com.kunzisoft.keepass.credentialprovider.passkey.data.UserVerificationRequirement -import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.isAuthenticatorsAllowed import com.kunzisoft.keepass.database.ContextualDatabase import com.kunzisoft.keepass.database.DatabaseTaskProvider import com.kunzisoft.keepass.database.exception.RegisterInReadOnlyDatabaseException @@ -67,7 +66,6 @@ class PasskeyProviderService : CredentialProviderService() { private var mDatabaseTaskProvider: DatabaseTaskProvider? = null private var mDatabase: ContextualDatabase? = null private lateinit var defaultIcon: Icon - private lateinit var relaunchIcon: Icon private var isAutoSelectAllowed: Boolean = false override fun onCreate() { @@ -86,11 +84,6 @@ class PasskeyProviderService : CredentialProviderService() { setTintBlendMode(BlendMode.DST) } - relaunchIcon = Icon.createWithResource( - this@PasskeyProviderService, - R.drawable.ic_clock_loader_red_24dp - ) - isAutoSelectAllowed = isPasskeyAutoSelectEnable(this) } @@ -157,7 +150,8 @@ class PasskeyProviderService : CredentialProviderService() { val credentialIdList = publicKeyCredentialRequestOptions.allowCredentials .map { b64Encode(it.id) } val searchInfo = buildPasskeySearchInfo(relyingPartyId, credentialIdList) - val userVerification = publicKeyCredentialRequestOptions.userVerification + // TODO remove + val userVerification = UserVerificationRequirement.REQUIRED//publicKeyCredentialRequestOptions.userVerification Log.d(TAG, "Build passkey search for UV $userVerification, " + "RP $relyingPartyId and Credential IDs $credentialIdList") SearchHelper.checkAutoSearchInfo( @@ -165,84 +159,70 @@ class PasskeyProviderService : CredentialProviderService() { database = mDatabase, searchInfo = searchInfo, onItemsFound = { database, items -> - manageUserVerification( - passkeyEntries = passkeyEntries, - searchInfo = searchInfo, - option = option, - userVerification = userVerification - ) { - Log.d(TAG, "Add pending intent for passkey selection with found items") - for (passkeyEntry in items) { - PasskeyLauncherActivity.getPendingIntent( - context = applicationContext, - specialMode = SpecialMode.SELECTION, - nodeId = passkeyEntry.id, - appOrigin = passkeyEntry.appOrigin, - userVerification = userVerification, - userVerifiedWithAuth = false - )?.let { usagePendingIntent -> - val passkey = passkeyEntry.passkey - passkeyEntries.add( - PublicKeyCredentialEntry( - context = applicationContext, - username = passkey?.username ?: "Unknown", - icon = passkeyEntry.buildIcon( - this@PasskeyProviderService, - database - )?.apply { - setTintBlendMode(BlendMode.DST) - } ?: defaultIcon, - pendingIntent = usagePendingIntent, - beginGetPublicKeyCredentialOption = option, - displayName = passkeyEntry.getVisualTitle(), - isAutoSelectAllowed = isAutoSelectAllowed - ) + Log.d(TAG, "Add pending intent for passkey selection with found items") + for (passkeyEntry in items) { + PasskeyLauncherActivity.getPendingIntent( + context = applicationContext, + specialMode = SpecialMode.SELECTION, + nodeId = passkeyEntry.id, + appOrigin = passkeyEntry.appOrigin, + userVerification = userVerification, + userVerifiedWithAuth = false + )?.let { usagePendingIntent -> + val passkey = passkeyEntry.passkey + passkeyEntries.add( + PublicKeyCredentialEntry( + context = applicationContext, + username = passkey?.username ?: "Unknown", + icon = passkeyEntry.buildIcon( + this@PasskeyProviderService, + database + )?.apply { + setTintBlendMode(BlendMode.DST) + } ?: defaultIcon, + pendingIntent = usagePendingIntent, + beginGetPublicKeyCredentialOption = option, + displayName = passkeyEntry.getVisualTitle(), + isAutoSelectAllowed = isAutoSelectAllowed ) - } + ) } } callback(passkeyEntries) }, onItemNotFound = { _ -> - manageUserVerification( - passkeyEntries = passkeyEntries, - searchInfo = searchInfo, - option = option, - userVerification = userVerification, - ) { - Log.w(TAG, "No passkey found in the database with this relying party : $relyingPartyId") - if (credentialIdList.isEmpty()) { - Log.d(TAG, "Add pending intent for passkey selection in opened database") - PasskeyLauncherActivity.getPendingIntent( - context = applicationContext, - specialMode = SpecialMode.SELECTION, - searchInfo = searchInfo, - userVerification = userVerification, - userVerifiedWithAuth = false - )?.let { pendingIntent -> - passkeyEntries.add( - PublicKeyCredentialEntry( - context = applicationContext, - username = getString(R.string.passkey_database_username), - displayName = getString(R.string.passkey_selection_description), - icon = defaultIcon, - pendingIntent = pendingIntent, - beginGetPublicKeyCredentialOption = option, - lastUsedTime = Instant.now(), - isAutoSelectAllowed = isAutoSelectAllowed - ) - ) - } - callback(passkeyEntries) - } else { - throw IOException( - getString( - R.string.error_passkey_credential_id, - relyingPartyId, - credentialIdList + Log.w(TAG, "No passkey found in the database with this relying party : $relyingPartyId") + if (credentialIdList.isEmpty()) { + Log.d(TAG, "Add pending intent for passkey selection in opened database") + PasskeyLauncherActivity.getPendingIntent( + context = applicationContext, + specialMode = SpecialMode.SELECTION, + searchInfo = searchInfo, + userVerification = userVerification, + userVerifiedWithAuth = false + )?.let { pendingIntent -> + passkeyEntries.add( + PublicKeyCredentialEntry( + context = applicationContext, + username = getString(R.string.passkey_database_username), + displayName = getString(R.string.passkey_selection_description), + icon = defaultIcon, + pendingIntent = pendingIntent, + beginGetPublicKeyCredentialOption = option, + lastUsedTime = Instant.now(), + isAutoSelectAllowed = isAutoSelectAllowed ) ) } + callback(passkeyEntries) + } else { + throw IOException( + getString( + R.string.error_passkey_credential_id, + relyingPartyId, + credentialIdList + ) + ) } }, onDatabaseClosed = { @@ -272,42 +252,6 @@ class PasskeyProviderService : CredentialProviderService() { ) } - /** - * To easily manage user verification condition - */ - private fun manageUserVerification( - passkeyEntries: MutableList, - searchInfo: SearchInfo, - option: BeginGetPublicKeyCredentialOption, - userVerification: UserVerificationRequirement, - standardAction: () -> Unit - ) { - if (userVerification == UserVerificationRequirement.REQUIRED && isAuthenticatorsAllowed().not()) { - PasskeyLauncherActivity.getPendingIntent( - context = applicationContext, - specialMode = SpecialMode.SELECTION, - searchInfo = searchInfo, - userVerification = userVerification, - userVerifiedWithAuth = true - )?.let { pendingIntent -> - passkeyEntries.add( - PublicKeyCredentialEntry( - context = applicationContext, - username = getString(R.string.passkey_database_username), - displayName = getString(R.string.passkey_relaunch_database_description), - icon = relaunchIcon, - pendingIntent = pendingIntent, - beginGetPublicKeyCredentialOption = option, - lastUsedTime = Instant.now(), - isAutoSelectAllowed = isAutoSelectAllowed - ) - ) - } - } else { - standardAction() - } - } - override fun onBeginCreateCredentialRequest( request: BeginCreateCredentialRequest, cancellationSignal: CancellationSignal, diff --git a/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/util/PasskeyHelper.kt b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/util/PasskeyHelper.kt index 0f8ed2d3d..83adbe3da 100644 --- a/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/util/PasskeyHelper.kt +++ b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/util/PasskeyHelper.kt @@ -30,10 +30,6 @@ import android.security.keystore.KeyProperties import android.util.Log import android.widget.Toast import androidx.annotation.RequiresApi -import androidx.biometric.BiometricManager -import androidx.biometric.BiometricManager.Authenticators.BIOMETRIC_WEAK -import androidx.biometric.BiometricManager.Authenticators.DEVICE_CREDENTIAL -import androidx.biometric.BiometricManager.BIOMETRIC_SUCCESS import androidx.credentials.CreatePublicKeyCredentialRequest import androidx.credentials.CreatePublicKeyCredentialResponse import androidx.credentials.GetPublicKeyCredentialOption @@ -60,7 +56,6 @@ import com.kunzisoft.keepass.credentialprovider.passkey.data.PublicKeyCredential import com.kunzisoft.keepass.credentialprovider.passkey.data.PublicKeyCredentialCreationParameters import com.kunzisoft.keepass.credentialprovider.passkey.data.PublicKeyCredentialRequestOptions 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.model.AndroidOrigin import com.kunzisoft.keepass.model.AppOrigin @@ -68,9 +63,7 @@ import com.kunzisoft.keepass.model.EntryInfo import com.kunzisoft.keepass.model.Passkey import com.kunzisoft.keepass.utils.AppUtil import com.kunzisoft.keepass.utils.StringUtil.toHexString -import com.kunzisoft.keepass.utils.getEnumExtra import com.kunzisoft.keepass.utils.getParcelableExtraCompat -import com.kunzisoft.keepass.utils.putEnumExtra import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import java.io.IOException @@ -98,8 +91,6 @@ object PasskeyHelper { 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_AUTHENTICATION_CODE = "com.kunzisoft.keepass.extra.authenticationCode" - 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 = "_" @@ -116,60 +107,6 @@ object PasskeyHelper { private val internalSecureRandom: SecureRandom = SecureRandom() - /** - * Add the User Verification to the intent - */ - fun Intent.addUserVerification( - userVerification: UserVerificationRequirement, - userVerifiedWithAuth: Boolean - ) { - putEnumExtra(EXTRA_USER_VERIFICATION, userVerification) - putExtra(EXTRA_USER_VERIFIED_WITH_AUTH, userVerifiedWithAuth) - } - - /** - * Get the User Verification from the intent - */ - fun Intent.getUserVerificationCondition(): Boolean { - return (getEnumExtra(EXTRA_USER_VERIFICATION) - ?: UserVerificationRequirement.PREFERRED) == UserVerificationRequirement.REQUIRED - } - - /** - * Define if the User is verified with authentification from the intent - */ - fun Intent.getUserVerifiedWithAuth(): Boolean { - 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 - */ - const val ALLOWED_AUTHENTICATORS = BIOMETRIC_WEAK or DEVICE_CREDENTIAL - - /** - * Check if the device supports the biometric prompt for User Verification - */ - fun Context.isAuthenticatorsAllowed(): Boolean { - return BiometricManager.from(this) - .canAuthenticate(ALLOWED_AUTHENTICATORS) == BIOMETRIC_SUCCESS - } - /** * Add an authentication code generated by an entry to the intent */ diff --git a/app/src/main/java/com/kunzisoft/keepass/credentialprovider/viewmodel/PasskeyLauncherViewModel.kt b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/viewmodel/PasskeyLauncherViewModel.kt index e3ec69f0e..86196970d 100644 --- a/app/src/main/java/com/kunzisoft/keepass/credentialprovider/viewmodel/PasskeyLauncherViewModel.kt +++ b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/viewmodel/PasskeyLauncherViewModel.kt @@ -18,13 +18,13 @@ 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.UserVerification.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 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.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.removeAppOrigin import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.removePasskey diff --git a/app/src/main/java/com/kunzisoft/keepass/viewmodels/MainCredentialViewModel.kt b/app/src/main/java/com/kunzisoft/keepass/viewmodels/MainCredentialViewModel.kt new file mode 100644 index 000000000..5f8824f84 --- /dev/null +++ b/app/src/main/java/com/kunzisoft/keepass/viewmodels/MainCredentialViewModel.kt @@ -0,0 +1,43 @@ +package com.kunzisoft.keepass.viewmodels + +import android.net.Uri +import androidx.lifecycle.ViewModel +import com.kunzisoft.keepass.database.MainCredential +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow + +/** + * ViewModel for the Main Credential Dialog + * Easily retrieves main credential from the database identified by its URI + */ +class MainCredentialViewModel: ViewModel() { + + private val mUiState = MutableStateFlow(UIState.Loading) + val uiState: StateFlow = mUiState + + fun validateMainCredential( + databaseUri: Uri, + mainCredential: MainCredential + ) { + mUiState.value = UIState.OnMainCredentialValidated(databaseUri, mainCredential) + } + + fun cancelMainCredential( + databaseUri: Uri + ) { + mUiState.value = UIState.OnMainCredentialCanceled(databaseUri, MainCredential()) + } + + sealed class UIState { + object Loading: UIState() + data class OnMainCredentialValidated( + val databaseUri: Uri, + val mainCredential: MainCredential + ): UIState() + data class OnMainCredentialCanceled( + val databaseUri: Uri, + val mainCredential: MainCredential + ): UIState() + } + +} \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_clock_loader_red_24dp.xml b/app/src/main/res/drawable/ic_clock_loader_red_24dp.xml deleted file mode 100644 index feba31601..000000000 --- a/app/src/main/res/drawable/ic_clock_loader_red_24dp.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index a2c91408a..0c1ca0e73 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -767,7 +767,6 @@ Select an existing passkey KeePassDX Database Select to unlock - Reauthenticate (No Device Auth) Passkey Username Passkey Private Key Passkey Credential Id