Compare commits
12 Commits
4.2.0beta0
...
4.2.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
208f1e97d5 | ||
|
|
e4e0628e20 | ||
|
|
f60f31771f | ||
|
|
ff6367bac4 | ||
|
|
540e72812e | ||
|
|
5fe4af8e9d | ||
|
|
ae42ab43b7 | ||
|
|
c463055971 | ||
|
|
1849dca81d | ||
|
|
b3dd3dcfb5 | ||
|
|
fef88ff270 | ||
|
|
f1f7dd1e6c |
10
CHANGELOG
@@ -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)
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
|
||||
@@ -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) { _, _ ->
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 -> {
|
||||
|
||||
@@ -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>
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
8
fastlane/metadata/android/en-US/changelogs/145.txt
Normal 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
|
||||
|
Before Width: | Height: | Size: 142 KiB After Width: | Height: | Size: 157 KiB |
|
Before Width: | Height: | Size: 234 KiB After Width: | Height: | Size: 179 KiB |
|
Before Width: | Height: | Size: 173 KiB After Width: | Height: | Size: 142 KiB |
|
Before Width: | Height: | Size: 188 KiB After Width: | Height: | Size: 234 KiB |
|
Before Width: | Height: | Size: 166 KiB After Width: | Height: | Size: 173 KiB |
|
Before Width: | Height: | Size: 204 KiB After Width: | Height: | Size: 166 KiB |
|
Before Width: | Height: | Size: 163 KiB After Width: | Height: | Size: 204 KiB |
|
Before Width: | Height: | Size: 179 KiB After Width: | Height: | Size: 163 KiB |
8
fastlane/metadata/android/fr-FR/changelogs/145.txt
Normal 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
|
||||