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.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()

View File

@@ -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<TextView>(R.id.title_database)?.text =
it.getDocumentFile(requireContext())?.name
}
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 ->

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.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<Intent>? =
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)
}

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.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,12 +159,6 @@ 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(
@@ -200,16 +188,9 @@ class PasskeyProviderService : CredentialProviderService() {
)
}
}
}
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")
@@ -243,7 +224,6 @@ class PasskeyProviderService : CredentialProviderService() {
)
)
}
}
},
onDatabaseClosed = {
Log.d(TAG, "Add pending intent for passkey selection in closed database")
@@ -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(
request: BeginCreateCredentialRequest,
cancellationSignal: CancellationSignal,

View File

@@ -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<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
*/

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.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

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_database_username">KeePassDX Database</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_private_key">Passkey Private Key</string>
<string name="passkey_credential_id">Passkey Credential Id</string>