fix: Add MainCredentialViewModel

This commit is contained in:
J-Jamet
2025-11-27 16:19:36 +01:00
parent 5fd25c6150
commit d087fcc930
10 changed files with 308 additions and 297 deletions

View File

@@ -52,6 +52,7 @@ import androidx.core.view.WindowInsetsCompat
import androidx.core.view.WindowInsetsControllerCompat import androidx.core.view.WindowInsetsControllerCompat
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.drawerlayout.widget.DrawerLayout import androidx.drawerlayout.widget.DrawerLayout
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.datepicker.MaterialDatePicker import com.google.android.material.datepicker.MaterialDatePicker
import com.google.android.material.timepicker.MaterialTimePicker 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.magikeyboard.MagikeyboardService
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.buildPasskeyResponseAndSetResult import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.buildPasskeyResponseAndSetResult
import com.kunzisoft.keepass.database.ContextualDatabase 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.DateInstant
import com.kunzisoft.keepass.database.element.Entry import com.kunzisoft.keepass.database.element.Entry
import com.kunzisoft.keepass.database.element.Group 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.view.updateLockPaddingStart
import com.kunzisoft.keepass.viewmodels.GroupEditViewModel import com.kunzisoft.keepass.viewmodels.GroupEditViewModel
import com.kunzisoft.keepass.viewmodels.GroupViewModel import com.kunzisoft.keepass.viewmodels.GroupViewModel
import com.kunzisoft.keepass.viewmodels.MainCredentialViewModel
import kotlinx.coroutines.launch
import org.joda.time.LocalDateTime import org.joda.time.LocalDateTime
import java.util.EnumSet import java.util.EnumSet
@@ -130,8 +132,7 @@ class GroupActivity : DatabaseLockActivity(),
GroupFragment.NodesActionMenuListener, GroupFragment.NodesActionMenuListener,
GroupFragment.OnScrollListener, GroupFragment.OnScrollListener,
GroupFragment.GroupRefreshedListener, GroupFragment.GroupRefreshedListener,
SortDialogFragment.SortSelectionListener, SortDialogFragment.SortSelectionListener {
MainCredentialDialogFragment.AskMainCredentialDialogListener {
// Views // Views
private var header: ViewGroup? = null private var header: ViewGroup? = null
@@ -157,6 +158,8 @@ class GroupActivity : DatabaseLockActivity(),
private val mGroupViewModel: GroupViewModel by viewModels() private val mGroupViewModel: GroupViewModel by viewModels()
private val mGroupEditViewModel: GroupEditViewModel by viewModels() private val mGroupEditViewModel: GroupEditViewModel by viewModels()
private val mMainCredentialViewModel: MainCredentialViewModel by viewModels()
private val mGroupActivityEducation = GroupActivityEducation(this) private val mGroupActivityEducation = GroupActivityEducation(this)
private var mBreadcrumbAdapter: BreadcrumbAdapter? = null 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? { override fun viewToInvalidateTimeout(): View? {
@@ -1133,20 +1151,6 @@ class GroupActivity : DatabaseLockActivity(),
return true return true
} }
override fun onAskMainCredentialDialogPositiveClick(
databaseUri: Uri?,
mainCredential: MainCredential
) {
databaseUri?.let {
mergeDatabaseFrom(it, mainCredential)
}
}
override fun onAskMainCredentialDialogNegativeClick(
databaseUri: Uri?,
mainCredential: MainCredential
) { }
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()

View File

@@ -20,45 +20,26 @@
package com.kunzisoft.keepass.activities.dialogs package com.kunzisoft.keepass.activities.dialogs
import android.app.Dialog import android.app.Dialog
import android.content.Context
import android.net.Uri import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.widget.TextView import android.widget.TextView
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.activityViewModels
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
import com.kunzisoft.keepass.database.MainCredential import com.kunzisoft.keepass.database.MainCredential
import com.kunzisoft.keepass.utils.UriUtil.getDocumentFile import com.kunzisoft.keepass.utils.UriUtil.getDocumentFile
import com.kunzisoft.keepass.utils.getParcelableCompat import com.kunzisoft.keepass.utils.getParcelableCompat
import com.kunzisoft.keepass.view.MainCredentialView import com.kunzisoft.keepass.view.MainCredentialView
import com.kunzisoft.keepass.viewmodels.MainCredentialViewModel
class MainCredentialDialogFragment : DatabaseDialogFragment() { class MainCredentialDialogFragment : DatabaseDialogFragment() {
private var mainCredentialView: MainCredentialView? = null private var mainCredentialView: MainCredentialView? = null
private var mListener: AskMainCredentialDialogListener? = null
private var mExternalFileHelper: ExternalFileHelper? = null private var mExternalFileHelper: ExternalFileHelper? = null
interface AskMainCredentialDialogListener { private val mMainCredentialViewModel: MainCredentialViewModel by activityViewModels()
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()
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
activity?.let { activity -> activity?.let { activity ->
@@ -76,22 +57,21 @@ class MainCredentialDialogFragment : DatabaseDialogFragment() {
databaseUri?.let { databaseUri?.let {
root.findViewById<TextView>(R.id.title_database)?.text = root.findViewById<TextView>(R.id.title_database)?.text =
it.getDocumentFile(requireContext())?.name it.getDocumentFile(requireContext())?.name
}
builder.setView(root) builder.setView(root)
// Add action buttons // Add action buttons
.setPositiveButton(android.R.string.ok) { _, _ -> .setPositiveButton(android.R.string.ok) { _, _ ->
mListener?.onAskMainCredentialDialogPositiveClick( mMainCredentialViewModel.validateMainCredential(
databaseUri, databaseUri = databaseUri,
retrieveMainCredential() mainCredential = retrieveMainCredential()
) )
} }
.setNegativeButton(android.R.string.cancel) { _, _ -> .setNegativeButton(android.R.string.cancel) { _, _ ->
mListener?.onAskMainCredentialDialogNegativeClick( mMainCredentialViewModel.cancelMainCredential(
databaseUri, databaseUri = databaseUri
retrieveMainCredential()
) )
} }
}
mExternalFileHelper = ExternalFileHelper(this) mExternalFileHelper = ExternalFileHelper(this)
mExternalFileHelper?.buildOpenDocument { uri -> mExternalFileHelper?.buildOpenDocument { uri ->

View File

@@ -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<UserVerificationRequirement>(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
)
}
}
}
}
}

View File

@@ -31,8 +31,6 @@ import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.biometric.BiometricPrompt
import androidx.core.content.ContextCompat
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.FileDatabaseSelectActivity 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.EntrySelectionHelper.setActivityResult
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.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.AndroidPrivilegedApp
import com.kunzisoft.keepass.credentialprovider.passkey.data.UserVerificationRequirement 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.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.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.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
@@ -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.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_ENTRY_TASK
import com.kunzisoft.keepass.tasks.ActionRunnable import com.kunzisoft.keepass.tasks.ActionRunnable
import com.kunzisoft.keepass.utils.AppUtil.randomRequestCode import com.kunzisoft.keepass.utils.AppUtil.randomRequestCode
import com.kunzisoft.keepass.utils.LOCK_ACTION
import com.kunzisoft.keepass.view.toastError import com.kunzisoft.keepass.view.toastError
import com.kunzisoft.keepass.viewmodels.MainCredentialViewModel
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.util.UUID import java.util.UUID
@@ -72,6 +67,7 @@ import java.util.UUID
class PasskeyLauncherActivity : DatabaseLockActivity() { class PasskeyLauncherActivity : DatabaseLockActivity() {
private val passkeyLauncherViewModel: PasskeyLauncherViewModel by viewModels() private val passkeyLauncherViewModel: PasskeyLauncherViewModel by viewModels()
private val mainCredentialViewModel: MainCredentialViewModel by viewModels()
private var mPasskeySelectionActivityResultLauncher: ActivityResultLauncher<Intent>? = private var mPasskeySelectionActivityResultLauncher: ActivityResultLauncher<Intent>? =
this.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { this.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
@@ -92,60 +88,7 @@ class PasskeyLauncherActivity : DatabaseLockActivity() {
} }
override fun onCreate(savedInstanceState: Bundle?) { 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) 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 { lifecycleScope.launch {
// Initialize the parameters // 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?) { override fun onUnknownDatabaseRetrieved(database: ContextualDatabase?) {
super.onUnknownDatabaseRetrieved(database) 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) passkeyLauncherViewModel.launchActionIfNeeded(intent, mSpecialMode, database)
} }

View File

@@ -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.PublicKeyCredentialCreationOptions
import com.kunzisoft.keepass.credentialprovider.passkey.data.PublicKeyCredentialRequestOptions import com.kunzisoft.keepass.credentialprovider.passkey.data.PublicKeyCredentialRequestOptions
import com.kunzisoft.keepass.credentialprovider.passkey.data.UserVerificationRequirement 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.ContextualDatabase
import com.kunzisoft.keepass.database.DatabaseTaskProvider import com.kunzisoft.keepass.database.DatabaseTaskProvider
import com.kunzisoft.keepass.database.exception.RegisterInReadOnlyDatabaseException import com.kunzisoft.keepass.database.exception.RegisterInReadOnlyDatabaseException
@@ -67,7 +66,6 @@ class PasskeyProviderService : CredentialProviderService() {
private var mDatabaseTaskProvider: DatabaseTaskProvider? = null private var mDatabaseTaskProvider: DatabaseTaskProvider? = null
private var mDatabase: ContextualDatabase? = null private var mDatabase: ContextualDatabase? = null
private lateinit var defaultIcon: Icon private lateinit var defaultIcon: Icon
private lateinit var relaunchIcon: Icon
private var isAutoSelectAllowed: Boolean = false private var isAutoSelectAllowed: Boolean = false
override fun onCreate() { override fun onCreate() {
@@ -86,11 +84,6 @@ class PasskeyProviderService : CredentialProviderService() {
setTintBlendMode(BlendMode.DST) setTintBlendMode(BlendMode.DST)
} }
relaunchIcon = Icon.createWithResource(
this@PasskeyProviderService,
R.drawable.ic_clock_loader_red_24dp
)
isAutoSelectAllowed = isPasskeyAutoSelectEnable(this) isAutoSelectAllowed = isPasskeyAutoSelectEnable(this)
} }
@@ -157,7 +150,8 @@ 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 userVerification = publicKeyCredentialRequestOptions.userVerification // TODO remove
val userVerification = UserVerificationRequirement.REQUIRED//publicKeyCredentialRequestOptions.userVerification
Log.d(TAG, "Build passkey search for UV $userVerification, " + 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(
@@ -165,84 +159,70 @@ class PasskeyProviderService : CredentialProviderService() {
database = mDatabase, database = mDatabase,
searchInfo = searchInfo, searchInfo = searchInfo,
onItemsFound = { database, items -> onItemsFound = { database, items ->
manageUserVerification( Log.d(TAG, "Add pending intent for passkey selection with found items")
passkeyEntries = passkeyEntries, for (passkeyEntry in items) {
searchInfo = searchInfo, PasskeyLauncherActivity.getPendingIntent(
option = option, context = applicationContext,
userVerification = userVerification specialMode = SpecialMode.SELECTION,
) { nodeId = passkeyEntry.id,
Log.d(TAG, "Add pending intent for passkey selection with found items") appOrigin = passkeyEntry.appOrigin,
for (passkeyEntry in items) { userVerification = userVerification,
PasskeyLauncherActivity.getPendingIntent( userVerifiedWithAuth = false
context = applicationContext, )?.let { usagePendingIntent ->
specialMode = SpecialMode.SELECTION, val passkey = passkeyEntry.passkey
nodeId = passkeyEntry.id, passkeyEntries.add(
appOrigin = passkeyEntry.appOrigin, PublicKeyCredentialEntry(
userVerification = userVerification, context = applicationContext,
userVerifiedWithAuth = false username = passkey?.username ?: "Unknown",
)?.let { usagePendingIntent -> icon = passkeyEntry.buildIcon(
val passkey = passkeyEntry.passkey this@PasskeyProviderService,
passkeyEntries.add( database
PublicKeyCredentialEntry( )?.apply {
context = applicationContext, setTintBlendMode(BlendMode.DST)
username = passkey?.username ?: "Unknown", } ?: defaultIcon,
icon = passkeyEntry.buildIcon( pendingIntent = usagePendingIntent,
this@PasskeyProviderService, beginGetPublicKeyCredentialOption = option,
database displayName = passkeyEntry.getVisualTitle(),
)?.apply { isAutoSelectAllowed = isAutoSelectAllowed
setTintBlendMode(BlendMode.DST)
} ?: defaultIcon,
pendingIntent = usagePendingIntent,
beginGetPublicKeyCredentialOption = option,
displayName = passkeyEntry.getVisualTitle(),
isAutoSelectAllowed = isAutoSelectAllowed
)
) )
} )
} }
} }
callback(passkeyEntries) callback(passkeyEntries)
}, },
onItemNotFound = { _ -> onItemNotFound = { _ ->
manageUserVerification( Log.w(TAG, "No passkey found in the database with this relying party : $relyingPartyId")
passkeyEntries = passkeyEntries, if (credentialIdList.isEmpty()) {
searchInfo = searchInfo, Log.d(TAG, "Add pending intent for passkey selection in opened database")
option = option, PasskeyLauncherActivity.getPendingIntent(
userVerification = userVerification, context = applicationContext,
) { specialMode = SpecialMode.SELECTION,
Log.w(TAG, "No passkey found in the database with this relying party : $relyingPartyId") searchInfo = searchInfo,
if (credentialIdList.isEmpty()) { userVerification = userVerification,
Log.d(TAG, "Add pending intent for passkey selection in opened database") userVerifiedWithAuth = false
PasskeyLauncherActivity.getPendingIntent( )?.let { pendingIntent ->
context = applicationContext, passkeyEntries.add(
specialMode = SpecialMode.SELECTION, PublicKeyCredentialEntry(
searchInfo = searchInfo, context = applicationContext,
userVerification = userVerification, username = getString(R.string.passkey_database_username),
userVerifiedWithAuth = false displayName = getString(R.string.passkey_selection_description),
)?.let { pendingIntent -> icon = defaultIcon,
passkeyEntries.add( pendingIntent = pendingIntent,
PublicKeyCredentialEntry( beginGetPublicKeyCredentialOption = option,
context = applicationContext, lastUsedTime = Instant.now(),
username = getString(R.string.passkey_database_username), isAutoSelectAllowed = isAutoSelectAllowed
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
) )
) )
} }
callback(passkeyEntries)
} else {
throw IOException(
getString(
R.string.error_passkey_credential_id,
relyingPartyId,
credentialIdList
)
)
} }
}, },
onDatabaseClosed = { onDatabaseClosed = {
@@ -272,42 +252,6 @@ class PasskeyProviderService : CredentialProviderService() {
) )
} }
/**
* To easily manage user verification condition
*/
private fun manageUserVerification(
passkeyEntries: MutableList<CredentialEntry>,
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( override fun onBeginCreateCredentialRequest(
request: BeginCreateCredentialRequest, request: BeginCreateCredentialRequest,
cancellationSignal: CancellationSignal, cancellationSignal: CancellationSignal,

View File

@@ -30,10 +30,6 @@ import android.security.keystore.KeyProperties
import android.util.Log import android.util.Log
import android.widget.Toast import android.widget.Toast
import androidx.annotation.RequiresApi 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.CreatePublicKeyCredentialRequest
import androidx.credentials.CreatePublicKeyCredentialResponse import androidx.credentials.CreatePublicKeyCredentialResponse
import androidx.credentials.GetPublicKeyCredentialOption 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.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
@@ -68,9 +63,7 @@ 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
@@ -98,8 +91,6 @@ 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_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 = "_"
@@ -116,60 +107,6 @@ object PasskeyHelper {
private val internalSecureRandom: SecureRandom = SecureRandom() 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<UserVerificationRequirement>(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 * Add an authentication code generated by an entry to the intent
*/ */

View File

@@ -18,13 +18,13 @@ import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.retrieveNod
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.retrieveSearchInfo import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.retrieveSearchInfo
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.UserVerification.Companion.getUserVerificationCondition
import com.kunzisoft.keepass.credentialprovider.passkey.data.AndroidPrivilegedApp import com.kunzisoft.keepass.credentialprovider.passkey.data.AndroidPrivilegedApp
import com.kunzisoft.keepass.credentialprovider.passkey.data.PublicKeyCredentialCreationParameters import com.kunzisoft.keepass.credentialprovider.passkey.data.PublicKeyCredentialCreationParameters
import com.kunzisoft.keepass.credentialprovider.passkey.data.PublicKeyCredentialUsageParameters 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.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.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

View File

@@ -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>(UIState.Loading)
val uiState: StateFlow<UIState> = 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()
}
}

View File

@@ -1,9 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:height="24dp"
android:width="24dp"
android:viewportHeight="960"
android:viewportWidth="960">
<path
android:fillColor="#E50808"
android:pathData="M480,880Q397,880 324,848.5Q251,817 197,763Q143,709 111.5,636Q80,563 80,480Q80,397 111.5,324Q143,251 197,197Q251,143 324,111.5Q397,80 480,80Q563,80 636,111.5Q709,143 763,197Q817,251 848.5,324Q880,397 880,480Q880,563 848.5,636Q817,709 763,763Q709,817 636,848.5Q563,880 480,880ZM253,707L480,480L480,160Q346,160 253,253Q160,346 160,480Q160,544 184,603Q208,662 253,707Z"/>
</vector>

View File

@@ -767,7 +767,6 @@
<string name="passkey_selection_description">Select an existing passkey</string> <string name="passkey_selection_description">Select an existing passkey</string>
<string name="passkey_database_username">KeePassDX Database</string> <string name="passkey_database_username">KeePassDX Database</string>
<string name="passkey_locked_database_description">Select to unlock</string> <string name="passkey_locked_database_description">Select to unlock</string>
<string name="passkey_relaunch_database_description">Reauthenticate (No Device Auth)</string>
<string name="passkey_username">Passkey Username</string> <string name="passkey_username">Passkey Username</string>
<string name="passkey_private_key">Passkey Private Key</string> <string name="passkey_private_key">Passkey Private Key</string>
<string name="passkey_credential_id">Passkey Credential Id</string> <string name="passkey_credential_id">Passkey Credential Id</string>