mirror of
https://github.com/Kunzisoft/KeePassDX.git
synced 2025-12-04 15:49:33 +01:00
Compare commits
25 Commits
4.2.0beta0
...
feature/Pa
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
01d778650c | ||
|
|
dd389dbab1 | ||
|
|
272ebd0c3f | ||
|
|
0aecc21f43 | ||
|
|
1e7e464e65 | ||
|
|
d5c378ac85 | ||
|
|
672f1ca37d | ||
|
|
2f9e1e4bf2 | ||
|
|
25d97e4f2e | ||
|
|
f49dcbd654 | ||
|
|
bf2d56b4fd | ||
|
|
5893541dd2 | ||
|
|
2230fe66ab | ||
|
|
84a62a32ff | ||
|
|
da8ef9340c | ||
|
|
af068349e4 | ||
|
|
56cb5953dd | ||
|
|
2fc2a9c7c1 | ||
|
|
69e7cdbc47 | ||
|
|
39d9a74a73 | ||
|
|
7212c73481 | ||
|
|
3ee4caa153 | ||
|
|
28e4d929bb | ||
|
|
803d637510 | ||
|
|
ccd5da0962 |
@@ -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)
|
||||
|
||||
@@ -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"
|
||||
@@ -152,7 +152,7 @@ dependencies {
|
||||
|
||||
// Credentials Provider
|
||||
implementation "androidx.credentials:credentials:1.2.2"
|
||||
|
||||
|
||||
// Modules import
|
||||
implementation project(path: ':database')
|
||||
implementation project(path: ':icon-pack')
|
||||
|
||||
@@ -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,41 +432,35 @@ 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 ->
|
||||
EntrySelectionHelper.doSpecialAction(
|
||||
intent = intent,
|
||||
defaultAction = {
|
||||
// Finish naturally
|
||||
finishForEntryResult(entry)
|
||||
},
|
||||
searchAction = {
|
||||
// Nothing when search retrieved
|
||||
},
|
||||
saveAction = {
|
||||
entryValidatedForSave(entry)
|
||||
},
|
||||
keyboardSelectionAction = {
|
||||
entryValidatedForKeyboardSelection(database, entry)
|
||||
},
|
||||
autofillSelectionAction = { _, _ ->
|
||||
entryValidatedForAutofillSelection(database, entry)
|
||||
},
|
||||
autofillRegistrationAction = {
|
||||
entryValidatedForAutofillRegistration(entry)
|
||||
},
|
||||
passkeySelectionAction = {
|
||||
entryValidatedForPasskeySelection(database, entry)
|
||||
},
|
||||
passkeyRegistrationAction = {
|
||||
entryValidatedForPasskeyRegistration(database, entry)
|
||||
}
|
||||
)
|
||||
}
|
||||
result.data?.getNewEntry(database)?.let { entry ->
|
||||
EntrySelectionHelper.doSpecialAction(
|
||||
intent = intent,
|
||||
defaultAction = {
|
||||
// Finish naturally
|
||||
finishForEntryResult(entry)
|
||||
},
|
||||
searchAction = {
|
||||
// Nothing when search retrieved
|
||||
},
|
||||
saveAction = {
|
||||
entryValidatedForSave(entry)
|
||||
},
|
||||
keyboardSelectionAction = {
|
||||
entryValidatedForKeyboardSelection(database, entry)
|
||||
},
|
||||
autofillSelectionAction = { _, _ ->
|
||||
entryValidatedForAutofillSelection(database, entry)
|
||||
},
|
||||
autofillRegistrationAction = {
|
||||
entryValidatedForAutofillRegistration(entry)
|
||||
},
|
||||
passkeySelectionAction = {
|
||||
entryValidatedForPasskeySelection(database, entry)
|
||||
},
|
||||
passkeyRegistrationAction = {
|
||||
entryValidatedForPasskeyRegistration(database, entry)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,29 +246,31 @@ class MainCredentialActivity : DatabaseModeActivity() {
|
||||
lifecycleScope.launch {
|
||||
repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
mDeviceUnlockViewModel.uiState.collect { uiState ->
|
||||
// New value received
|
||||
uiState.credentialRequiredCipher?.let { cipher ->
|
||||
mDeviceUnlockViewModel.encryptCredential(
|
||||
credential = getCredentialForEncryption(),
|
||||
cipher = cipher
|
||||
)
|
||||
}
|
||||
uiState.cipherEncryptDatabase?.let { cipherEncryptDatabase ->
|
||||
onCredentialEncrypted(cipherEncryptDatabase)
|
||||
mDeviceUnlockViewModel.consumeCredentialEncrypted()
|
||||
}
|
||||
uiState.cipherDecryptDatabase?.let { cipherDecryptDatabase ->
|
||||
onCredentialDecrypted(cipherDecryptDatabase)
|
||||
mDeviceUnlockViewModel.consumeCredentialDecrypted()
|
||||
}
|
||||
uiState.exception?.let { error ->
|
||||
Snackbar.make(
|
||||
coordinatorLayout,
|
||||
deviceUnlockError(error, this@MainCredentialActivity),
|
||||
Snackbar.LENGTH_LONG
|
||||
).asError().show()
|
||||
mDeviceUnlockViewModel.exceptionShown()
|
||||
mDeviceUnlockViewModel?.let { deviceUnlockViewModel ->
|
||||
deviceUnlockViewModel.uiState.collect { uiState ->
|
||||
// New value received
|
||||
uiState.credentialRequiredCipher?.let { cipher ->
|
||||
deviceUnlockViewModel.encryptCredential(
|
||||
credential = getCredentialForEncryption(),
|
||||
cipher = cipher
|
||||
)
|
||||
}
|
||||
uiState.cipherEncryptDatabase?.let { cipherEncryptDatabase ->
|
||||
onCredentialEncrypted(cipherEncryptDatabase)
|
||||
deviceUnlockViewModel.consumeCredentialEncrypted()
|
||||
}
|
||||
uiState.cipherDecryptDatabase?.let { cipherDecryptDatabase ->
|
||||
onCredentialDecrypted(cipherDecryptDatabase)
|
||||
deviceUnlockViewModel.consumeCredentialDecrypted()
|
||||
}
|
||||
uiState.exception?.let { error ->
|
||||
Snackbar.make(
|
||||
coordinatorLayout,
|
||||
deviceUnlockError(error, this@MainCredentialActivity),
|
||||
Snackbar.LENGTH_LONG
|
||||
).asError().show()
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
bundle.putBoolean(SORT_RECYCLE_BIN_BOTTOM_BUNDLE_KEY, recycleBinBottom)
|
||||
recycleBinBottom?.let {
|
||||
bundle.putBoolean(SORT_RECYCLE_BIN_BOTTOM_BUNDLE_KEY, recycleBinBottom)
|
||||
}
|
||||
val fragment = SortDialogFragment()
|
||||
fragment.arguments = bundle
|
||||
return fragment
|
||||
|
||||
@@ -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),
|
||||
SortDialogFragment.getInstance(
|
||||
PreferencesUtil.getListSort(context),
|
||||
PreferencesUtil.getAscendingSort(context),
|
||||
PreferencesUtil.getGroupsBeforeSort(context),
|
||||
if (mDatabase?.isRecycleBinEnabled == true) {
|
||||
PreferencesUtil.getRecycleBinBottomSort(context)
|
||||
)
|
||||
} else {
|
||||
SortDialogFragment.getInstance(
|
||||
PreferencesUtil.getListSort(context),
|
||||
PreferencesUtil.getAscendingSort(context),
|
||||
PreferencesUtil.getGroupsBeforeSort(context)
|
||||
)
|
||||
}
|
||||
|
||||
} else null
|
||||
)
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -23,139 +23,54 @@ 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")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to create selection response for passkey", e)
|
||||
showError(e)
|
||||
}
|
||||
// Return the response
|
||||
responseIntent
|
||||
}
|
||||
)
|
||||
this.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
||||
passkeyLauncherViewModel.manageSelectionResult(it)
|
||||
}
|
||||
|
||||
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
|
||||
)
|
||||
)
|
||||
}
|
||||
} 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
|
||||
}
|
||||
)
|
||||
|
||||
this.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
||||
passkeyLauncherViewModel.manageRegistrationResult(it)
|
||||
}
|
||||
|
||||
override fun applyCustomStyle(): Boolean {
|
||||
return false
|
||||
}
|
||||
@@ -164,52 +79,120 @@ 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()
|
||||
}
|
||||
|
||||
/**
|
||||
* 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)
|
||||
}
|
||||
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 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
||||
super.onDatabaseRetrieved(database)
|
||||
passkeyLauncherViewModel.launchPasskeyActionIfNeeded(intent, mSpecialMode, database)
|
||||
}
|
||||
|
||||
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))
|
||||
setMessage(StringBuilder()
|
||||
.append(
|
||||
getString(
|
||||
R.string.passkeys_privileged_apps_ask_message,
|
||||
e.temptingApp.toString()
|
||||
R.string.passkeys_privileged_apps_ask_message,
|
||||
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)
|
||||
)
|
||||
launchPasskeyAction(mDatabase)
|
||||
}
|
||||
passkeyLauncherViewModel.saveCustomPrivilegedApp(
|
||||
intent = intent,
|
||||
specialMode = mSpecialMode,
|
||||
database = mDatabase,
|
||||
temptingApp = temptingApp
|
||||
)
|
||||
}
|
||||
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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
setNegativeButton(android.R.string.cancel) { _, _ ->
|
||||
passkeyLauncherViewModel.cancelResult()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun showError(e: Throwable) {
|
||||
Toast.makeText(this, e.localizedMessage, Toast.LENGTH_LONG).show()
|
||||
setOnCancelListener {
|
||||
passkeyLauncherViewModel.cancelResult()
|
||||
}
|
||||
}.create().show()
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
signature = Signature.sign(privateKey, dataToSign())
|
||||
?: throw GetCredentialUnknownException("signing failed")
|
||||
try {
|
||||
signature = Signature.sign(privateKey, dataToSign())
|
||||
} catch (e: Exception) {
|
||||
Log.e(this::class.java.simpleName, "Unable to sign: ${e.message}")
|
||||
throw GetCredentialUnknownException("Signing failed")
|
||||
}
|
||||
}
|
||||
|
||||
private fun dataToSign(): ByteArray {
|
||||
|
||||
@@ -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
|
||||
)
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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>>()
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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 ->
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -481,7 +481,7 @@
|
||||
<string name="data">Données</string>
|
||||
<string name="show_uuid_summary">Affiche l’UUID lié à une entrée ou un groupe</string>
|
||||
<string name="show_uuid_title">Afficher l’UUID</string>
|
||||
<string name="autofill_read_only_save">L’enregistrement des données n’est pas autorisé pour une base de données ouverte en lecture seule.</string>
|
||||
<string name="error_save_read_only">L’enregistrement des données n’est 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 d’enregistrer les informations de recherche lors de la sélection manuelle d’une entrée pour faciliter les utilisations futures</string>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">S’lejohet 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">S’lejohet 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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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
12
art/ic_passkey_dark.svg
Normal 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
12
art/ic_passkey_light.svg
Normal 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 |
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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,32 +304,113 @@ 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
|
||||
if (databaseEntry != null
|
||||
&& deletedObject.deletionTime.isAfter(databaseEntry.lastModificationTime)) {
|
||||
database.removeEntryFrom(databaseEntry, databaseEntry.parent)
|
||||
}
|
||||
if (databaseGroup != null
|
||||
&& deletedObject.deletionTime.isAfter(databaseGroup.lastModificationTime)) {
|
||||
database.removeGroupFrom(databaseGroup, databaseGroup.parent)
|
||||
}
|
||||
if (databaseIcon != null
|
||||
&& (
|
||||
databaseIconModificationTime == null
|
||||
|| (deletedObject.deletionTime.isAfter(databaseIconModificationTime))
|
||||
)
|
||||
) {
|
||||
database.removeCustomIcon(deletedObjectId)
|
||||
}
|
||||
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
|
||||
&& 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
|
||||
&& 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
|
||||
|| (deletedIcon.deletionTime.isAfter(databaseIconModificationTime)))
|
||||
) {
|
||||
database.removeCustomIcon(deletedObjectId)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge [customDataToMerge] in [customData]
|
||||
*/
|
||||
|
||||
@@ -154,7 +154,7 @@ class SearchHelper {
|
||||
if (searchParameters.searchByDomain) {
|
||||
try {
|
||||
stringToCheck.inTheSameDomainAs(word, sameSubDomain = true)
|
||||
} catch (e: Exception) {
|
||||
} catch (_: Exception) {
|
||||
false
|
||||
}
|
||||
} else null
|
||||
@@ -220,10 +220,18 @@ class SearchHelper {
|
||||
regex.matches(stringToCheck)
|
||||
} else {
|
||||
specialComparison?.invoke(stringToCheck, searchParameters.searchQuery)
|
||||
?: stringToCheck.contains(
|
||||
searchParameters.searchQuery,
|
||||
!searchParameters.caseSensitive
|
||||
)
|
||||
?: run {
|
||||
// Search with space separator #175
|
||||
var searchFound = true
|
||||
searchParameters.searchQuery.split(" ").forEach { word ->
|
||||
searchFound = searchFound
|
||||
&& stringToCheck.contains(
|
||||
word,
|
||||
!searchParameters.caseSensitive
|
||||
)
|
||||
}
|
||||
searchFound
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
6
fastlane/metadata/android/en-US/changelogs/141.txt
Normal file
6
fastlane/metadata/android/en-US/changelogs/141.txt
Normal 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
|
||||
2
fastlane/metadata/android/en-US/changelogs/142.txt
Normal file
2
fastlane/metadata/android/en-US/changelogs/142.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
* Passkeys management #1421 #2097 (Thx @cali-95)
|
||||
* Small fixes
|
||||
6
fastlane/metadata/android/fr-FR/changelogs/141.txt
Normal file
6
fastlane/metadata/android/fr-FR/changelogs/141.txt
Normal 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
|
||||
2
fastlane/metadata/android/fr-FR/changelogs/142.txt
Normal file
2
fastlane/metadata/android/fr-FR/changelogs/142.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
* Gestion des Passkeys #1421 #2097 (Thx @cali-95)
|
||||
* Petites corrections
|
||||
Reference in New Issue
Block a user