Compare commits

...

25 Commits

Author SHA1 Message Date
J-Jamet
01d778650c feat: Setting for auto select #2165 2025-09-18 12:26:56 +02:00
J-Jamet
dd389dbab1 fix: Passkey coroutine 2025-09-18 11:03:07 +02:00
J-Jamet
272ebd0c3f fix: Passkey auto save Signature 2025-09-17 23:23:41 +02:00
J-Jamet
0aecc21f43 fix: Passkey workflow 2025-09-17 20:02:55 +02:00
J-Jamet
1e7e464e65 feat: Add dialog 2025-09-17 13:58:12 +02:00
J-Jamet
d5c378ac85 fix: Private key format #2164 2025-09-14 23:48:27 +02:00
J-Jamet
672f1ca37d fix: Add toast error #2159 2025-09-14 13:17:49 +02:00
J-Jamet
2f9e1e4bf2 fix: Error message 2025-09-12 22:03:54 +02:00
J-Jamet
25d97e4f2e fix: Passkey Database Username 2025-09-12 21:20:12 +02:00
J-Jamet
f49dcbd654 fix: Unrecognized app that is not a browser #2157 2025-09-12 20:55:53 +02:00
J-Jamet
bf2d56b4fd feat: Add AAGUID Icons 2025-09-12 20:55:24 +02:00
J-Jamet
5893541dd2 Merge branch 'develop' into release/4.2.0 2025-09-12 16:14:19 +02:00
J-Jamet
2230fe66ab Merge tag '4.1.8' into develop
4.1.8
2025-09-12 16:04:08 +02:00
J-Jamet
84a62a32ff Merge branch 'release/4.1.8' 2025-09-12 16:03:59 +02:00
J-Jamet
da8ef9340c fix: Loading ViewModel 2025-09-12 15:23:32 +02:00
J-Jamet
af068349e4 fix: Upgrade to 4.1.8 2025-09-12 14:14:06 +02:00
J-Jamet
56cb5953dd fix: Deletable recycle bin #2163 2025-09-12 13:00:56 +02:00
J-Jamet
2fc2a9c7c1 fix: Delete algo during merge #1516 2025-09-11 21:19:40 +02:00
J-Jamet
69e7cdbc47 fix: Search with space #175 2025-09-11 16:43:40 +02:00
J-Jamet
39d9a74a73 fix: Warnings 2025-09-11 16:36:35 +02:00
J-Jamet
7212c73481 fix: Warnings 2025-09-11 14:55:37 +02:00
J-Jamet
3ee4caa153 fix: Warnings 2025-09-11 14:53:41 +02:00
J-Jamet
28e4d929bb fix: Warnings 2025-09-11 14:51:35 +02:00
J-Jamet
803d637510 fix: Backup parameters init 2025-09-11 12:04:39 +02:00
J-Jamet
ccd5da0962 feat: Add backup as setting #2135 2025-09-11 00:00:22 +02:00
73 changed files with 1298 additions and 614 deletions

View File

@@ -1,7 +1,12 @@
KeePassDX(4.2.0)
* Updated to API 35 minimum SDK 19 #2073 #2138 #2067 #2133 (Thx @Dev-ClayP)
* Passkeys management #1421 #2097 (Thx @cali-95)
KeePassDX(4.1.8)
* Updated to API 35 minimum SDK 19 #2073 #2138 #2067 #2133 #1687 (Thx @Dev-ClayP)
* Remember last read-only state #2099 #2100 (Thx @rmacklin)
* Fix merge deletion #1516
* Fix space in search #175
* Fix deletable recycle bin #2163
* Small fixes
KeePassDX(4.1.7)

View File

@@ -11,8 +11,8 @@ android {
applicationId "com.kunzisoft.keepass"
minSdkVersion 19
targetSdkVersion 35
versionCode = 140
versionName = "4.2.0beta01"
versionCode = 142
versionName = "4.2.0beta02"
multiDexEnabled true
testApplicationId = "com.kunzisoft.keepass.tests"

View File

@@ -69,7 +69,6 @@ import com.kunzisoft.keepass.database.element.Attachment
import com.kunzisoft.keepass.database.element.DateInstant
import com.kunzisoft.keepass.database.element.Entry
import com.kunzisoft.keepass.database.element.Field
import com.kunzisoft.keepass.database.element.node.Node
import com.kunzisoft.keepass.database.element.node.NodeId
import com.kunzisoft.keepass.database.element.template.Template
import com.kunzisoft.keepass.education.EntryEditActivityEducation
@@ -81,9 +80,9 @@ import com.kunzisoft.keepass.model.SearchInfo
import com.kunzisoft.keepass.otp.OtpElement
import com.kunzisoft.keepass.services.AttachmentFileNotificationService
import com.kunzisoft.keepass.services.ClipboardEntryNotificationService
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CREATE_ENTRY_TASK
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_ENTRY_TASK
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.getNewEntry
import com.kunzisoft.keepass.services.KeyboardEntryNotificationService
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.tasks.ActionRunnable
@@ -433,12 +432,7 @@ class EntryEditActivity : DatabaseLockActivity(),
ACTION_DATABASE_UPDATE_ENTRY_TASK -> {
try {
if (result.isSuccess) {
var newNodes: List<Node> = ArrayList()
result.data?.getBundle(DatabaseTaskNotificationService.NEW_NODES_KEY)?.let { newNodesBundle ->
newNodes = DatabaseTaskNotificationService.getListNodesFromBundle(database, newNodesBundle)
}
if (newNodes.size == 1) {
(newNodes[0] as? Entry?)?.let { entry ->
result.data?.getNewEntry(database)?.let { entry ->
EntrySelectionHelper.doSpecialAction(
intent = intent,
defaultAction = {
@@ -469,7 +463,6 @@ class EntryEditActivity : DatabaseLockActivity(),
)
}
}
}
} catch (e: Exception) {
Log.e(TAG, "Unable to retrieve entry after database action", e)
}

View File

@@ -39,7 +39,6 @@ import android.view.ViewGroup
import android.widget.ImageView
import android.widget.ProgressBar
import android.widget.TextView
import android.widget.Toast
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.viewModels
import androidx.annotation.RequiresApi
@@ -84,6 +83,7 @@ import com.kunzisoft.keepass.database.element.node.Node
import com.kunzisoft.keepass.database.element.node.NodeId
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.search.SearchParameters
import com.kunzisoft.keepass.education.GroupActivityEducation
@@ -92,8 +92,7 @@ import com.kunzisoft.keepass.model.GroupInfo
import com.kunzisoft.keepass.model.RegisterInfo
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.NEW_NODES_KEY
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.getListNodesFromBundle
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.getNewEntry
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.settings.SettingsActivity
import com.kunzisoft.keepass.tasks.ActionRunnable
@@ -116,6 +115,7 @@ import com.kunzisoft.keepass.view.applyWindowInsets
import com.kunzisoft.keepass.view.hideByFading
import com.kunzisoft.keepass.view.setTransparentNavigationBar
import com.kunzisoft.keepass.view.showActionErrorIfNeeded
import com.kunzisoft.keepass.view.toastError
import com.kunzisoft.keepass.view.updateLockPaddingStart
import com.kunzisoft.keepass.viewmodels.GroupEditViewModel
import com.kunzisoft.keepass.viewmodels.GroupViewModel
@@ -698,9 +698,7 @@ class GroupActivity : DatabaseLockActivity(),
var entry: Entry? = null
try {
result.data?.getBundle(NEW_NODES_KEY)?.let { newNodesBundle ->
entry = getListNodesFromBundle(database, newNodesBundle)[0] as Entry
}
entry = result.data?.getNewEntry(database)
} catch (e: Exception) {
Log.e(TAG, "Unable to retrieve entry action for selection", e)
}
@@ -942,7 +940,6 @@ class GroupActivity : DatabaseLockActivity(),
passkeySelectionAction = { searchInfo ->
if (!database.isReadOnly
&& searchInfo != null
// TODO Passkey setting && PreferencesUtil.isAutofillSaveSearchInfoEnable(this@GroupActivity)
) {
updateEntryWithSearchInfo(database, entryVersioned, searchInfo)
}
@@ -1768,12 +1765,7 @@ class GroupActivity : DatabaseLockActivity(),
)
onLaunchActivitySpecialMode()
} else {
Toast.makeText(
activity.applicationContext,
R.string.autofill_read_only_save,
Toast.LENGTH_LONG
)
.show()
activity.toastError(RegisterInReadOnlyDatabaseException())
onCancelSpecialMode()
}
}
@@ -1885,10 +1877,7 @@ class GroupActivity : DatabaseLockActivity(),
}
)
} else {
Toast.makeText(activity.applicationContext,
R.string.autofill_read_only_save,
Toast.LENGTH_LONG)
.show()
activity.toastError(RegisterInReadOnlyDatabaseException())
onCancelSpecialMode()
}
},
@@ -1950,10 +1939,7 @@ class GroupActivity : DatabaseLockActivity(),
)
onLaunchActivitySpecialMode()
} else {
Toast.makeText(activity.applicationContext,
R.string.autofill_read_only_save,
Toast.LENGTH_LONG)
.show()
activity.toastError(RegisterInReadOnlyDatabaseException())
onCancelSpecialMode()
}
}

View File

@@ -43,6 +43,7 @@ import androidx.biometric.BiometricManager
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.fragment.app.commit
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import com.google.android.material.snackbar.Snackbar
@@ -108,7 +109,11 @@ class MainCredentialActivity : DatabaseModeActivity() {
private var deviceUnlockFragment: DeviceUnlockFragment? = null
private val mDatabaseFileViewModel: DatabaseFileViewModel by viewModels()
private val mDeviceUnlockViewModel: DeviceUnlockViewModel by viewModels()
private val mDeviceUnlockViewModel: DeviceUnlockViewModel? by lazy {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
ViewModelProvider(this)[DeviceUnlockViewModel::class.java]
} else null
}
private val mPasswordActivityEducation = PasswordActivityEducation(this)
@@ -176,7 +181,7 @@ class MainCredentialActivity : DatabaseModeActivity() {
// Listen password checkbox to init advanced unlock and confirmation button
mainCredentialView?.onConditionToStoreCredentialChanged = { _, verified ->
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
mDeviceUnlockViewModel.checkConditionToStoreCredential(
mDeviceUnlockViewModel?.checkConditionToStoreCredential(
condition = verified
)
}
@@ -241,21 +246,22 @@ class MainCredentialActivity : DatabaseModeActivity() {
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
mDeviceUnlockViewModel.uiState.collect { uiState ->
mDeviceUnlockViewModel?.let { deviceUnlockViewModel ->
deviceUnlockViewModel.uiState.collect { uiState ->
// New value received
uiState.credentialRequiredCipher?.let { cipher ->
mDeviceUnlockViewModel.encryptCredential(
deviceUnlockViewModel.encryptCredential(
credential = getCredentialForEncryption(),
cipher = cipher
)
}
uiState.cipherEncryptDatabase?.let { cipherEncryptDatabase ->
onCredentialEncrypted(cipherEncryptDatabase)
mDeviceUnlockViewModel.consumeCredentialEncrypted()
deviceUnlockViewModel.consumeCredentialEncrypted()
}
uiState.cipherDecryptDatabase?.let { cipherDecryptDatabase ->
onCredentialDecrypted(cipherDecryptDatabase)
mDeviceUnlockViewModel.consumeCredentialDecrypted()
deviceUnlockViewModel.consumeCredentialDecrypted()
}
uiState.exception?.let { error ->
Snackbar.make(
@@ -263,7 +269,8 @@ class MainCredentialActivity : DatabaseModeActivity() {
deviceUnlockError(error, this@MainCredentialActivity),
Snackbar.LENGTH_LONG
).asError().show()
mDeviceUnlockViewModel.exceptionShown()
deviceUnlockViewModel.exceptionShown()
}
}
}
}
@@ -516,7 +523,7 @@ class MainCredentialActivity : DatabaseModeActivity() {
} else {
// Init Biometric elements
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
mDeviceUnlockViewModel.connect(databaseFileUri)
mDeviceUnlockViewModel?.connect(databaseFileUri)
}
}
@@ -569,9 +576,9 @@ class MainCredentialActivity : DatabaseModeActivity() {
mSpecialMode == SpecialMode.SAVE
|| mSpecialMode == SpecialMode.REGISTRATION)
) {
Log.e(TAG, getString(R.string.autofill_read_only_save))
Log.e(TAG, getString(R.string.error_save_read_only))
Snackbar.make(coordinatorLayout,
R.string.autofill_read_only_save,
R.string.error_save_read_only,
Snackbar.LENGTH_LONG).asError().show()
} else {
databaseFileUri?.let { databaseUri ->
@@ -660,7 +667,7 @@ class MainCredentialActivity : DatabaseModeActivity() {
try {
menu.findItem(R.id.menu_open_file_read_mode_key)
} catch (e: Exception) {
Log.e(TAG, "Unable to find read mode menu")
Log.e(TAG, "Unable to find read mode menu", e)
}
performedNextEducation(menu)
},
@@ -689,7 +696,7 @@ class MainCredentialActivity : DatabaseModeActivity() {
})
}
}
} catch (ignored: Exception) {}
} catch (_: Exception) {}
}
}
@@ -726,7 +733,7 @@ class MainCredentialActivity : DatabaseModeActivity() {
override fun onDestroy() {
super.onDestroy()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
mDeviceUnlockViewModel.disconnect()
mDeviceUnlockViewModel?.disconnect()
}
}

View File

