Compare commits

..

12 Commits

Author SHA1 Message Date
J-Jamet
208f1e97d5 Merge branch 'release/4.2.0' 2025-10-15 23:30:40 +02:00
J-Jamet
e4e0628e20 fix: Upgrade CHANGELOG 2025-10-15 23:22:41 +02:00
J-Jamet
f60f31771f fix: Upgrade to 4.2.0 2025-10-15 23:16:45 +02:00
J-Jamet
ff6367bac4 fix: Change screens 2025-10-15 23:08:16 +02:00
J-Jamet
540e72812e fix: Passkey browser signature #2213 2025-10-15 21:15:10 +02:00
J-Jamet
5fe4af8e9d fix: Passkey multiple instance #2215 2025-10-15 20:09:27 +02:00
J-Jamet
ae42ab43b7 fix: Passkey multiple instance #2215 2025-10-15 19:37:56 +02:00
J-Jamet
c463055971 fix: Passkey back #2215 2025-10-15 15:46:26 +02:00
J-Jamet
1849dca81d fix: Form filling auto search #2204 2025-10-15 15:14:30 +02:00
J-Jamet
b3dd3dcfb5 fix: Toast "Usage parameter is null"
#2214
2025-10-14 15:36:08 +02:00
J-Jamet
fef88ff270 feat: Add KPEX_PASSKEY_FLAG_BE and KPEX_PASSKEY_FLAG_BS flags #2212 2025-10-14 14:21:52 +02:00
J-Jamet
f1f7dd1e6c fix: Upgrade to 4.2.0beta04 2025-10-14 14:12:01 +02:00
28 changed files with 265 additions and 113 deletions

View File

@@ -1,12 +1,12 @@
KeePassDX(4.2.0)
* Passkeys management #1421 #2097 (Thx @cali-95)
* Passkeys management #1421 #2097 (@cali-95)
* Confirm usage of passkey #2165 #2124
* Dialog to manage missing signature #2152 #2155 #2161 #2160
* Capture error #2159
* Change Passkey Backup Eligibility & Backup State #2135 #2150
* Search settings #2112 #2181 #2187
* Capture error #2159 #2215
* Change Passkey Backup Eligibility & Backup State #2135 #2150 #2212
* Search settings #2112 #2181 #2187 #2204
* Autofill refactoring #765 #2196
* Small fixes #2157 #2164 #2171 #2122 #2180 #2209
* Small fixes #2157 #2164 #2171 #2122 #2180 #2209 #2214
KeePassDX(4.1.9)
* Fix landscape UI #2198 #2200 (@chenxiaolong)

View File