@@ -176,21 +176,14 @@ class SortDialogFragment : DatabaseDialogFragment() {
return bundle
}
fun getInstance(sortNodeEnum: SortNodeEnum,
ascending: Boolean,
groupsBefore: Boolean): SortDialogFragment {
val bundle = buildBundle(sortNodeEnum, ascending, groupsBefore)
val fragment = SortDialogFragment()
fragment.arguments = bundle
return fragment
}
fun getInstance(sortNodeEnum: SortNodeEnum,
ascending: Boolean,
groupsBefore: Boolean,
recycleBinBottom: Boolean): SortDialogFragment {
recycleBinBottom: Boolean?): SortDialogFragment {
val bundle = buildBundle(sortNodeEnum, ascending, groupsBefore)
recycleBinBottom?.let {
bundle.putBoolean(SORT_RECYCLE_BIN_BOTTOM_BUNDLE_KEY, recycleBinBottom)
}
val fragment = SortDialogFragment()
fragment.arguments = bundle
return fragment

View File

@@ -76,9 +76,6 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
private var specialMode: SpecialMode = SpecialMode.DEFAULT
private var mRecycleBinEnable: Boolean = false
private var mRecycleBin: Group? = null
private var mRecycleViewScrollListener = object : RecyclerView.OnScrollListener() {
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
super.onScrollStateChanged(recyclerView, newState)
@@ -102,21 +99,14 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
R.id.menu_sort -> {
context?.let { context ->
val sortDialogFragment: SortDialogFragment =
if (mRecycleBinEnable) {
SortDialogFragment.getInstance(
PreferencesUtil.getListSort(context),
PreferencesUtil.getAscendingSort(context),
PreferencesUtil.getGroupsBeforeSort(context),
if (mDatabase?.isRecycleBinEnabled == true) {
PreferencesUtil.getRecycleBinBottomSort(context)
} else null
)
} else {
SortDialogFragment.getInstance(
PreferencesUtil.getListSort(context),
PreferencesUtil.getAscendingSort(context),
PreferencesUtil.getGroupsBeforeSort(context)
)
}
sortDialogFragment.show(childFragmentManager, "sortDialog")
}
true
@@ -165,9 +155,6 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
}
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
mRecycleBinEnable = database?.isRecycleBinEnabled == true
mRecycleBin = database?.recycleBin
context?.let { context ->
database?.let { database ->
mAdapter = NodesAdapter(context, database).apply {
@@ -312,6 +299,11 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
}
}
private fun containsRecycleBin(nodes: List<Node>): Boolean {
return mDatabase?.isRecycleBinEnabled == true
&& nodes.any { it == mDatabase?.recycleBin }
}
fun actionNodesCallback(database: ContextualDatabase,
nodes: List<Node>,
menuListener: NodesActionMenuListener?,
@@ -336,8 +328,7 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
// Open and Edit for a single item
if (nodes.size == 1) {
// Edition
if (database.isReadOnly
|| (mRecycleBinEnable && nodes[0] == mRecycleBin)) {
if (database.isReadOnly || containsRecycleBin(nodes)) {
menu?.removeItem(R.id.menu_edit)
}
} else {
@@ -357,8 +348,7 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
}
// Deletion
if (database.isReadOnly
|| (mRecycleBinEnable && nodes.any { it == mRecycleBin })) {
if (database.isReadOnly || containsRecycleBin(nodes)) {
menu?.removeItem(R.id.menu_delete)
}
}

View File

@@ -52,6 +52,28 @@ object EntrySelectionHelper {
private const val KEY_SEARCH_INFO = "com.kunzisoft.keepass.extra.SEARCH_INFO"
private const val KEY_REGISTER_INFO = "com.kunzisoft.keepass.extra.REGISTER_INFO"
/**
* Finish the activity by passing the result code and by locking the database if necessary
*/
fun Activity.setActivityResult(
lockDatabase: Boolean = false,
resultCode: Int,
data: Intent? = null,
) {
when (resultCode) {
Activity.RESULT_OK ->
this.setResult(resultCode, data)
Activity.RESULT_CANCELED ->
this.setResult(resultCode)
}
this.finish()
if (lockDatabase && PreferencesUtil.isAutofillCloseDatabaseEnable(this)) {
// Close the database
this.sendBroadcast(Intent(LOCK_ACTION))
}
}
/**
* Utility method to build a registerForActivityResult,
* Used recursively, close each activity with return data
@@ -63,19 +85,11 @@ object EntrySelectionHelper {
return this.registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) {
val resultCode = it.resultCode
if (resultCode == Activity.RESULT_OK) {
this.setResult(resultCode, dataTransformation(it.data))
}
if (resultCode == Activity.RESULT_CANCELED) {
this.setResult(Activity.RESULT_CANCELED)
}
this.finish()
if (lockDatabase && PreferencesUtil.isAutofillCloseDatabaseEnable(this)) {
// Close the database
this.sendBroadcast(Intent(LOCK_ACTION))
}
setActivityResult(
lockDatabase,
it.resultCode,
dataTransformation(it.data)
)
}
}

View File

@@ -41,12 +41,14 @@ import com.kunzisoft.keepass.credentialprovider.autofill.AutofillHelper
import com.kunzisoft.keepass.credentialprovider.autofill.CompatInlineSuggestionsRequest
import com.kunzisoft.keepass.credentialprovider.autofill.KeeAutofillService
import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.database.exception.RegisterInReadOnlyDatabaseException
import com.kunzisoft.keepass.database.helper.SearchHelper
import com.kunzisoft.keepass.model.RegisterInfo
import com.kunzisoft.keepass.model.SearchInfo
import com.kunzisoft.keepass.utils.AppUtil
import com.kunzisoft.keepass.utils.getParcelableCompat
import com.kunzisoft.keepass.utils.getParcelableExtraCompat
import com.kunzisoft.keepass.view.toastError
@RequiresApi(api = Build.VERSION_CODES.O)
class AutofillLauncherActivity : DatabaseModeActivity() {
@@ -236,7 +238,7 @@ class AutofillLauncherActivity : DatabaseModeActivity() {
}
private fun showReadOnlySaveMessage() {
Toast.makeText(this.applicationContext, R.string.autofill_read_only_save, Toast.LENGTH_LONG).show()
toastError(RegisterInReadOnlyDatabaseException())
}
companion object {

View File

@@ -30,12 +30,14 @@ import com.kunzisoft.keepass.activities.GroupActivity
import com.kunzisoft.keepass.activities.legacy.DatabaseModeActivity
import com.kunzisoft.keepass.credentialprovider.magikeyboard.MagikeyboardService
import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.database.exception.RegisterInReadOnlyDatabaseException
import com.kunzisoft.keepass.database.helper.SearchHelper
import com.kunzisoft.keepass.model.SearchInfo
import com.kunzisoft.keepass.otp.OtpEntryFields
import com.kunzisoft.keepass.utils.AppUtil
import com.kunzisoft.keepass.utils.KeyboardUtil.isKeyboardActivatedInSettings
import com.kunzisoft.keepass.utils.getParcelableCompat
import com.kunzisoft.keepass.view.toastError
/**
* Activity to search or select entry in database,
@@ -138,10 +140,7 @@ class EntrySelectionLauncherActivity : DatabaseModeActivity() {
false
)
} else {
Toast.makeText(applicationContext,
R.string.autofill_read_only_save,
Toast.LENGTH_LONG)
.show()
toastError(RegisterInReadOnlyDatabaseException())
}
} else if (searchShareForMagikeyboard) {
MagikeyboardService.performSelection(
@@ -182,10 +181,7 @@ class EntrySelectionLauncherActivity : DatabaseModeActivity() {
false
)
} else {
Toast.makeText(applicationContext,
R.string.autofill_read_only_save,
Toast.LENGTH_LONG)
.show()
toastError(RegisterInReadOnlyDatabaseException())
}
} else if (searchShareForMagikeyboard) {
GroupActivity.launchForKeyboardSelectionResult(

View File

@@ -23,138 +23,53 @@ import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.os.Build
import android.os.Bundle
import android.util.Log
import android.widget.Toast
import android.view.View
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
import androidx.annotation.RequiresApi
import androidx.appcompat.app.AlertDialog
import androidx.credentials.GetCredentialResponse
import androidx.credentials.exceptions.GetCredentialUnknownException
import androidx.credentials.provider.PendingIntentHandler
import androidx.lifecycle.lifecycleScope
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.FileDatabaseSelectActivity
import com.kunzisoft.keepass.activities.GroupActivity
import com.kunzisoft.keepass.activities.legacy.DatabaseModeActivity
import com.kunzisoft.keepass.activities.legacy.DatabaseLockActivity
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.addSpecialMode
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.addTypeMode
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.buildActivityResultLauncher
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.setActivityResult
import com.kunzisoft.keepass.credentialprovider.SpecialMode
import com.kunzisoft.keepass.credentialprovider.TypeMode
import com.kunzisoft.keepass.credentialprovider.passkey.data.PublicKeyCredentialCreationParameters
import com.kunzisoft.keepass.credentialprovider.passkey.data.PublicKeyCredentialUsageParameters
import com.kunzisoft.keepass.credentialprovider.passkey.data.AndroidPrivilegedApp
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.addNodeId
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.addSearchInfo
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.getVerifiedGETClientDataResponse
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.retrieveAppOrigin
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.retrieveNodeId
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.retrievePasskey
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.retrievePasskeyCreationRequestParameters
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.retrievePasskeyUsageRequestParameters
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.retrieveSearchInfo
import com.kunzisoft.keepass.credentialprovider.passkey.util.PrivilegedAllowLists
import com.kunzisoft.keepass.credentialprovider.passkey.util.PrivilegedAllowLists.saveCustomPrivilegedApps
import com.kunzisoft.keepass.credentialprovider.viewmodel.PasskeyLauncherViewModel
import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.database.element.node.NodeIdUUID
import com.kunzisoft.keepass.database.helper.SearchHelper
import com.kunzisoft.keepass.model.AppOrigin
import com.kunzisoft.keepass.model.Passkey
import com.kunzisoft.keepass.model.RegisterInfo
import com.kunzisoft.keepass.model.SearchInfo
import kotlinx.coroutines.CoroutineExceptionHandler
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_ENTRY_TASK
import com.kunzisoft.keepass.tasks.ActionRunnable
import com.kunzisoft.keepass.view.toastError
import kotlinx.coroutines.launch
import java.io.IOException
import java.io.InvalidObjectException
import java.util.UUID
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
class PasskeyLauncherActivity : DatabaseModeActivity() {
class PasskeyLauncherActivity : DatabaseLockActivity() {
private var mUsageParameters: PublicKeyCredentialUsageParameters? = null
private var mCreationParameters: PublicKeyCredentialCreationParameters? = null
private var mPasskey: Passkey? = null
private val passkeyLauncherViewModel: PasskeyLauncherViewModel by viewModels()
private var mPasskeySelectionActivityResultLauncher: ActivityResultLauncher<Intent>? =
this.buildActivityResultLauncher(
lockDatabase = true,
dataTransformation = { intent ->
// Build a new formatted response from the selection response
val responseIntent = Intent()
try {
Log.d(TAG, "Passkey selection result")
if (intent == null)
throw IOException("Intent is null")
val passkey = intent.retrievePasskey()
?: throw IOException("Passkey is null")
val appOrigin = intent.retrieveAppOrigin()
?: throw IOException("App origin is null")
intent.removePasskey()
intent.removeAppOrigin()
mUsageParameters?.let { usageParameters ->
// Check verified origin
PendingIntentHandler.setGetCredentialResponse(
responseIntent,
GetCredentialResponse(
buildPasskeyPublicKeyCredential(
requestOptions = usageParameters.publicKeyCredentialRequestOptions,
clientDataResponse = getVerifiedGETClientDataResponse(
usageParameters = usageParameters,
appOrigin = appOrigin
),
passkey = passkey
)
)
)
} ?: run {
throw IOException("Usage parameters is null")
this.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
passkeyLauncherViewModel.manageSelectionResult(it)
}
} catch (e: Exception) {
Log.e(TAG, "Unable to create selection response for passkey", e)
showError(e)
}
// Return the response
responseIntent
}
)
private var mPasskeyRegistrationActivityResultLauncher: ActivityResultLauncher<Intent>? =
this.buildActivityResultLauncher(
lockDatabase = true,
dataTransformation = { intent ->
// Build a new formatted response from the creation response
val responseIntent = Intent()
try {
Log.d(TAG, "Passkey registration result")
val passkey = intent?.retrievePasskey()
intent?.removePasskey()
intent?.removeAppOrigin()
// If registered passkey is the same as the one we want to validate,
if (mPasskey == passkey) {
mCreationParameters?.let {
PendingIntentHandler.setCreateCredentialResponse(
intent = responseIntent,
response = buildCreatePublicKeyCredentialResponse(
publicKeyCredentialCreationParameters = it
)
)
this.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
passkeyLauncherViewModel.manageRegistrationResult(it)
}
} else {
throw SecurityException("Passkey was modified before registration")
}
} catch (e: Exception) {
Log.e(TAG, "Unable to create registration response for passkey", e)
showError(e)
}
responseIntent
}
)
override fun applyCustomStyle(): Boolean {
return false
@@ -164,44 +79,112 @@ class PasskeyLauncherActivity : DatabaseModeActivity() {
return false
}
private fun cancelRequest() {
setResult(RESULT_CANCELED)
finish()
override fun finishActivityIfDatabaseNotLoaded(): Boolean {
return false
}
private fun cancelRequest(e: Throwable) {
Log.e(TAG, "Passkey launch error", e)
showError(e)
cancelRequest()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
lifecycleScope.launch {
// Initialize the parameters
passkeyLauncherViewModel.initialize()
// Retrieve the UI
passkeyLauncherViewModel.uiState.collect { uiState ->
when (uiState) {
is PasskeyLauncherViewModel.UIState.Loading -> {
// Nothing to do
}
is PasskeyLauncherViewModel.UIState.ShowAppPrivilegedDialog -> {
showAppPrivilegedDialog(
temptingApp = uiState.temptingApp
)
}
is PasskeyLauncherViewModel.UIState.ShowAppSignatureDialog -> {
showAppSignatureDialog(
temptingApp = uiState.temptingApp,
nodeId = uiState.nodeId
)
}
is PasskeyLauncherViewModel.UIState.SetActivityResult -> {
setActivityResult(
lockDatabase = uiState.lockDatabase,
resultCode = uiState.resultCode,
data = uiState.data
)
}
is PasskeyLauncherViewModel.UIState.ShowError -> {
toastError(uiState.error)
passkeyLauncherViewModel.cancelResult()
}
is PasskeyLauncherViewModel.UIState.LaunchGroupActivityForSelection -> {
GroupActivity.launchForPasskeySelectionResult(
context = this@PasskeyLauncherActivity,
database = uiState.database,
activityResultLauncher = mPasskeySelectionActivityResultLauncher,
searchInfo = null,
autoSearch = false
)
}
is PasskeyLauncherViewModel.UIState.LaunchGroupActivityForRegistration -> {
GroupActivity.launchForRegistration(
context = this@PasskeyLauncherActivity,
activityResultLauncher = mPasskeyRegistrationActivityResultLauncher,
database = uiState.database,
registerInfo = uiState.registerInfo,
typeMode = uiState.typeMode
)
}
is PasskeyLauncherViewModel.UIState.LaunchFileDatabaseSelectActivityForSelection -> {
FileDatabaseSelectActivity.launchForPasskeySelectionResult(
activity = this@PasskeyLauncherActivity,
activityResultLauncher = mPasskeySelectionActivityResultLauncher,
searchInfo = uiState.searchInfo,
)
}
is PasskeyLauncherViewModel.UIState.LaunchFileDatabaseSelectActivityForRegistration -> {
FileDatabaseSelectActivity.launchForRegistration(
context = this@PasskeyLauncherActivity,
activityResultLauncher = mPasskeyRegistrationActivityResultLauncher,
registerInfo = uiState.registerInfo,
typeMode = uiState.typeMode
)
}
is PasskeyLauncherViewModel.UIState.UpdateEntry -> {
updateEntry(uiState.oldEntry, uiState.newEntry)
}
}
}
}
}
/**
* Launch the main action to manage Passkey
*/
private suspend fun launchPasskeyAction(database: ContextualDatabase?) {
val searchInfo = intent.retrieveSearchInfo() ?: SearchInfo()
val appOrigin = intent.retrieveAppOrigin() ?: AppOrigin(verified = false)
val nodeId = intent.retrieveNodeId()
checkSecurity(intent, nodeId)
when (mSpecialMode) {
SpecialMode.SELECTION -> {
launchSelection(database, nodeId, searchInfo, appOrigin)
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
super.onDatabaseRetrieved(database)
passkeyLauncherViewModel.launchPasskeyActionIfNeeded(intent, mSpecialMode, database)
}
SpecialMode.REGISTRATION -> {
// TODO Registration in predefined group
// launchRegistration(database, nodeId, mSearchInfo)
launchRegistration(database, null, searchInfo)
}
else -> {
throw InvalidObjectException("Passkey launch mode not supported")
override fun onDatabaseActionFinished(
database: ContextualDatabase,
actionTask: String,
result: ActionRunnable.Result
) {
super.onDatabaseActionFinished(database, actionTask, result)
when (actionTask) {
ACTION_DATABASE_UPDATE_ENTRY_TASK -> {
passkeyLauncherViewModel.autoSelectPasskey(result, database)
}
}
}
override fun viewToInvalidateTimeout(): View? {
return null
}
/**
* Display a dialog that asks the user to add an app to the list of privileged apps.
*/
private fun showAppPrivilegedDialog(e: PrivilegedAllowLists.PrivilegedException) {
private fun showAppPrivilegedDialog(
temptingApp: AndroidPrivilegedApp
) {
Log.w(javaClass.simpleName, "No privileged apps file found")
AlertDialog.Builder(this@PasskeyLauncherActivity).apply {
setTitle(getString(R.string.passkeys_privileged_apps_ask_title))
@@ -209,7 +192,7 @@ class PasskeyLauncherActivity : DatabaseModeActivity() {
.append(
getString(
R.string.passkeys_privileged_apps_ask_message,
e.temptingApp.toString()
temptingApp.toString()
)
)
.append("\n\n")
@@ -217,210 +200,56 @@ class PasskeyLauncherActivity : DatabaseModeActivity() {
.toString()
)
setPositiveButton(android.R.string.ok) { _, _ ->
lifecycleScope.launch(CoroutineExceptionHandler { _, e ->
cancelRequest(e)
}) {
saveCustomPrivilegedApps(
context = application,
privilegedApps = listOf(e.temptingApp)
passkeyLauncherViewModel.saveCustomPrivilegedApp(
intent = intent,
specialMode = mSpecialMode,
database = mDatabase,
temptingApp = temptingApp
)
launchPasskeyAction(mDatabase)
}
}
setNegativeButton(android.R.string.cancel) { _, _ ->
cancelRequest()
passkeyLauncherViewModel.cancelResult()
}
setOnCancelListener {
cancelRequest()
passkeyLauncherViewModel.cancelResult()
}
}.create().show()
}
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
super.onDatabaseRetrieved(database)
lifecycleScope.launch(CoroutineExceptionHandler { _, e ->
when (e) {
is PrivilegedAllowLists.PrivilegedException -> showAppPrivilegedDialog(e)
else -> cancelRequest(e)
}
}) {
launchPasskeyAction(database)
}
}
private fun autoSelectPasskeyAndSetResult(
database: ContextualDatabase?,
nodeId: UUID,
appOrigin: AppOrigin
/**
* Display a dialog that asks the user to add an app signature in an existing passkey
*/
private fun showAppSignatureDialog(
temptingApp: AppOrigin,
nodeId: UUID
) {
mUsageParameters?.let { usageParameters ->
// To get the passkey from the database
val passkey = database
?.getEntryById(NodeIdUUID(nodeId))
?.getEntryInfo(database)
?.passkey
?: throw GetCredentialUnknownException("No passkey with nodeId $nodeId found")
val result = Intent()
PendingIntentHandler.setGetCredentialResponse(
result,
GetCredentialResponse(
buildPasskeyPublicKeyCredential(
requestOptions = usageParameters.publicKeyCredentialRequestOptions,
clientDataResponse = getVerifiedGETClientDataResponse(
usageParameters = usageParameters,
appOrigin = appOrigin
),
passkey = passkey
AlertDialog.Builder(this@PasskeyLauncherActivity).apply {
setTitle(getString(R.string.passkeys_missing_signature_app_ask_title))
setMessage(StringBuilder()
.append(
getString(
R.string.passkeys_missing_signature_app_ask_message,
temptingApp.toString()
)
)
.append("\n\n")
.append(getString(R.string.passkeys_missing_signature_app_ask_explanation))
.toString()
)
setResult(RESULT_OK, result)
finish()
} ?: run {
Log.e(TAG, "Unable to auto select passkey, usage parameters are empty")
setResult(RESULT_CANCELED)
finish()
}
}
private suspend fun launchSelection(
database: ContextualDatabase?,
nodeId: UUID?,
searchInfo: SearchInfo,
appOrigin: AppOrigin
) {
Log.d(TAG, "Launch passkey selection")
retrievePasskeyUsageRequestParameters(
intent = intent,
context = applicationContext
) { usageParameters ->
// Save the requested parameters
mUsageParameters = usageParameters
// Manage the passkey to use
nodeId?.let { nodeId ->
autoSelectPasskeyAndSetResult(database, nodeId, appOrigin)
} ?: run {
SearchHelper.checkAutoSearchInfo(
context = this,
database = database,
searchInfo = searchInfo,
onItemsFound = { _, _ ->
Log.w(
TAG, "Passkey found for auto selection, should not append," +
"use PasskeyProviderService instead"
)
finish()
},
onItemNotFound = { openedDatabase ->
Log.d(
TAG, "No Passkey found for selection," +
"launch manual selection in opened database"
)
GroupActivity.launchForPasskeySelectionResult(
context = this,
database = openedDatabase,
activityResultLauncher = mPasskeySelectionActivityResultLauncher,
searchInfo = null,
autoSearch = false
)
},
onDatabaseClosed = {
Log.d(TAG, "Manual passkey selection in closed database")
FileDatabaseSelectActivity.launchForPasskeySelectionResult(
activity = this,
activityResultLauncher = mPasskeySelectionActivityResultLauncher,
searchInfo = searchInfo,
setPositiveButton(android.R.string.ok) { _, _ ->
passkeyLauncherViewModel.saveAppSignature(
database = mDatabase,
temptingApp = temptingApp,
nodeId = nodeId
)
}
)
setNegativeButton(android.R.string.cancel) { _, _ ->
passkeyLauncherViewModel.cancelResult()
}
setOnCancelListener {
passkeyLauncherViewModel.cancelResult()
}
}
private fun autoRegisterPasskeyAndSetResult(
database: ContextualDatabase?,
nodeId: UUID,
passkey: Passkey
) {
// TODO Overwrite and Register in a predefined group
mCreationParameters?.let { creationParameters ->
// To set the passkey to the database
setResult(RESULT_OK)
finish()
} ?: run {
Log.e(TAG, "Unable to auto select passkey, usage parameters are empty")
setResult(RESULT_CANCELED)
finish()
}
}
private suspend fun launchRegistration(
database: ContextualDatabase?,
nodeId: UUID?,
searchInfo: SearchInfo
) {
Log.d(TAG, "Launch passkey registration")
retrievePasskeyCreationRequestParameters(
intent = intent,
context = applicationContext,
passkeyCreated = { passkey, appInfoToStore, publicKeyCredentialParameters ->
// Save the requested parameters
mPasskey = passkey
mCreationParameters = publicKeyCredentialParameters
// Manage the passkey and create a register info
val registerInfo = RegisterInfo(
searchInfo = searchInfo,
passkey = passkey,
appOrigin = appInfoToStore
)
// If nodeId already provided
nodeId?.let { nodeId ->
autoRegisterPasskeyAndSetResult(database, nodeId, passkey)
} ?: run {
SearchHelper.checkAutoSearchInfo(
context = this,
database = database,
searchInfo = searchInfo,
onItemsFound = { openedDatabase, _ ->
Log.w(TAG, "Passkey found for registration, " +
"but launch manual registration for a new entry")
GroupActivity.launchForRegistration(
context = this,
activityResultLauncher = mPasskeyRegistrationActivityResultLauncher,
database = openedDatabase,
registerInfo = registerInfo,
typeMode = TypeMode.PASSKEY
)
},
onItemNotFound = { openedDatabase ->
Log.d(TAG, "Launch new manual registration in opened database")
GroupActivity.launchForRegistration(
context = this,
activityResultLauncher = mPasskeyRegistrationActivityResultLauncher,
database = openedDatabase,
registerInfo = registerInfo,
typeMode = TypeMode.PASSKEY
)
},
onDatabaseClosed = {
Log.d(TAG, "Manual passkey registration in closed database")
FileDatabaseSelectActivity.launchForRegistration(
context = this,
activityResultLauncher = mPasskeyRegistrationActivityResultLauncher,
registerInfo = registerInfo,
typeMode = TypeMode.PASSKEY
)
}
)
}
}
)
}
private fun showError(e: Throwable) {
Toast.makeText(this, e.localizedMessage, Toast.LENGTH_LONG).show()
}.create().show()
}
companion object {

View File

@@ -50,8 +50,12 @@ import com.kunzisoft.keepass.credentialprovider.passkey.data.PublicKeyCredential
import com.kunzisoft.keepass.credentialprovider.passkey.data.PublicKeyCredentialRequestOptions
import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.database.DatabaseTaskProvider
import com.kunzisoft.keepass.database.exception.RegisterInReadOnlyDatabaseException
import com.kunzisoft.keepass.database.helper.SearchHelper
import com.kunzisoft.keepass.model.SearchInfo
import com.kunzisoft.keepass.settings.PreferencesUtil.isPasskeyAutoSelectEnable
import com.kunzisoft.keepass.view.toastError
import java.io.IOException
import java.time.Instant
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
@@ -60,6 +64,7 @@ class PasskeyProviderService : CredentialProviderService() {
private var mDatabaseTaskProvider: DatabaseTaskProvider? = null
private var mDatabase: ContextualDatabase? = null
private lateinit var defaultIcon: Icon
private var isAutoSelectAllowed: Boolean = false
override fun onCreate() {
super.onCreate()
@@ -76,6 +81,8 @@ class PasskeyProviderService : CredentialProviderService() {
).apply {
setTintBlendMode(BlendMode.DST)
}
isAutoSelectAllowed = isPasskeyAutoSelectEnable(this)
}
override fun onDestroy() {
@@ -157,7 +164,7 @@ class PasskeyProviderService : CredentialProviderService() {
pendingIntent = usagePendingIntent,
beginGetPublicKeyCredentialOption = option,
displayName = passkeyEntry.getVisualTitle(),
isAutoSelectAllowed = true
isAutoSelectAllowed = isAutoSelectAllowed
)
)
}
@@ -174,13 +181,13 @@ class PasskeyProviderService : CredentialProviderService() {
passkeyEntries.add(
PublicKeyCredentialEntry(
context = applicationContext,
username = getString(R.string.passkey_locked_database_username),
username = getString(R.string.passkey_database_username),
displayName = getString(R.string.passkey_selection_description),
icon = defaultIcon,
pendingIntent = pendingIntent,
beginGetPublicKeyCredentialOption = option,
lastUsedTime = Instant.now(),
isAutoSelectAllowed = false
isAutoSelectAllowed = isAutoSelectAllowed
)
)
}
@@ -196,13 +203,13 @@ class PasskeyProviderService : CredentialProviderService() {
passkeyEntries.add(
PublicKeyCredentialEntry(
context = applicationContext,
username = getString(R.string.passkey_locked_database_username),
username = getString(R.string.passkey_database_username),
displayName = getString(R.string.passkey_locked_database_description),
icon = defaultIcon,
pendingIntent = pendingIntent,
beginGetPublicKeyCredentialOption = option,
lastUsedTime = Instant.now(),
isAutoSelectAllowed = true
isAutoSelectAllowed = isAutoSelectAllowed
)
)
}
@@ -218,18 +225,15 @@ class PasskeyProviderService : CredentialProviderService() {
) {
Log.d(javaClass.simpleName, "onBeginCreateCredentialRequest called")
try {
processCreateCredentialRequest(request)?.let { response ->
callback.onResult(response)
} ?: let {
callback.onError(CreateCredentialUnknownException())
}
callback.onResult(processCreateCredentialRequest(request))
} catch (e: Exception) {
Log.e(javaClass.simpleName, "onBeginCreateCredentialRequest error", e)
callback.onError(CreateCredentialUnknownException())
toastError(e)
callback.onError(CreateCredentialUnknownException(e.localizedMessage))
}
}
private fun processCreateCredentialRequest(request: BeginCreateCredentialRequest): BeginCreateCredentialResponse? {
private fun processCreateCredentialRequest(request: BeginCreateCredentialRequest): BeginCreateCredentialResponse {
when (request) {
is BeginCreatePublicKeyCredentialRequest -> {
// Request is passkey type
@@ -237,8 +241,7 @@ class PasskeyProviderService : CredentialProviderService() {
}
}
// request type not supported
Log.w(javaClass.simpleName, "unknown type of BeginCreateCredentialRequest")
return null
throw IOException("unknown type of BeginCreateCredentialRequest")
}
private fun MutableList<CreateEntry>.addPendingIntentCreationNewEntry(
@@ -265,7 +268,7 @@ class PasskeyProviderService : CredentialProviderService() {
private fun handleCreatePasskeyQuery(request: BeginCreatePublicKeyCredentialRequest): BeginCreateCredentialResponse {
val accountName = mDatabase?.name ?: getString(R.string.passkey_locked_database_username)
val accountName = mDatabase?.name ?: getString(R.string.passkey_database_username)
val createEntries: MutableList<CreateEntry> = mutableListOf()
val relyingPartyId = PublicKeyCredentialCreationOptions(
requestJson = request.requestJson,
@@ -279,9 +282,7 @@ class PasskeyProviderService : CredentialProviderService() {
searchInfo = searchInfo,
onItemsFound = { database, items ->
if (database.isReadOnly) {
throw CreateCredentialUnknownException(
"Unable to register or overwrite a passkey in a database that is read only"
)
throw RegisterInReadOnlyDatabaseException()
} else {
// To create a new entry
createEntries.addPendingIntentCreationNewEntry(accountName, searchInfo)
@@ -312,9 +313,7 @@ class PasskeyProviderService : CredentialProviderService() {
onItemNotFound = { database ->
// To create a new entry
if (database.isReadOnly) {
throw CreateCredentialUnknownException(
"Unable to register a new passkey in a database that is read only"
)
throw RegisterInReadOnlyDatabaseException()
} else {
createEntries.addPendingIntentCreationNewEntry(accountName, searchInfo)
}

View File

@@ -19,6 +19,7 @@
*/
package com.kunzisoft.keepass.credentialprovider.passkey.data
import android.util.Log
import androidx.credentials.exceptions.GetCredentialUnknownException
import com.kunzisoft.encrypt.Signature
import com.kunzisoft.encrypt.Base64Helper.Companion.b64Encode
@@ -46,8 +47,12 @@ class AuthenticatorAssertionResponse(
private var signature: ByteArray = byteArrayOf()
init {
try {
signature = Signature.sign(privateKey, dataToSign())
?: throw GetCredentialUnknownException("signing failed")
} catch (e: Exception) {
Log.e(this::class.java.simpleName, "Unable to sign: ${e.message}")
throw GetCredentialUnknownException("Signing failed")
}
}
private fun dataToSign(): ByteArray {

View File

@@ -24,5 +24,5 @@ import com.kunzisoft.keepass.model.AppOrigin
data class PublicKeyCredentialUsageParameters(
val publicKeyCredentialRequestOptions: PublicKeyCredentialRequestOptions,
val clientDataResponse: ClientDataResponse,
val appOrigin: AppOrigin
var appOrigin: AppOrigin
)

View File

@@ -28,6 +28,7 @@ import android.os.ParcelUuid
import android.security.keystore.KeyGenParameterSpec
import android.security.keystore.KeyProperties
import android.util.Log
import android.widget.Toast
import androidx.annotation.RequiresApi
import androidx.credentials.CreatePublicKeyCredentialRequest
import androidx.credentials.CreatePublicKeyCredentialResponse
@@ -42,6 +43,7 @@ import androidx.credentials.provider.ProviderGetCredentialRequest
import com.kunzisoft.encrypt.Base64Helper.Companion.b64Encode
import com.kunzisoft.encrypt.Signature
import com.kunzisoft.encrypt.Signature.getApplicationFingerprints
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.credentialprovider.passkey.data.AuthenticatorAssertionResponse
import com.kunzisoft.keepass.credentialprovider.passkey.data.AuthenticatorAttestationResponse
import com.kunzisoft.keepass.credentialprovider.passkey.data.Cbor
@@ -59,10 +61,12 @@ import com.kunzisoft.keepass.model.AppOrigin
import com.kunzisoft.keepass.model.EntryInfo
import com.kunzisoft.keepass.model.Passkey
import com.kunzisoft.keepass.model.SearchInfo
import com.kunzisoft.keepass.utils.AppUtil
import com.kunzisoft.keepass.utils.StringUtil.toHexString
import com.kunzisoft.keepass.utils.getParcelableExtraCompat
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.io.IOException
import java.security.KeyStore
import java.security.MessageDigest
import java.security.SecureRandom
@@ -114,21 +118,26 @@ object PasskeyHelper {
extras: Bundle? = null
) {
try {
entryInfo.passkey?.let {
entryInfo.passkey?.let { passkey ->
val mReplyIntent = Intent()
Log.d(javaClass.name, "Success Passkey manual selection")
mReplyIntent.putExtra(EXTRA_PASSKEY, entryInfo.passkey)
mReplyIntent.putExtra(EXTRA_APP_ORIGIN, entryInfo.appOrigin)
mReplyIntent.addPasskey(passkey)
mReplyIntent.addAppOrigin(entryInfo.appOrigin)
mReplyIntent.addNodeId(entryInfo.id)
extras?.let {
mReplyIntent.putExtras(it)
}
setResult(Activity.RESULT_OK, mReplyIntent)
} ?: run {
Log.w(javaClass.name, "Failed Passkey manual selection")
setResult(Activity.RESULT_CANCELED)
throw IOException("No passkey found")
}
} catch (e: Exception) {
Log.e(javaClass.name, "Cant add passkey entry as result", e)
Log.e(javaClass.name, "Unable to add the passkey as result", e)
Toast.makeText(
this,
getString(R.string.error_passkey_result),
Toast.LENGTH_SHORT
).show()
setResult(Activity.RESULT_CANCELED)
}
}
@@ -149,6 +158,15 @@ object PasskeyHelper {
})
}
/**
* Add the passkey to the intent
*/
fun Intent.addPasskey(passkey: Passkey?) {
passkey?.let {
putExtra(EXTRA_PASSKEY, passkey)
}
}
/**
* Retrieve the passkey from the intent
*/
@@ -342,8 +360,8 @@ object PasskeyHelper {
providedClientDataHash: ByteArray?,
callingAppInfo: CallingAppInfo?,
context: Context,
onOriginRetrieved: (appOrigin: AppOrigin, clientDataHash: ByteArray) -> Unit,
onOriginNotRetrieved: (appOrigin: AppOrigin, androidOriginString: String) -> Unit
onOriginRetrieved: suspend (appOrigin: AppOrigin, clientDataHash: ByteArray) -> Unit,
onOriginNotRetrieved: suspend (appOrigin: AppOrigin, androidOriginString: String) -> Unit
) {
if (callingAppInfo == null) {
throw SecurityException("Calling app info cannot be retrieved")
@@ -351,7 +369,17 @@ object PasskeyHelper {
withContext(Dispatchers.IO) {
// For trusted browsers like Chrome and Firefox
val callOrigin = getOriginFromPrivilegedAllowLists(callingAppInfo, context)
val callOrigin = try {
getOriginFromPrivilegedAllowLists(callingAppInfo, context)
} catch (e: Exception) {
// Throw the Privileged Exception only if it's a browser
if (e is PrivilegedAllowLists.PrivilegedException
&& AppUtil.getInstalledBrowsersWithSignatures(context).any {
it.packageName == e.temptingApp.packageName
}
) throw e
null
}
// Build the default Android origin
val androidOrigin = AndroidOrigin(
@@ -401,7 +429,7 @@ object PasskeyHelper {
suspend fun retrievePasskeyCreationRequestParameters(
intent: Intent,
context: Context,
passkeyCreated: (Passkey, AppOrigin?, PublicKeyCredentialCreationParameters) -> Unit
passkeyCreated: suspend (Passkey, AppOrigin?, PublicKeyCredentialCreationParameters) -> Unit
) {
val createCredentialRequest = PendingIntentHandler.retrieveProviderCreateCredentialRequest(intent)
if (createCredentialRequest == null)
@@ -472,7 +500,9 @@ object PasskeyHelper {
* by calling this method the user is always recognized as present and verified
*/
fun buildCreatePublicKeyCredentialResponse(
publicKeyCredentialCreationParameters: PublicKeyCredentialCreationParameters
publicKeyCredentialCreationParameters: PublicKeyCredentialCreationParameters,
backupEligibility: Boolean,
backupState: Boolean
): CreatePublicKeyCredentialResponse {
val keyPair = publicKeyCredentialCreationParameters.signatureKey.first
@@ -489,8 +519,8 @@ object PasskeyHelper {
) ?: mapOf<Int, Any>()),
userPresent = true,
userVerified = true,
backupEligibility = BACKUP_ELIGIBILITY,
backupState = false, // TODO Setting to add a backup manually #2135
backupEligibility = backupEligibility,
backupState = backupState,
publicKeyTypeId = keyTypeId,
publicKeyCbor = Signature.convertPublicKey(keyPair.public, keyTypeId)!!,
clientDataResponse = publicKeyCredentialCreationParameters.clientDataResponse
@@ -511,7 +541,7 @@ object PasskeyHelper {
suspend fun retrievePasskeyUsageRequestParameters(
intent: Intent,
context: Context,
result: (PublicKeyCredentialUsageParameters) -> Unit
result: suspend (PublicKeyCredentialUsageParameters) -> Unit
) {
val getCredentialRequest = PendingIntentHandler.retrieveProviderGetCredentialRequest(intent)
if (getCredentialRequest == null)
@@ -559,7 +589,9 @@ object PasskeyHelper {
fun buildPasskeyPublicKeyCredential(
requestOptions: PublicKeyCredentialRequestOptions,
clientDataResponse: ClientDataResponse,
passkey: Passkey
passkey: Passkey,
backupEligibility: Boolean,
backupState: Boolean
): PublicKeyCredential {
val getCredentialResponse = FidoPublicKeyCredential(
id = passkey.credentialId,
@@ -567,8 +599,8 @@ object PasskeyHelper {
requestOptions = requestOptions,
userPresent = true,
userVerified = true,
backupEligibility = BACKUP_ELIGIBILITY,
backupState = false, // TODO Setting to add a backup manually #2135
backupEligibility = backupEligibility,
backupState = backupState,
userHandle = passkey.userHandle,
privateKey = passkey.privateKeyPem,
clientDataResponse = clientDataResponse
@@ -599,6 +631,4 @@ object PasskeyHelper {
)
}
}
private const val BACKUP_ELIGIBILITY = true
}

View File

@@ -0,0 +1,584 @@
package com.kunzisoft.keepass.credentialprovider.viewmodel
import android.app.Activity.RESULT_CANCELED
import android.app.Activity.RESULT_OK
import android.app.Application
import android.content.Intent
import android.os.Build
import android.util.Log
import androidx.activity.result.ActivityResult
import androidx.annotation.RequiresApi
import androidx.credentials.GetCredentialResponse
import androidx.credentials.exceptions.GetCredentialUnknownException
import androidx.credentials.provider.PendingIntentHandler
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope
import com.kunzisoft.keepass.credentialprovider.SpecialMode
import com.kunzisoft.keepass.credentialprovider.TypeMode
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.getVerifiedGETClientDataResponse
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.retrieveAppOrigin
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.retrieveNodeId
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.retrievePasskey
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.retrievePasskeyCreationRequestParameters
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.retrievePasskeyUsageRequestParameters
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.retrieveSearchInfo
import com.kunzisoft.keepass.credentialprovider.passkey.util.PrivilegedAllowLists
import com.kunzisoft.keepass.credentialprovider.passkey.util.PrivilegedAllowLists.saveCustomPrivilegedApps
import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.database.element.Entry
import com.kunzisoft.keepass.database.element.node.NodeIdUUID
import com.kunzisoft.keepass.database.exception.RegisterInReadOnlyDatabaseException
import com.kunzisoft.keepass.database.helper.SearchHelper
import com.kunzisoft.keepass.model.AppOrigin
import com.kunzisoft.keepass.model.Passkey
import com.kunzisoft.keepass.model.RegisterInfo
import com.kunzisoft.keepass.model.SearchInfo
import com.kunzisoft.keepass.model.SignatureNotFoundException
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.getNewEntry
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.tasks.ActionRunnable
import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.IOException
import java.io.InvalidObjectException
import java.util.UUID
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
class PasskeyLauncherViewModel(application: Application): AndroidViewModel(application) {
private var mUsageParameters: PublicKeyCredentialUsageParameters? = null
private var mCreationParameters: PublicKeyCredentialCreationParameters? = null
private var mPasskey: Passkey? = null
private var mBackupEligibility: Boolean = true
private var mBackupState: Boolean = false
private var mLockDatabase: Boolean = true
private var isResultLauncherRegistered: Boolean = false
private val _uiState = MutableStateFlow<UIState>(UIState.Loading)
val uiState: StateFlow<UIState> = _uiState
fun initialize() {
mBackupEligibility = PreferencesUtil.isPasskeyBackupEligibilityEnable(getApplication())
mBackupState = PreferencesUtil.isPasskeyBackupStateEnable(getApplication())
}
fun showAppPrivilegedDialog(
temptingApp: AndroidPrivilegedApp
) {
_uiState.value = UIState.ShowAppPrivilegedDialog(temptingApp)
}
fun showAppSignatureDialog(
temptingApp: AppOrigin,
nodeId: UUID
) {
_uiState.value = UIState.ShowAppSignatureDialog(temptingApp, nodeId)
}
fun showError(error: Throwable) {
Log.e(TAG, "Error on passkey launch", error)
_uiState.value = UIState.ShowError(error)
}
fun saveCustomPrivilegedApp(
intent: Intent,
specialMode: SpecialMode,
database: ContextualDatabase?,
temptingApp: AndroidPrivilegedApp
) {
viewModelScope.launch(CoroutineExceptionHandler { _, e ->
showError(e)
}) {
saveCustomPrivilegedApps(
context = getApplication(),
privilegedApps = listOf(temptingApp)
)
launchPasskeyAction(
intent = intent,
specialMode = specialMode,
database = database
)
}
}
fun saveAppSignature(
database: ContextualDatabase?,
temptingApp: AppOrigin,
nodeId: UUID
) {
viewModelScope.launch(CoroutineExceptionHandler { _, e ->
showError(e)
}) {
// Update the entry with app signature
val entry = database
?.getEntryById(NodeIdUUID(nodeId))
?: throw GetCredentialUnknownException(
"No passkey with nodeId $nodeId found"
)
if (database.isReadOnly)
throw RegisterInReadOnlyDatabaseException()
val newEntry = Entry(entry)
val entryInfo = newEntry.getEntryInfo(
database,
raw = true,
removeTemplateConfiguration = false
)
entryInfo.saveAppOrigin(database, temptingApp)
newEntry.setEntryInfo(database, entryInfo)
_uiState.value = UIState.UpdateEntry(
oldEntry = entry,
newEntry = newEntry
)
}
}
fun setResult(intent: Intent) {
// Remove the launcher register
isResultLauncherRegistered = false
_uiState.value = UIState.SetActivityResult(
lockDatabase = mLockDatabase,
resultCode = RESULT_OK,
data = intent
)
}
fun cancelResult() {
isResultLauncherRegistered = false
_uiState.value = UIState.SetActivityResult(
lockDatabase = mLockDatabase,
resultCode = RESULT_CANCELED
)
}
fun launchPasskeyActionIfNeeded(
intent: Intent,
specialMode: SpecialMode,
database: ContextualDatabase?
) {
if (isResultLauncherRegistered.not()) {
isResultLauncherRegistered = true
viewModelScope.launch(CoroutineExceptionHandler { _, e ->
if (e is PrivilegedAllowLists.PrivilegedException) {
showAppPrivilegedDialog(e.temptingApp)
} else {
showError(e)
}
}) {
launchPasskeyAction(intent, specialMode, database)
}
}
}
/**
* Launch the main action to manage Passkey
*/
private suspend fun launchPasskeyAction(
intent: Intent,
specialMode: SpecialMode,
database: ContextualDatabase?
) {
val searchInfo = intent.retrieveSearchInfo() ?: SearchInfo()
val appOrigin = intent.retrieveAppOrigin() ?: AppOrigin(verified = false)
val nodeId = intent.retrieveNodeId()
checkSecurity(intent, nodeId)
when (specialMode) {
SpecialMode.SELECTION -> {
launchSelection(
intent = intent,
database = database,
nodeId = nodeId,
searchInfo = searchInfo,
appOrigin = appOrigin
)
}
SpecialMode.REGISTRATION -> {
// TODO Registration in predefined group
// launchRegistration(database, nodeId, mSearchInfo)
launchRegistration(
intent = intent,
database = database,
nodeId = null,
searchInfo = searchInfo
)
}
else -> {
throw InvalidObjectException("Passkey launch mode not supported")
}
}
}
// -------------
// Selection
// -------------
private suspend fun launchSelection(
intent: Intent,
database: ContextualDatabase?,
nodeId: UUID?,
searchInfo: SearchInfo,
appOrigin: AppOrigin
) {
withContext(Dispatchers.IO) {
Log.d(TAG, "Launch passkey selection")
retrievePasskeyUsageRequestParameters(
intent = intent,
context = getApplication()
) { usageParameters ->
// Save the requested parameters
mUsageParameters = usageParameters
// Manage the passkey to use
nodeId?.let { nodeId ->
autoSelectPasskeyAndSetResult(database, nodeId, appOrigin)
} ?: run {
SearchHelper.checkAutoSearchInfo(
context = getApplication(),
database = database,
searchInfo = searchInfo,
onItemsFound = { _, _ ->
Log.w(
TAG, "Passkey found for auto selection, should not append," +
"use PasskeyProviderService instead"
)
cancelResult()
},
onItemNotFound = { openedDatabase ->
Log.d(
TAG, "No Passkey found for selection," +
"launch manual selection in opened database"
)
_uiState.value = UIState.LaunchGroupActivityForSelection(
database = openedDatabase
)
},
onDatabaseClosed = {
Log.d(TAG, "Manual passkey selection in closed database")
_uiState.value =
UIState.LaunchFileDatabaseSelectActivityForSelection(
searchInfo = searchInfo
)
}
)
}
}
}
}
fun autoSelectPasskey(
result: ActionRunnable.Result,
database: ContextualDatabase
) {
viewModelScope.launch(CoroutineExceptionHandler { _, e ->
showError(e)
}) {
if (result.isSuccess) {
val entry = result.data?.getNewEntry(database)
?: throw IOException("No passkey entry found")
autoSelectPasskeyAndSetResult(
database = database,
nodeId = entry.nodeId.id,
appOrigin = entry.getAppOrigin()
?: throw IOException("No App origin found")
)
} else throw result.exception
?: IOException("Unable to auto select passkey")
}
}
private suspend fun autoSelectPasskeyAndSetResult(
database: ContextualDatabase?,
nodeId: UUID,
appOrigin: AppOrigin
) {
withContext(Dispatchers.IO) {
mUsageParameters?.let { usageParameters ->
// To get the passkey from the database
val passkey = database
?.getEntryById(NodeIdUUID(nodeId))
?.getEntryInfo(database)
?.passkey
?: throw IOException(
"No passkey with nodeId $nodeId found"
)
// Build the response
val result = Intent()
try {
PendingIntentHandler.setGetCredentialResponse(
result,
GetCredentialResponse(
buildPasskeyPublicKeyCredential(
requestOptions = usageParameters.publicKeyCredentialRequestOptions,
clientDataResponse = getVerifiedGETClientDataResponse(
usageParameters = usageParameters,
appOrigin = appOrigin
),
passkey = passkey,
backupEligibility = mBackupEligibility,
backupState = mBackupState
)
)
)
setResult(result)
} catch (e: SignatureNotFoundException) {
// Request the dialog if signature exception
showAppSignatureDialog(e.temptingApp, nodeId)
}
} ?: throw IOException("Usage parameters is null")
}
}
fun manageSelectionResult(
activityResult: ActivityResult
) {
val intent = activityResult.data
viewModelScope.launch(CoroutineExceptionHandler { _, e ->
Log.e(TAG, "Unable to create selection response for passkey", e)
if (e is SignatureNotFoundException) {
intent?.retrieveNodeId()?.let { nodeId ->
showAppSignatureDialog(e.temptingApp, nodeId)
} ?: cancelResult()
} else {
showError(e)
}
}) {
// Build a new formatted response from the selection response
val responseIntent = Intent()
when (activityResult.resultCode) {
RESULT_OK -> {
withContext(Dispatchers.IO) {
Log.d(TAG, "Passkey selection result")
if (intent == null)
throw IOException("Intent is null")
val passkey = intent.retrievePasskey()
?: throw IOException("Passkey is null")
val appOrigin = intent.retrieveAppOrigin()
?: throw IOException("App origin is null")
intent.removePasskey()
intent.removeAppOrigin()
mUsageParameters?.let { usageParameters ->
// Check verified origin
PendingIntentHandler.setGetCredentialResponse(
responseIntent,
GetCredentialResponse(
buildPasskeyPublicKeyCredential(
requestOptions = usageParameters.publicKeyCredentialRequestOptions,
clientDataResponse = getVerifiedGETClientDataResponse(
usageParameters = usageParameters,
appOrigin = appOrigin
),
passkey = passkey,
backupEligibility = mBackupEligibility,
backupState = mBackupState
)
)
)
} ?: run {
throw IOException("Usage parameters is null")
}
withContext(Dispatchers.Main) {
setResult(responseIntent)
}
}
}
RESULT_CANCELED -> {
withContext(Dispatchers.Main) {
cancelResult()
}
}
}
}
}
// -------------
// Registration
// -------------
private suspend fun launchRegistration(
intent: Intent,
database: ContextualDatabase?,
nodeId: UUID?,
searchInfo: SearchInfo
) {
withContext(Dispatchers.IO) {
Log.d(TAG, "Launch passkey registration")
retrievePasskeyCreationRequestParameters(
intent = intent,
context = getApplication(),
passkeyCreated = { passkey, appInfoToStore, publicKeyCredentialParameters ->
// Save the requested parameters
mPasskey = passkey
mCreationParameters = publicKeyCredentialParameters
// Manage the passkey and create a register info
val registerInfo = RegisterInfo(
searchInfo = searchInfo,
passkey = passkey,
appOrigin = appInfoToStore
)
// If nodeId already provided
nodeId?.let { nodeId ->
autoRegisterPasskeyAndSetResult(database, nodeId, passkey)
} ?: run {
SearchHelper.checkAutoSearchInfo(
context = getApplication(),
database = database,
searchInfo = searchInfo,
onItemsFound = { openedDatabase, _ ->
Log.w(
TAG, "Passkey found for registration, " +
"but launch manual registration for a new entry"
)
_uiState.value = UIState.LaunchGroupActivityForRegistration(
database = openedDatabase,
registerInfo = registerInfo,
typeMode = TypeMode.PASSKEY
)
},
onItemNotFound = { openedDatabase ->
Log.d(TAG, "Launch new manual registration in opened database")
_uiState.value = UIState.LaunchGroupActivityForRegistration(
database = openedDatabase,
registerInfo = registerInfo,
typeMode = TypeMode.PASSKEY
)
},
onDatabaseClosed = {
Log.d(TAG, "Manual passkey registration in closed database")
_uiState.value =
UIState.LaunchFileDatabaseSelectActivityForRegistration(
registerInfo = registerInfo,
typeMode = TypeMode.PASSKEY
)
}
)
}
}
)
}
}
private suspend fun autoRegisterPasskeyAndSetResult(
database: ContextualDatabase?,
nodeId: UUID,
passkey: Passkey
) {
withContext(Dispatchers.IO) {
mCreationParameters?.let { creationParameters ->
// To set the passkey to the database
// TODO Overwrite and Register in a predefined group
withContext(Dispatchers.Main) {
setResult(Intent())
}
} ?: run {
withContext(Dispatchers.Main) {
Log.e(TAG, "Unable to auto select passkey, usage parameters are empty")
cancelResult()
}
}
}
}
fun manageRegistrationResult(activityResult: ActivityResult) {
val intent = activityResult.data
viewModelScope.launch(CoroutineExceptionHandler { _, e ->
Log.e(TAG, "Unable to create registration response for passkey", e)
if (e is SignatureNotFoundException) {
intent?.retrieveNodeId()?.let { nodeId ->
showAppSignatureDialog(e.temptingApp, nodeId)
} ?: cancelResult()
} else {
showError(e)
}
}) {
// Build a new formatted response from the creation response
val responseIntent = Intent()
when (activityResult.resultCode) {
RESULT_OK -> {
withContext(Dispatchers.IO) {
Log.d(TAG, "Passkey registration result")
val passkey = intent?.retrievePasskey()
intent?.removePasskey()
intent?.removeAppOrigin()
// If registered passkey is the same as the one we want to validate,
if (mPasskey == passkey) {
mCreationParameters?.let {
PendingIntentHandler.setCreateCredentialResponse(
intent = responseIntent,
response = buildCreatePublicKeyCredentialResponse(
publicKeyCredentialCreationParameters = it,
backupEligibility = mBackupEligibility,
backupState = mBackupState
)
)
}
} else {
throw SecurityException("Passkey was modified before registration")
}
withContext(Dispatchers.Main) {
setResult(responseIntent)
}
}
}
RESULT_CANCELED -> {
withContext(Dispatchers.Main) {
cancelResult()
}
}
}
}
}
sealed class UIState {
object Loading : UIState()
data class ShowAppPrivilegedDialog(
val temptingApp: AndroidPrivilegedApp
): UIState()
data class ShowAppSignatureDialog(
val temptingApp: AppOrigin,
val nodeId: UUID
): UIState()
data class LaunchGroupActivityForSelection(
val database: ContextualDatabase
): UIState()
data class LaunchGroupActivityForRegistration(
val database: ContextualDatabase,
val registerInfo: RegisterInfo,
val typeMode: TypeMode
): UIState()
data class LaunchFileDatabaseSelectActivityForSelection(
val searchInfo: SearchInfo
): UIState()
data class LaunchFileDatabaseSelectActivityForRegistration(
val registerInfo: RegisterInfo,
val typeMode: TypeMode
): UIState()
data class SetActivityResult(
val lockDatabase: Boolean,
val resultCode: Int,
val data: Intent? = null
): UIState()
data class ShowError(
val error: Throwable
): UIState()
data class UpdateEntry(
val oldEntry: Entry,
val newEntry: Entry
): UIState()
}
companion object {
private val TAG = PasskeyLauncherViewModel::class.java.name
}
}

View File

@@ -27,7 +27,6 @@ import com.kunzisoft.keepass.database.element.template.TemplateField
import com.kunzisoft.keepass.database.exception.CopyEntryDatabaseException
import com.kunzisoft.keepass.database.exception.CopyGroupDatabaseException
import com.kunzisoft.keepass.database.exception.CorruptedDatabaseException
import com.kunzisoft.keepass.database.exception.DatabaseException
import com.kunzisoft.keepass.database.exception.DatabaseInputException
import com.kunzisoft.keepass.database.exception.DatabaseOutputException
import com.kunzisoft.keepass.database.exception.DuplicateUuidDatabaseException
@@ -37,10 +36,12 @@ import com.kunzisoft.keepass.database.exception.HardwareKeyDatabaseException
import com.kunzisoft.keepass.database.exception.InvalidAlgorithmDatabaseException
import com.kunzisoft.keepass.database.exception.InvalidCredentialsDatabaseException
import com.kunzisoft.keepass.database.exception.KDFMemoryDatabaseException
import com.kunzisoft.keepass.database.exception.LocalizedException
import com.kunzisoft.keepass.database.exception.MergeDatabaseKDBException
import com.kunzisoft.keepass.database.exception.MoveEntryDatabaseException
import com.kunzisoft.keepass.database.exception.MoveGroupDatabaseException
import com.kunzisoft.keepass.database.exception.NoMemoryDatabaseException
import com.kunzisoft.keepass.database.exception.RegisterInReadOnlyDatabaseException
import com.kunzisoft.keepass.database.exception.SignatureDatabaseException
import com.kunzisoft.keepass.database.exception.UnknownDatabaseLocationException
import com.kunzisoft.keepass.database.exception.VersionDatabaseException
@@ -52,12 +53,13 @@ import com.kunzisoft.keepass.model.PasskeyEntryFields.FIELD_USERNAME
import com.kunzisoft.keepass.model.PasskeyEntryFields.FIELD_USER_HANDLE
import com.kunzisoft.keepass.model.PasskeyEntryFields.PASSKEY_FIELD
fun DatabaseException.getLocalizedMessage(resources: Resources): String? =
fun LocalizedException.getLocalizedMessage(resources: Resources): String? =
when (this) {
is FileNotFoundDatabaseException -> resources.getString(R.string.file_not_found_content)
is CorruptedDatabaseException -> resources.getString(R.string.corrupted_file)
is InvalidAlgorithmDatabaseException -> resources.getString(R.string.invalid_algorithm)
is UnknownDatabaseLocationException -> resources.getString(R.string.error_location_unknown)
is RegisterInReadOnlyDatabaseException -> resources.getString(R.string.error_save_read_only)
is HardwareKeyDatabaseException -> resources.getString(R.string.error_hardware_key_unsupported)
is EmptyKeyDatabaseException -> resources.getString(R.string.error_empty_key)
is SignatureDatabaseException -> resources.getString(R.string.invalid_db_sig)

View File

@@ -1385,6 +1385,15 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
return nodesAction
}
fun Bundle.getNewEntry(database: ContextualDatabase): Entry? {
getBundle(NEW_NODES_KEY)
?.getParcelableList<NodeId<UUID>>(ENTRIES_ID_KEY)
?.get(0)?.let {
return database.getEntryById(it)
}
return null
}
fun getBundleFromListNodes(nodes: List<Node>): Bundle {
val groupsId = mutableListOf<NodeId<*>>()
val entriesId = mutableListOf<NodeId<UUID>>()

View File

@@ -686,6 +686,26 @@ object PreferencesUtil {
context.resources.getBoolean(R.bool.keyboard_previous_lock_default))
}
fun isPasskeyBackupEligibilityEnable(context: Context): Boolean {
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
return prefs.getBoolean(context.getString(R.string.passkeys_backup_eligibility_key),
context.resources.getBoolean(R.bool.passkeys_backup_eligibility_default))
}
fun isPasskeyAutoSelectEnable(context: Context): Boolean {
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
return prefs.getBoolean(context.getString(R.string.passkeys_auto_select_key),
context.resources.getBoolean(R.bool.passkeys_auto_select_default))
}
fun isPasskeyBackupStateEnable(context: Context): Boolean {
if (!isPasskeyBackupEligibilityEnable(context))
return false
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
return prefs.getBoolean(context.getString(R.string.passkeys_backup_state_key),
context.resources.getBoolean(R.bool.passkeys_backup_state_default))
}
fun isAutofillCloseDatabaseEnable(context: Context): Boolean {
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
return prefs.getBoolean(context.getString(R.string.autofill_close_database_key),

View File

@@ -62,6 +62,7 @@ import androidx.core.view.updatePaddingRelative
import com.google.android.material.appbar.CollapsingToolbarLayout
import com.google.android.material.snackbar.Snackbar
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.database.exception.LocalizedException
import com.kunzisoft.keepass.database.helper.getLocalizedMessage
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.tasks.ActionRunnable
@@ -237,6 +238,17 @@ fun View.updateLockPaddingStart() {
}
}
fun Context.toastError(e: Throwable) {
Toast.makeText(
applicationContext,
if (e is LocalizedException)
e.getLocalizedMessage(resources)
else
e.localizedMessage,
Toast.LENGTH_LONG
).show()
}
fun Context.showActionErrorIfNeeded(result: ActionRunnable.Result) {
if (!result.isSuccess) {
result.exception?.getLocalizedMessage(resources)?.let { errorMessage ->

View File

@@ -493,7 +493,7 @@
<string name="regex">تعابير نمطية</string>
<string name="enable_keep_screen_on_title">أبقِ الشاشة شغّالة</string>
<string name="enable_education_screens_summary">أبرز العناصر لتعلم طريقة عمل التطبيق</string>
<string name="autofill_read_only_save">غير مسموح حفظ البيانات في قاعدة بيانات مفتوحة للقراءة فقط.</string>
<string name="error_save_read_only">غير مسموح حفظ البيانات في قاعدة بيانات مفتوحة للقراءة فقط.</string>
<string name="autofill_inline_suggestions_keyboard">أُضيف اقتراح ملء تلقائي.</string>
<string name="keyboard_previous_database_credentials_summary">الرجوع للوحة المفاتيح السابقة تلقائيًا في شاشة بيانات اعتماد قاعدة البيانات</string>
<string name="autofill_manual_selection_summary">اعرض خيارًا يسمح للمستخدم باختيار مدخلة من قاعدة البيانات</string>

View File

@@ -566,7 +566,7 @@
<string name="autofill_web_domain_blocklist_summary">Veb domenlərin avtomatik olaraq doldurulmasını əngəlləyən bloklama siyahısı</string>
<string name="autofill_block">Avtomatik doldurmanı blokla</string>
<string name="autofill_block_restart">Bloklamanı aktiv etmək üçün anketin daxil olduğu tətbiqi yenidən başladın.</string>
<string name="autofill_read_only_save">Yazma-qorumalı (dəyişməz) olaraq açılan məlumat bazasında yeni məlumatları yadda saxlamağa icazə verilmir.</string>
<string name="error_save_read_only">Yazma-qorumalı (dəyişməz) olaraq açılan məlumat bazasında yeni məlumatları yadda saxlamağa icazə verilmir.</string>
<string name="allow_no_password_summary">Əgər şəxsiyyəti təsdiq edən məlumatlar seçilməyibsə, \"Aç\" düyməsinin sıxılmasına icazə ver</string>
<string name="delete_entered_password_title">Şifrəni sil</string>
<string name="delete_entered_password_summary">Məlumat bazasına bağlantı cəhdindən sonra daxil edilmiş şifrəni sil</string>

View File

@@ -549,7 +549,7 @@
<string name="autofill_web_domain_blocklist_summary">Чорны спіс, які перашкаджае аўтазапаўненню вэб-даменаў</string>
<string name="autofill_block">Заблакаваць аўтазапаўненне</string>
<string name="autofill_block_restart">Перазапусціце праграму, якая змяшчае форму, каб актываваць блакаванне.</string>
<string name="autofill_read_only_save">Захаванне дадзеных недапушчальна для базы дадзеных, адкрытай толькі для чытання.</string>
<string name="error_save_read_only">Захаванне дадзеных недапушчальна для базы дадзеных, адкрытай толькі для чытання.</string>
<string name="autofill_inline_suggestions_keyboard">Прапановы аўтазапаўнення дададзены.</string>
<string name="allow_no_password_title">Дазволіць без галоўнага ключа</string>
<string name="allow_no_password_summary">Дазваляе націснуць кнопку Адкрыць, калі не выбраны ўліковыя дадзеныя</string>

View File

@@ -571,7 +571,7 @@
<string name="error_start_database_action">Възникна грешка при извършване на действие с хранилището.</string>
<string name="keyboard_entry_timeout_title">Време за изчакване</string>
<string name="autofill_application_id_blocklist_title">Черен списък на приложения</string>
<string name="autofill_read_only_save">В хранилище, отворено само за четене не могат да бъдат запазвани промени.</string>
<string name="error_save_read_only">В хранилище, отворено само за четене не могат да бъдат запазвани промени.</string>
<string name="error_move_group_here">Група не може да бъде преместена тук.</string>
<string name="keyboard_notification_entry_clear_close_title">Изчистване при затваряне</string>
<string name="autofill_save_search_info_summary">При ръчен избор на запис прави опит за запазване на информация от търсене за по-лесно бъдещо използване</string>

View File

@@ -643,7 +643,7 @@
<string name="keyboard_previous_lock_summary">Torna automàticament al teclat anterior després de bloquejar la base de dades</string>
<string name="select_entry">Selecciona una entrada</string>
<string name="autofill_ask_to_save_data_summary">Demana desar les dades quan es completi l\'emplenament d\'un formulari</string>
<string name="autofill_read_only_save">No es permet desar dades en una base de dades oberta en mode només de lectura.</string>
<string name="error_save_read_only">No es permet desar dades en una base de dades oberta en mode només de lectura.</string>
<string name="reset_education_screens_summary">Torna a mostrar tota la informació educativa</string>
<string name="reset_education_screens_text">Reinicialitza els consells educatius</string>
<string name="education_select_database_title">Obre una base de dades existent</string>

View File

@@ -465,7 +465,7 @@
<string name="education_add_attachment_summary">Nahrát přílohu k záznamu pro uložení důležitých externích dat.</string>
<string name="show_uuid_summary">Ukáže UUID propojené se záznamem nebo skupinou</string>
<string name="show_uuid_title">Ukázat UUID</string>
<string name="autofill_read_only_save">Uložení dat není povoleno, je-li databáze v režimu pouze pro čtení.</string>
<string name="error_save_read_only">Uložení dat není povoleno, je-li databáze v režimu pouze pro čtení.</string>
<string name="autofill_ask_to_save_data_summary">Po dokončení vyplnění formuláře se tázat na uložení dat</string>
<string name="autofill_ask_to_save_data_title">Tázat se před uložením</string>
<string name="autofill_save_search_info_summary">Pokusit se uložit údaje hledání pro příští použití, vybíráte-li záznam manuálně</string>

View File

@@ -451,7 +451,7 @@
<string name="show_uuid_title">Vis UUID</string>
<string name="upload_attachment">Overfør %1$s</string>
<string name="education_add_attachment_title">Vedhæft fil</string>
<string name="autofill_read_only_save">Det er ikke tilladt at gemme data i en database, der er åbnet som skrivebeskyttet.</string>
<string name="error_save_read_only">Det er ikke tilladt at gemme data i en database, der er åbnet som skrivebeskyttet.</string>
<string name="autofill_ask_to_save_data_summary">Spørg om du vil gemme data, når en formular er udfyldt</string>
<string name="autofill_ask_to_save_data_title">Spørg om du vil gemme data</string>
<string name="autofill_save_search_info_title">Gem søgeoplysninger</string>

View File

@@ -479,7 +479,7 @@
<string name="warning_sure_add_file">Datei trotzdem hinzufügen\?</string>
<string name="show_uuid_summary">Zeigt die mit einem Eintrag oder einer Gruppe verknüpfte UUID an</string>
<string name="show_uuid_title">UUID anzeigen</string>
<string name="autofill_read_only_save">Das Speichern von Daten ist bei einer schreibgeschützt geöffneten Datenbank nicht möglich.</string>
<string name="error_save_read_only">Das Speichern von Daten ist bei einer schreibgeschützt geöffneten Datenbank nicht möglich.</string>
<string name="autofill_close_database_title">Datenbank schließen</string>
<string name="keyboard_previous_lock_summary">Nach dem Sperren der Datenbank automatisch zur vorherigen Tastatur wechseln</string>
<string name="keyboard_previous_lock_title">Datenbank sperren</string>

View File

@@ -464,7 +464,7 @@
<string name="content_description_add_item">Προσθήκη είδους</string>
<string name="show_uuid_summary">Εμφανίζει το UUID που είναι συνδεδεμένο σε μια καταχώρηση ή σε μια ομάδα</string>
<string name="show_uuid_title">Εμφάνιση UUID</string>
<string name="autofill_read_only_save">Δεν επιτρέπεται η αποθήκευση δεδομένων για μια βάση δεδομένων που ανοίγει ως μόνο για ανάγνωση.</string>
<string name="error_save_read_only">Δεν επιτρέπεται η αποθήκευση δεδομένων για μια βάση δεδομένων που ανοίγει ως μόνο για ανάγνωση.</string>
<string name="autofill_ask_to_save_data_summary">Ζητήστε αποθήκευση δεδομένων όταν ολοκληρωθεί η συμπλήρωση μιας φόρμας</string>
<string name="autofill_ask_to_save_data_title">Ζητήστε να αποθηκεύσετε δεδομένα</string>
<string name="autofill_save_search_info_summary">Προσπαθήστε να αποθηκεύσετε πληροφορίες αναζήτησης όταν κάνετε μια χειροκίνητη επιλογή καταχώρησης για ευκολότερες μελλοντικές χρήσεις</string>

View File

@@ -473,7 +473,7 @@
<string name="education_setup_OTP_title">Establecer contraseña de un solo uso</string>
<string name="education_device_unlock_summary">Vincule su contraseña con su credencial biométrica o del dispositivo escaneada para desbloquear rápidamente su base de datos.</string>
<string name="education_device_unlock_title">Desbloqueo de la base de datos de los dispositivos</string>
<string name="autofill_read_only_save">No se permite guardar datos en una base de datos abierta como de solo lectura.</string>
<string name="error_save_read_only">No se permite guardar datos en una base de datos abierta como de solo lectura.</string>
<string name="autofill_block_restart">Reinicia la aplicación que contiene el formulario para activar el bloqueo.</string>
<string name="autofill_web_domain_blocklist_summary">Lista de dominios web en los que se impide el autocompletado</string>
<string name="autofill_web_domain_blocklist_title">Lista de bloqueo de dominios web</string>

View File

@@ -528,7 +528,7 @@
<string name="autofill_application_id_blocklist_title">Keelatud rakenduste loend</string>
<string name="autofill_block_restart">Keelamise jõustamiseks käivita antud sisendvormiga rakendus uuesti.</string>
<string name="autofill_inline_suggestions_keyboard">Automaattäite soovitused on lisatud.</string>
<string name="autofill_read_only_save">Kui andmebaas on avatud ainult lugemiseks, siis andmete salvestamine pole võimalik.</string>
<string name="error_save_read_only">Kui andmebaas on avatud ainult lugemiseks, siis andmete salvestamine pole võimalik.</string>
<string name="delete_entered_password_summary">Kustutab salasõna, mis oli kasutusel andmebaasiga ühenduse loomise ajal</string>
<string name="enable_screenshot_mode_summary">Luba teistel rakendusel teha sellest rakendusest ekraanitõmmist või salvestada tema ekraanivaadet</string>
<string name="autofill_close_database_title">Sulge andmebaas</string>

View File

@@ -273,7 +273,7 @@
<string name="autofill_inline_suggestions_title">Lerroko iradokizuna</string>
<string name="autofill_manual_selection_title">Eskuzko hautatzea</string>
<string name="autofill_inline_suggestions_summary">Betetze automatikorako gomendioak erakusten saiatzen da teklatu bateragarri baten bidez</string>
<string name="autofill_read_only_save">Ezin dira datuak gorde irakurketarako soilik irekitako datu-base batean.</string>
<string name="error_save_read_only">Ezin dira datuak gorde irakurketarako soilik irekitako datu-base batean.</string>
<string name="autofill_block_restart">Berrabiarazi formularioa duen aplikazioa blokeoa aktibatzeko.</string>
<string name="delete_entered_password_title">Ezabatu pasahitza</string>
<string name="allow_no_password_summary">Baimendu \"Ireki\" botoia sakatzea kredentzialak hautatu gabe</string>

View File

@@ -481,7 +481,7 @@
<string name="data">Données</string>
<string name="show_uuid_summary">Affiche lUUID lié à une entrée ou un groupe</string>
<string name="show_uuid_title">Afficher lUUID</string>
<string name="autofill_read_only_save">Lenregistrement des données nest pas autorisé pour une base de données ouverte en lecture seule.</string>
<string name="error_save_read_only">Lenregistrement des données nest pas autorisé pour une base de données ouverte en lecture seule.</string>
<string name="autofill_ask_to_save_data_summary">Demande de sauvegarde des données à la fin du remplissage d\'un formulaire</string>
<string name="autofill_ask_to_save_data_title">Demander à enregistrer des données</string>
<string name="autofill_save_search_info_summary">Essaye denregistrer les informations de recherche lors de la sélection manuelle dune entrée pour faciliter les utilisations futures</string>

View File

@@ -546,7 +546,7 @@
<string name="keyboard_selection_entry_summary">Ao ver unha entrada en KeePassDX, completar con esta o Magikeyboard</string>
<string name="keyboard_previous_lock_summary">Mudar automaticamente ao teclado previo despois de bloquear a base de datos</string>
<string name="autofill_ask_to_save_data_title">Pedir para gardar datos</string>
<string name="autofill_read_only_save">Non é posíbel gardar nunha base datos aberta en modo só lectura.</string>
<string name="error_save_read_only">Non é posíbel gardar nunha base datos aberta en modo só lectura.</string>
<string name="autofill_ask_to_save_data_summary">Pedir para gardar datos cando terminar de autocompletar un formulario</string>
<string name="delete_entered_password_summary">Borrar o contrasinal introducido após un intento de conexión a unha base de datos</string>
<string name="reset_education_screens_title">Restabelecer suxestións educativas</string>

View File

@@ -461,7 +461,7 @@
<string name="autofill_save_search_info_summary">Pokušaj spremiti podatke prilikom odabira ručnog unosa za jednostavniju buduću upotrebu</string>
<string name="notification">Obavijest</string>
<string name="error_registration_read_only">Nije dopušteno spremati novi element u zaštićenoj bazi podataka.</string>
<string name="autofill_read_only_save">Spremanje podataka nije dopušteno za bazu podataka koja je otvorena u zaštićenom stanju.</string>
<string name="error_save_read_only">Spremanje podataka nije dopušteno za bazu podataka koja je otvorena u zaštićenom stanju.</string>
<string name="show_uuid_summary">Prikazuje UUID povezan s unosom ili grupom</string>
<string name="show_uuid_title">Prikaži UUID</string>
<string name="autofill_ask_to_save_data_summary">Zatraži spremanje podataka kad se obrazac ispuni</string>

View File

@@ -564,7 +564,7 @@
<string name="autofill_web_domain_blocklist_summary">Tiltólista, amely megakadályozza a webes domainek automatikus kitöltését</string>
<string name="autofill_block">Automatikus kitöltés letiltása</string>
<string name="autofill_block_restart">Indítsa újra az űrlapot tartalmazó alkalmazást a tiltás aktiválásához.</string>
<string name="autofill_read_only_save">Az adatmentés nem engedélyezett, mert az adatbázis írásvédettként van megnyitva.</string>
<string name="error_save_read_only">Az adatmentés nem engedélyezett, mert az adatbázis írásvédettként van megnyitva.</string>
<string name="autofill_inline_suggestions_keyboard">Automatikus kitöltési javaslatok hozzáadva.</string>
<string name="education_add_attachment_summary">Töltsön fel egy mellékletet a bejegyzéséhez, hogy mentse a fontos külső adatokat.</string>
<string name="download_canceled">Megszakítva!</string>

View File

@@ -549,7 +549,7 @@
<string name="keyboard_previous_fill_in_summary">Secara otomatis beralih kembali ke keyboard sebelumnya setelah menjalankan \"Tindakan tombol otomatis\"</string>
<string name="keyboard_previous_database_credentials_summary">Secara otomatis beralih kembali ke keyboard sebelumnya di layar kredensial basis data</string>
<string name="autofill_manual_selection_summary">Tampilkan opsi untuk memungkinkan pengguna memilih entri basis data</string>
<string name="autofill_read_only_save">Penyimpanan data tidak diperbolehkan untuk basis data yang dibuka sebagai baca-saja.</string>
<string name="error_save_read_only">Penyimpanan data tidak diperbolehkan untuk basis data yang dibuka sebagai baca-saja.</string>
<string name="allow_no_password_title">Izinkan tidak ada kunci utama</string>
<string name="allow_no_password_summary">Memungkinkan mengetuk tombol \"Buka\" jika tidak ada kredensial yang dipilih</string>
<string name="delete_entered_password_summary">Menghapus kata sandi yang dimasukkan setelah upaya koneksi ke basis data</string>

View File

@@ -467,7 +467,7 @@
<string name="content_description_credentials_information">Info credenziali</string>
<string name="show_uuid_summary">Visualizza l\'UUID collegato a una voce o a un gruppo</string>
<string name="show_uuid_title">Mostra UUID</string>
<string name="autofill_read_only_save">Il salvataggio dei dati non è consentito per un database aperto in sola lettura.</string>
<string name="error_save_read_only">Il salvataggio dei dati non è consentito per un database aperto in sola lettura.</string>
<string name="autofill_ask_to_save_data_summary">Chiedi di salvare i dati quando l\'immissione dei dati in un form viene completata</string>
<string name="autofill_ask_to_save_data_title">Chiedi di salvare i dati</string>
<string name="autofill_save_search_info_summary">Provare a salvare le informazioni di ricerca quando viene selezionato manualmente un elemento per facilitarne gli utilizzi futuri</string>

View File

@@ -505,7 +505,7 @@
<string name="upload_attachment">העלה %1$s</string>
<string name="download_canceled">בוטל!</string>
<string name="unit_byte">B</string>
<string name="autofill_read_only_save">שמירת נתונים אינה מורשת עבור מסד נתונים שנפתח לקריאה בלבד.</string>
<string name="error_save_read_only">שמירת נתונים אינה מורשת עבור מסד נתונים שנפתח לקריאה בלבד.</string>
<string name="enter">Enter</string>
<string name="autofill_block">חסום מילוי אוטומטי</string>
<string name="warning_replace_file">העלאת קובץ זה תחליף את הקובץ הקיים.</string>

View File

@@ -472,7 +472,7 @@
<string name="hide_expired_entries_summary">有効期限切れのエントリーは非表示になります</string>
<string name="show_uuid_title">UUID を表示</string>
<string name="show_uuid_summary">エントリーやグループにリンクされた UUID を表示します</string>
<string name="autofill_read_only_save">データの保存は読み取り専用として開かれたデータベースでは許可されていません。</string>
<string name="error_save_read_only">データの保存は読み取り専用として開かれたデータベースでは許可されていません。</string>
<string name="save_mode">保存モード</string>
<string name="search_mode">検索モード</string>
<string name="error_field_name_already_exists">フィールド名はすでに存在します。</string>

View File

@@ -427,7 +427,7 @@
<string name="autofill_save_search_info_summary">Prøv å lagre søkeinformasjon når du velger manuell inntasting for enklere fremtidig bruk</string>
<string name="autofill_block">Blokker autofyll</string>
<string name="autofill_block_restart">Start appen på nytt som inneholder skjemaet for å aktivere blokkeringen.</string>
<string name="autofill_read_only_save">Datalagring er ikke tillatt for en database som er skrivebeskyttet.</string>
<string name="error_save_read_only">Datalagring er ikke tillatt for en database som er skrivebeskyttet.</string>
<string name="autofill_inline_suggestions_keyboard">Forslag til autofyll er lagt til.</string>
<string name="education_device_unlock_summary">Koble passordet ditt til den skannede biometriske eller enhetslegitimasjonen for å raskt låse opp databasen din.</string>
<string name="education_setup_OTP_title">Sett opp OTP</string>

View File

@@ -466,7 +466,7 @@
<string name="database_data_remove_unlinked_attachments_summary">Verwijdert bijlagen die in de database staan, maar niet aan een item zijn gekoppeld</string>
<string name="show_uuid_summary">Toont de UUID die is gekoppeld aan een item of een groep</string>
<string name="show_uuid_title">UUID tonen</string>
<string name="autofill_read_only_save">Het opslaan van gegevens is niet toegestaan voor een database die is geopend als alleen-lezen.</string>
<string name="error_save_read_only">Het opslaan van gegevens is niet toegestaan voor een database die is geopend als alleen-lezen.</string>
<string name="autofill_ask_to_save_data_summary">Vraag om gegevens op te slaan wanneer het invullen van een formulier is voltooid</string>
<string name="autofill_ask_to_save_data_title">Vragen om gegevens op te slaan</string>
<string name="autofill_save_search_info_summary">Probeer zoekinformatie op te slaan bij het maken van een handmatige invoerselectie voor eenvoudiger toekomstig gebruik</string>

View File

@@ -463,7 +463,7 @@
<string name="content_description_credentials_information">Informacje o poświadczeniach</string>
<string name="show_uuid_summary">Wyświetla identyfikator UUID powiązany z wpisem lub grupą</string>
<string name="show_uuid_title">Pokaż UUID</string>
<string name="autofill_read_only_save">Zapisywanie danych nie jest dozwolone dla bazy danych otwartej tylko do odczytu.</string>
<string name="error_save_read_only">Zapisywanie danych nie jest dozwolone dla bazy danych otwartej tylko do odczytu.</string>
<string name="autofill_ask_to_save_data_summary">Pytaj o zapisanie danych po zakończeniu wypełniania formularza</string>
<string name="autofill_ask_to_save_data_title">Pytaj o zapisanie danych</string>
<string name="autofill_save_search_info_summary">Staraj się zapisywać informacje wyszukiwania podczas dokonywania ręcznego wyboru wpisu, aby ułatwić sobie przyszłe użycie</string>

View File

@@ -550,7 +550,7 @@
<string name="education_device_unlock_summary">Vincule sua senha à credencial biométrica ou do dispositivo digitalizada para desbloquear rapidamente seu banco de dados.</string>
<string name="education_device_unlock_title">Desbloqueio do banco de dados do dispositivo</string>
<string name="autofill_inline_suggestions_keyboard">Sugestões de preenchimento automático adicionadas.</string>
<string name="autofill_read_only_save">A salvação de dados não é permitida para um banco de dados aberto apenas como leitura.</string>
<string name="error_save_read_only">A salvação de dados não é permitida para um banco de dados aberto apenas como leitura.</string>
<string name="autofill_ask_to_save_data_summary">Pedir para salvar dados ao terminar de preencher um formulário</string>
<string name="autofill_ask_to_save_data_title">Perguntar para salvar dados</string>
<string name="autofill_save_search_info_summary">Tente salvar informações de pesquisa ao fazer uma seleção manual de entrada para facilitar usos posteriores</string>

View File

@@ -503,7 +503,7 @@
<string name="education_device_unlock_summary">Ligue a sua palavra-passe às suas credenciais biométricas ou do dispositivo para desbloquear rapidamente a sua base de dados.</string>
<string name="education_device_unlock_title">Desbloqueio da base de dados do dispositivo</string>
<string name="autofill_inline_suggestions_keyboard">Adicionadas sugestões de preenchimento automático.</string>
<string name="autofill_read_only_save">Não é possível guardar dados numa base de dados aberta apenas com permissão de leitura.</string>
<string name="error_save_read_only">Não é possível guardar dados numa base de dados aberta apenas com permissão de leitura.</string>
<string name="autofill_ask_to_save_data_summary">Pedir para guardar dados quando terminar de preencher um formulário</string>
<string name="autofill_ask_to_save_data_title">Pedir para guardar dados</string>
<string name="autofill_save_search_info_summary">Tentar guardar as informações de pesquisas ao fazer uma seleção de entrada manual para facilitar utilizações posteriores</string>

View File

@@ -484,7 +484,7 @@
<string name="download_canceled">Cancelado!</string>
<string name="education_device_unlock_title">Desbloqueio da base de dados do dispositivo</string>
<string name="autofill_inline_suggestions_keyboard">Adicionadas sugestões de preenchimento automático.</string>
<string name="autofill_read_only_save">Não é possível guardar dados numa base de dados aberta apenas com permissão de leitura.</string>
<string name="error_save_read_only">Não é possível guardar dados numa base de dados aberta apenas com permissão de leitura.</string>
<string name="autofill_ask_to_save_data_summary">Pedir para guardar dados quando terminar de preencher um formulário</string>
<string name="autofill_ask_to_save_data_title">Pedir para guardar dados</string>
<string name="autofill_save_search_info_summary">Tentar guardar as informações de pesquisas ao fazer uma seleção de entrada manual para facilitar utilizações posteriores</string>

View File

@@ -661,7 +661,7 @@
<string name="autofill_inline_suggestions_title">Sugestii în linie</string>
<string name="autofill_ask_to_save_data_summary">Solicitați salvarea datelor atunci când se finalizează completarea unui formular</string>
<string name="autofill_block_restart">Reporniți aplicația care conține formularul pentru a activa blocarea.</string>
<string name="autofill_read_only_save">Salvarea datelor nu este permisă pentru o bază de date deschisă ca fiind doar pentru citire.</string>
<string name="error_save_read_only">Salvarea datelor nu este permisă pentru o bază de date deschisă ca fiind doar pentru citire.</string>
<string name="education_validate_entry_summary">Nu uitați să validați datele introduse și să salvați baza de date.
\n
\nDacă este activată o blocare automată și uitați că ați făcut o modificare, riscați să vă pierdeți datele.</string>

View File

@@ -471,7 +471,7 @@
<string name="keyboard_previous_lock_summary">Автоматически переключаться на предыдущую клавиатуру после блокировки базы</string>
<string name="show_uuid_summary">Показывать UUID, связанный с записью или группой</string>
<string name="show_uuid_title">Показывать UUID</string>
<string name="autofill_read_only_save">Сохранение данных невозможно для базы, открытой только для чтения.</string>
<string name="error_save_read_only">Сохранение данных невозможно для базы, открытой только для чтения.</string>
<string name="autofill_ask_to_save_data_summary">Запрашивать сохранение данных после завершения заполнения формы</string>
<string name="autofill_ask_to_save_data_title">Запрос сохранения данных</string>
<string name="autofill_close_database_summary">Закрывать базу после выбора автозаполнения</string>

View File

@@ -303,7 +303,7 @@
<string name="autofill_save_search_info_summary">Pokúste sa uložiť informácie o vyhľadávaní pri ručnom výbere pre jednoduchšie budúce použitie</string>
<string name="autofill_ask_to_save_data_title">Požiadajte o uloženie údajov</string>
<string name="autofill_ask_to_save_data_summary">Po vyplnení formulára požiadajte o uloženie údajov</string>
<string name="autofill_read_only_save">Ukladanie údajov nie je povolené pre databázu otvorenú len na čítanie.</string>
<string name="error_save_read_only">Ukladanie údajov nie je povolené pre databázu otvorenú len na čítanie.</string>
<string name="autofill_inline_suggestions_keyboard">Boli pridané návrhy automatického dopĺňania.</string>
<string name="education_new_node_title">Pridajte položky do databázy</string>
<string name="education_device_unlock_title">Odomykanie databázy zariadenia</string>

View File

@@ -607,7 +607,7 @@
<string name="keyboard_previous_database_credentials_summary">Kthehu automatikisht te tastiera e mëparshme, te skena e kredencialeve për bazën e të dhënave</string>
<string name="autofill_manual_selection_summary">Shfaq mundësi për ta lënë përdoruesin të përzgjedhë zë baze të dhënash</string>
<string name="autofill_save_search_info_summary">Provo të ruash informacion, kur bëhet një përzgjedhje dorazi e zërit, për përdorim më të kollajtë në të ardhmen</string>
<string name="autofill_read_only_save">Slejohet ruajtje të dhënash për një bazë të dhënash të hapur vetëm-për-lexim.</string>
<string name="error_save_read_only">Slejohet ruajtje të dhënash për një bazë të dhënash të hapur vetëm-për-lexim.</string>
<string name="enable_auto_save_database_summary">Ruaje bazën e të dhënave pas çdo veprimi të rëndësishëm (nën mënyrën “E ndryshueshme”)</string>
<string name="enable_keep_screen_on_summary">Mbaje hapur ekranin, kur shihet ose përpunohet një zë</string>
<string name="enable_screenshot_mode_summary">Lejo aplikacione palësh të treta të regjistrojnë, ose bëjnë foto ekrani të aplikacionit</string>

View File

@@ -644,7 +644,7 @@
<string name="autofill_application_id_blocklist_title">பயன்பாட்டு பிளாக்லிச்ட்</string>
<string name="autofill_application_id_blocklist_summary">பயன்பாடுகளை தானாக நிரப்புவதைத் தடுக்கும் பிளாக்லிச்ட்</string>
<string name="autofill_web_domain_blocklist_summary">வலை களங்களை தானாக நிரப்புவதைத் தடுக்கும் பிளாக்லிச்ட்</string>
<string name="autofill_read_only_save">படிக்க மட்டும் திறக்கப்பட்ட தரவுத்தளத்திற்கு தரவு சேமிப்பு அனுமதிக்கப்படவில்லை.</string>
<string name="error_save_read_only">படிக்க மட்டும் திறக்கப்பட்ட தரவுத்தளத்திற்கு தரவு சேமிப்பு அனுமதிக்கப்படவில்லை.</string>
<string name="autofill_inline_suggestions_keyboard">ஆட்டோஃபில் பரிந்துரைகள் சேர்க்கப்பட்டன.</string>
<string name="education_new_node_summary">உங்கள் டிசிட்டல் அடையாளங்களை நிர்வகிக்க உள்ளீடுகள் உதவுகின்றன.\n\n குழுக்கள் (~ கோப்புறைகள்) உங்கள் தரவுத்தளத்தில் உள்ளீடுகளை ஒழுங்கமைக்கின்றன.</string>
<string name="education_search_title">உள்ளீடுகள் மூலம் தேடுங்கள்</string>

View File

@@ -612,7 +612,7 @@
<string name="autofill_application_id_blocklist_summary">รายการที่บล็อกเพื่อกันไม่ให้กรอกข้อมูลในแอปอัตโนมัติ</string>
<string name="autofill_web_domain_blocklist_summary">รายการที่บล็อกเพื่อกันไม่ให้กรอกข้อมูลในเว็บอัตโนมัติ</string>
<string name="biometric_delete_all_key_summary">ลบกุญแจเข้ารหัสทั้งหมดที่เกี่ยวข้องกับการปลดล็อกด้วยอุปกรณ์</string>
<string name="autofill_read_only_save">การบันทึกฐานข้อมูลไม่อนุญาตสำหรับฐานข้อมูลที่อ่านอย่างเดียว</string>
<string name="error_save_read_only">การบันทึกฐานข้อมูลไม่อนุญาตสำหรับฐานข้อมูลที่อ่านอย่างเดียว</string>
<string name="autofill_block_restart">เปิดแอปที่แบบฟอร์มนั้นขั้นมาใหม่เพื่อใช้งานการบล็อก</string>
<string name="autofill_inline_suggestions_keyboard">เพื่มการแนะนำการกรอกอัตโนมัติ</string>
<string name="allow_no_password_title">อนุญาตให้ไม่มีรหัสผ่านหลัก</string>

View File

@@ -458,7 +458,7 @@
<string name="database_data_remove_unlinked_attachments_summary">Veri tabanında bulunan ancak bir girdiye bağlı olmayan ekleri kaldırır</string>
<string name="show_uuid_summary">Bir girdiye veya gruba bağlı UUID\'yi görüntüler</string>
<string name="show_uuid_title">UUID\'yi göster</string>
<string name="autofill_read_only_save">Salt okunur olarak açılan bir veri tabanı için veri kaydına izin verilmiyor.</string>
<string name="error_save_read_only">Salt okunur olarak açılan bir veri tabanı için veri kaydına izin verilmiyor.</string>
<string name="autofill_ask_to_save_data_summary">Form doldurma işlemi tamamlandığında verileri kaydetmek için sor</string>
<string name="autofill_ask_to_save_data_title">Verileri kaydetmek için sor</string>
<string name="autofill_save_search_info_summary">Gelecekteki daha kolay kullanımlar için el ile giriş seçimi yaparken arama bilgilerini kaydetmeyi deneyin</string>

View File

@@ -464,7 +464,7 @@
<string name="database_data_remove_unlinked_attachments_summary">Вилучає вкладення, що містяться в базі даних, але не пов’язані з записом</string>
<string name="show_uuid_summary">Показ пов\'язаного з записом чи групою UUID</string>
<string name="show_uuid_title">Показувати UUID</string>
<string name="autofill_read_only_save">Збереження даних заборонено для бази даних, відкритої лише для читання.</string>
<string name="error_save_read_only">Збереження даних заборонено для бази даних, відкритої лише для читання.</string>
<string name="autofill_ask_to_save_data_summary">Запитувати зберігати дані після заповнення форми</string>
<string name="autofill_ask_to_save_data_title">Запит збереження даних</string>
<string name="autofill_save_search_info_summary">Намагатися зберегти подробиці пошуку під час вибору запису вручну для простішого користування в подальшому</string>

View File

@@ -561,7 +561,7 @@
<string name="autofill_web_domain_blocklist_summary">Danh sách chặn ngăn việc tự động điền tên miền web</string>
<string name="autofill_block">Chặn tính năng tự động điền</string>
<string name="autofill_block_restart">Khởi động lại ứng dụng chứa biểu mẫu để kích hoạt tính năng chặn.</string>
<string name="autofill_read_only_save">Không cho phép lưu dữ liệu đối với cơ sở dữ liệu được mở ở dạng chỉ đọc.</string>
<string name="error_save_read_only">Không cho phép lưu dữ liệu đối với cơ sở dữ liệu được mở ở dạng chỉ đọc.</string>
<string name="autofill_inline_suggestions_keyboard">Đã thêm đề xuất tự động điền.</string>
<string name="allow_no_password_title">Không cho phép khóa chính</string>
<string name="allow_no_password_summary">Cho phép nhấn vào nút \"Mở\" nếu không có thông tin xác thực nào được chọn</string>

View File

@@ -463,7 +463,7 @@
<string name="database_data_remove_unlinked_attachments_summary">删除包含于数据库中但未连接到一个条目的附件</string>
<string name="show_uuid_summary">显示与一个条目或分组相链接的 UUID</string>
<string name="show_uuid_title">显示 UUID</string>
<string name="autofill_read_only_save">以只读方式打开的数据库不允许保存数据。</string>
<string name="error_save_read_only">以只读方式打开的数据库不允许保存数据。</string>
<string name="autofill_ask_to_save_data_summary">填写完表单后,询问是否保存数据</string>
<string name="autofill_ask_to_save_data_title">询问是否保存数据</string>
<string name="autofill_save_search_info_summary">手动选择条目时尝试保存搜索信息,以便将来使用</string>

View File

@@ -60,7 +60,7 @@
<string name="autofill_manual_selection_summary">顯示選項讓用戶選擇資料庫條目</string>
<string name="autofill_manual_selection_title">手動選擇</string>
<string name="autofill_preference_title">自動填入設定</string>
<string name="autofill_read_only_save">以唯讀方式開啟的資料庫不允許保存資料。</string>
<string name="error_save_read_only">以唯讀方式開啟的資料庫不允許保存資料。</string>
<string name="autofill_save_search_info_summary">進行手動輸入選擇時嘗試儲存搜尋信息,以便將來使用</string>
<string name="autofill_save_search_info_title">保存搜尋資訊</string>
<string name="autofill_select_entry">選擇項目…</string>

View File

@@ -135,6 +135,12 @@
<string name="passkeys_explanation_key" translatable="false">passkeys_explanation_key</string>
<string name="settings_passkeys_key" translatable="false">settings_passkeys_key</string>
<string name="passkeys_privileged_apps_key" translatable="false">passkeys_privileged_apps_key</string>
<string name="passkeys_auto_select_key" translatable="false">passkeys_auto_select_key</string>
<bool name="passkeys_auto_select_default" translatable="false">false</bool>
<string name="passkeys_backup_eligibility_key" translatable="false">passkeys_backup_eligibility_key</string>
<bool name="passkeys_backup_eligibility_default" translatable="false">true</bool>
<string name="passkeys_backup_state_key" translatable="false">passkeys_backup_state_key</string>
<bool name="passkeys_backup_state_default" translatable="false">false</bool>
<string name="keyboard_notification_entry_key" translatable="false">keyboard_notification_entry_key</string>
<bool name="keyboard_notification_entry_default" translatable="false">true</bool>
<string name="keyboard_notification_entry_clear_close_key" translatable="false">keyboard_notification_entry_clear_close_key</string>

View File

@@ -424,9 +424,18 @@
<string name="passkeys_preference_title">Passkeys settings</string>
<string name="passkeys_privileged_apps_title">Privileged apps</string>
<string name="passkeys_privileged_apps_summary">Manage browsers in the custom list of privileged apps</string>
<string name="passkeys_privileged_apps_explanation">WARNING : A privileged app acts as a gateway to retrieve the origin of an authentication. Ensure its legitimacy to avoid security issues.</string>
<string name="passkeys_privileged_apps_explanation">WARNING: A privileged app acts as a gateway to retrieve the origin of an authentication. Ensure its legitimacy to avoid security issues.</string>
<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\nWould you like to add it to the list of privileged apps?</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_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>
<string name="passkeys_backup_eligibility_summary">Determine at creation time whether the public key credential source is allowed to be backed up</string>
<string name="passkeys_backup_state_title">Backup State</string>
<string name="passkeys_backup_state_summary">Indicate that credentials are backed up and protected against the loss of a single device</string>
<string name="autofill">Autofill</string>
<string name="credential_provider_service_subtitle">Passkeys, Autofill credential provider</string>
<string name="autofill_sign_in_prompt">Sign in with KeePassDX</string>
@@ -578,7 +587,7 @@
<string name="autofill_web_domain_blocklist_summary">Blocklist that prevents auto filling of web domains</string>
<string name="autofill_block">Block autofill</string>
<string name="autofill_block_restart">Restart the app containing the form to activate the blocking.</string>
<string name="autofill_read_only_save">Data save is not allowed for a database opened as read-only.</string>
<string name="error_save_read_only">Data save is not allowed for a database opened as read-only.</string>
<string name="autofill_inline_suggestions_keyboard">Autofill suggestions added.</string>
<string name="allow_no_password_title">Allow no master key</string>
<string name="allow_no_password_summary">Allows tapping the \"Open\" button if no credentials are selected</string>
@@ -751,11 +760,12 @@
<string name="passkey_update_description">Update passkey in "%1$s"</string>
<string name="passkey_selection_username">No passkey found</string>
<string name="passkey_selection_description">Select an existing passkey</string>
<string name="passkey_locked_database_username">KeePassDX Database Locked</string>
<string name="passkey_database_username">KeePassDX Database</string>
<string name="passkey_locked_database_description">Select to unlock</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>
<string name="passkey_user_handle">Passkey User Handle</string>
<string name="passkey_relying_party">Passkey Relying Party</string>
<string name="error_passkey_result">Unable to return the passkey</string>
</resources>

View File

@@ -24,14 +24,22 @@
android:key="@string/passkeys_privileged_apps_key"
android:title="@string/passkeys_privileged_apps_title"
android:summary="@string/passkeys_privileged_apps_summary"/>
<!--
// TODO Backup state #2135
<SwitchPreferenceCompat
android:key="@string/passkeys_auto_select_key"
android:title="@string/passkeys_auto_select_title"
android:summary="@string/passkeys_auto_select_summary"
android:defaultValue="@bool/passkeys_auto_select_default"/>
<SwitchPreferenceCompat
android:key="@string/passkeys_backup_eligibility_key"
android:title="@string/passkeys_backup_eligibility_title"
android:summary="@string/passkeys_backup_eligibility_summary"
android:defaultValue="@bool/passkeys_backup_eligibility_default"/>
<SwitchPreferenceCompat
android:key="@string/passkeys_backup_state_key"
android:dependency="@string/passkeys_backup_eligibility_key"
android:title="@string/passkeys_backup_state_title"
android:summary="@string/passkeys_backup_state_summary"
android:defaultValue="@bool/passkeys_backup_state_default"/>
-->
</PreferenceCategory>
<!--
// TODO Passkeys default group #2123

12
art/ic_passkey_dark.svg Normal file
View File

@@ -0,0 +1,12 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" width="192" height="192">
<defs>
<linearGradient id="a">
<stop offset="0" stop-color="#439447"/>
<stop offset="1" stop-color="#42ab46"/>
</linearGradient>
<linearGradient xlink:href="#a" id="b" x1="0" x2="192" y1="0" y2="192" gradientUnits="userSpaceOnUse"/>
</defs>
<circle cx="96" cy="96" r="88" fill="url(#b)"/>
<path fill="#ffa726" d="M56 56v10l70 70h10v-9L65 56Z"/>
<path fill="#fff" d="m83 99-27 28v9h9l10-10h10v-9h9v-7zm25-46L95 79l21 21 25-13-4-29zm19 18c4 0 8 5 7 9s-6 7-10 5c-4-1-6-7-3-11 1-1 3-3 6-3z"/>
</svg>

After

Width:  |  Height:  |  Size: 660 B

12
art/ic_passkey_light.svg Normal file
View File

@@ -0,0 +1,12 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" width="192" height="192">
<defs>
<linearGradient id="a">
<stop offset="0" stop-color="#42ab46"/>
<stop offset="1" stop-color="#439447"/>
</linearGradient>
<linearGradient xlink:href="#a" id="b" x1="0" x2="192" y1="0" y2="192" gradientUnits="userSpaceOnUse"/>
</defs>
<circle cx="96" cy="96" r="88" fill="url(#b)"/>
<path fill="#ffa726" d="M56 56v10l70 70h10v-9L65 56Z"/>
<path fill="#fff" d="m83 99-27 28v9h9l10-10h10v-9h9v-7zm25-46L95 79l21 21 25-13-4-29zm19 18c4 0 8 5 7 9s-6 7-10 5c-4-1-6-7-3-11 1-1 3-3 6-3z"/>
</svg>

After

Width:  |  Height:  |  Size: 660 B

View File

@@ -58,12 +58,17 @@ object Signature {
const val ED_DSA_ALGORITHM: Long = -8
private const val BEGIN_PRIVATE_KEY = "-----BEGIN PRIVATE KEY-----"
private const val BEGIN_PRIVATE_KEY_LINE_BREAK = "$BEGIN_PRIVATE_KEY\n"
private const val END_PRIVATE_KEY = "-----END PRIVATE KEY-----"
private const val END_PRIVATE_KEY_LINE_BREAK = "\n$END_PRIVATE_KEY"
init {
Security.removeProvider(BouncyCastleProvider.PROVIDER_NAME)
Security.addProvider(BouncyCastleProvider())
}
fun sign(privateKeyPem: String, message: ByteArray): ByteArray? {
fun sign(privateKeyPem: String, message: ByteArray): ByteArray {
val privateKey = createPrivateKey(privateKeyPem)
val algorithmKey = privateKey.algorithm
val algorithmSignature = when (algorithmKey) {
@@ -71,22 +76,30 @@ object Signature {
"ECDSA" -> "SHA256withECDSA"
"RSA" -> "SHA256withRSA"
"Ed25519" -> "Ed25519"
else -> null
else -> throw SecurityException("$algorithmKey algorithm is unknown")
}
if (algorithmSignature == null) {
Log.e(this::class.java.simpleName, "sign: the algorithm $algorithmKey is unknown")
return null
}
val sig = Signature.getInstance(algorithmSignature, BouncyCastleProvider.PROVIDER_NAME)
val sig = Signature.getInstance(
algorithmSignature,
BouncyCastleProvider.PROVIDER_NAME
)
sig.initSign(privateKey)
sig.update(message)
return sig.sign()
}
fun createPrivateKey(privateKeyPem: String): PrivateKey {
val targetReader = StringReader(privateKeyPem)
var privateKeyString = privateKeyPem
if (privateKeyPem.startsWith(BEGIN_PRIVATE_KEY_LINE_BREAK).not()) {
privateKeyString = privateKeyString.removePrefix(BEGIN_PRIVATE_KEY)
privateKeyString = "$BEGIN_PRIVATE_KEY_LINE_BREAK$privateKeyString"
}
if (privateKeyPem.endsWith(END_PRIVATE_KEY_LINE_BREAK).not()) {
privateKeyString = privateKeyString.removeSuffix(END_PRIVATE_KEY)
privateKeyString += END_PRIVATE_KEY_LINE_BREAK
}
val targetReader = StringReader(privateKeyString)
val pemParser = PEMParser(targetReader)
val privateKeyInfo = pemParser.readObject() as PrivateKeyInfo
val privateKeyInfo = pemParser.readObject() as? PrivateKeyInfo?
val privateKey = JcaPEMKeyConverter().getPrivateKey(privateKeyInfo)
pemParser.close()
targetReader.close()

View File

@@ -19,12 +19,20 @@
*/
package com.kunzisoft.keepass.database.exception
import android.content.res.Resources
import com.kunzisoft.keepass.database.element.node.NodeId
import com.kunzisoft.keepass.database.element.node.Type
import java.io.PrintStream
import java.io.PrintWriter
abstract class DatabaseException : Exception {
abstract class LocalizedException : Exception {
constructor() : super()
constructor(message: String) : super(message)
// TODO
// open fun getLocalizedMessage(resources: Resources): String? = localizedMessage
}
abstract class DatabaseException : LocalizedException {
var innerMessage: String? = null
var parameters = mutableListOf<String>()
@@ -75,6 +83,8 @@ class InvalidAlgorithmDatabaseException : DatabaseInputException {
class UnknownDatabaseLocationException : DatabaseException()
class RegisterInReadOnlyDatabaseException() : DatabaseException()
class HardwareKeyDatabaseException : DatabaseException()
class EmptyKeyDatabaseException : DatabaseException()

View File

@@ -22,6 +22,7 @@ package com.kunzisoft.keepass.database.merge
import com.kunzisoft.keepass.database.element.Attachment
import com.kunzisoft.keepass.database.element.CustomData
import com.kunzisoft.keepass.database.element.DateInstant
import com.kunzisoft.keepass.database.element.DeletedObject
import com.kunzisoft.keepass.database.element.database.DatabaseKDB
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX
import com.kunzisoft.keepass.database.element.entry.EntryKDB
@@ -32,9 +33,10 @@ import com.kunzisoft.keepass.database.element.node.NodeHandler
import com.kunzisoft.keepass.database.element.node.NodeId
import com.kunzisoft.keepass.database.element.node.NodeIdInt
import com.kunzisoft.keepass.database.element.node.NodeIdUUID
import com.kunzisoft.keepass.database.element.node.NodeVersioned
import com.kunzisoft.keepass.utils.readAllBytes
import java.io.IOException
import java.util.*
import java.util.UUID
class DatabaseKDBXMerger(private var database: DatabaseKDBX) {
@@ -180,7 +182,7 @@ class DatabaseKDBXMerger(private var database: DatabaseKDBX) {
}
/**
* Merge a KDB> database in a KDBX database,
* Merge a KDBX database in a KDBX database,
* Try to take into account the modification date of each element
* To make a merge as accurate as possible
*/
@@ -302,30 +304,111 @@ class DatabaseKDBXMerger(private var database: DatabaseKDBX) {
}
// Manage deleted objects
databaseToMerge.deletedObjects.forEach { deletedObject ->
val deletedObjectId = deletedObject.uuid
val databaseEntry = database.getEntryById(deletedObjectId)
val databaseGroup = database.getGroupById(deletedObjectId)
val databaseIcon = database.iconsManager.getIcon(deletedObjectId)
val databaseIconModificationTime = databaseIcon?.lastModificationTime
val deletedObjects = databaseToMerge.deletedObjects
deletedObjects.forEach { deletedObject ->
deleteEntry(deletedObject)
deleteGroup(deletedObject, deletedObjects)
deleteIcon(deletedObject)
// Attachments are removed and optimized during the database save
}
}
/**
* Delete an entry from the database with the [deletedEntry] id
*/
private fun deleteEntry(deletedEntry: DeletedObject) {
val databaseEntry = database.getEntryById(deletedEntry.uuid)
if (databaseEntry != null
&& deletedObject.deletionTime.isAfter(databaseEntry.lastModificationTime)) {
&& deletedEntry.deletionTime.isAfter(databaseEntry.lastModificationTime)) {
database.removeEntryFrom(databaseEntry, databaseEntry.parent)
}
}
/**
* Check whether a node is in the list of deleted objects
*/
private fun Set<DeletedObject>.containsNode(node: NodeVersioned<UUID, GroupKDBX, EntryKDBX>): Boolean {
return this.any { it.uuid == node.nodeId.id }
}
/**
* Check whether a node is not in the list of deleted objects
*/
private fun Set<DeletedObject>.notContainsNode(node: NodeVersioned<UUID, GroupKDBX, EntryKDBX>): Boolean {
return !this.containsNode(node)
}
/**
* Get the first parent not deleted
*/
private fun firstNotDeletedParent(
node: NodeVersioned<UUID, GroupKDBX, EntryKDBX>,
deletedObjects: Set<DeletedObject>
): GroupKDBX? {
var parent = node.parent
while (parent != null && deletedObjects.containsNode(parent)) {
parent = node.parent
}
return parent
}
/**
* Delete a group from the database with the [deletedGroup] id
* Recursively check whether a group to be deleted contains a node not to be deleted with [deletedObjects]
* and move it to the first parent that has not been deleted.
*/
private fun deleteGroup(deletedGroup: DeletedObject, deletedObjects: Set<DeletedObject>) {
val databaseGroup = database.getGroupById(deletedGroup.uuid)
if (databaseGroup != null
&& deletedObject.deletionTime.isAfter(databaseGroup.lastModificationTime)) {
&& deletedGroup.deletionTime.isAfter(databaseGroup.lastModificationTime)) {
// Must be in dedicated list to prevent modification collision
val entriesToMove = mutableListOf<EntryKDBX>()
databaseGroup.getChildEntries().forEach { child ->
// If the child entry is not a deleted object,
if (deletedObjects.notContainsNode(child)) {
entriesToMove.add(child)
}
}
val groupsToMove = mutableListOf<GroupKDBX>()
databaseGroup.getChildGroups().forEach { child ->
// Move the group to the first parent not deleted
// the deleted objects will take care of remove it later
groupsToMove.add(child)
}
// For each node to move, move it
// try to move the child entry in the first parent not deleted
entriesToMove.forEach { child ->
database.removeEntryFrom(child, child.parent)
database.addEntryTo(
child,
firstNotDeletedParent(databaseGroup, deletedObjects)
)
}
groupsToMove.forEach { child ->
database.removeGroupFrom(child, child.parent)
database.addGroupTo(
child,
firstNotDeletedParent(databaseGroup, deletedObjects)
)
}
// Then delete the group
database.removeGroupFrom(databaseGroup, databaseGroup.parent)
}
}
/**
* Delete an icon from the database with the [deletedIcon] id
*/
private fun deleteIcon(deletedIcon: DeletedObject) {
val deletedObjectId = deletedIcon.uuid
val databaseIcon = database.iconsManager.getIcon(deletedObjectId)
val databaseIconModificationTime = databaseIcon?.lastModificationTime
if (databaseIcon != null
&& (
databaseIconModificationTime == null
|| (deletedObject.deletionTime.isAfter(databaseIconModificationTime))
)
&& (databaseIconModificationTime == null
|| (deletedIcon.deletionTime.isAfter(databaseIconModificationTime)))
) {
database.removeCustomIcon(deletedObjectId)
}
// Attachments are removed and optimized during the database save
}
}
/**

View File

@@ -154,7 +154,7 @@ class SearchHelper {
if (searchParameters.searchByDomain) {
try {
stringToCheck.inTheSameDomainAs(word, sameSubDomain = true)
} catch (e: Exception) {
} catch (_: Exception) {
false
}
} else null
@@ -220,11 +220,19 @@ class SearchHelper {
regex.matches(stringToCheck)
} else {
specialComparison?.invoke(stringToCheck, searchParameters.searchQuery)
?: stringToCheck.contains(
searchParameters.searchQuery,
?: run {
// Search with space separator #175
var searchFound = true
searchParameters.searchQuery.split(" ").forEach { word ->
searchFound = searchFound
&& stringToCheck.contains(
word,
!searchParameters.caseSensitive
)
}
searchFound
}
}
}
}
}

View File

@@ -44,11 +44,21 @@ data class AppOrigin(
this.webOrigins.add(webOrigin)
}
/**
* Determine whether at least one signature is present in the Android origins
*/
fun containsAndroidOriginSignature(): Boolean {
return androidOrigins.any { !it.fingerprint.isNullOrEmpty() }
}
/**
* Verify the app origin by comparing it to the list of android origins,
* return the first verified origin or throw an exception if none is found
*/
fun checkAppOrigin(compare: AppOrigin): String {
if (compare.containsAndroidOriginSignature().not()) {
throw SignatureNotFoundException(this, "Android origin not found")
}
return androidOrigins.firstOrNull { androidOrigin ->
compare.androidOrigins.any {
it.packageName == androidOrigin.packageName
@@ -79,6 +89,14 @@ data class AppOrigin(
} else null
}
override fun toString(): String {
return if (androidOrigins.isNotEmpty()) {
androidOrigins.first().toString()
} else if (webOrigins.isNotEmpty()) {
webOrigins.first().toString()
} else super.toString()
}
companion object {
private val TAG = AppOrigin::class.java.simpleName
@@ -100,6 +118,14 @@ data class AppOrigin(
}
}
/**
* Exception indicating that no signature is present for the Android origin
*/
class SignatureNotFoundException(
val temptingApp: AppOrigin,
message: String
) : Exception(message)
/**
* Represents an Android app origin, the [packageName] is the applicationId of the app
* and the [fingerprint] is the

View File

@@ -213,15 +213,19 @@ class EntryInfo : NodeInfo {
registerInfo.password?.let { password = it }
setCreditCard(registerInfo.creditCard)
setPasskey(registerInfo.passkey)
setAppOrigin(
registerInfo.appOrigin,
database?.allowEntryCustomFields() == true
)
saveAppOrigin(database, registerInfo.appOrigin)
if (title.isEmpty()) {
title = registerInfo.toString().toTitle()
}
}
/**
* Add AppOrigin
*/
fun saveAppOrigin(database: Database?, appOrigin: AppOrigin?) {
setAppOrigin(appOrigin, database?.allowEntryCustomFields() == true)
}
fun getVisualTitle(): String {
return title.ifEmpty {
url.ifEmpty {

View File

@@ -119,19 +119,19 @@ fun <K : Parcelable, V : Parcelable> Parcel.writeParcelableMap(map: Map<K, V>, f
inline fun <reified K : Parcelable, reified V : Parcelable> Parcel.readParcelableMap(): Map<K, V> {
val size = readInt()
val map = HashMap<K, V>(size)
for (i in 0 until size) {
(0 until size).forEach { i ->
val key: K? = try {
when {
SDK_INT >= 33 -> readParcelable(K::class.java.classLoader, K::class.java)
else -> @Suppress("DEPRECATION") readParcelable(K::class.java.classLoader)
}
} catch (e: Exception) { null }
} catch (_: Exception) { null }
val value: V? = try {
when {
SDK_INT >= 33 -> readParcelable(V::class.java.classLoader, V::class.java)
else -> @Suppress("DEPRECATION") readParcelable(V::class.java.classLoader)
}
} catch (e: Exception) { null }
} catch (_: Exception) { null }
if (key != null && value != null)
map[key] = value
}
@@ -151,14 +151,14 @@ fun <V : Parcelable> Parcel.writeStringParcelableMap(map: HashMap<String, V>, fl
inline fun <reified V : Parcelable> Parcel.readStringParcelableMap(): LinkedHashMap<String, V> {
val size = readInt()
val map = LinkedHashMap<String, V>(size)
for (i in 0 until size) {
(0 until size).forEach { i ->
val key: String? = readString()
val value: V? = try {
when {
SDK_INT >= 33 -> readParcelable(V::class.java.classLoader, V::class.java)
else -> @Suppress("DEPRECATION") readParcelable(V::class.java.classLoader)
}
} catch (e: Exception) { null }
} catch (_: Exception) { null }
if (key != null && value != null)
map[key] = value
}
@@ -178,7 +178,7 @@ fun Parcel.writeStringIntMap(map: LinkedHashMap<String, Int>) {
fun Parcel.readStringIntMap(): LinkedHashMap<String, Int> {
val size = readInt()
val map = LinkedHashMap<String, Int>(size)
for (i in 0 until size) {
(0 until size).forEach { i ->
val key: String? = readString()
val value: Int = readInt()
if (key != null)
@@ -200,7 +200,7 @@ fun Parcel.writeStringStringMap(map: MutableMap<String, String>) {
fun Parcel.readStringStringMap(): LinkedHashMap<String, String> {
val size = readInt()
val map = LinkedHashMap<String, String>(size)
for (i in 0 until size) {
(0 until size).forEach { i ->
val key: String? = readString()
val value: String? = readString()
if (key != null && value != null)

View File

@@ -0,0 +1,6 @@
* Updated to API 35 minimum SDK 19 #2073 #2138 #2067 #2133 #1687 (Thx @Dev-ClayP)
* Remember last read-only state #2099 #2100 (Thx @rmacklin)
* Fix merge deletion #1516
* Fix space in search #175
* Fix deletable recycle bin #2163
* Small fixes

View File

@@ -0,0 +1,2 @@
* Passkeys management #1421 #2097 (Thx @cali-95)
* Small fixes

View File

@@ -0,0 +1,6 @@
* Mise à jour vers API 35 minimum SDK 19 #2073 #2138 #2067 #2133 #1687 (Thx @Dev-ClayP)
* Sauvegarde du dernier état lecture seule #2099 #2100 (Thx @rmacklin)
* Correction de la suppression lors d'un merge #1516
* Correction des espaces dans la recherche #175
* Correction de la poubelle supprimable #2163
* Petites corrections

View File

@@ -0,0 +1,2 @@
* Gestion des Passkeys #1421 #2097 (Thx @cali-95)
* Petites corrections