@@ -11,8 +11,8 @@ android {
applicationId "com.kunzisoft.keepass"
minSdkVersion 19
targetSdkVersion 35
versionCode = 144
versionName = "4.2.0beta03"
versionCode = 145
versionName = "4.2.0"
multiDexEnabled true
testApplicationId = "com.kunzisoft.keepass.tests"

View File

@@ -520,7 +520,7 @@ class EntryActivity : DatabaseLockActivity() {
// Transit data in previous Activity after an update
Intent().apply {
putExtra(EntryEditActivity.ADD_OR_UPDATE_ENTRY_KEY, mMainEntryId)
setResult(Activity.RESULT_OK, this)
setResult(RESULT_OK, this)
}
super.finish()
}

View File

@@ -86,6 +86,7 @@ import com.kunzisoft.keepass.database.element.node.NodeIdUUID
import com.kunzisoft.keepass.database.element.node.Type
import com.kunzisoft.keepass.database.exception.RegisterInReadOnlyDatabaseException
import com.kunzisoft.keepass.database.helper.SearchHelper
import com.kunzisoft.keepass.database.helper.SearchHelper.getSearchParametersFromSearchInfo
import com.kunzisoft.keepass.database.search.SearchParameters
import com.kunzisoft.keepass.education.GroupActivityEducation
import com.kunzisoft.keepass.model.DataTime
@@ -173,6 +174,7 @@ class GroupActivity : DatabaseLockActivity(),
// Manage group
private var mSearchState: SearchState? = null
private var mAutoSearch: Boolean = false // To mainly manage keyboard
private var mTempSearchInfo: Boolean = false // To manage temp search
private var mMainGroupState: GroupState? = null // Group state, not a search
private var mRootGroup: Group? = null // Root group in the tree
private var mMainGroup: Group? = null // Main group currently in memory
@@ -214,6 +216,7 @@ class GroupActivity : DatabaseLockActivity(),
private val mOnSearchActionExpandListener = object : MenuItem.OnActionExpandListener {
override fun onMenuItemActionExpand(p0: MenuItem): Boolean {
searchFiltersView?.visibility = View.VISIBLE
searchFiltersView?.showSearchExpandButton(!mTempSearchInfo)
searchView?.setOnQueryTextListener(mOnSearchQueryTextListener)
searchFiltersView?.onParametersChangeListener = mOnSearchFiltersChangeListener
@@ -258,6 +261,7 @@ class GroupActivity : DatabaseLockActivity(),
private fun removeSearch() {
mSearchState = null
mTempSearchInfo = false
intent.removeExtra(AUTO_SEARCH_KEY)
if (Intent.ACTION_SEARCH == intent.action) {
intent.action = Intent.ACTION_DEFAULT
@@ -710,37 +714,34 @@ class GroupActivity : DatabaseLockActivity(),
finishNodeAction()
}
/**
* Transform the AUTO_SEARCH_KEY in ACTION_SEARCH, return true if AUTO_SEARCH_KEY was present
*/
private fun transformSearchInfoIntent(intent: Intent) {
// To relaunch the activity as ACTION_SEARCH
val searchInfo: SearchInfo? = intent.retrieveSearchInfo()
val autoSearch = intent.getBooleanExtra(AUTO_SEARCH_KEY, false)
intent.removeExtra(AUTO_SEARCH_KEY)
if (searchInfo != null && autoSearch) {
intent.action = Intent.ACTION_SEARCH
intent.putExtra(SearchManager.QUERY, searchInfo.toString())
}
}
private fun manageIntent(intent: Intent?) {
intent?.let {
if (intent.extras?.containsKey(GROUP_STATE_KEY) == true) {
mMainGroupState = intent.getParcelableExtraCompat(GROUP_STATE_KEY)
intent.removeExtra(GROUP_STATE_KEY)
}
// To transform KEY_SEARCH_INFO in ACTION_SEARCH
transformSearchInfoIntent(intent)
// To get the form filling search as temp search
val searchInfo: SearchInfo? = intent.retrieveSearchInfo()
val autoSearch = intent.getBooleanExtra(AUTO_SEARCH_KEY, false)
// Get search query
if (intent.action == Intent.ACTION_SEARCH) {
if (searchInfo != null && autoSearch) {
mAutoSearch = true
val stringQuery = intent.getStringExtra(SearchManager.QUERY)?.trim { it <= ' ' } ?: ""
intent.action = Intent.ACTION_DEFAULT
intent.removeExtra(SearchManager.QUERY)
mSearchState = SearchState(PreferencesUtil.getDefaultSearchParameters(this).apply {
searchQuery = stringQuery
}, mSearchState?.firstVisibleItem ?: 0)
mTempSearchInfo = true
searchInfo.getSearchParametersFromSearchInfo(this) {
mSearchState = SearchState(
searchParameters = it,
firstVisibleItem = mSearchState?.firstVisibleItem ?: 0
)
}
} else if (intent.action == Intent.ACTION_SEARCH) {
mAutoSearch = true
mSearchState = SearchState(
searchParameters = PreferencesUtil.getDefaultSearchParameters(this).apply {
searchQuery = intent.getStringExtra(SearchManager.QUERY)
?.trim { it <= ' ' } ?: ""
},
firstVisibleItem = mSearchState?.firstVisibleItem ?: 0
)
} else if (mRequestStartupSearch
&& PreferencesUtil.automaticallyFocusSearch(this@GroupActivity)) {
// Expand the search view if defined in settings
@@ -748,6 +749,8 @@ class GroupActivity : DatabaseLockActivity(),
mRequestStartupSearch = false
addSearch()
}
intent.action = Intent.ACTION_DEFAULT
intent.removeExtra(SearchManager.QUERY)
}
}
@@ -772,7 +775,7 @@ class GroupActivity : DatabaseLockActivity(),
// Assign title
if (group?.isVirtual == true) {
searchFiltersView?.setNumbers(group.numberOfChildEntries)
searchFiltersView?.setCurrentGroupText(mMainGroup?.title ?: "")
searchFiltersView?.setCurrentGroupText(mMainGroup?.title ?: getString(R.string.search))
searchFiltersView?.availableOther(mDatabase?.allowEntryCustomFields() ?: false)
searchFiltersView?.availableApplicationIds(mDatabase?.allowEntryCustomFields() ?: false)
searchFiltersView?.availableTags(mDatabase?.allowTags() ?: false)
@@ -892,7 +895,6 @@ class GroupActivity : DatabaseLockActivity(),
},
registrationAction = { intentSenderMode, typeMode, registerInfo ->
if (!database.isReadOnly) {
// TODO Ask to overwrite data
entrySelectedForRegistration(
database = database,
entry = entryVersioned,
@@ -1151,7 +1153,9 @@ class GroupActivity : DatabaseLockActivity(),
finishNodeAction()
searchView?.setOnQueryTextListener(null)
searchFiltersView?.saveSearchParameters()
if (!mTempSearchInfo) {
searchFiltersView?.saveSearchParameters()
}
}
private fun addSearchQueryInSearchView(searchQuery: String) {
@@ -1216,7 +1220,9 @@ class GroupActivity : DatabaseLockActivity(),
if (searchState != null) {
it.expandActionView()
addSearchQueryInSearchView(searchState.searchParameters.searchQuery)
searchFiltersView?.searchParameters = searchState.searchParameters
if (mTempSearchInfo.not()) {
searchFiltersView?.searchParameters = searchState.searchParameters
}
}
}
if (it.isActionViewExpanded) {

View File

@@ -11,6 +11,8 @@ import androidx.appcompat.app.AlertDialog
import androidx.lifecycle.lifecycleScope
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.legacy.DatabaseModeActivity
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.setActivityResult
import com.kunzisoft.keepass.credentialprovider.viewmodel.CredentialLauncherViewModel
import com.kunzisoft.keepass.credentialprovider.viewmodel.HardwareKeyLauncherViewModel
import com.kunzisoft.keepass.credentialprovider.viewmodel.HardwareKeyLauncherViewModel.Companion.addHardwareKey
import com.kunzisoft.keepass.credentialprovider.viewmodel.HardwareKeyLauncherViewModel.Companion.addSeed
@@ -21,6 +23,7 @@ import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.hardware.HardwareKey
import com.kunzisoft.keepass.tasks.ActionRunnable
import com.kunzisoft.keepass.utils.AppUtil.openExternalApp
import com.kunzisoft.keepass.view.toastError
import kotlinx.coroutines.launch
/**
@@ -70,6 +73,24 @@ class HardwareKeyActivity: DatabaseModeActivity(){
}
}
}
lifecycleScope.launch {
mHardwareKeyLauncherViewModel.credentialUiState.collect { uiState ->
when (uiState) {
is CredentialLauncherViewModel.UIState.SetActivityResult -> {
setActivityResult(
lockDatabase = uiState.lockDatabase,
resultCode = uiState.resultCode,
data = uiState.data
)
}
is CredentialLauncherViewModel.UIState.ShowError -> {
toastError(uiState.error)
mHardwareKeyLauncherViewModel.cancelResult()
}
else -> {}
}
}
}
}
override fun onDatabaseRetrieved(database: ContextualDatabase) {
@@ -119,7 +140,8 @@ class HardwareKeyActivity: DatabaseModeActivity(){
Intent(
context,
HardwareKeyActivity::class.java).apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK
flags = Intent.FLAG_ACTIVITY_NEW_TASK or
Intent.FLAG_ACTIVITY_MULTIPLE_TASK
addHardwareKey(hardwareKey)
addSeed(seed)
})

View File

@@ -176,7 +176,8 @@ class PasskeyLauncherActivity : DatabaseLockActivity() {
super.onDatabaseActionFinished(database, actionTask, result)
when (actionTask) {
ACTION_DATABASE_UPDATE_ENTRY_TASK -> {
passkeyLauncherViewModel.autoSelectPasskey(result, database)
// TODO When auto save is enabled, WARNING filter by the calling activity
// passkeyLauncherViewModel.autoSelectPasskey(result, database)
}
}
}
@@ -240,6 +241,8 @@ class PasskeyLauncherActivity : DatabaseLockActivity() {
)
.append("\n\n")
.append(getString(R.string.passkeys_missing_signature_app_ask_explanation))
.append("\n\n")
.append(getString(R.string.passkeys_missing_signature_app_ask_question))
.toString()
)
setPositiveButton(android.R.string.ok) { _, _ ->

View File

@@ -388,11 +388,15 @@ object PasskeyHelper {
* Utility method to create a passkey and the associated creation request parameters
* [intent] allows to retrieve the request
* [context] context to manage package verification files
* [defaultBackupEligibility] the default backup eligibility to add the the passkey entry
* [defaultBackupState] the default backup state to add the the passkey entry
* [passkeyCreated] is called asynchronously when the passkey has been created
*/
suspend fun retrievePasskeyCreationRequestParameters(
intent: Intent,
context: Context,
defaultBackupEligibility: Boolean?,
defaultBackupState: Boolean?,
passkeyCreated: suspend (Passkey, AppOrigin?, PublicKeyCredentialCreationParameters) -> Unit
) {
val createCredentialRequest = PendingIntentHandler.retrieveProviderCreateCredentialRequest(intent)
@@ -420,7 +424,9 @@ object PasskeyHelper {
privateKeyPem = privateKeyPem,
credentialId = b64Encode(credentialId),
userHandle = b64Encode(userHandle),
relyingParty = relyingParty
relyingParty = relyingParty,
backupEligibility = defaultBackupEligibility,
backupState = defaultBackupState
)
// create new entry in database
@@ -554,8 +560,8 @@ object PasskeyHelper {
requestOptions: PublicKeyCredentialRequestOptions,
clientDataResponse: ClientDataResponse,
passkey: Passkey,
backupEligibility: Boolean,
backupState: Boolean
defaultBackupEligibility: Boolean,
defaultBackupState: Boolean
): PublicKeyCredential {
val getCredentialResponse = FidoPublicKeyCredential(
id = passkey.credentialId,
@@ -563,8 +569,8 @@ object PasskeyHelper {
requestOptions = requestOptions,
userPresent = true,
userVerified = true,
backupEligibility = backupEligibility,
backupState = backupState,
backupEligibility = passkey.backupEligibility ?: defaultBackupEligibility,
backupState = passkey.backupState ?: defaultBackupState,
userHandle = passkey.userHandle,
privateKey = passkey.privateKeyPem,
clientDataResponse = clientDataResponse

View File

@@ -307,8 +307,8 @@ class PasskeyLauncherViewModel(application: Application): CredentialLauncherView
appOrigin = appOrigin
),
passkey = passkey,
backupEligibility = mBackupEligibility,
backupState = mBackupState
defaultBackupEligibility = mBackupEligibility,
defaultBackupState = mBackupState
)
)
)
@@ -363,8 +363,8 @@ class PasskeyLauncherViewModel(application: Application): CredentialLauncherView
appOrigin = appOrigin
),
passkey = passkey,
backupEligibility = mBackupEligibility,
backupState = mBackupState
defaultBackupEligibility = mBackupEligibility,
defaultBackupState = mBackupState
)
)
)
@@ -400,6 +400,8 @@ class PasskeyLauncherViewModel(application: Application): CredentialLauncherView
retrievePasskeyCreationRequestParameters(
intent = intent,
context = getApplication(),
defaultBackupEligibility = mBackupEligibility,
defaultBackupState = mBackupState,
passkeyCreated = { passkey, appInfoToStore, publicKeyCredentialParameters ->
// Save the requested parameters
mPasskey = passkey
@@ -503,8 +505,10 @@ class PasskeyLauncherViewModel(application: Application): CredentialLauncherView
intent = responseIntent,
response = buildCreatePublicKeyCredentialResponse(
publicKeyCredentialCreationParameters = it,
backupEligibility = mBackupEligibility,
backupState = mBackupState
backupEligibility = passkey?.backupEligibility
?: mBackupEligibility,
backupState = passkey?.backupState
?: mBackupState
)
)
}

View File

@@ -47,6 +47,8 @@ import com.kunzisoft.keepass.database.exception.UnknownDatabaseLocationException
import com.kunzisoft.keepass.database.exception.VersionDatabaseException
import com.kunzisoft.keepass.database.exception.XMLMalformedDatabaseException
import com.kunzisoft.keepass.model.PasskeyEntryFields.FIELD_CREDENTIAL_ID
import com.kunzisoft.keepass.model.PasskeyEntryFields.FIELD_FLAG_BE
import com.kunzisoft.keepass.model.PasskeyEntryFields.FIELD_FLAG_BS
import com.kunzisoft.keepass.model.PasskeyEntryFields.FIELD_PRIVATE_KEY
import com.kunzisoft.keepass.model.PasskeyEntryFields.FIELD_RELYING_PARTY
import com.kunzisoft.keepass.model.PasskeyEntryFields.FIELD_USERNAME
@@ -146,6 +148,8 @@ fun TemplateField.getLocalizedName(context: Context?, name: String): String {
FIELD_CREDENTIAL_ID.equals(name, true) -> context.getString(R.string.passkey_credential_id)
FIELD_USER_HANDLE.equals(name, true) -> context.getString(R.string.passkey_user_handle)
FIELD_RELYING_PARTY.equals(name, true) -> context.getString(R.string.passkey_relying_party)
FIELD_FLAG_BE.equals(name, true) -> context.getString(R.string.passkey_backup_eligibility)
FIELD_FLAG_BS.equals(name, true) -> context.getString(R.string.passkey_backup_state)
else -> name
}

View File

@@ -53,26 +53,67 @@ object SearchHelper {
private fun getConcreteWebDomain(
context: Context,
webDomain: String?,
concreteWebDomain: (String?) -> Unit
concreteWebDomain: (searchSubDomains: Boolean, concreteWebDomain: String?) -> Unit
) {
val domain = webDomain
val searchSubDomains = searchSubDomains(context)
if (domain != null) {
// Warning, web domain can contains IP, don't crop in this case
if (searchSubDomains(context)
if (searchSubDomains
|| Regex(SearchInfo.WEB_IP_REGEX).matches(domain)) {
concreteWebDomain.invoke(webDomain)
concreteWebDomain.invoke(searchSubDomains, webDomain)
} else {
CoroutineScope(Dispatchers.IO).launch {
val publicSuffixList = PublicSuffixList(context)
val publicSuffix = publicSuffixList
.getPublicSuffixPlusOne(domain).await()
withContext(Dispatchers.Main) {
concreteWebDomain.invoke(publicSuffix)
concreteWebDomain.invoke(false, publicSuffix)
}
}
}
} else {
concreteWebDomain.invoke(null)
concreteWebDomain.invoke(searchSubDomains, null)
}
}
/**
* Create search parameters asynchronously from [SearchInfo]
*/
fun SearchInfo.getSearchParametersFromSearchInfo(
context: Context,
callback: (SearchParameters) -> Unit
) {
getConcreteWebDomain(
context,
webDomain
) { searchSubDomains, concreteDomain ->
var query = this.toString()
if (isDomainSearch && concreteDomain != null)
query = concreteDomain
callback.invoke(
SearchParameters().apply {
searchQuery = query
allowEmptyQuery = false
searchInTitles = false
searchInUsernames = false
searchInPasswords = false
searchInAppIds = isAppIdSearch
searchInUrls = isDomainSearch
searchByDomain = true
searchBySubDomain = searchSubDomains
searchInRelyingParty = isPasskeySearch
searchInNotes = false
searchInOTP = isOTPSearch
searchInOther = false
searchInUUIDs = false
searchInTags = isTagSearch
searchInCurrentGroup = false
searchInSearchableGroup = true
searchInRecycleBin = false
searchInTemplates = false
}
)
}
}
@@ -96,36 +137,10 @@ object SearchHelper {
&& !searchInfo.manualSelection
&& !searchInfo.containsOnlyNullValues()
) {
getConcreteWebDomain(
context,
searchInfo.webDomain
) { concreteDomain ->
var query = searchInfo.toString()
if (searchInfo.isDomainSearch && concreteDomain != null)
query = concreteDomain
searchInfo.getSearchParametersFromSearchInfo(context) { searchParameters ->
// If search provide results
database.createVirtualGroupFromSearchInfo(
searchParameters = SearchParameters().apply {
searchQuery = query
allowEmptyQuery = false
searchInTitles = false
searchInUsernames = false
searchInPasswords = false
searchInAppIds = searchInfo.isAppIdSearch
searchInUrls = searchInfo.isDomainSearch
searchByDomain = true
searchBySubDomain = searchSubDomains(context)
searchInRelyingParty = searchInfo.isPasskeySearch
searchInNotes = false
searchInOTP = searchInfo.isOTPSearch
searchInOther = false
searchInUUIDs = false
searchInTags = searchInfo.isTagSearch
searchInCurrentGroup = false
searchInSearchableGroup = true
searchInRecycleBin = false
searchInTemplates = false
},
searchParameters = searchParameters,
max = MAX_SEARCH_ENTRY
)?.let { searchGroup ->
if (searchGroup.numberOfChildEntries > 0) {

View File

@@ -716,9 +716,9 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
notifyProgressMessage()
HardwareKeyActivity
.launchHardwareKeyActivity(
this@DatabaseTaskNotificationService,
hardwareKey,
seed
context = this@DatabaseTaskNotificationService,
hardwareKey = hardwareKey,
seed = seed
)
// Wait the response
mProgressMessage.apply {

View File

@@ -210,10 +210,10 @@ class SearchFiltersView @JvmOverloads constructor(context: Context,
searchNumbers.text = SearchHelper.showNumberOfSearchResults(numbers)
}
fun setCurrentGroupText(text: String) {
fun setCurrentGroupText(text: String?) {
val maxChars = 12
searchCurrentGroup.text = when {
text.isEmpty() -> context.getString(R.string.current_group)
text.isNullOrEmpty() -> context.getString(R.string.current_group)
text.length > maxChars -> text.substring(0, maxChars) + ""
else -> text
}
@@ -257,6 +257,10 @@ class SearchFiltersView @JvmOverloads constructor(context: Context,
)
}
fun showSearchExpandButton(show: Boolean) {
searchExpandButton.isVisible = show
}
override fun setVisibility(visibility: Int) {
when (visibility) {
VISIBLE -> {

View File

@@ -432,8 +432,9 @@
<string name="passkeys_privileged_apps_ask_title">App not recognized</string>
<string name="passkeys_privileged_apps_ask_message">%1$s attempts to perform a Passkey action.\n\nAdd it to the list of privileged apps?</string>
<string name="passkeys_missing_signature_app_ask_title">Signature missing</string>
<string name="passkeys_missing_signature_app_ask_explanation">WARNING: The passkey was created from another client or the signature has been deleted. Ensure the app you want to authenticate is part of the same service and is legitimate to avoid security issues.</string>
<string name="passkeys_missing_signature_app_ask_message">%1$s is unrecognised and attempts to authenticate with an existing passkey.\n\nAdd app signature to passkey entry?</string>
<string name="passkeys_missing_signature_app_ask_explanation">WARNING: The passkey was created from another client or the signature has been deleted. Ensure the app you want to authenticate is part of the same service and is legitimate to avoid security issues.\nIf the app is a browser, do not add its signature to the entry, but to the list of privileged apps in the settings.</string>
<string name="passkeys_missing_signature_app_ask_message">%1$s is unrecognised and attempts to authenticate with an existing passkey.</string>
<string name="passkeys_missing_signature_app_ask_question">Add app signature to passkey entry?</string>
<string name="passkeys_auto_select_title">Auto select</string>
<string name="passkeys_auto_select_summary">Auto select if only one entry and the database is open, only if the requesting app is compatible</string>
<string name="passkeys_backup_eligibility_title">Backup Eligibility</string>
@@ -771,5 +772,7 @@
<string name="passkey_credential_id">Passkey Credential Id</string>
<string name="passkey_user_handle">Passkey User Handle</string>
<string name="passkey_relying_party">Passkey Relying Party</string>
<string name="passkey_backup_eligibility">Passkey Backup Eligibility</string>
<string name="passkey_backup_state">Passkey Backup State</string>
<string name="error_passkey_result">Unable to return the passkey</string>
</resources>

View File

@@ -37,6 +37,11 @@
android:title="@string/enable_auto_save_database_title"
android:summary="@string/enable_auto_save_database_summary"
android:defaultValue="@bool/enable_auto_save_database_default"/>
<SwitchPreferenceCompat
android:key="@string/auto_focus_search_key"
android:title="@string/auto_focus_search_title"
android:summary="@string/auto_focus_search_summary"
android:defaultValue="@bool/auto_focus_search_default"/>
<SwitchPreferenceCompat
android:key="@string/enable_keep_screen_on_key"
android:title="@string/enable_keep_screen_on_title"
@@ -47,22 +52,6 @@
android:title="@string/enable_screenshot_mode_title"
android:summary="@string/enable_screenshot_mode_summary"
android:defaultValue="@bool/enable_screenshot_mode_key_default"/>
</PreferenceCategory>
<PreferenceCategory
android:title="@string/search">
<SwitchPreferenceCompat
android:key="@string/auto_focus_search_key"
android:title="@string/auto_focus_search_title"
android:summary="@string/auto_focus_search_summary"
android:defaultValue="@bool/auto_focus_search_default"/>
<SwitchPreferenceCompat
android:key="@string/subdomain_search_key"
android:title="@string/subdomain_search_title"
android:summary="@string/subdomain_search_summary"
android:defaultValue="@bool/subdomain_search_default"/>
</PreferenceCategory>
<PreferenceCategory

View File

@@ -19,6 +19,15 @@
-->
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
<PreferenceCategory
android:title="@string/general">
<SwitchPreferenceCompat
android:key="@string/subdomain_search_key"
android:title="@string/subdomain_search_title"
android:summary="@string/subdomain_search_summary"
android:defaultValue="@bool/subdomain_search_default"/>
</PreferenceCategory>
<PreferenceCategory
android:title="@string/keyboard">
<Preference

View File

@@ -74,6 +74,17 @@ class ProtectedString : Parcelable {
return arrayOfNulls(size)
}
}
fun String.toBooleanCompat(): Boolean {
return if (this.equals("1", ignoreCase = true))
true
else
this.toBoolean()
}
fun Boolean.toFieldValue(): String {
return if (this) "1" else "0"
}
}
}

View File

@@ -28,5 +28,32 @@ data class Passkey(
val privateKeyPem: String,
val credentialId: String,
val userHandle: String,
val relyingParty: String
): Parcelable
val relyingParty: String,
val backupEligibility: Boolean?,
val backupState: Boolean?
): Parcelable {
// Do not compare BE and BS because are modifiable by the user
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as Passkey
if (username != other.username) return false
if (privateKeyPem != other.privateKeyPem) return false
if (credentialId != other.credentialId) return false
if (userHandle != other.userHandle) return false
if (relyingParty != other.relyingParty) return false
return true
}
override fun hashCode(): Int {
var result = username.hashCode()
result = 31 * result + privateKeyPem.hashCode()
result = 31 * result + credentialId.hashCode()
result = 31 * result + userHandle.hashCode()
result = 31 * result + relyingParty.hashCode()
return result
}
}

View File

@@ -2,6 +2,8 @@ package com.kunzisoft.keepass.model
import com.kunzisoft.keepass.database.element.Field
import com.kunzisoft.keepass.database.element.security.ProtectedString
import com.kunzisoft.keepass.database.element.security.ProtectedString.Companion.toBooleanCompat
import com.kunzisoft.keepass.database.element.security.ProtectedString.Companion.toFieldValue
object PasskeyEntryFields {
@@ -12,6 +14,8 @@ object PasskeyEntryFields {
const val FIELD_CREDENTIAL_ID = "KPEX_PASSKEY_CREDENTIAL_ID"
const val FIELD_USER_HANDLE = "KPEX_PASSKEY_USER_HANDLE"
const val FIELD_RELYING_PARTY = "KPEX_PASSKEY_RELYING_PARTY"
const val FIELD_FLAG_BE = "KPEX_PASSKEY_FLAG_BE"
const val FIELD_FLAG_BS = "KPEX_PASSKEY_FLAG_BS"
const val PASSKEY_FIELD = "Passkey"
const val PASSKEY_TAG = "Passkey"
@@ -20,11 +24,14 @@ object PasskeyEntryFields {
* Parse fields of an entry to retrieve a Passkey
*/
fun parseFields(getField: (id: String) -> String?): Passkey? {
val usernameField = getField(FIELD_USERNAME)
val privateKeyField = getField(FIELD_PRIVATE_KEY)
val credentialIdField = getField(FIELD_CREDENTIAL_ID)
val userHandleField = getField(FIELD_USER_HANDLE)
val relyingPartyField = getField(FIELD_RELYING_PARTY)
val usernameField: String? = getField(FIELD_USERNAME)
val privateKeyField: String? = getField(FIELD_PRIVATE_KEY)
val credentialIdField: String? = getField(FIELD_CREDENTIAL_ID)
val userHandleField: String? = getField(FIELD_USER_HANDLE)
val relyingPartyField: String? = getField(FIELD_RELYING_PARTY)
// Optional fields
val backupEligibilityField: Boolean? = getField(FIELD_FLAG_BE)?.toBooleanCompat()
val backupStateField: Boolean? = getField(FIELD_FLAG_BS)?.toBooleanCompat()
if (usernameField == null
|| privateKeyField == null
|| credentialIdField == null
@@ -36,7 +43,9 @@ object PasskeyEntryFields {
privateKeyPem = privateKeyField,
credentialId = credentialIdField,
userHandle = userHandleField,
relyingParty = relyingPartyField
relyingParty = relyingPartyField,
backupEligibility = backupEligibilityField,
backupState = backupStateField
)
}
@@ -91,6 +100,24 @@ object PasskeyEntryFields {
ProtectedString(enableProtection = false, passkey.relyingParty)
)
)
passkey.backupEligibility?.let { backupEligibility ->
addOrReplaceField(
Field(
FIELD_FLAG_BE,
ProtectedString(enableProtection = false,
backupEligibility.toFieldValue())
)
)
}
passkey.backupState?.let { backupState ->
addOrReplaceField(
Field(
FIELD_FLAG_BS,
ProtectedString(enableProtection = false,
backupState.toFieldValue())
)
)
}
}
return overwrite
}
@@ -107,17 +134,23 @@ object PasskeyEntryFields {
val credentialIdField = Field(FIELD_CREDENTIAL_ID)
val userHandleField = Field(FIELD_USER_HANDLE)
val relyingPartyField = Field(FIELD_RELYING_PARTY)
val backupEligibilityField = Field(FIELD_FLAG_BE)
val backupStateField = Field(FIELD_FLAG_BS)
newCustomFields.remove(usernameField)
newCustomFields.remove(privateKeyField)
newCustomFields.remove(credentialIdField)
newCustomFields.remove(userHandleField)
newCustomFields.remove(relyingPartyField)
// Empty auto generated OTP Token field
newCustomFields.remove(backupEligibilityField)
newCustomFields.remove(backupStateField)
// Empty auto generated Passkey field
if (fieldsToParse.contains(usernameField)
|| fieldsToParse.contains(privateKeyField)
|| fieldsToParse.contains(credentialIdField)
|| fieldsToParse.contains(userHandleField)
|| fieldsToParse.contains(relyingPartyField)
|| fieldsToParse.contains(backupEligibilityField)
|| fieldsToParse.contains(backupStateField)
)
newCustomFields.add(
Field(

View File

@@ -0,0 +1,8 @@
* Passkeys management #1421 #2097 (@cali-95)
* Confirm usage of passkey #2165 #2124
* Dialog to manage missing signature #2152 #2155 #2161 #2160
* Capture error #2159 #2215
* Change Passkey Backup Eligibility & Backup State #2135 #2150 #2212
* Search settings #2112 #2181 #2187 #2204
* Autofill refactoring #765 #2196
* Small fixes #2157 #2164 #2171 #2122 #2180 #2209 #2214

Binary file not shown.

Before

Width:  |  Height:  |  Size: 142 KiB

After

Width:  |  Height:  |  Size: 157 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 234 KiB

After

Width:  |  Height:  |  Size: 179 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 173 KiB

After

Width:  |  Height:  |  Size: 142 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 188 KiB

After

Width:  |  Height:  |  Size: 234 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 166 KiB

After

Width:  |  Height:  |  Size: 173 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 204 KiB

After

Width:  |  Height:  |  Size: 166 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 163 KiB

After

Width:  |  Height:  |  Size: 204 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 179 KiB

After

Width:  |  Height:  |  Size: 163 KiB

View File

@@ -0,0 +1,8 @@
* Gestion de Passkeys #1421 #2097 (@cali-95)
* Confirmation de l'usage de Passkey #2165 #2124
* Dialogue pour la gestion des signatures manquantes #2152 #2155 #2161 #2160
* Capture des erreurs #2159 #2215
* Configuration de Passkey Backup Eligibility & Backup State #2135 #2150 #2212
* Paramètres de recherche #2112 #2181 #2187 #2204
* Refactorisation du remplissage automatique #765 #2196
* Corrections mineures #2157 #2164 #2171 #2122 #2180 #2209 #2214