mirror of
https://github.com/Kunzisoft/KeePassDX.git
synced 2025-12-04 15:49:33 +01:00
fix: Refactoring Credential Provider
This commit is contained in:
@@ -161,7 +161,7 @@
|
|||||||
<activity
|
<activity
|
||||||
android:name="com.kunzisoft.keepass.settings.SettingsActivity" />
|
android:name="com.kunzisoft.keepass.settings.SettingsActivity" />
|
||||||
<activity
|
<activity
|
||||||
android:name="com.kunzisoft.keepass.activities.AutofillLauncherActivity"
|
android:name="com.kunzisoft.keepass.credentialprovider.activity.AutofillLauncherActivity"
|
||||||
android:theme="@style/Theme.Transparent"
|
android:theme="@style/Theme.Transparent"
|
||||||
android:configChanges="keyboardHidden"
|
android:configChanges="keyboardHidden"
|
||||||
android:excludeFromRecents="true"/>
|
android:excludeFromRecents="true"/>
|
||||||
@@ -175,7 +175,7 @@
|
|||||||
android:name="com.kunzisoft.keepass.hardware.HardwareKeyActivity"
|
android:name="com.kunzisoft.keepass.hardware.HardwareKeyActivity"
|
||||||
android:theme="@style/Theme.Transparent" />
|
android:theme="@style/Theme.Transparent" />
|
||||||
<activity
|
<activity
|
||||||
android:name="com.kunzisoft.keepass.activities.EntrySelectionLauncherActivity"
|
android:name="com.kunzisoft.keepass.credentialprovider.activity.EntrySelectionLauncherActivity"
|
||||||
android:theme="@style/Theme.Transparent"
|
android:theme="@style/Theme.Transparent"
|
||||||
android:launchMode="singleInstance"
|
android:launchMode="singleInstance"
|
||||||
android:exported="true">
|
android:exported="true">
|
||||||
@@ -201,19 +201,13 @@
|
|||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name="com.kunzisoft.keepass.credentialprovider.activity.CreatePasskeyActivity"
|
android:name="com.kunzisoft.keepass.credentialprovider.activity.PasskeyLauncherActivity"
|
||||||
android:label="CreatePasskeyActivity"
|
android:theme="@style/Theme.Transparent"
|
||||||
android:exported="true"
|
android:configChanges="keyboardHidden"
|
||||||
|
android:excludeFromRecents="true"
|
||||||
|
android:exported="false"
|
||||||
tools:targetApi="upside_down_cake" />
|
tools:targetApi="upside_down_cake" />
|
||||||
|
|
||||||
<activity
|
|
||||||
android:name="com.kunzisoft.keepass.credentialprovider.activity.UsePasskeyActivity"
|
|
||||||
android:label="UsePasskeyActivity"
|
|
||||||
android:exported="true"
|
|
||||||
tools:targetApi="upside_down_cake" />
|
|
||||||
|
|
||||||
<service
|
<service
|
||||||
android:name="com.kunzisoft.keepass.services.DatabaseTaskNotificationService"
|
android:name="com.kunzisoft.keepass.services.DatabaseTaskNotificationService"
|
||||||
android:foregroundServiceType="dataSync"
|
android:foregroundServiceType="dataSync"
|
||||||
@@ -241,7 +235,7 @@
|
|||||||
android:exported="false" />
|
android:exported="false" />
|
||||||
<!-- Receiver for Autofill -->
|
<!-- Receiver for Autofill -->
|
||||||
<service
|
<service
|
||||||
android:name="com.kunzisoft.keepass.autofill.KeeAutofillService"
|
android:name="com.kunzisoft.keepass.credentialprovider.autofill.KeeAutofillService"
|
||||||
android:label="@string/autofill_service_name"
|
android:label="@string/autofill_service_name"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:permission="android.permission.BIND_AUTOFILL_SERVICE">
|
android:permission="android.permission.BIND_AUTOFILL_SERVICE">
|
||||||
@@ -253,7 +247,7 @@
|
|||||||
</intent-filter>
|
</intent-filter>
|
||||||
</service>
|
</service>
|
||||||
<service
|
<service
|
||||||
android:name="com.kunzisoft.keepass.magikeyboard.MagikeyboardService"
|
android:name="com.kunzisoft.keepass.credentialprovider.magikeyboard.MagikeyboardService"
|
||||||
android:label="@string/keyboard_label"
|
android:label="@string/keyboard_label"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:permission="android.permission.BIND_INPUT_METHOD" >
|
android:permission="android.permission.BIND_INPUT_METHOD" >
|
||||||
@@ -263,12 +257,11 @@
|
|||||||
<action android:name="android.view.InputMethod" />
|
<action android:name="android.view.InputMethod" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</service>
|
</service>
|
||||||
|
|
||||||
<service
|
<service
|
||||||
android:name="com.kunzisoft.keepass.credentialprovider.service.KeePassDXCredentialProviderService"
|
android:name="com.kunzisoft.keepass.credentialprovider.passkey.PasskeyProviderService"
|
||||||
android:enabled="true"
|
android:enabled="true"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:label="KeePassDX Credential Provider"
|
android:label="@string/passkey_service_name"
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
android:permission="android.permission.BIND_CREDENTIAL_PROVIDER_SERVICE"
|
android:permission="android.permission.BIND_CREDENTIAL_PROVIDER_SERVICE"
|
||||||
tools:targetApi="upside_down_cake">
|
tools:targetApi="upside_down_cake">
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ import android.content.Intent
|
|||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
import android.graphics.drawable.ColorDrawable
|
import android.graphics.drawable.ColorDrawable
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Build
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
@@ -38,13 +37,10 @@ import androidx.activity.result.ActivityResultLauncher
|
|||||||
import androidx.activity.viewModels
|
import androidx.activity.viewModels
|
||||||
import androidx.appcompat.widget.Toolbar
|
import androidx.appcompat.widget.Toolbar
|
||||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||||
import androidx.core.content.ContextCompat
|
|
||||||
import androidx.core.content.res.ResourcesCompat
|
|
||||||
import androidx.core.graphics.BlendModeColorFilterCompat
|
import androidx.core.graphics.BlendModeColorFilterCompat
|
||||||
import androidx.core.graphics.BlendModeCompat
|
import androidx.core.graphics.BlendModeCompat
|
||||||
import androidx.core.graphics.ColorUtils
|
import androidx.core.graphics.ColorUtils
|
||||||
import androidx.core.view.ViewCompat
|
import androidx.core.view.ViewCompat
|
||||||
import androidx.core.view.WindowCompat
|
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.google.android.material.appbar.AppBarLayout
|
import com.google.android.material.appbar.AppBarLayout
|
||||||
@@ -54,7 +50,7 @@ import com.google.android.material.tabs.TabLayout
|
|||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.activities.fragments.EntryFragment
|
import com.kunzisoft.keepass.activities.fragments.EntryFragment
|
||||||
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
|
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
|
||||||
import com.kunzisoft.keepass.activities.helpers.SpecialMode
|
import com.kunzisoft.keepass.credentialprovider.SpecialMode
|
||||||
import com.kunzisoft.keepass.activities.legacy.DatabaseLockActivity
|
import com.kunzisoft.keepass.activities.legacy.DatabaseLockActivity
|
||||||
import com.kunzisoft.keepass.adapters.TagsAdapter
|
import com.kunzisoft.keepass.adapters.TagsAdapter
|
||||||
import com.kunzisoft.keepass.database.ContextualDatabase
|
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||||
@@ -62,7 +58,7 @@ import com.kunzisoft.keepass.database.element.Attachment
|
|||||||
import com.kunzisoft.keepass.database.element.icon.IconImage
|
import com.kunzisoft.keepass.database.element.icon.IconImage
|
||||||
import com.kunzisoft.keepass.database.element.node.NodeId
|
import com.kunzisoft.keepass.database.element.node.NodeId
|
||||||
import com.kunzisoft.keepass.education.EntryActivityEducation
|
import com.kunzisoft.keepass.education.EntryActivityEducation
|
||||||
import com.kunzisoft.keepass.magikeyboard.MagikeyboardService
|
import com.kunzisoft.keepass.credentialprovider.magikeyboard.MagikeyboardService
|
||||||
import com.kunzisoft.keepass.model.EntryAttachmentState
|
import com.kunzisoft.keepass.model.EntryAttachmentState
|
||||||
import com.kunzisoft.keepass.otp.OtpType
|
import com.kunzisoft.keepass.otp.OtpType
|
||||||
import com.kunzisoft.keepass.services.AttachmentFileNotificationService
|
import com.kunzisoft.keepass.services.AttachmentFileNotificationService
|
||||||
|
|||||||
@@ -55,12 +55,15 @@ import com.kunzisoft.keepass.activities.dialogs.FileTooBigDialogFragment.Compani
|
|||||||
import com.kunzisoft.keepass.activities.dialogs.ReplaceFileDialogFragment
|
import com.kunzisoft.keepass.activities.dialogs.ReplaceFileDialogFragment
|
||||||
import com.kunzisoft.keepass.activities.dialogs.SetOTPDialogFragment
|
import com.kunzisoft.keepass.activities.dialogs.SetOTPDialogFragment
|
||||||
import com.kunzisoft.keepass.activities.fragments.EntryEditFragment
|
import com.kunzisoft.keepass.activities.fragments.EntryEditFragment
|
||||||
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
|
||||||
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
|
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
|
||||||
import com.kunzisoft.keepass.activities.legacy.DatabaseLockActivity
|
import com.kunzisoft.keepass.activities.legacy.DatabaseLockActivity
|
||||||
import com.kunzisoft.keepass.adapters.TemplatesSelectorAdapter
|
import com.kunzisoft.keepass.adapters.TemplatesSelectorAdapter
|
||||||
import com.kunzisoft.keepass.autofill.AutofillComponent
|
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper
|
||||||
import com.kunzisoft.keepass.autofill.AutofillHelper
|
import com.kunzisoft.keepass.credentialprovider.TypeMode
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.autofill.AutofillComponent
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.autofill.AutofillHelper
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.magikeyboard.MagikeyboardService
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.buildPasskeyResponseAndSetResult
|
||||||
import com.kunzisoft.keepass.database.ContextualDatabase
|
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||||
import com.kunzisoft.keepass.database.element.Attachment
|
import com.kunzisoft.keepass.database.element.Attachment
|
||||||
import com.kunzisoft.keepass.database.element.DateInstant
|
import com.kunzisoft.keepass.database.element.DateInstant
|
||||||
@@ -70,7 +73,6 @@ import com.kunzisoft.keepass.database.element.node.Node
|
|||||||
import com.kunzisoft.keepass.database.element.node.NodeId
|
import com.kunzisoft.keepass.database.element.node.NodeId
|
||||||
import com.kunzisoft.keepass.database.element.template.Template
|
import com.kunzisoft.keepass.database.element.template.Template
|
||||||
import com.kunzisoft.keepass.education.EntryEditActivityEducation
|
import com.kunzisoft.keepass.education.EntryEditActivityEducation
|
||||||
import com.kunzisoft.keepass.magikeyboard.MagikeyboardService
|
|
||||||
import com.kunzisoft.keepass.model.AttachmentState
|
import com.kunzisoft.keepass.model.AttachmentState
|
||||||
import com.kunzisoft.keepass.model.DataTime
|
import com.kunzisoft.keepass.model.DataTime
|
||||||
import com.kunzisoft.keepass.model.EntryAttachmentState
|
import com.kunzisoft.keepass.model.EntryAttachmentState
|
||||||
@@ -376,18 +378,25 @@ class EntryEditActivity : DatabaseLockActivity(),
|
|||||||
|
|
||||||
// Don't wait for saving if it's to provide autofill
|
// Don't wait for saving if it's to provide autofill
|
||||||
mDatabase?.let { database ->
|
mDatabase?.let { database ->
|
||||||
EntrySelectionHelper.doSpecialAction(intent,
|
EntrySelectionHelper.doSpecialAction(
|
||||||
{},
|
intent = intent,
|
||||||
{},
|
defaultAction = {},
|
||||||
{},
|
searchAction = {},
|
||||||
{
|
saveAction = {},
|
||||||
|
keyboardSelectionAction = {
|
||||||
entryValidatedForKeyboardSelection(database, entrySave.newEntry)
|
entryValidatedForKeyboardSelection(database, entrySave.newEntry)
|
||||||
},
|
},
|
||||||
{ _, _ ->
|
autofillSelectionAction = { _, _ ->
|
||||||
entryValidatedForAutofillSelection(database, entrySave.newEntry)
|
entryValidatedForAutofillSelection(database, entrySave.newEntry)
|
||||||
},
|
},
|
||||||
{
|
autofillRegistrationAction = {
|
||||||
entryValidatedForAutofillRegistration(entrySave.newEntry)
|
entryValidatedForAutofillRegistration(entrySave.newEntry)
|
||||||
|
},
|
||||||
|
passkeySelectionAction = {
|
||||||
|
entryValidatedForPasskeySelection(database, entrySave.newEntry)
|
||||||
|
},
|
||||||
|
passkeyRegistrationAction = {
|
||||||
|
entryValidatedForPasskeyRegistration(database, entrySave.newEntry)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -430,25 +439,32 @@ class EntryEditActivity : DatabaseLockActivity(),
|
|||||||
}
|
}
|
||||||
if (newNodes.size == 1) {
|
if (newNodes.size == 1) {
|
||||||
(newNodes[0] as? Entry?)?.let { entry ->
|
(newNodes[0] as? Entry?)?.let { entry ->
|
||||||
EntrySelectionHelper.doSpecialAction(intent,
|
EntrySelectionHelper.doSpecialAction(
|
||||||
{
|
intent = intent,
|
||||||
|
defaultAction = {
|
||||||
// Finish naturally
|
// Finish naturally
|
||||||
finishForEntryResult(entry)
|
finishForEntryResult(entry)
|
||||||
},
|
},
|
||||||
{
|
searchAction = {
|
||||||
// Nothing when search retrieved
|
// Nothing when search retrieved
|
||||||
},
|
},
|
||||||
{
|
saveAction = {
|
||||||
entryValidatedForSave(entry)
|
entryValidatedForSave(entry)
|
||||||
},
|
},
|
||||||
{
|
keyboardSelectionAction = {
|
||||||
entryValidatedForKeyboardSelection(database, entry)
|
entryValidatedForKeyboardSelection(database, entry)
|
||||||
},
|
},
|
||||||
{ _, _ ->
|
autofillSelectionAction = { _, _ ->
|
||||||
entryValidatedForAutofillSelection(database, entry)
|
entryValidatedForAutofillSelection(database, entry)
|
||||||
},
|
},
|
||||||
{
|
autofillRegistrationAction = {
|
||||||
entryValidatedForAutofillRegistration(entry)
|
entryValidatedForAutofillRegistration(entry)
|
||||||
|
},
|
||||||
|
passkeySelectionAction = {
|
||||||
|
entryValidatedForPasskeySelection(database, entry)
|
||||||
|
},
|
||||||
|
passkeyRegistrationAction = {
|
||||||
|
entryValidatedForPasskeyRegistration(database, entry)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -488,9 +504,33 @@ class EntryEditActivity : DatabaseLockActivity(),
|
|||||||
onValidateSpecialMode()
|
onValidateSpecialMode()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun entryValidatedForAutofillRegistration(entry: Entry) {
|
private fun entryValidatedForPasskeySelection(database: ContextualDatabase, entry: Entry) {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
||||||
|
this.buildPasskeyResponseAndSetResult(
|
||||||
|
entryInfo = entry.getEntryInfo(database)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
onValidateSpecialMode()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun entryValidatedForAutofillRegistration(entry: Entry) {
|
||||||
|
//if (isIntentSender()) {
|
||||||
|
// TODO Autofill Callback #765
|
||||||
|
//}
|
||||||
|
onValidateSpecialMode()
|
||||||
|
if (!isIntentSender()) {
|
||||||
|
finishForEntryResult(entry)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun entryValidatedForPasskeyRegistration(database: ContextualDatabase, entry: Entry) {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
||||||
|
this.buildPasskeyResponseAndSetResult(
|
||||||
|
entryInfo = entry.getEntryInfo(database),
|
||||||
|
extras = buildEntryResult(entry) // To update the previous screen
|
||||||
|
)
|
||||||
|
}
|
||||||
onValidateSpecialMode()
|
onValidateSpecialMode()
|
||||||
finishForEntryResult(entry)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
@@ -742,12 +782,17 @@ class EntryEditActivity : DatabaseLockActivity(),
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun buildEntryResult(entry: Entry): Bundle {
|
||||||
|
return Bundle().apply {
|
||||||
|
putParcelable(ADD_OR_UPDATE_ENTRY_KEY, entry.nodeId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun finishForEntryResult(entry: Entry) {
|
private fun finishForEntryResult(entry: Entry) {
|
||||||
// Assign entry callback as a result
|
// Assign entry callback as a result
|
||||||
try {
|
try {
|
||||||
val bundle = Bundle()
|
val bundle = buildEntryResult(entry)
|
||||||
val intentEntry = Intent()
|
val intentEntry = Intent()
|
||||||
bundle.putParcelable(ADD_OR_UPDATE_ENTRY_KEY, entry.nodeId)
|
|
||||||
intentEntry.putExtras(bundle)
|
intentEntry.putExtras(bundle)
|
||||||
setResult(Activity.RESULT_OK, intentEntry)
|
setResult(Activity.RESULT_OK, intentEntry)
|
||||||
super.finish()
|
super.finish()
|
||||||
@@ -892,7 +937,7 @@ class EntryEditActivity : DatabaseLockActivity(),
|
|||||||
if (TimeoutHelper.checkTimeAndLockIfTimeout(activity)) {
|
if (TimeoutHelper.checkTimeAndLockIfTimeout(activity)) {
|
||||||
val intent = Intent(activity, EntryEditActivity::class.java)
|
val intent = Intent(activity, EntryEditActivity::class.java)
|
||||||
intent.putExtra(KEY_PARENT, groupId)
|
intent.putExtra(KEY_PARENT, groupId)
|
||||||
AutofillHelper.startActivityForAutofillResult(
|
EntrySelectionHelper.startActivityForAutofillSelectionModeResult(
|
||||||
activity,
|
activity,
|
||||||
intent,
|
intent,
|
||||||
activityResultLauncher,
|
activityResultLauncher,
|
||||||
@@ -903,21 +948,48 @@ class EntryEditActivity : DatabaseLockActivity(),
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Launch EntryEditActivity to add a new passkey entry
|
||||||
|
*/
|
||||||
|
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
|
||||||
|
fun launchForPasskeySelectionResult(context: Context,
|
||||||
|
database: ContextualDatabase,
|
||||||
|
activityResultLauncher: ActivityResultLauncher<Intent>?,
|
||||||
|
groupId: NodeId<*>,
|
||||||
|
searchInfo: SearchInfo? = null) {
|
||||||
|
if (database.loaded && !database.isReadOnly) {
|
||||||
|
if (TimeoutHelper.checkTimeAndLockIfTimeout(context)) {
|
||||||
|
val intent = Intent(context, EntryEditActivity::class.java)
|
||||||
|
intent.putExtra(KEY_PARENT, groupId)
|
||||||
|
EntrySelectionHelper.startActivityForPasskeySelectionModeResult(
|
||||||
|
context,
|
||||||
|
intent,
|
||||||
|
activityResultLauncher,
|
||||||
|
searchInfo
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Launch EntryEditActivity to register an updated entry (from autofill)
|
* Launch EntryEditActivity to register an updated entry (from autofill)
|
||||||
*/
|
*/
|
||||||
fun launchToUpdateForRegistration(context: Context,
|
fun launchToUpdateForRegistration(context: Context,
|
||||||
database: ContextualDatabase,
|
database: ContextualDatabase,
|
||||||
|
activityResultLauncher: ActivityResultLauncher<Intent>?,
|
||||||
entryId: NodeId<UUID>,
|
entryId: NodeId<UUID>,
|
||||||
registerInfo: RegisterInfo? = null) {
|
registerInfo: RegisterInfo?,
|
||||||
|
typeMode: TypeMode) {
|
||||||
if (database.loaded && !database.isReadOnly) {
|
if (database.loaded && !database.isReadOnly) {
|
||||||
if (TimeoutHelper.checkTimeAndLockIfTimeout(context)) {
|
if (TimeoutHelper.checkTimeAndLockIfTimeout(context)) {
|
||||||
val intent = Intent(context, EntryEditActivity::class.java)
|
val intent = Intent(context, EntryEditActivity::class.java)
|
||||||
intent.putExtra(KEY_ENTRY, entryId)
|
intent.putExtra(KEY_ENTRY, entryId)
|
||||||
EntrySelectionHelper.startActivityForRegistrationModeResult(
|
EntrySelectionHelper.startActivityForRegistrationModeResult(
|
||||||
context,
|
context,
|
||||||
|
activityResultLauncher,
|
||||||
intent,
|
intent,
|
||||||
registerInfo
|
registerInfo,
|
||||||
|
typeMode
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -928,16 +1000,20 @@ class EntryEditActivity : DatabaseLockActivity(),
|
|||||||
*/
|
*/
|
||||||
fun launchToCreateForRegistration(context: Context,
|
fun launchToCreateForRegistration(context: Context,
|
||||||
database: ContextualDatabase,
|
database: ContextualDatabase,
|
||||||
|
activityResultLauncher: ActivityResultLauncher<Intent>?,
|
||||||
groupId: NodeId<*>,
|
groupId: NodeId<*>,
|
||||||
registerInfo: RegisterInfo? = null) {
|
registerInfo: RegisterInfo? = null,
|
||||||
|
typeMode: TypeMode) {
|
||||||
if (database.loaded && !database.isReadOnly) {
|
if (database.loaded && !database.isReadOnly) {
|
||||||
if (TimeoutHelper.checkTimeAndLockIfTimeout(context)) {
|
if (TimeoutHelper.checkTimeAndLockIfTimeout(context)) {
|
||||||
val intent = Intent(context, EntryEditActivity::class.java)
|
val intent = Intent(context, EntryEditActivity::class.java)
|
||||||
intent.putExtra(KEY_PARENT, groupId)
|
intent.putExtra(KEY_PARENT, groupId)
|
||||||
EntrySelectionHelper.startActivityForRegistrationModeResult(
|
EntrySelectionHelper.startActivityForRegistrationModeResult(
|
||||||
context,
|
context,
|
||||||
|
activityResultLauncher,
|
||||||
intent,
|
intent,
|
||||||
registerInfo
|
registerInfo,
|
||||||
|
typeMode
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,15 +44,16 @@ import androidx.recyclerview.widget.SimpleItemAnimator
|
|||||||
import com.google.android.material.snackbar.Snackbar
|
import com.google.android.material.snackbar.Snackbar
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.activities.dialogs.SetMainCredentialDialogFragment
|
import com.kunzisoft.keepass.activities.dialogs.SetMainCredentialDialogFragment
|
||||||
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper
|
||||||
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
|
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
|
||||||
import com.kunzisoft.keepass.activities.helpers.SpecialMode
|
import com.kunzisoft.keepass.credentialprovider.SpecialMode
|
||||||
import com.kunzisoft.keepass.activities.helpers.setOpenDocumentClickListener
|
import com.kunzisoft.keepass.activities.helpers.setOpenDocumentClickListener
|
||||||
import com.kunzisoft.keepass.activities.legacy.DatabaseModeActivity
|
import com.kunzisoft.keepass.activities.legacy.DatabaseModeActivity
|
||||||
import com.kunzisoft.keepass.adapters.FileDatabaseHistoryAdapter
|
import com.kunzisoft.keepass.adapters.FileDatabaseHistoryAdapter
|
||||||
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
|
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
|
||||||
import com.kunzisoft.keepass.autofill.AutofillComponent
|
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.buildActivityResultLauncher
|
||||||
import com.kunzisoft.keepass.autofill.AutofillHelper
|
import com.kunzisoft.keepass.credentialprovider.TypeMode
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.autofill.AutofillComponent
|
||||||
import com.kunzisoft.keepass.database.ContextualDatabase
|
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||||
import com.kunzisoft.keepass.database.MainCredential
|
import com.kunzisoft.keepass.database.MainCredential
|
||||||
import com.kunzisoft.keepass.education.FileDatabaseSelectActivityEducation
|
import com.kunzisoft.keepass.education.FileDatabaseSelectActivityEducation
|
||||||
@@ -99,10 +100,8 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
|
|||||||
|
|
||||||
private var mExternalFileHelper: ExternalFileHelper? = null
|
private var mExternalFileHelper: ExternalFileHelper? = null
|
||||||
|
|
||||||
private var mAutofillActivityResultLauncher: ActivityResultLauncher<Intent>? =
|
private var mCredentialActivityResultLauncher: ActivityResultLauncher<Intent>? =
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
|
this.buildActivityResultLauncher()
|
||||||
AutofillHelper.buildActivityResultLauncher(this)
|
|
||||||
else null
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
@@ -299,7 +298,7 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
|
|||||||
},
|
},
|
||||||
{ onCancelSpecialMode() },
|
{ onCancelSpecialMode() },
|
||||||
{ onLaunchActivitySpecialMode() },
|
{ onLaunchActivitySpecialMode() },
|
||||||
mAutofillActivityResultLauncher)
|
mCredentialActivityResultLauncher)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun launchGroupActivityIfLoaded(database: ContextualDatabase) {
|
private fun launchGroupActivityIfLoaded(database: ContextualDatabase) {
|
||||||
@@ -309,7 +308,7 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
|
|||||||
{ onValidateSpecialMode() },
|
{ onValidateSpecialMode() },
|
||||||
{ onCancelSpecialMode() },
|
{ onCancelSpecialMode() },
|
||||||
{ onLaunchActivitySpecialMode() },
|
{ onLaunchActivitySpecialMode() },
|
||||||
mAutofillActivityResultLauncher)
|
mCredentialActivityResultLauncher)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -493,23 +492,46 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
|
|||||||
activityResultLauncher: ActivityResultLauncher<Intent>?,
|
activityResultLauncher: ActivityResultLauncher<Intent>?,
|
||||||
autofillComponent: AutofillComponent,
|
autofillComponent: AutofillComponent,
|
||||||
searchInfo: SearchInfo? = null) {
|
searchInfo: SearchInfo? = null) {
|
||||||
AutofillHelper.startActivityForAutofillResult(activity,
|
EntrySelectionHelper.startActivityForAutofillSelectionModeResult(activity,
|
||||||
Intent(activity, FileDatabaseSelectActivity::class.java),
|
Intent(activity, FileDatabaseSelectActivity::class.java),
|
||||||
activityResultLauncher,
|
activityResultLauncher,
|
||||||
autofillComponent,
|
autofillComponent,
|
||||||
searchInfo)
|
searchInfo)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* -------------------------
|
||||||
|
* Passkey Launch
|
||||||
|
* -------------------------
|
||||||
|
*/
|
||||||
|
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
|
||||||
|
fun launchForPasskeySelectionResult(activity: Activity,
|
||||||
|
activityResultLauncher: ActivityResultLauncher<Intent>?,
|
||||||
|
searchInfo: SearchInfo? = null) {
|
||||||
|
EntrySelectionHelper.startActivityForPasskeySelectionModeResult(
|
||||||
|
activity,
|
||||||
|
Intent(activity, FileDatabaseSelectActivity::class.java),
|
||||||
|
activityResultLauncher,
|
||||||
|
searchInfo
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* -------------------------
|
* -------------------------
|
||||||
* Registration Launch
|
* Registration Launch
|
||||||
* -------------------------
|
* -------------------------
|
||||||
*/
|
*/
|
||||||
fun launchForRegistration(context: Context,
|
fun launchForRegistration(context: Context,
|
||||||
registerInfo: RegisterInfo? = null) {
|
activityResultLauncher: ActivityResultLauncher<Intent>?,
|
||||||
EntrySelectionHelper.startActivityForRegistrationModeResult(context,
|
registerInfo: RegisterInfo? = null,
|
||||||
Intent(context, FileDatabaseSelectActivity::class.java),
|
typeMode: TypeMode) {
|
||||||
registerInfo)
|
EntrySelectionHelper.startActivityForRegistrationModeResult(
|
||||||
|
context,
|
||||||
|
activityResultLauncher,
|
||||||
|
Intent(context, FileDatabaseSelectActivity::class.java),
|
||||||
|
registerInfo,
|
||||||
|
typeMode
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -63,13 +63,15 @@ import com.kunzisoft.keepass.activities.dialogs.GroupEditDialogFragment
|
|||||||
import com.kunzisoft.keepass.activities.dialogs.MainCredentialDialogFragment
|
import com.kunzisoft.keepass.activities.dialogs.MainCredentialDialogFragment
|
||||||
import com.kunzisoft.keepass.activities.dialogs.SortDialogFragment
|
import com.kunzisoft.keepass.activities.dialogs.SortDialogFragment
|
||||||
import com.kunzisoft.keepass.activities.fragments.GroupFragment
|
import com.kunzisoft.keepass.activities.fragments.GroupFragment
|
||||||
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper
|
||||||
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
|
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
|
||||||
import com.kunzisoft.keepass.activities.helpers.SpecialMode
|
import com.kunzisoft.keepass.credentialprovider.SpecialMode
|
||||||
import com.kunzisoft.keepass.activities.legacy.DatabaseLockActivity
|
import com.kunzisoft.keepass.activities.legacy.DatabaseLockActivity
|
||||||
import com.kunzisoft.keepass.adapters.BreadcrumbAdapter
|
import com.kunzisoft.keepass.adapters.BreadcrumbAdapter
|
||||||
import com.kunzisoft.keepass.autofill.AutofillComponent
|
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.buildActivityResultLauncher
|
||||||
import com.kunzisoft.keepass.autofill.AutofillHelper
|
import com.kunzisoft.keepass.credentialprovider.TypeMode
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.autofill.AutofillComponent
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.autofill.AutofillHelper
|
||||||
import com.kunzisoft.keepass.database.ContextualDatabase
|
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||||
import com.kunzisoft.keepass.database.MainCredential
|
import com.kunzisoft.keepass.database.MainCredential
|
||||||
import com.kunzisoft.keepass.database.element.DateInstant
|
import com.kunzisoft.keepass.database.element.DateInstant
|
||||||
@@ -83,7 +85,8 @@ import com.kunzisoft.keepass.database.element.node.Type
|
|||||||
import com.kunzisoft.keepass.database.helper.SearchHelper
|
import com.kunzisoft.keepass.database.helper.SearchHelper
|
||||||
import com.kunzisoft.keepass.database.search.SearchParameters
|
import com.kunzisoft.keepass.database.search.SearchParameters
|
||||||
import com.kunzisoft.keepass.education.GroupActivityEducation
|
import com.kunzisoft.keepass.education.GroupActivityEducation
|
||||||
import com.kunzisoft.keepass.magikeyboard.MagikeyboardService
|
import com.kunzisoft.keepass.credentialprovider.magikeyboard.MagikeyboardService
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.buildPasskeyResponseAndSetResult
|
||||||
import com.kunzisoft.keepass.model.DataTime
|
import com.kunzisoft.keepass.model.DataTime
|
||||||
import com.kunzisoft.keepass.model.GroupInfo
|
import com.kunzisoft.keepass.model.GroupInfo
|
||||||
import com.kunzisoft.keepass.model.RegisterInfo
|
import com.kunzisoft.keepass.model.RegisterInfo
|
||||||
@@ -264,10 +267,8 @@ class GroupActivity : DatabaseLockActivity(),
|
|||||||
mGroupEditViewModel.selectIcon(icon)
|
mGroupEditViewModel.selectIcon(icon)
|
||||||
}
|
}
|
||||||
|
|
||||||
private var mAutofillActivityResultLauncher: ActivityResultLauncher<Intent>? =
|
private var mCredentialActivityResultLauncher: ActivityResultLauncher<Intent>? =
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
|
this.buildActivityResultLauncher()
|
||||||
AutofillHelper.buildActivityResultLauncher(this)
|
|
||||||
else null
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
@@ -484,59 +485,87 @@ class GroupActivity : DatabaseLockActivity(),
|
|||||||
addNodeButtonView?.setAddEntryClickListener {
|
addNodeButtonView?.setAddEntryClickListener {
|
||||||
mDatabase?.let { database ->
|
mDatabase?.let { database ->
|
||||||
mMainGroup?.let { currentGroup ->
|
mMainGroup?.let { currentGroup ->
|
||||||
EntrySelectionHelper.doSpecialAction(intent,
|
EntrySelectionHelper.doSpecialAction(
|
||||||
{
|
intent = intent,
|
||||||
|
defaultAction = {
|
||||||
mMainGroup?.nodeId?.let { currentParentGroupId ->
|
mMainGroup?.nodeId?.let { currentParentGroupId ->
|
||||||
EntryEditActivity.launchToCreate(
|
EntryEditActivity.launchToCreate(
|
||||||
this@GroupActivity,
|
activity = this@GroupActivity,
|
||||||
database,
|
database = database,
|
||||||
currentParentGroupId,
|
groupId = currentParentGroupId,
|
||||||
mEntryActivityResultLauncher
|
activityResultLauncher = mEntryActivityResultLauncher
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
searchAction = {
|
||||||
// Search not used
|
// Search not used
|
||||||
},
|
},
|
||||||
{ searchInfo ->
|
saveAction = { searchInfo ->
|
||||||
EntryEditActivity.launchToCreateForSave(
|
EntryEditActivity.launchToCreateForSave(
|
||||||
this@GroupActivity,
|
context = this@GroupActivity,
|
||||||
database,
|
database = database,
|
||||||
currentGroup.nodeId,
|
groupId = currentGroup.nodeId,
|
||||||
searchInfo
|
searchInfo = searchInfo
|
||||||
)
|
)
|
||||||
onLaunchActivitySpecialMode()
|
onLaunchActivitySpecialMode()
|
||||||
},
|
},
|
||||||
{ searchInfo ->
|
keyboardSelectionAction = { searchInfo ->
|
||||||
EntryEditActivity.launchForKeyboardSelectionResult(
|
EntryEditActivity.launchForKeyboardSelectionResult(
|
||||||
this@GroupActivity,
|
context = this@GroupActivity,
|
||||||
database,
|
database = database,
|
||||||
currentGroup.nodeId,
|
groupId = currentGroup.nodeId,
|
||||||
searchInfo
|
searchInfo = searchInfo
|
||||||
)
|
)
|
||||||
onLaunchActivitySpecialMode()
|
onLaunchActivitySpecialMode()
|
||||||
},
|
},
|
||||||
{ searchInfo, autofillComponent ->
|
autofillSelectionAction = { searchInfo, autofillComponent ->
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
EntryEditActivity.launchForAutofillResult(
|
EntryEditActivity.launchForAutofillResult(
|
||||||
this@GroupActivity,
|
activity = this@GroupActivity,
|
||||||
database,
|
database = database,
|
||||||
mAutofillActivityResultLauncher,
|
activityResultLauncher = mCredentialActivityResultLauncher,
|
||||||
autofillComponent,
|
autofillComponent = autofillComponent,
|
||||||
currentGroup.nodeId,
|
groupId = currentGroup.nodeId,
|
||||||
searchInfo
|
searchInfo = searchInfo
|
||||||
)
|
)
|
||||||
onLaunchActivitySpecialMode()
|
onLaunchActivitySpecialMode()
|
||||||
} else {
|
} else {
|
||||||
onCancelSpecialMode()
|
onCancelSpecialMode()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ searchInfo ->
|
autofillRegistrationAction = { registerInfo ->
|
||||||
EntryEditActivity.launchToCreateForRegistration(
|
EntryEditActivity.launchToCreateForRegistration(
|
||||||
this@GroupActivity,
|
context = this@GroupActivity,
|
||||||
database,
|
database = database,
|
||||||
currentGroup.nodeId,
|
activityResultLauncher = null,
|
||||||
searchInfo
|
groupId = currentGroup.nodeId,
|
||||||
|
registerInfo = registerInfo,
|
||||||
|
typeMode = TypeMode.AUTOFILL
|
||||||
|
)
|
||||||
|
onLaunchActivitySpecialMode()
|
||||||
|
},
|
||||||
|
passkeySelectionAction = { searchInfo ->
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
||||||
|
EntryEditActivity.launchForPasskeySelectionResult(
|
||||||
|
context = this@GroupActivity,
|
||||||
|
database = database,
|
||||||
|
activityResultLauncher = mCredentialActivityResultLauncher,
|
||||||
|
groupId = currentGroup.nodeId,
|
||||||
|
searchInfo = searchInfo,
|
||||||
|
)
|
||||||
|
onLaunchActivitySpecialMode()
|
||||||
|
} else {
|
||||||
|
onCancelSpecialMode()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
passkeyRegistrationAction = { registerInfo ->
|
||||||
|
EntryEditActivity.launchToCreateForRegistration(
|
||||||
|
context = this@GroupActivity,
|
||||||
|
database = database,
|
||||||
|
activityResultLauncher = mCredentialActivityResultLauncher,
|
||||||
|
groupId = currentGroup.nodeId,
|
||||||
|
registerInfo = registerInfo,
|
||||||
|
typeMode = TypeMode.PASSKEY
|
||||||
)
|
)
|
||||||
onLaunchActivitySpecialMode()
|
onLaunchActivitySpecialMode()
|
||||||
}
|
}
|
||||||
@@ -679,30 +708,40 @@ class GroupActivity : DatabaseLockActivity(),
|
|||||||
when (actionTask) {
|
when (actionTask) {
|
||||||
ACTION_DATABASE_UPDATE_ENTRY_TASK -> {
|
ACTION_DATABASE_UPDATE_ENTRY_TASK -> {
|
||||||
if (result.isSuccess) {
|
if (result.isSuccess) {
|
||||||
EntrySelectionHelper.doSpecialAction(intent,
|
EntrySelectionHelper.doSpecialAction(
|
||||||
{
|
intent = intent,
|
||||||
|
defaultAction = {
|
||||||
// Standard not used after task
|
// Standard not used after task
|
||||||
},
|
},
|
||||||
{
|
searchAction = {
|
||||||
// Search not used
|
// Search not used
|
||||||
},
|
},
|
||||||
{
|
saveAction = {
|
||||||
// Save not used
|
// Save not used
|
||||||
},
|
},
|
||||||
{
|
keyboardSelectionAction = {
|
||||||
// Keyboard selection
|
// Keyboard selection
|
||||||
entry?.let {
|
entry?.let {
|
||||||
entrySelectedForKeyboardSelection(database, it)
|
entrySelectedForKeyboardSelection(database, it)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ _, _ ->
|
autofillSelectionAction = { _, _ ->
|
||||||
// Autofill selection
|
// Autofill selection
|
||||||
entry?.let {
|
entry?.let {
|
||||||
entrySelectedForAutofillSelection(database, it)
|
entrySelectedForAutofillSelection(database, it)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
autofillRegistrationAction = {
|
||||||
// Not use
|
// Not use
|
||||||
|
},
|
||||||
|
passkeySelectionAction = {
|
||||||
|
// Passkey selection
|
||||||
|
entry?.let {
|
||||||
|
entrySelectedForPasskeySelection(database, it)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
passkeyRegistrationAction = {
|
||||||
|
// TODO Passkey Registration
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -846,27 +885,28 @@ class GroupActivity : DatabaseLockActivity(),
|
|||||||
|
|
||||||
Type.ENTRY -> try {
|
Type.ENTRY -> try {
|
||||||
val entryVersioned = node as Entry
|
val entryVersioned = node as Entry
|
||||||
EntrySelectionHelper.doSpecialAction(intent,
|
EntrySelectionHelper.doSpecialAction(
|
||||||
{
|
intent = intent,
|
||||||
|
defaultAction = {
|
||||||
EntryActivity.launch(
|
EntryActivity.launch(
|
||||||
this@GroupActivity,
|
activity = this@GroupActivity,
|
||||||
database,
|
database = database,
|
||||||
entryVersioned.nodeId,
|
entryId = entryVersioned.nodeId,
|
||||||
mEntryActivityResultLauncher
|
activityResultLauncher = mEntryActivityResultLauncher
|
||||||
)
|
)
|
||||||
// Do not reload group here
|
// Do not reload group here
|
||||||
},
|
},
|
||||||
{
|
searchAction = {
|
||||||
// Nothing here, a search is simply performed
|
// Nothing here, a search is simply performed
|
||||||
},
|
},
|
||||||
{ searchInfo ->
|
saveAction = { searchInfo ->
|
||||||
if (!database.isReadOnly) {
|
if (!database.isReadOnly) {
|
||||||
entrySelectedForSave(database, entryVersioned, searchInfo)
|
entrySelectedForSave(database, entryVersioned, searchInfo)
|
||||||
loadGroup()
|
loadGroup()
|
||||||
} else
|
} else
|
||||||
finish()
|
finish()
|
||||||
},
|
},
|
||||||
{ searchInfo ->
|
keyboardSelectionAction = { searchInfo ->
|
||||||
if (!database.isReadOnly
|
if (!database.isReadOnly
|
||||||
&& searchInfo != null
|
&& searchInfo != null
|
||||||
&& PreferencesUtil.isKeyboardSaveSearchInfoEnable(this@GroupActivity)
|
&& PreferencesUtil.isKeyboardSaveSearchInfoEnable(this@GroupActivity)
|
||||||
@@ -876,7 +916,7 @@ class GroupActivity : DatabaseLockActivity(),
|
|||||||
entrySelectedForKeyboardSelection(database, entryVersioned)
|
entrySelectedForKeyboardSelection(database, entryVersioned)
|
||||||
loadGroup()
|
loadGroup()
|
||||||
},
|
},
|
||||||
{ searchInfo, _ ->
|
autofillSelectionAction = { searchInfo, _ ->
|
||||||
if (!database.isReadOnly
|
if (!database.isReadOnly
|
||||||
&& searchInfo != null
|
&& searchInfo != null
|
||||||
&& PreferencesUtil.isAutofillSaveSearchInfoEnable(this@GroupActivity)
|
&& PreferencesUtil.isAutofillSaveSearchInfoEnable(this@GroupActivity)
|
||||||
@@ -886,9 +926,39 @@ class GroupActivity : DatabaseLockActivity(),
|
|||||||
entrySelectedForAutofillSelection(database, entryVersioned)
|
entrySelectedForAutofillSelection(database, entryVersioned)
|
||||||
loadGroup()
|
loadGroup()
|
||||||
},
|
},
|
||||||
{ registerInfo ->
|
autofillRegistrationAction = { registerInfo ->
|
||||||
if (!database.isReadOnly) {
|
if (!database.isReadOnly) {
|
||||||
entrySelectedForRegistration(database, entryVersioned, registerInfo)
|
entrySelectedForRegistration(
|
||||||
|
database = database,
|
||||||
|
entry = entryVersioned,
|
||||||
|
registerInfo = registerInfo,
|
||||||
|
typeMode = TypeMode.AUTOFILL,
|
||||||
|
activityResultLauncher = null // TODO Result launcher autofill #765
|
||||||
|
)
|
||||||
|
loadGroup()
|
||||||
|
} else
|
||||||
|
finish()
|
||||||
|
},
|
||||||
|
passkeySelectionAction = { searchInfo ->
|
||||||
|
if (!database.isReadOnly
|
||||||
|
&& searchInfo != null
|
||||||
|
// TODO Passkey setting && PreferencesUtil.isAutofillSaveSearchInfoEnable(this@GroupActivity)
|
||||||
|
) {
|
||||||
|
updateEntryWithSearchInfo(database, entryVersioned, searchInfo)
|
||||||
|
}
|
||||||
|
entrySelectedForPasskeySelection(database, entryVersioned)
|
||||||
|
loadGroup()
|
||||||
|
},
|
||||||
|
passkeyRegistrationAction = { registerInfo ->
|
||||||
|
if (!database.isReadOnly) {
|
||||||
|
// TODO Passkey setting && PreferencesUtil.isAutofillOverwriteEnable(this@GroupActivity)
|
||||||
|
entrySelectedForRegistration(
|
||||||
|
database = database,
|
||||||
|
entry = entryVersioned,
|
||||||
|
registerInfo = registerInfo,
|
||||||
|
typeMode = TypeMode.PASSKEY,
|
||||||
|
activityResultLauncher = mCredentialActivityResultLauncher
|
||||||
|
)
|
||||||
loadGroup()
|
loadGroup()
|
||||||
} else
|
} else
|
||||||
finish()
|
finish()
|
||||||
@@ -934,18 +1004,33 @@ class GroupActivity : DatabaseLockActivity(),
|
|||||||
onValidateSpecialMode()
|
onValidateSpecialMode()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun entrySelectedForPasskeySelection(database: ContextualDatabase, entry: Entry) {
|
||||||
|
removeSearch()
|
||||||
|
// Build response with the entry selected
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
||||||
|
buildPasskeyResponseAndSetResult(
|
||||||
|
entryInfo = entry.getEntryInfo(database)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
onValidateSpecialMode()
|
||||||
|
}
|
||||||
|
|
||||||
private fun entrySelectedForRegistration(
|
private fun entrySelectedForRegistration(
|
||||||
database: ContextualDatabase,
|
database: ContextualDatabase,
|
||||||
entry: Entry,
|
entry: Entry,
|
||||||
registerInfo: RegisterInfo?
|
activityResultLauncher: ActivityResultLauncher<Intent>?,
|
||||||
|
registerInfo: RegisterInfo?,
|
||||||
|
typeMode: TypeMode
|
||||||
) {
|
) {
|
||||||
removeSearch()
|
removeSearch()
|
||||||
// Registration to update the entry
|
// Registration to update the entry
|
||||||
EntryEditActivity.launchToUpdateForRegistration(
|
EntryEditActivity.launchToUpdateForRegistration(
|
||||||
this@GroupActivity,
|
context = this@GroupActivity,
|
||||||
database,
|
database = database,
|
||||||
entry.nodeId,
|
activityResultLauncher = activityResultLauncher,
|
||||||
registerInfo
|
entryId = entry.nodeId,
|
||||||
|
registerInfo = registerInfo,
|
||||||
|
typeMode = typeMode
|
||||||
)
|
)
|
||||||
onLaunchActivitySpecialMode()
|
onLaunchActivitySpecialMode()
|
||||||
}
|
}
|
||||||
@@ -1569,19 +1654,19 @@ class GroupActivity : DatabaseLockActivity(),
|
|||||||
* -------------------------
|
* -------------------------
|
||||||
*/
|
*/
|
||||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||||
fun launchForAutofillResult(activity: AppCompatActivity,
|
fun launchForAutofillSelectionResult(activity: AppCompatActivity,
|
||||||
database: ContextualDatabase,
|
database: ContextualDatabase,
|
||||||
activityResultLaunch: ActivityResultLauncher<Intent>?,
|
activityResultLauncher: ActivityResultLauncher<Intent>?,
|
||||||
autofillComponent: AutofillComponent,
|
autofillComponent: AutofillComponent,
|
||||||
searchInfo: SearchInfo? = null,
|
searchInfo: SearchInfo? = null,
|
||||||
autoSearch: Boolean = false) {
|
autoSearch: Boolean = false) {
|
||||||
if (database.loaded) {
|
if (database.loaded) {
|
||||||
checkTimeAndBuildIntent(activity, null) { intent ->
|
checkTimeAndBuildIntent(activity, null) { intent ->
|
||||||
intent.putExtra(AUTO_SEARCH_KEY, autoSearch)
|
intent.putExtra(AUTO_SEARCH_KEY, autoSearch)
|
||||||
AutofillHelper.startActivityForAutofillResult(
|
EntrySelectionHelper.startActivityForAutofillSelectionModeResult(
|
||||||
activity,
|
activity,
|
||||||
intent,
|
intent,
|
||||||
activityResultLaunch,
|
activityResultLauncher,
|
||||||
autofillComponent,
|
autofillComponent,
|
||||||
searchInfo
|
searchInfo
|
||||||
)
|
)
|
||||||
@@ -1589,21 +1674,49 @@ class GroupActivity : DatabaseLockActivity(),
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* -------------------------
|
||||||
|
* Passkey Launch
|
||||||
|
* -------------------------
|
||||||
|
*/
|
||||||
|
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
|
||||||
|
fun launchForPasskeySelectionResult(context: Context,
|
||||||
|
database: ContextualDatabase,
|
||||||
|
activityResultLauncher: ActivityResultLauncher<Intent>?,
|
||||||
|
searchInfo: SearchInfo? = null,
|
||||||
|
autoSearch: Boolean = false) {
|
||||||
|
if (database.loaded) {
|
||||||
|
checkTimeAndBuildIntent(context, null) { intent ->
|
||||||
|
intent.putExtra(AUTO_SEARCH_KEY, autoSearch)
|
||||||
|
EntrySelectionHelper.startActivityForPasskeySelectionModeResult(
|
||||||
|
context,
|
||||||
|
intent,
|
||||||
|
activityResultLauncher,
|
||||||
|
searchInfo
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* -------------------------
|
* -------------------------
|
||||||
* Registration Launch
|
* Registration Launch
|
||||||
* -------------------------
|
* -------------------------
|
||||||
*/
|
*/
|
||||||
fun launchForRegistration(context: Context,
|
fun launchForRegistration(context: Context,
|
||||||
|
activityResultLauncher: ActivityResultLauncher<Intent>?,
|
||||||
database: ContextualDatabase,
|
database: ContextualDatabase,
|
||||||
registerInfo: RegisterInfo? = null) {
|
registerInfo: RegisterInfo? = null,
|
||||||
|
typeMode: TypeMode) {
|
||||||
if (database.loaded && !database.isReadOnly) {
|
if (database.loaded && !database.isReadOnly) {
|
||||||
checkTimeAndBuildIntent(context, null) { intent ->
|
checkTimeAndBuildIntent(context, null) { intent ->
|
||||||
intent.putExtra(AUTO_SEARCH_KEY, false)
|
intent.putExtra(AUTO_SEARCH_KEY, false)
|
||||||
EntrySelectionHelper.startActivityForRegistrationModeResult(
|
EntrySelectionHelper.startActivityForRegistrationModeResult(
|
||||||
context,
|
context,
|
||||||
|
activityResultLauncher,
|
||||||
intent,
|
intent,
|
||||||
registerInfo
|
registerInfo,
|
||||||
|
typeMode
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1619,153 +1732,231 @@ class GroupActivity : DatabaseLockActivity(),
|
|||||||
onValidateSpecialMode: () -> Unit,
|
onValidateSpecialMode: () -> Unit,
|
||||||
onCancelSpecialMode: () -> Unit,
|
onCancelSpecialMode: () -> Unit,
|
||||||
onLaunchActivitySpecialMode: () -> Unit,
|
onLaunchActivitySpecialMode: () -> Unit,
|
||||||
autofillActivityResultLauncher: ActivityResultLauncher<Intent>?) {
|
activityResultLauncher: ActivityResultLauncher<Intent>?) {
|
||||||
EntrySelectionHelper.doSpecialAction(activity.intent,
|
EntrySelectionHelper.doSpecialAction(
|
||||||
{
|
intent = activity.intent,
|
||||||
// Default action
|
defaultAction = {
|
||||||
launch(
|
// Default action
|
||||||
activity,
|
launch(
|
||||||
|
activity,
|
||||||
|
database,
|
||||||
|
true
|
||||||
|
)
|
||||||
|
},
|
||||||
|
searchAction = { searchInfo ->
|
||||||
|
// Search action
|
||||||
|
if (database.loaded) {
|
||||||
|
launchForSearchResult(activity,
|
||||||
database,
|
database,
|
||||||
true
|
searchInfo,
|
||||||
)
|
true)
|
||||||
},
|
onLaunchActivitySpecialMode()
|
||||||
{ searchInfo ->
|
} else {
|
||||||
// Search action
|
// Simply close if database not opened
|
||||||
if (database.loaded) {
|
onCancelSpecialMode()
|
||||||
launchForSearchResult(activity,
|
}
|
||||||
|
},
|
||||||
|
saveAction = { searchInfo ->
|
||||||
|
// Save info
|
||||||
|
if (database.loaded) {
|
||||||
|
if (!database.isReadOnly) {
|
||||||
|
launchForSaveResult(
|
||||||
|
activity,
|
||||||
database,
|
database,
|
||||||
searchInfo,
|
searchInfo,
|
||||||
true)
|
false
|
||||||
|
)
|
||||||
onLaunchActivitySpecialMode()
|
onLaunchActivitySpecialMode()
|
||||||
} else {
|
} else {
|
||||||
// Simply close if database not opened
|
Toast.makeText(
|
||||||
|
activity.applicationContext,
|
||||||
|
R.string.autofill_read_only_save,
|
||||||
|
Toast.LENGTH_LONG
|
||||||
|
)
|
||||||
|
.show()
|
||||||
onCancelSpecialMode()
|
onCancelSpecialMode()
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
{ searchInfo ->
|
},
|
||||||
// Save info
|
keyboardSelectionAction = { searchInfo ->
|
||||||
if (database.loaded) {
|
// Keyboard selection
|
||||||
if (!database.isReadOnly) {
|
SearchHelper.checkAutoSearchInfo(
|
||||||
launchForSaveResult(
|
context = activity,
|
||||||
activity,
|
database = database,
|
||||||
database,
|
searchInfo = searchInfo,
|
||||||
searchInfo,
|
onItemsFound = { _, items ->
|
||||||
false
|
MagikeyboardService.performSelection(
|
||||||
)
|
items,
|
||||||
onLaunchActivitySpecialMode()
|
{ entryInfo ->
|
||||||
} else {
|
// Keyboard populated
|
||||||
Toast.makeText(
|
MagikeyboardService.populateKeyboardAndMoveAppToBackground(
|
||||||
activity.applicationContext,
|
activity,
|
||||||
R.string.autofill_read_only_save,
|
entryInfo
|
||||||
Toast.LENGTH_LONG
|
|
||||||
)
|
|
||||||
.show()
|
|
||||||
onCancelSpecialMode()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{ searchInfo ->
|
|
||||||
// Keyboard selection
|
|
||||||
SearchHelper.checkAutoSearchInfo(activity,
|
|
||||||
database,
|
|
||||||
searchInfo,
|
|
||||||
{ _, items ->
|
|
||||||
MagikeyboardService.performSelection(
|
|
||||||
items,
|
|
||||||
{ entryInfo ->
|
|
||||||
// Keyboard populated
|
|
||||||
MagikeyboardService.populateKeyboardAndMoveAppToBackground(
|
|
||||||
activity,
|
|
||||||
entryInfo
|
|
||||||
)
|
|
||||||
onValidateSpecialMode()
|
|
||||||
},
|
|
||||||
{ autoSearch ->
|
|
||||||
launchForKeyboardSelectionResult(activity,
|
|
||||||
database,
|
|
||||||
searchInfo,
|
|
||||||
autoSearch)
|
|
||||||
onLaunchActivitySpecialMode()
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
onValidateSpecialMode()
|
||||||
},
|
},
|
||||||
{
|
{ autoSearch ->
|
||||||
// Here no search info found, disable auto search
|
|
||||||
launchForKeyboardSelectionResult(activity,
|
launchForKeyboardSelectionResult(activity,
|
||||||
database,
|
database,
|
||||||
searchInfo,
|
searchInfo,
|
||||||
false)
|
autoSearch)
|
||||||
onLaunchActivitySpecialMode()
|
onLaunchActivitySpecialMode()
|
||||||
},
|
|
||||||
{
|
|
||||||
// Simply close if database not opened, normally not happened
|
|
||||||
onCancelSpecialMode()
|
|
||||||
}
|
}
|
||||||
|
)
|
||||||
|
},
|
||||||
|
onItemNotFound = {
|
||||||
|
// Here no search info found, disable auto search
|
||||||
|
launchForKeyboardSelectionResult(activity,
|
||||||
|
database,
|
||||||
|
searchInfo,
|
||||||
|
false)
|
||||||
|
onLaunchActivitySpecialMode()
|
||||||
|
},
|
||||||
|
onDatabaseClosed = {
|
||||||
|
// Simply close if database not opened, normally not happened
|
||||||
|
onCancelSpecialMode()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
},
|
||||||
|
autofillSelectionAction = { searchInfo, autofillComponent ->
|
||||||
|
// Autofill selection
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
SearchHelper.checkAutoSearchInfo(
|
||||||
|
context = activity,
|
||||||
|
database = database,
|
||||||
|
searchInfo = searchInfo,
|
||||||
|
onItemsFound = { openedDatabase, items ->
|
||||||
|
// Response is build
|
||||||
|
AutofillHelper.buildResponseAndSetResult(activity, openedDatabase, items)
|
||||||
|
onValidateSpecialMode()
|
||||||
|
},
|
||||||
|
onItemNotFound = {
|
||||||
|
// Here no search info found, disable auto search
|
||||||
|
launchForAutofillSelectionResult(
|
||||||
|
activity = activity,
|
||||||
|
database = database,
|
||||||
|
autofillComponent = autofillComponent,
|
||||||
|
searchInfo = searchInfo,
|
||||||
|
autoSearch = false,
|
||||||
|
activityResultLauncher = activityResultLauncher)
|
||||||
|
onLaunchActivitySpecialMode()
|
||||||
|
},
|
||||||
|
onDatabaseClosed = {
|
||||||
|
// Simply close if database not opened, normally not happened
|
||||||
|
onCancelSpecialMode()
|
||||||
|
}
|
||||||
)
|
)
|
||||||
},
|
} else {
|
||||||
{ searchInfo, autofillComponent ->
|
onCancelSpecialMode()
|
||||||
// Autofill selection
|
}
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
},
|
||||||
SearchHelper.checkAutoSearchInfo(activity,
|
autofillRegistrationAction = { registerInfo ->
|
||||||
database,
|
// Autofill registration
|
||||||
searchInfo,
|
if (!database.isReadOnly) {
|
||||||
{ openedDatabase, items ->
|
SearchHelper.checkAutoSearchInfo(
|
||||||
// Response is build
|
context = activity,
|
||||||
AutofillHelper.buildResponseAndSetResult(activity, openedDatabase, items)
|
database = database,
|
||||||
|
searchInfo = registerInfo?.searchInfo,
|
||||||
|
onItemsFound = { _, _ ->
|
||||||
|
// No auto search, it's a registration
|
||||||
|
launchForRegistration(
|
||||||
|
context = activity,
|
||||||
|
activityResultLauncher = null, // TODO Autofill result Launcher #765
|
||||||
|
database = database,
|
||||||
|
registerInfo = registerInfo,
|
||||||
|
typeMode = TypeMode.AUTOFILL
|
||||||
|
)
|
||||||
|
onLaunchActivitySpecialMode()
|
||||||
|
},
|
||||||
|
onItemNotFound = {
|
||||||
|
// Here no search info found, disable auto search
|
||||||
|
launchForRegistration(
|
||||||
|
context = activity,
|
||||||
|
activityResultLauncher = null, // TODO Autofill result Launcher #765
|
||||||
|
database = database,
|
||||||
|
registerInfo = registerInfo,
|
||||||
|
typeMode = TypeMode.AUTOFILL
|
||||||
|
)
|
||||||
|
onLaunchActivitySpecialMode()
|
||||||
|
},
|
||||||
|
onDatabaseClosed = {
|
||||||
|
// Simply close if database not opened, normally not happened
|
||||||
|
onCancelSpecialMode()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Toast.makeText(activity.applicationContext,
|
||||||
|
R.string.autofill_read_only_save,
|
||||||
|
Toast.LENGTH_LONG)
|
||||||
|
.show()
|
||||||
|
onCancelSpecialMode()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
passkeySelectionAction = { searchInfo ->
|
||||||
|
// Passkey selection
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
||||||
|
SearchHelper.checkAutoSearchInfo(
|
||||||
|
context = activity,
|
||||||
|
database = database,
|
||||||
|
searchInfo = searchInfo,
|
||||||
|
onItemsFound = { _, items ->
|
||||||
|
// Response is build
|
||||||
|
EntrySelectionHelper.performSelection(
|
||||||
|
items = items,
|
||||||
|
actionPopulateCredentialProvider = { entryInfo ->
|
||||||
|
activity.buildPasskeyResponseAndSetResult(entryInfo)
|
||||||
onValidateSpecialMode()
|
onValidateSpecialMode()
|
||||||
},
|
},
|
||||||
{
|
actionEntrySelection = {
|
||||||
// Here no search info found, disable auto search
|
launchForPasskeySelectionResult(
|
||||||
launchForAutofillResult(activity,
|
context = activity,
|
||||||
database,
|
database = database,
|
||||||
autofillActivityResultLauncher,
|
searchInfo = searchInfo,
|
||||||
autofillComponent,
|
activityResultLauncher = activityResultLauncher
|
||||||
searchInfo,
|
)
|
||||||
false)
|
|
||||||
onLaunchActivitySpecialMode()
|
onLaunchActivitySpecialMode()
|
||||||
},
|
|
||||||
{
|
|
||||||
// Simply close if database not opened, normally not happened
|
|
||||||
onCancelSpecialMode()
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
} else {
|
},
|
||||||
onCancelSpecialMode()
|
onItemNotFound = {
|
||||||
}
|
// Here no search info found, disable auto search
|
||||||
},
|
launchForPasskeySelectionResult(
|
||||||
{ registerInfo ->
|
context = activity,
|
||||||
// Autofill registration
|
database = database,
|
||||||
if (!database.isReadOnly) {
|
searchInfo = searchInfo,
|
||||||
SearchHelper.checkAutoSearchInfo(activity,
|
activityResultLauncher = activityResultLauncher
|
||||||
database,
|
)
|
||||||
registerInfo?.searchInfo,
|
onLaunchActivitySpecialMode()
|
||||||
{ _, _ ->
|
},
|
||||||
// No auto search, it's a registration
|
onDatabaseClosed = {
|
||||||
launchForRegistration(activity,
|
// Simply close if database not opened, normally not happened
|
||||||
database,
|
onCancelSpecialMode()
|
||||||
registerInfo)
|
}
|
||||||
onLaunchActivitySpecialMode()
|
)
|
||||||
},
|
} else {
|
||||||
{
|
onCancelSpecialMode()
|
||||||
// Here no search info found, disable auto search
|
}
|
||||||
launchForRegistration(activity,
|
},
|
||||||
database,
|
passkeyRegistrationAction = { registerInfo ->
|
||||||
registerInfo)
|
// Passkey registration
|
||||||
onLaunchActivitySpecialMode()
|
if (!database.isReadOnly) {
|
||||||
},
|
launchForRegistration(
|
||||||
{
|
context = activity,
|
||||||
// Simply close if database not opened, normally not happened
|
activityResultLauncher = activityResultLauncher,
|
||||||
onCancelSpecialMode()
|
database = database,
|
||||||
}
|
registerInfo = registerInfo,
|
||||||
)
|
typeMode = TypeMode.PASSKEY
|
||||||
} else {
|
)
|
||||||
Toast.makeText(activity.applicationContext,
|
onLaunchActivitySpecialMode()
|
||||||
R.string.autofill_read_only_save,
|
} else {
|
||||||
Toast.LENGTH_LONG)
|
Toast.makeText(activity.applicationContext,
|
||||||
.show()
|
R.string.autofill_read_only_save,
|
||||||
onCancelSpecialMode()
|
Toast.LENGTH_LONG)
|
||||||
}
|
.show()
|
||||||
})
|
onCancelSpecialMode()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,15 +46,16 @@ import androidx.fragment.app.commit
|
|||||||
import com.google.android.material.snackbar.Snackbar
|
import com.google.android.material.snackbar.Snackbar
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.activities.dialogs.DuplicateUuidDialog
|
import com.kunzisoft.keepass.activities.dialogs.DuplicateUuidDialog
|
||||||
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
|
||||||
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
|
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
|
||||||
import com.kunzisoft.keepass.activities.helpers.SpecialMode
|
|
||||||
import com.kunzisoft.keepass.activities.legacy.DatabaseLockActivity
|
import com.kunzisoft.keepass.activities.legacy.DatabaseLockActivity
|
||||||
import com.kunzisoft.keepass.activities.legacy.DatabaseModeActivity
|
import com.kunzisoft.keepass.activities.legacy.DatabaseModeActivity
|
||||||
import com.kunzisoft.keepass.autofill.AutofillComponent
|
|
||||||
import com.kunzisoft.keepass.autofill.AutofillHelper
|
|
||||||
import com.kunzisoft.keepass.biometric.AdvancedUnlockFragment
|
import com.kunzisoft.keepass.biometric.AdvancedUnlockFragment
|
||||||
import com.kunzisoft.keepass.biometric.AdvancedUnlockManager
|
import com.kunzisoft.keepass.biometric.AdvancedUnlockManager
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.buildActivityResultLauncher
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.SpecialMode
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.TypeMode
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.autofill.AutofillComponent
|
||||||
import com.kunzisoft.keepass.database.ContextualDatabase
|
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||||
import com.kunzisoft.keepass.database.MainCredential
|
import com.kunzisoft.keepass.database.MainCredential
|
||||||
import com.kunzisoft.keepass.database.exception.DuplicateUuidDatabaseException
|
import com.kunzisoft.keepass.database.exception.DuplicateUuidDatabaseException
|
||||||
@@ -113,10 +114,8 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
|
|||||||
private var mReadOnly: Boolean = false
|
private var mReadOnly: Boolean = false
|
||||||
private var mForceReadOnly: Boolean = false
|
private var mForceReadOnly: Boolean = false
|
||||||
|
|
||||||
private var mAutofillActivityResultLauncher: ActivityResultLauncher<Intent>? =
|
private var mCredentialActivityResultLauncher: ActivityResultLauncher<Intent>? =
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
|
this.buildActivityResultLauncher()
|
||||||
AutofillHelper.buildActivityResultLauncher(this)
|
|
||||||
else null
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
@@ -395,7 +394,7 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
|
|||||||
{ onValidateSpecialMode() },
|
{ onValidateSpecialMode() },
|
||||||
{ onCancelSpecialMode() },
|
{ onCancelSpecialMode() },
|
||||||
{ onLaunchActivitySpecialMode() },
|
{ onLaunchActivitySpecialMode() },
|
||||||
mAutofillActivityResultLauncher
|
mCredentialActivityResultLauncher
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -806,14 +805,14 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
|
|||||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||||
@Throws(FileNotFoundException::class)
|
@Throws(FileNotFoundException::class)
|
||||||
fun launchForAutofillResult(activity: AppCompatActivity,
|
fun launchForAutofillResult(activity: AppCompatActivity,
|
||||||
|
activityResultLauncher: ActivityResultLauncher<Intent>?,
|
||||||
databaseFile: Uri,
|
databaseFile: Uri,
|
||||||
keyFile: Uri?,
|
keyFile: Uri?,
|
||||||
hardwareKey: HardwareKey?,
|
hardwareKey: HardwareKey?,
|
||||||
activityResultLauncher: ActivityResultLauncher<Intent>?,
|
|
||||||
autofillComponent: AutofillComponent,
|
autofillComponent: AutofillComponent,
|
||||||
searchInfo: SearchInfo?) {
|
searchInfo: SearchInfo?) {
|
||||||
buildAndLaunchIntent(activity, databaseFile, keyFile, hardwareKey) { intent ->
|
buildAndLaunchIntent(activity, databaseFile, keyFile, hardwareKey) { intent ->
|
||||||
AutofillHelper.startActivityForAutofillResult(
|
EntrySelectionHelper.startActivityForAutofillSelectionModeResult(
|
||||||
activity,
|
activity,
|
||||||
intent,
|
intent,
|
||||||
activityResultLauncher,
|
activityResultLauncher,
|
||||||
@@ -822,21 +821,51 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* -------------------------
|
||||||
|
* Passkey Launch
|
||||||
|
* -------------------------
|
||||||
|
*/
|
||||||
|
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
|
||||||
|
@Throws(FileNotFoundException::class)
|
||||||
|
fun launchForPasskeyResult(activity: Activity,
|
||||||
|
activityResultLauncher: ActivityResultLauncher<Intent>?,
|
||||||
|
databaseFile: Uri,
|
||||||
|
keyFile: Uri?,
|
||||||
|
hardwareKey: HardwareKey?,
|
||||||
|
searchInfo: SearchInfo?) {
|
||||||
|
buildAndLaunchIntent(activity, databaseFile, keyFile, hardwareKey) { intent ->
|
||||||
|
EntrySelectionHelper.startActivityForPasskeySelectionModeResult(
|
||||||
|
activity,
|
||||||
|
intent,
|
||||||
|
activityResultLauncher,
|
||||||
|
searchInfo
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* -------------------------
|
* -------------------------
|
||||||
* Registration Launch
|
* Registration Launch
|
||||||
* -------------------------
|
* -------------------------
|
||||||
*/
|
*/
|
||||||
fun launchForRegistration(activity: Activity,
|
fun launchForRegistration(
|
||||||
databaseFile: Uri,
|
activity: Activity,
|
||||||
keyFile: Uri?,
|
activityResultLauncher: ActivityResultLauncher<Intent>?,
|
||||||
hardwareKey: HardwareKey?,
|
databaseFile: Uri,
|
||||||
registerInfo: RegisterInfo?) {
|
keyFile: Uri?,
|
||||||
|
hardwareKey: HardwareKey?,
|
||||||
|
typeMode: TypeMode,
|
||||||
|
registerInfo: RegisterInfo?
|
||||||
|
) {
|
||||||
buildAndLaunchIntent(activity, databaseFile, keyFile, hardwareKey) { intent ->
|
buildAndLaunchIntent(activity, databaseFile, keyFile, hardwareKey) { intent ->
|
||||||
EntrySelectionHelper.startActivityForRegistrationModeResult(
|
EntrySelectionHelper.startActivityForRegistrationModeResult(
|
||||||
activity,
|
context = activity,
|
||||||
intent,
|
activityResultLauncher = activityResultLauncher,
|
||||||
registerInfo)
|
intent = intent,
|
||||||
|
typeMode = typeMode,
|
||||||
|
registerInfo = registerInfo
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -852,74 +881,104 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
|
|||||||
fileNoFoundAction: (exception: FileNotFoundException) -> Unit,
|
fileNoFoundAction: (exception: FileNotFoundException) -> Unit,
|
||||||
onCancelSpecialMode: () -> Unit,
|
onCancelSpecialMode: () -> Unit,
|
||||||
onLaunchActivitySpecialMode: () -> Unit,
|
onLaunchActivitySpecialMode: () -> Unit,
|
||||||
autofillActivityResultLauncher: ActivityResultLauncher<Intent>?) {
|
activityResultLauncher: ActivityResultLauncher<Intent>?) {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
EntrySelectionHelper.doSpecialAction(activity.intent,
|
EntrySelectionHelper.doSpecialAction(
|
||||||
{
|
intent = activity.intent,
|
||||||
launch(
|
defaultAction = {
|
||||||
activity,
|
launch(
|
||||||
databaseUri,
|
activity = activity,
|
||||||
keyFile,
|
databaseFile = databaseUri,
|
||||||
hardwareKey
|
keyFile = keyFile,
|
||||||
)
|
hardwareKey = hardwareKey
|
||||||
},
|
)
|
||||||
{ searchInfo -> // Search Action
|
},
|
||||||
launchForSearchResult(
|
searchAction = { searchInfo ->
|
||||||
activity,
|
launchForSearchResult(
|
||||||
databaseUri,
|
activity = activity,
|
||||||
keyFile,
|
databaseFile = databaseUri,
|
||||||
hardwareKey,
|
keyFile = keyFile,
|
||||||
searchInfo
|
hardwareKey = hardwareKey,
|
||||||
)
|
searchInfo = searchInfo
|
||||||
onLaunchActivitySpecialMode()
|
)
|
||||||
},
|
onLaunchActivitySpecialMode()
|
||||||
{ searchInfo -> // Save Action
|
},
|
||||||
launchForSaveResult(
|
saveAction = { searchInfo ->
|
||||||
activity,
|
launchForSaveResult(
|
||||||
databaseUri,
|
activity = activity,
|
||||||
keyFile,
|
databaseFile = databaseUri,
|
||||||
hardwareKey,
|
keyFile = keyFile,
|
||||||
searchInfo
|
hardwareKey = hardwareKey,
|
||||||
)
|
searchInfo = searchInfo
|
||||||
onLaunchActivitySpecialMode()
|
)
|
||||||
},
|
onLaunchActivitySpecialMode()
|
||||||
{ searchInfo -> // Keyboard Selection Action
|
},
|
||||||
launchForKeyboardResult(
|
keyboardSelectionAction = { searchInfo ->
|
||||||
activity,
|
launchForKeyboardResult(
|
||||||
databaseUri,
|
activity = activity,
|
||||||
keyFile,
|
databaseFile = databaseUri,
|
||||||
hardwareKey,
|
keyFile = keyFile,
|
||||||
searchInfo
|
hardwareKey = hardwareKey,
|
||||||
)
|
searchInfo = searchInfo
|
||||||
onLaunchActivitySpecialMode()
|
)
|
||||||
},
|
onLaunchActivitySpecialMode()
|
||||||
{ searchInfo, autofillComponent -> // Autofill Selection Action
|
},
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
autofillSelectionAction = { searchInfo, autofillComponent ->
|
||||||
launchForAutofillResult(
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
activity,
|
launchForAutofillResult(
|
||||||
databaseUri,
|
activity = activity,
|
||||||
keyFile,
|
activityResultLauncher = activityResultLauncher,
|
||||||
hardwareKey,
|
databaseFile = databaseUri,
|
||||||
autofillActivityResultLauncher,
|
keyFile = keyFile,
|
||||||
autofillComponent,
|
hardwareKey = hardwareKey,
|
||||||
searchInfo
|
autofillComponent = autofillComponent,
|
||||||
)
|
searchInfo = searchInfo
|
||||||
onLaunchActivitySpecialMode()
|
|
||||||
} else {
|
|
||||||
onCancelSpecialMode()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{ registerInfo -> // Registration Action
|
|
||||||
launchForRegistration(
|
|
||||||
activity,
|
|
||||||
databaseUri,
|
|
||||||
keyFile,
|
|
||||||
hardwareKey,
|
|
||||||
registerInfo
|
|
||||||
)
|
)
|
||||||
onLaunchActivitySpecialMode()
|
onLaunchActivitySpecialMode()
|
||||||
|
} else {
|
||||||
|
onCancelSpecialMode()
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
autofillRegistrationAction = { registerInfo ->
|
||||||
|
launchForRegistration(
|
||||||
|
activity = activity,
|
||||||
|
activityResultLauncher = activityResultLauncher,
|
||||||
|
databaseFile = databaseUri,
|
||||||
|
keyFile = keyFile,
|
||||||
|
hardwareKey = hardwareKey,
|
||||||
|
typeMode = TypeMode.AUTOFILL,
|
||||||
|
registerInfo = registerInfo
|
||||||
|
)
|
||||||
|
onLaunchActivitySpecialMode()
|
||||||
|
},
|
||||||
|
passkeySelectionAction = { searchInfo ->
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
||||||
|
launchForPasskeyResult(
|
||||||
|
activity = activity,
|
||||||
|
activityResultLauncher = activityResultLauncher,
|
||||||
|
databaseFile = databaseUri,
|
||||||
|
keyFile = keyFile,
|
||||||
|
hardwareKey = hardwareKey,
|
||||||
|
searchInfo = searchInfo
|
||||||
|
)
|
||||||
|
onLaunchActivitySpecialMode()
|
||||||
|
} else {
|
||||||
|
onCancelSpecialMode()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
passkeyRegistrationAction = { registerInfo ->
|
||||||
|
launchForRegistration(
|
||||||
|
activity = activity,
|
||||||
|
activityResultLauncher = activityResultLauncher,
|
||||||
|
databaseFile = databaseUri,
|
||||||
|
keyFile = keyFile,
|
||||||
|
hardwareKey = hardwareKey,
|
||||||
|
typeMode = TypeMode.PASSKEY,
|
||||||
|
registerInfo = registerInfo
|
||||||
|
)
|
||||||
|
onLaunchActivitySpecialMode()
|
||||||
|
}
|
||||||
)
|
)
|
||||||
} catch (e: FileNotFoundException) {
|
} catch (e: FileNotFoundException) {
|
||||||
fileNoFoundAction(e)
|
fileNoFoundAction(e)
|
||||||
|
|||||||
@@ -36,8 +36,8 @@ import androidx.recyclerview.widget.RecyclerView
|
|||||||
import androidx.recyclerview.widget.RecyclerView.SCROLL_STATE_IDLE
|
import androidx.recyclerview.widget.RecyclerView.SCROLL_STATE_IDLE
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.activities.dialogs.SortDialogFragment
|
import com.kunzisoft.keepass.activities.dialogs.SortDialogFragment
|
||||||
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper
|
||||||
import com.kunzisoft.keepass.activities.helpers.SpecialMode
|
import com.kunzisoft.keepass.credentialprovider.SpecialMode
|
||||||
import com.kunzisoft.keepass.adapters.NodesAdapter
|
import com.kunzisoft.keepass.adapters.NodesAdapter
|
||||||
import com.kunzisoft.keepass.database.ContextualDatabase
|
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||||
import com.kunzisoft.keepass.database.element.Group
|
import com.kunzisoft.keepass.database.element.Group
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
package com.kunzisoft.keepass.activities.helpers
|
|
||||||
|
|
||||||
enum class TypeMode {
|
|
||||||
DEFAULT, MAGIKEYBOARD, AUTOFILL
|
|
||||||
}
|
|
||||||
@@ -34,8 +34,8 @@ import androidx.appcompat.app.AlertDialog
|
|||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.activities.dialogs.DeleteNodesDialogFragment
|
import com.kunzisoft.keepass.activities.dialogs.DeleteNodesDialogFragment
|
||||||
import com.kunzisoft.keepass.activities.dialogs.PasswordEncodingDialogFragment
|
import com.kunzisoft.keepass.activities.dialogs.PasswordEncodingDialogFragment
|
||||||
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper
|
||||||
import com.kunzisoft.keepass.activities.helpers.SpecialMode
|
import com.kunzisoft.keepass.credentialprovider.SpecialMode
|
||||||
import com.kunzisoft.keepass.database.ContextualDatabase
|
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||||
import com.kunzisoft.keepass.database.MainCredential
|
import com.kunzisoft.keepass.database.MainCredential
|
||||||
import com.kunzisoft.keepass.database.element.Entry
|
import com.kunzisoft.keepass.database.element.Entry
|
||||||
|
|||||||
@@ -5,9 +5,10 @@ import android.view.View
|
|||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.activity.OnBackPressedCallback
|
import androidx.activity.OnBackPressedCallback
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper
|
||||||
import com.kunzisoft.keepass.activities.helpers.SpecialMode
|
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.isIntentSenderMode
|
||||||
import com.kunzisoft.keepass.activities.helpers.TypeMode
|
import com.kunzisoft.keepass.credentialprovider.SpecialMode
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.TypeMode
|
||||||
import com.kunzisoft.keepass.model.SearchInfo
|
import com.kunzisoft.keepass.model.SearchInfo
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
import com.kunzisoft.keepass.view.ToolbarSpecial
|
import com.kunzisoft.keepass.view.ToolbarSpecial
|
||||||
@@ -42,18 +43,15 @@ abstract class DatabaseModeActivity : DatabaseActivity() {
|
|||||||
/**
|
/**
|
||||||
* Intent sender uses special retains data in callback
|
* Intent sender uses special retains data in callback
|
||||||
*/
|
*/
|
||||||
private fun isIntentSender(): Boolean {
|
protected fun isIntentSender(): Boolean {
|
||||||
return (mSpecialMode == SpecialMode.SELECTION
|
return isIntentSenderMode(mSpecialMode, mTypeMode)
|
||||||
&& mTypeMode == TypeMode.AUTOFILL)
|
|
||||||
/* TODO Registration callback #765
|
|
||||||
|| (mSpecialMode == SpecialMode.REGISTRATION
|
|
||||||
&& mTypeMode == TypeMode.AUTOFILL
|
|
||||||
&& Build.VERSION.SDK_INT >= Build.VERSION_CODES.P)
|
|
||||||
*/
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onLaunchActivitySpecialMode() {
|
fun onLaunchActivitySpecialMode() {
|
||||||
if (!isIntentSender()) {
|
// TODO Verify behavior for Autofill Callback #765
|
||||||
|
if (isIntentSender()) {
|
||||||
|
onValidateSpecialMode()
|
||||||
|
} else {
|
||||||
EntrySelectionHelper.removeModesFromIntent(intent)
|
EntrySelectionHelper.removeModesFromIntent(intent)
|
||||||
EntrySelectionHelper.removeInfoFromIntent(intent)
|
EntrySelectionHelper.removeInfoFromIntent(intent)
|
||||||
finish()
|
finish()
|
||||||
@@ -136,6 +134,7 @@ abstract class DatabaseModeActivity : DatabaseActivity() {
|
|||||||
TypeMode.DEFAULT, // Not important because hidden
|
TypeMode.DEFAULT, // Not important because hidden
|
||||||
TypeMode.MAGIKEYBOARD -> R.string.magic_keyboard_title
|
TypeMode.MAGIKEYBOARD -> R.string.magic_keyboard_title
|
||||||
TypeMode.AUTOFILL -> R.string.autofill
|
TypeMode.AUTOFILL -> R.string.autofill
|
||||||
|
TypeMode.PASSKEY -> R.string.passkey
|
||||||
}
|
}
|
||||||
title = getString(selectionModeStringId)
|
title = getString(selectionModeStringId)
|
||||||
if (mTypeMode != TypeMode.DEFAULT)
|
if (mTypeMode != TypeMode.DEFAULT)
|
||||||
|
|||||||
@@ -17,17 +17,32 @@
|
|||||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.activities.helpers
|
package com.kunzisoft.keepass.credentialprovider
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.graphics.drawable.Icon
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import com.kunzisoft.keepass.autofill.AutofillComponent
|
import android.util.Log
|
||||||
import com.kunzisoft.keepass.autofill.AutofillHelper
|
import android.widget.RemoteViews
|
||||||
|
import androidx.activity.result.ActivityResultLauncher
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import androidx.annotation.RequiresApi
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import com.kunzisoft.keepass.R
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.autofill.AutofillComponent
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.autofill.AutofillHelper
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.autofill.AutofillHelper.addAutofillComponent
|
||||||
|
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||||
|
import com.kunzisoft.keepass.model.EntryInfo
|
||||||
import com.kunzisoft.keepass.model.RegisterInfo
|
import com.kunzisoft.keepass.model.RegisterInfo
|
||||||
import com.kunzisoft.keepass.model.SearchInfo
|
import com.kunzisoft.keepass.model.SearchInfo
|
||||||
import com.kunzisoft.keepass.utils.getParcelableExtraCompat
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
|
import com.kunzisoft.keepass.utils.LOCK_ACTION
|
||||||
import com.kunzisoft.keepass.utils.getEnumExtra
|
import com.kunzisoft.keepass.utils.getEnumExtra
|
||||||
|
import com.kunzisoft.keepass.utils.getParcelableExtraCompat
|
||||||
import com.kunzisoft.keepass.utils.putEnumExtra
|
import com.kunzisoft.keepass.utils.putEnumExtra
|
||||||
|
|
||||||
object EntrySelectionHelper {
|
object EntrySelectionHelper {
|
||||||
@@ -37,6 +52,33 @@ object EntrySelectionHelper {
|
|||||||
private const val KEY_SEARCH_INFO = "com.kunzisoft.keepass.extra.SEARCH_INFO"
|
private const val KEY_SEARCH_INFO = "com.kunzisoft.keepass.extra.SEARCH_INFO"
|
||||||
private const val KEY_REGISTER_INFO = "com.kunzisoft.keepass.extra.REGISTER_INFO"
|
private const val KEY_REGISTER_INFO = "com.kunzisoft.keepass.extra.REGISTER_INFO"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility method to build a registerForActivityResult,
|
||||||
|
* Used recursively, close each activity with return data
|
||||||
|
*/
|
||||||
|
fun AppCompatActivity.buildActivityResultLauncher(
|
||||||
|
lockDatabase: Boolean = false,
|
||||||
|
dataTransformation: (data: Intent?) -> Intent? = { it },
|
||||||
|
): ActivityResultLauncher<Intent> {
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun startActivityForSearchModeResult(context: Context,
|
fun startActivityForSearchModeResult(context: Context,
|
||||||
intent: Intent,
|
intent: Intent,
|
||||||
searchInfo: SearchInfo) {
|
searchInfo: SearchInfo) {
|
||||||
@@ -66,15 +108,52 @@ object EntrySelectionHelper {
|
|||||||
context.startActivity(intent)
|
context.startActivity(intent)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun startActivityForRegistrationModeResult(context: Context,
|
/**
|
||||||
intent: Intent,
|
* Utility method to start an activity with an Autofill for result
|
||||||
registerInfo: RegisterInfo?) {
|
*/
|
||||||
addSpecialModeInIntent(intent, SpecialMode.REGISTRATION)
|
@RequiresApi(Build.VERSION_CODES.O)
|
||||||
// At the moment, only autofill for registration
|
fun startActivityForAutofillSelectionModeResult(
|
||||||
|
context: Context,
|
||||||
|
intent: Intent,
|
||||||
|
activityResultLauncher: ActivityResultLauncher<Intent>?,
|
||||||
|
autofillComponent: AutofillComponent,
|
||||||
|
searchInfo: SearchInfo?
|
||||||
|
) {
|
||||||
|
addSpecialModeInIntent(intent, SpecialMode.SELECTION)
|
||||||
addTypeModeInIntent(intent, TypeMode.AUTOFILL)
|
addTypeModeInIntent(intent, TypeMode.AUTOFILL)
|
||||||
|
intent.addAutofillComponent(context, autofillComponent)
|
||||||
|
addSearchInfoInIntent(intent, searchInfo)
|
||||||
|
activityResultLauncher?.launch(intent)
|
||||||
|
}
|
||||||
|
|
||||||
|
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
|
||||||
|
fun startActivityForPasskeySelectionModeResult(
|
||||||
|
context: Context,
|
||||||
|
intent: Intent,
|
||||||
|
activityResultLauncher: ActivityResultLauncher<Intent>?,
|
||||||
|
searchInfo: SearchInfo?
|
||||||
|
) {
|
||||||
|
addSpecialModeInIntent(intent, SpecialMode.SELECTION)
|
||||||
|
addTypeModeInIntent(intent, TypeMode.PASSKEY)
|
||||||
|
addSearchInfoInIntent(intent, searchInfo)
|
||||||
|
activityResultLauncher?.launch(intent)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun startActivityForRegistrationModeResult(
|
||||||
|
context: Context?,
|
||||||
|
activityResultLauncher: ActivityResultLauncher<Intent>?,
|
||||||
|
intent: Intent,
|
||||||
|
registerInfo: RegisterInfo?,
|
||||||
|
typeMode: TypeMode
|
||||||
|
) {
|
||||||
|
addSpecialModeInIntent(intent, SpecialMode.REGISTRATION)
|
||||||
|
addTypeModeInIntent(intent, typeMode)
|
||||||
addRegisterInfoInIntent(intent, registerInfo)
|
addRegisterInfoInIntent(intent, registerInfo)
|
||||||
intent.flags = intent.flags or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
if (activityResultLauncher == null) {
|
||||||
context.startActivity(intent)
|
intent.flags = intent.flags or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||||
|
}
|
||||||
|
context?.startActivity(intent) ?: activityResultLauncher?.launch(intent) ?:
|
||||||
|
throw IllegalStateException("At least Context or ActivityResultLauncher must not be null")
|
||||||
}
|
}
|
||||||
|
|
||||||
fun addSearchInfoInIntent(intent: Intent, searchInfo: SearchInfo?) {
|
fun addSearchInfoInIntent(intent: Intent, searchInfo: SearchInfo?) {
|
||||||
@@ -103,8 +182,13 @@ object EntrySelectionHelper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun addSpecialModeInIntent(intent: Intent, specialMode: SpecialMode) {
|
fun addSpecialModeInIntent(intent: Intent, specialMode: SpecialMode) {
|
||||||
|
// TODO Replace by Intent.addSpecialMode
|
||||||
intent.putEnumExtra(KEY_SPECIAL_MODE, specialMode)
|
intent.putEnumExtra(KEY_SPECIAL_MODE, specialMode)
|
||||||
}
|
}
|
||||||
|
fun Intent.addSpecialMode(specialMode: SpecialMode): Intent {
|
||||||
|
this.putEnumExtra(KEY_SPECIAL_MODE, specialMode)
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
fun retrieveSpecialModeFromIntent(intent: Intent): SpecialMode {
|
fun retrieveSpecialModeFromIntent(intent: Intent): SpecialMode {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
@@ -131,6 +215,17 @@ object EntrySelectionHelper {
|
|||||||
intent.removeExtra(KEY_TYPE_MODE)
|
intent.removeExtra(KEY_TYPE_MODE)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Intent sender uses special retains data in callback
|
||||||
|
*/
|
||||||
|
fun isIntentSenderMode(specialMode: SpecialMode, typeMode: TypeMode): Boolean {
|
||||||
|
return (specialMode == SpecialMode.SELECTION
|
||||||
|
&& (typeMode == TypeMode.AUTOFILL || typeMode == TypeMode.PASSKEY))
|
||||||
|
// TODO Autofill Registration callback #765 && Build.VERSION.SDK_INT >= Build.VERSION_CODES.P
|
||||||
|
|| (specialMode == SpecialMode.REGISTRATION
|
||||||
|
&& typeMode == TypeMode.PASSKEY)
|
||||||
|
}
|
||||||
|
|
||||||
fun doSpecialAction(intent: Intent,
|
fun doSpecialAction(intent: Intent,
|
||||||
defaultAction: () -> Unit,
|
defaultAction: () -> Unit,
|
||||||
searchAction: (searchInfo: SearchInfo) -> Unit,
|
searchAction: (searchInfo: SearchInfo) -> Unit,
|
||||||
@@ -138,7 +233,9 @@ object EntrySelectionHelper {
|
|||||||
keyboardSelectionAction: (searchInfo: SearchInfo?) -> Unit,
|
keyboardSelectionAction: (searchInfo: SearchInfo?) -> Unit,
|
||||||
autofillSelectionAction: (searchInfo: SearchInfo?,
|
autofillSelectionAction: (searchInfo: SearchInfo?,
|
||||||
autofillComponent: AutofillComponent) -> Unit,
|
autofillComponent: AutofillComponent) -> Unit,
|
||||||
autofillRegistrationAction: (registerInfo: RegisterInfo?) -> Unit) {
|
autofillRegistrationAction: (registerInfo: RegisterInfo?) -> Unit,
|
||||||
|
passkeySelectionAction: (searchInfo: SearchInfo?) -> Unit,
|
||||||
|
passkeyRegistrationAction: (registerInfo: RegisterInfo?) -> Unit) {
|
||||||
|
|
||||||
when (retrieveSpecialModeFromIntent(intent)) {
|
when (retrieveSpecialModeFromIntent(intent)) {
|
||||||
SpecialMode.DEFAULT -> {
|
SpecialMode.DEFAULT -> {
|
||||||
@@ -186,6 +283,7 @@ object EntrySelectionHelper {
|
|||||||
defaultAction.invoke()
|
defaultAction.invoke()
|
||||||
}
|
}
|
||||||
TypeMode.MAGIKEYBOARD -> keyboardSelectionAction.invoke(searchInfo)
|
TypeMode.MAGIKEYBOARD -> keyboardSelectionAction.invoke(searchInfo)
|
||||||
|
TypeMode.PASSKEY -> passkeySelectionAction.invoke(searchInfo)
|
||||||
else -> {
|
else -> {
|
||||||
// In this case, error
|
// In this case, error
|
||||||
removeModesFromIntent(intent)
|
removeModesFromIntent(intent)
|
||||||
@@ -202,10 +300,59 @@ object EntrySelectionHelper {
|
|||||||
}
|
}
|
||||||
SpecialMode.REGISTRATION -> {
|
SpecialMode.REGISTRATION -> {
|
||||||
val registerInfo: RegisterInfo? = retrieveRegisterInfoFromIntent(intent)
|
val registerInfo: RegisterInfo? = retrieveRegisterInfoFromIntent(intent)
|
||||||
removeModesFromIntent(intent)
|
if (!isIntentSenderMode(
|
||||||
removeInfoFromIntent(intent)
|
specialMode = retrieveSpecialModeFromIntent(intent),
|
||||||
autofillRegistrationAction.invoke(registerInfo)
|
typeMode = retrieveTypeModeFromIntent(intent))
|
||||||
|
) {
|
||||||
|
removeModesFromIntent(intent)
|
||||||
|
removeInfoFromIntent(intent)
|
||||||
|
}
|
||||||
|
when (retrieveTypeModeFromIntent(intent)) {
|
||||||
|
TypeMode.AUTOFILL -> {
|
||||||
|
autofillRegistrationAction.invoke(registerInfo)
|
||||||
|
}
|
||||||
|
TypeMode.PASSKEY -> {
|
||||||
|
passkeyRegistrationAction.invoke(registerInfo)
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
// Do other registration type
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun performSelection(items: List<EntryInfo>,
|
||||||
|
actionPopulateCredentialProvider: (entryInfo: EntryInfo) -> Unit,
|
||||||
|
actionEntrySelection: (autoSearch: Boolean) -> Unit) {
|
||||||
|
if (items.size == 1) {
|
||||||
|
val itemFound = items[0]
|
||||||
|
actionPopulateCredentialProvider.invoke(itemFound)
|
||||||
|
} else if (items.size > 1) {
|
||||||
|
// Select the one we want in the selection
|
||||||
|
actionEntrySelection.invoke(true)
|
||||||
|
} else {
|
||||||
|
// Select an arbitrary one
|
||||||
|
actionEntrySelection.invoke(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method to assign a drawable to a new icon from a database icon
|
||||||
|
*/
|
||||||
|
@RequiresApi(Build.VERSION_CODES.M)
|
||||||
|
fun EntryInfo.buildIcon(
|
||||||
|
context: Context,
|
||||||
|
database: ContextualDatabase
|
||||||
|
): Icon? {
|
||||||
|
try {
|
||||||
|
database.iconDrawableFactory.getBitmapFromIcon(context,
|
||||||
|
this.icon, ContextCompat.getColor(context, R.color.green))?.let { bitmap ->
|
||||||
|
return Icon.createWithBitmap(bitmap)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(RemoteViews::class.java.name, "Unable to assign icon in remote view", e)
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.kunzisoft.keepass.activities.helpers
|
package com.kunzisoft.keepass.credentialprovider
|
||||||
|
|
||||||
enum class SpecialMode {
|
enum class SpecialMode {
|
||||||
DEFAULT,
|
DEFAULT,
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
package com.kunzisoft.keepass.credentialprovider
|
||||||
|
|
||||||
|
enum class TypeMode {
|
||||||
|
DEFAULT, MAGIKEYBOARD, AUTOFILL, PASSKEY
|
||||||
|
}
|
||||||
@@ -17,7 +17,7 @@
|
|||||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.activities
|
package com.kunzisoft.keepass.credentialprovider.activity
|
||||||
|
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.app.PendingIntent
|
import android.app.PendingIntent
|
||||||
@@ -30,13 +30,17 @@ import android.widget.Toast
|
|||||||
import androidx.activity.result.ActivityResultLauncher
|
import androidx.activity.result.ActivityResultLauncher
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
import com.kunzisoft.keepass.activities.FileDatabaseSelectActivity
|
||||||
import com.kunzisoft.keepass.activities.helpers.SpecialMode
|
import com.kunzisoft.keepass.activities.GroupActivity
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.SpecialMode
|
||||||
import com.kunzisoft.keepass.activities.legacy.DatabaseModeActivity
|
import com.kunzisoft.keepass.activities.legacy.DatabaseModeActivity
|
||||||
import com.kunzisoft.keepass.autofill.AutofillComponent
|
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.buildActivityResultLauncher
|
||||||
import com.kunzisoft.keepass.autofill.AutofillHelper
|
import com.kunzisoft.keepass.credentialprovider.TypeMode
|
||||||
import com.kunzisoft.keepass.autofill.CompatInlineSuggestionsRequest
|
import com.kunzisoft.keepass.credentialprovider.autofill.AutofillComponent
|
||||||
import com.kunzisoft.keepass.autofill.KeeAutofillService
|
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.ContextualDatabase
|
||||||
import com.kunzisoft.keepass.database.helper.SearchHelper
|
import com.kunzisoft.keepass.database.helper.SearchHelper
|
||||||
import com.kunzisoft.keepass.model.RegisterInfo
|
import com.kunzisoft.keepass.model.RegisterInfo
|
||||||
@@ -48,10 +52,8 @@ import com.kunzisoft.keepass.utils.getParcelableExtraCompat
|
|||||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||||
class AutofillLauncherActivity : DatabaseModeActivity() {
|
class AutofillLauncherActivity : DatabaseModeActivity() {
|
||||||
|
|
||||||
private var mAutofillActivityResultLauncher: ActivityResultLauncher<Intent>? =
|
private var mCredentialActivityResultLauncher: ActivityResultLauncher<Intent>? =
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
|
this.buildActivityResultLauncher(lockDatabase = true)
|
||||||
AutofillHelper.buildActivityResultLauncher(this, true)
|
|
||||||
else null
|
|
||||||
|
|
||||||
override fun applyCustomStyle(): Boolean {
|
override fun applyCustomStyle(): Boolean {
|
||||||
return false
|
return false
|
||||||
@@ -72,7 +74,9 @@ class AutofillLauncherActivity : DatabaseModeActivity() {
|
|||||||
// To pass extra inline request
|
// To pass extra inline request
|
||||||
var compatInlineSuggestionsRequest: CompatInlineSuggestionsRequest? = null
|
var compatInlineSuggestionsRequest: CompatInlineSuggestionsRequest? = null
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||||
compatInlineSuggestionsRequest = bundle.getParcelableCompat(KEY_INLINE_SUGGESTION)
|
compatInlineSuggestionsRequest = bundle.getParcelableCompat(
|
||||||
|
KEY_INLINE_SUGGESTION
|
||||||
|
)
|
||||||
}
|
}
|
||||||
// Build search param
|
// Build search param
|
||||||
bundle.getParcelableCompat<SearchInfo>(KEY_SEARCH_INFO)?.let { searchInfo ->
|
bundle.getParcelableCompat<SearchInfo>(KEY_SEARCH_INFO)?.let { searchInfo ->
|
||||||
@@ -102,7 +106,9 @@ class AutofillLauncherActivity : DatabaseModeActivity() {
|
|||||||
}
|
}
|
||||||
SpecialMode.REGISTRATION -> {
|
SpecialMode.REGISTRATION -> {
|
||||||
// To register info
|
// To register info
|
||||||
val registerInfo = intent.getParcelableExtraCompat<RegisterInfo>(KEY_REGISTER_INFO)
|
val registerInfo = intent.getParcelableExtraCompat<RegisterInfo>(
|
||||||
|
KEY_REGISTER_INFO
|
||||||
|
)
|
||||||
val searchInfo = SearchInfo(registerInfo?.searchInfo)
|
val searchInfo = SearchInfo(registerInfo?.searchInfo)
|
||||||
WebDomain.getConcreteWebDomain(this, searchInfo.webDomain) { concreteWebDomain ->
|
WebDomain.getConcreteWebDomain(this, searchInfo.webDomain) { concreteWebDomain ->
|
||||||
searchInfo.webDomain = concreteWebDomain
|
searchInfo.webDomain = concreteWebDomain
|
||||||
@@ -134,30 +140,35 @@ class AutofillLauncherActivity : DatabaseModeActivity() {
|
|||||||
finish()
|
finish()
|
||||||
} else {
|
} else {
|
||||||
// If database is open
|
// If database is open
|
||||||
SearchHelper.checkAutoSearchInfo(this,
|
SearchHelper.checkAutoSearchInfo(
|
||||||
database,
|
context = this,
|
||||||
searchInfo,
|
database = database,
|
||||||
{ openedDatabase, items ->
|
searchInfo = searchInfo,
|
||||||
// Items found
|
onItemsFound = { openedDatabase, items ->
|
||||||
AutofillHelper.buildResponseAndSetResult(this, openedDatabase, items)
|
// Items found
|
||||||
finish()
|
AutofillHelper.buildResponseAndSetResult(this, openedDatabase, items)
|
||||||
},
|
finish()
|
||||||
{ openedDatabase ->
|
},
|
||||||
// Show the database UI to select the entry
|
onItemNotFound = { openedDatabase ->
|
||||||
GroupActivity.launchForAutofillResult(this,
|
// Show the database UI to select the entry
|
||||||
openedDatabase,
|
GroupActivity.launchForAutofillSelectionResult(
|
||||||
mAutofillActivityResultLauncher,
|
this,
|
||||||
autofillComponent,
|
openedDatabase,
|
||||||
searchInfo,
|
mCredentialActivityResultLauncher,
|
||||||
false)
|
autofillComponent,
|
||||||
},
|
searchInfo,
|
||||||
{
|
false
|
||||||
// If database not open
|
)
|
||||||
FileDatabaseSelectActivity.launchForAutofillResult(this,
|
},
|
||||||
mAutofillActivityResultLauncher,
|
onDatabaseClosed = {
|
||||||
autofillComponent,
|
// If database not open
|
||||||
searchInfo)
|
FileDatabaseSelectActivity.launchForAutofillResult(
|
||||||
}
|
this,
|
||||||
|
mCredentialActivityResultLauncher,
|
||||||
|
autofillComponent,
|
||||||
|
searchInfo
|
||||||
|
)
|
||||||
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -174,34 +185,47 @@ class AutofillLauncherActivity : DatabaseModeActivity() {
|
|||||||
setResult(Activity.RESULT_CANCELED)
|
setResult(Activity.RESULT_CANCELED)
|
||||||
} else {
|
} else {
|
||||||
val readOnly = database?.isReadOnly != false
|
val readOnly = database?.isReadOnly != false
|
||||||
SearchHelper.checkAutoSearchInfo(this,
|
SearchHelper.checkAutoSearchInfo(
|
||||||
database,
|
context = this,
|
||||||
searchInfo,
|
database = database,
|
||||||
{ openedDatabase, _ ->
|
searchInfo = searchInfo,
|
||||||
if (!readOnly) {
|
onItemsFound = { openedDatabase, _ ->
|
||||||
// Show the database UI to select the entry
|
if (!readOnly) {
|
||||||
GroupActivity.launchForRegistration(this,
|
// Show the database UI to select the entry
|
||||||
openedDatabase,
|
GroupActivity.launchForRegistration(
|
||||||
registerInfo)
|
context = this,
|
||||||
} else {
|
activityResultLauncher = null, // TODO Autofill result launcher #765
|
||||||
showReadOnlySaveMessage()
|
database = openedDatabase,
|
||||||
}
|
registerInfo = registerInfo,
|
||||||
},
|
typeMode = TypeMode.AUTOFILL
|
||||||
{ openedDatabase ->
|
)
|
||||||
if (!readOnly) {
|
} else {
|
||||||
// Show the database UI to select the entry
|
showReadOnlySaveMessage()
|
||||||
GroupActivity.launchForRegistration(this,
|
|
||||||
openedDatabase,
|
|
||||||
registerInfo)
|
|
||||||
} else {
|
|
||||||
showReadOnlySaveMessage()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
// If database not open
|
|
||||||
FileDatabaseSelectActivity.launchForRegistration(this,
|
|
||||||
registerInfo)
|
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
onItemNotFound = { openedDatabase ->
|
||||||
|
if (!readOnly) {
|
||||||
|
// Show the database UI to select the entry
|
||||||
|
GroupActivity.launchForRegistration(
|
||||||
|
context = this,
|
||||||
|
activityResultLauncher = null, // TODO Autofill result launcher #765
|
||||||
|
database = openedDatabase,
|
||||||
|
registerInfo = registerInfo,
|
||||||
|
typeMode = TypeMode.AUTOFILL
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
showReadOnlySaveMessage()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onDatabaseClosed = {
|
||||||
|
// If database not open
|
||||||
|
FileDatabaseSelectActivity.launchForRegistration(
|
||||||
|
context = this,
|
||||||
|
activityResultLauncher = null, // TODO Autofill result launcher #765
|
||||||
|
registerInfo = registerInfo,
|
||||||
|
typeMode = TypeMode.AUTOFILL
|
||||||
|
)
|
||||||
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
finish()
|
finish()
|
||||||
@@ -1,281 +0,0 @@
|
|||||||
package com.kunzisoft.keepass.credentialprovider.activity
|
|
||||||
|
|
||||||
import android.app.Activity
|
|
||||||
import android.content.Intent
|
|
||||||
import android.os.Build
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.util.Log
|
|
||||||
import androidx.annotation.RequiresApi
|
|
||||||
import androidx.biometric.BiometricManager
|
|
||||||
import androidx.biometric.BiometricPrompt
|
|
||||||
import androidx.credentials.CreatePublicKeyCredentialRequest
|
|
||||||
import androidx.credentials.CreatePublicKeyCredentialResponse
|
|
||||||
import androidx.credentials.exceptions.CreateCredentialUnknownException
|
|
||||||
import androidx.credentials.exceptions.GetCredentialUnknownException
|
|
||||||
import androidx.credentials.provider.PendingIntentHandler
|
|
||||||
import com.kunzisoft.asymmetric.Signature
|
|
||||||
import com.kunzisoft.keepass.R
|
|
||||||
import com.kunzisoft.keepass.activities.legacy.DatabaseActivity
|
|
||||||
import com.kunzisoft.keepass.credentialprovider.data.Passkey
|
|
||||||
import com.kunzisoft.keepass.credentialprovider.data.PublicKeyCredentialCreationOptions
|
|
||||||
import com.kunzisoft.keepass.credentialprovider.util.AppRelyingPartyRelation
|
|
||||||
import com.kunzisoft.keepass.credentialprovider.util.Base64Helper
|
|
||||||
import com.kunzisoft.keepass.credentialprovider.util.DatabaseHelper
|
|
||||||
import com.kunzisoft.keepass.credentialprovider.util.IntentHelper
|
|
||||||
import com.kunzisoft.keepass.credentialprovider.util.JsonHelper
|
|
||||||
import com.kunzisoft.keepass.credentialprovider.util.OriginHelper
|
|
||||||
import com.kunzisoft.keepass.database.ContextualDatabase
|
|
||||||
import com.kunzisoft.keepass.database.DatabaseTaskProvider
|
|
||||||
import com.kunzisoft.random.KeePassDXRandom
|
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
|
|
||||||
class CreatePasskeyActivity : DatabaseActivity() {
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
Log.d(javaClass.simpleName, "onCreate called")
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
|
||||||
Log.d(javaClass.simpleName, "onDatabaseRetrieved called")
|
|
||||||
super.onDatabaseRetrieved(database)
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (database == null) {
|
|
||||||
throw CreateCredentialUnknownException("retrievedDatabase is null, maybe database is locked")
|
|
||||||
}
|
|
||||||
|
|
||||||
if (intent == null) {
|
|
||||||
throw CreateCredentialUnknownException("intent is null")
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mDatabaseTaskProvider == null) {
|
|
||||||
throw CreateCredentialUnknownException("mDatabaseTaskProvider is null")
|
|
||||||
}
|
|
||||||
|
|
||||||
createPasskeyAfterPrompt(database, mDatabaseTaskProvider!!, intent)
|
|
||||||
} catch (e: CreateCredentialUnknownException) {
|
|
||||||
Log.e(this::class.java.simpleName, "CreateCredentialUnknownException was thrown", e)
|
|
||||||
setResult(RESULT_CANCELED)
|
|
||||||
finish()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(this::class.java.simpleName, "other exception was thrown", e)
|
|
||||||
setResult(RESULT_CANCELED)
|
|
||||||
finish()
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun createPasskeyAfterPrompt(
|
|
||||||
database: ContextualDatabase,
|
|
||||||
databaseTaskProvider: DatabaseTaskProvider,
|
|
||||||
intent: Intent
|
|
||||||
) {
|
|
||||||
|
|
||||||
val request = PendingIntentHandler.retrieveProviderCreateCredentialRequest(intent)
|
|
||||||
?: throw CreateCredentialUnknownException("could not retrieve request from intent")
|
|
||||||
|
|
||||||
if (request.callingRequest !is CreatePublicKeyCredentialRequest) {
|
|
||||||
throw CreateCredentialUnknownException("callingRequest is of wrong type: ${request.callingRequest.type}")
|
|
||||||
}
|
|
||||||
val publicKeyRequest = request.callingRequest as CreatePublicKeyCredentialRequest
|
|
||||||
|
|
||||||
val creationOptions = JsonHelper.parseJsonToCreateOptions(publicKeyRequest.requestJson)
|
|
||||||
|
|
||||||
val relyingParty = creationOptions.relyingParty
|
|
||||||
|
|
||||||
val biometricPrompt = BiometricPrompt(
|
|
||||||
this,
|
|
||||||
object : BiometricPrompt.AuthenticationCallback() {
|
|
||||||
override fun onAuthenticationError(
|
|
||||||
errorCode: Int, errString: CharSequence
|
|
||||||
) {
|
|
||||||
super.onAuthenticationError(errorCode, errString)
|
|
||||||
throw CreateCredentialUnknownException("authentication error: errorCode = $errorCode, errString = $errString")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onAuthenticationFailed() {
|
|
||||||
super.onAuthenticationFailed()
|
|
||||||
throw CreateCredentialUnknownException("authentication failed")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
|
|
||||||
super.onAuthenticationSucceeded(result)
|
|
||||||
createPasskey(database, databaseTaskProvider, intent, creationOptions)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
val title = getString(R.string.passkey_creation_biometric_prompt_title)
|
|
||||||
val subtitle =
|
|
||||||
getString(
|
|
||||||
R.string.passkey_creation_biometric_prompt_subtitle,
|
|
||||||
relyingParty
|
|
||||||
)
|
|
||||||
val negativeButtonText =
|
|
||||||
getString(R.string.passkey_creation_biometric_prompt_negative_button_text)
|
|
||||||
val promptInfo = BiometricPrompt.PromptInfo.Builder()
|
|
||||||
.setTitle(title)
|
|
||||||
.setSubtitle(subtitle)
|
|
||||||
.setAllowedAuthenticators(BiometricManager.Authenticators.BIOMETRIC_STRONG)
|
|
||||||
.setNegativeButtonText(negativeButtonText)
|
|
||||||
.build()
|
|
||||||
biometricPrompt.authenticate(promptInfo)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun createPasskey(
|
|
||||||
database: ContextualDatabase,
|
|
||||||
databaseTaskProvider: DatabaseTaskProvider,
|
|
||||||
intent: Intent,
|
|
||||||
creationOptions: PublicKeyCredentialCreationOptions
|
|
||||||
) {
|
|
||||||
val request = PendingIntentHandler.retrieveProviderCreateCredentialRequest(intent)
|
|
||||||
|
|
||||||
val nodeId = IntentHelper.getVerifiedNodeId(intent)
|
|
||||||
?: throw CreateCredentialUnknownException("could not get verified nodeId from intent")
|
|
||||||
|
|
||||||
val callingAppInfo = request!!.callingAppInfo
|
|
||||||
val relyingParty = creationOptions.relyingParty
|
|
||||||
val challenge = creationOptions.challenge
|
|
||||||
val keyTypeIdList = creationOptions.keyTypeIdList
|
|
||||||
val webOrigin = OriginHelper.getWebOrigin(callingAppInfo, assets)
|
|
||||||
val apkSigningCertificate =
|
|
||||||
callingAppInfo.signingInfo.apkContentsSigners.getOrNull(0)?.toByteArray()
|
|
||||||
|
|
||||||
createPasskeyWithParameters(
|
|
||||||
relyingParty,
|
|
||||||
creationOptions.username,
|
|
||||||
creationOptions.userId,
|
|
||||||
database,
|
|
||||||
databaseTaskProvider,
|
|
||||||
keyTypeIdList,
|
|
||||||
challenge,
|
|
||||||
webOrigin,
|
|
||||||
apkSigningCertificate,
|
|
||||||
nodeId
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun createPasskeyWithParameters(
|
|
||||||
relyingParty: String,
|
|
||||||
username: String,
|
|
||||||
userHandle: ByteArray,
|
|
||||||
database: ContextualDatabase,
|
|
||||||
databaseTaskProvider: DatabaseTaskProvider,
|
|
||||||
keyTypeIdList: List<Long>,
|
|
||||||
challenge: ByteArray,
|
|
||||||
webOrigin: String?,
|
|
||||||
apkSigningCertificate: ByteArray?,
|
|
||||||
nodeId: String
|
|
||||||
) {
|
|
||||||
|
|
||||||
val isPrivilegedApp =
|
|
||||||
(webOrigin != null && webOrigin == OriginHelper.DEFAULT_PROTOCOL + relyingParty)
|
|
||||||
Log.d(this::class.java.simpleName, "isPrivilegedApp = $isPrivilegedApp")
|
|
||||||
|
|
||||||
if (!isPrivilegedApp) {
|
|
||||||
val isValid =
|
|
||||||
AppRelyingPartyRelation.isRelationValid(relyingParty, apkSigningCertificate)
|
|
||||||
if (!isValid) {
|
|
||||||
throw CreateCredentialUnknownException(
|
|
||||||
"could not verify relation between app " +
|
|
||||||
"and relyingParty $relyingParty"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val credentialId = KeePassDXRandom.generateCredentialId()
|
|
||||||
|
|
||||||
val (keyPair, keyTypeId) = Signature.generateKeyPair(keyTypeIdList)
|
|
||||||
?: throw CreateCredentialUnknownException("no known public key type found")
|
|
||||||
val privateKeyPem = Signature.convertPrivateKeyToPem(keyPair.private)
|
|
||||||
|
|
||||||
if (IntentHelper.isPlaceholderNodeId(nodeId)) {
|
|
||||||
// create new entry in database
|
|
||||||
|
|
||||||
val displayName = "$relyingParty (Passkey)"
|
|
||||||
val newPasskey = Passkey(
|
|
||||||
nodeId = "", // created by the database
|
|
||||||
username = username,
|
|
||||||
displayName = displayName,
|
|
||||||
privateKeyPem = privateKeyPem,
|
|
||||||
credId = Base64Helper.b64Encode(credentialId),
|
|
||||||
userHandle = Base64Helper.b64Encode(userHandle),
|
|
||||||
relyingParty = relyingParty,
|
|
||||||
databaseEntry = null
|
|
||||||
)
|
|
||||||
|
|
||||||
DatabaseHelper.saveNewEntry(database, databaseTaskProvider, newPasskey)
|
|
||||||
} else {
|
|
||||||
// update an existing entry in database
|
|
||||||
val oldPasskey = DatabaseHelper.searchPassKeyByNodeId(database, nodeId)
|
|
||||||
?: throw GetCredentialUnknownException("no passkey with nodeId $nodeId found")
|
|
||||||
|
|
||||||
val updatedPasskey = Passkey(
|
|
||||||
nodeId = "", // unchanged
|
|
||||||
username = username,
|
|
||||||
displayName = oldPasskey.displayName,
|
|
||||||
privateKeyPem = privateKeyPem,
|
|
||||||
credId = Base64Helper.b64Encode(credentialId),
|
|
||||||
userHandle = Base64Helper.b64Encode(userHandle),
|
|
||||||
relyingParty = relyingParty,
|
|
||||||
databaseEntry = oldPasskey.databaseEntry
|
|
||||||
)
|
|
||||||
|
|
||||||
DatabaseHelper.updateEntry(database, databaseTaskProvider, updatedPasskey)
|
|
||||||
}
|
|
||||||
|
|
||||||
val publicKeyEncoded = Signature.convertPublicKey(keyPair.public, keyTypeId)
|
|
||||||
|
|
||||||
val publicKeyMap = Signature.convertPublicKeyToMap(keyPair.public, keyTypeId)
|
|
||||||
val publicKeyCbor = JsonHelper.generateCborFromMap(publicKeyMap!!)
|
|
||||||
|
|
||||||
val authData = JsonHelper.generateAuthDataForCreate(
|
|
||||||
userPresent = true,
|
|
||||||
userVerified = true,
|
|
||||||
backupEligibility = true,
|
|
||||||
backupState = true,
|
|
||||||
rpId = relyingParty.toByteArray(),
|
|
||||||
credentialId = credentialId,
|
|
||||||
credentialPublicKey = publicKeyCbor
|
|
||||||
)
|
|
||||||
|
|
||||||
val attestationObject = JsonHelper.generateAttestationObject(authData)
|
|
||||||
|
|
||||||
val clientJson: String
|
|
||||||
if (isPrivilegedApp) {
|
|
||||||
clientJson = JsonHelper.generateClientDataJsonPrivileged()
|
|
||||||
} else {
|
|
||||||
val origin = OriginHelper.DEFAULT_PROTOCOL + relyingParty
|
|
||||||
clientJson = JsonHelper.generateClientDataJsonNonPrivileged(
|
|
||||||
challenge,
|
|
||||||
origin,
|
|
||||||
packageName,
|
|
||||||
isCrossOriginAdded = true,
|
|
||||||
isGet = false
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
val responseJson = JsonHelper.createAuthenticatorAttestationResponseJSON(
|
|
||||||
credentialId,
|
|
||||||
clientJson,
|
|
||||||
attestationObject,
|
|
||||||
publicKeyEncoded!!,
|
|
||||||
authData,
|
|
||||||
keyTypeId
|
|
||||||
)
|
|
||||||
|
|
||||||
// log only the length to prevent logging sensitive information
|
|
||||||
Log.d(javaClass.simpleName, "responseJson with length ${responseJson.length} created")
|
|
||||||
val createPublicKeyCredResponse = CreatePublicKeyCredentialResponse(responseJson)
|
|
||||||
|
|
||||||
val resultOfActivity = Intent()
|
|
||||||
|
|
||||||
PendingIntentHandler.setCreateCredentialResponse(
|
|
||||||
resultOfActivity, createPublicKeyCredResponse
|
|
||||||
)
|
|
||||||
setResult(Activity.RESULT_OK, resultOfActivity)
|
|
||||||
finish()
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -17,23 +17,25 @@
|
|||||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.activities
|
package com.kunzisoft.keepass.credentialprovider.activity
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.Uri
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
|
import androidx.core.net.toUri
|
||||||
import com.kunzisoft.keepass.R
|
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.DatabaseModeActivity
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.magikeyboard.MagikeyboardService
|
||||||
import com.kunzisoft.keepass.database.ContextualDatabase
|
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||||
import com.kunzisoft.keepass.database.helper.SearchHelper
|
import com.kunzisoft.keepass.database.helper.SearchHelper
|
||||||
import com.kunzisoft.keepass.magikeyboard.MagikeyboardService
|
|
||||||
import com.kunzisoft.keepass.model.SearchInfo
|
import com.kunzisoft.keepass.model.SearchInfo
|
||||||
import com.kunzisoft.keepass.otp.OtpEntryFields
|
import com.kunzisoft.keepass.otp.OtpEntryFields
|
||||||
import com.kunzisoft.keepass.utils.KeyboardUtil.isKeyboardActivatedInSettings
|
import com.kunzisoft.keepass.utils.KeyboardUtil.isKeyboardActivatedInSettings
|
||||||
import com.kunzisoft.keepass.utils.getParcelableCompat
|
|
||||||
import com.kunzisoft.keepass.utils.WebDomain
|
import com.kunzisoft.keepass.utils.WebDomain
|
||||||
|
import com.kunzisoft.keepass.utils.getParcelableCompat
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Activity to search or select entry in database,
|
* Activity to search or select entry in database,
|
||||||
@@ -73,7 +75,7 @@ class EntrySelectionLauncherActivity : DatabaseModeActivity() {
|
|||||||
if (OtpEntryFields.isOTPUri(extra))
|
if (OtpEntryFields.isOTPUri(extra))
|
||||||
otpString = extra
|
otpString = extra
|
||||||
else
|
else
|
||||||
sharedWebDomain = Uri.parse(extra).host
|
sharedWebDomain = extra.toUri().host
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
launchSelection(database, sharedWebDomain, otpString)
|
launchSelection(database, sharedWebDomain, otpString)
|
||||||
@@ -121,87 +123,105 @@ class EntrySelectionLauncherActivity : DatabaseModeActivity() {
|
|||||||
|
|
||||||
// If database is open
|
// If database is open
|
||||||
val readOnly = database?.isReadOnly != false
|
val readOnly = database?.isReadOnly != false
|
||||||
SearchHelper.checkAutoSearchInfo(this,
|
SearchHelper.checkAutoSearchInfo(
|
||||||
database,
|
context = this,
|
||||||
searchInfo,
|
database = database,
|
||||||
{ openedDatabase, items ->
|
searchInfo = searchInfo,
|
||||||
// Items found
|
onItemsFound = { openedDatabase, items ->
|
||||||
if (searchInfo.otpString != null) {
|
// Items found
|
||||||
if (!readOnly) {
|
if (searchInfo.otpString != null) {
|
||||||
GroupActivity.launchForSaveResult(
|
if (!readOnly) {
|
||||||
|
GroupActivity.launchForSaveResult(
|
||||||
|
this,
|
||||||
|
openedDatabase,
|
||||||
|
searchInfo,
|
||||||
|
false
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Toast.makeText(applicationContext,
|
||||||
|
R.string.autofill_read_only_save,
|
||||||
|
Toast.LENGTH_LONG)
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
} else if (searchShareForMagikeyboard) {
|
||||||
|
MagikeyboardService.performSelection(
|
||||||
|
items,
|
||||||
|
{ entryInfo ->
|
||||||
|
// Automatically populate keyboard
|
||||||
|
MagikeyboardService.populateKeyboardAndMoveAppToBackground(
|
||||||
|
this,
|
||||||
|
entryInfo
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{ autoSearch ->
|
||||||
|
GroupActivity.launchForKeyboardSelectionResult(
|
||||||
this,
|
this,
|
||||||
openedDatabase,
|
openedDatabase,
|
||||||
searchInfo,
|
searchInfo,
|
||||||
false)
|
autoSearch
|
||||||
} else {
|
)
|
||||||
Toast.makeText(applicationContext,
|
|
||||||
R.string.autofill_read_only_save,
|
|
||||||
Toast.LENGTH_LONG)
|
|
||||||
.show()
|
|
||||||
}
|
}
|
||||||
} else if (searchShareForMagikeyboard) {
|
)
|
||||||
MagikeyboardService.performSelection(
|
} else {
|
||||||
items,
|
GroupActivity.launchForSearchResult(
|
||||||
{ entryInfo ->
|
this,
|
||||||
// Automatically populate keyboard
|
openedDatabase,
|
||||||
MagikeyboardService.populateKeyboardAndMoveAppToBackground(
|
searchInfo,
|
||||||
this,
|
true
|
||||||
entryInfo
|
)
|
||||||
)
|
}
|
||||||
},
|
},
|
||||||
{ autoSearch ->
|
onItemNotFound = { openedDatabase ->
|
||||||
GroupActivity.launchForKeyboardSelectionResult(this,
|
// Show the database UI to select the entry
|
||||||
openedDatabase,
|
if (searchInfo.otpString != null) {
|
||||||
searchInfo,
|
if (!readOnly) {
|
||||||
autoSearch)
|
GroupActivity.launchForSaveResult(
|
||||||
}
|
this,
|
||||||
|
openedDatabase,
|
||||||
|
searchInfo,
|
||||||
|
false
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
GroupActivity.launchForSearchResult(this,
|
Toast.makeText(applicationContext,
|
||||||
openedDatabase,
|
R.string.autofill_read_only_save,
|
||||||
searchInfo,
|
Toast.LENGTH_LONG)
|
||||||
true)
|
.show()
|
||||||
}
|
|
||||||
},
|
|
||||||
{ openedDatabase ->
|
|
||||||
// Show the database UI to select the entry
|
|
||||||
if (searchInfo.otpString != null) {
|
|
||||||
if (!readOnly) {
|
|
||||||
GroupActivity.launchForSaveResult(this,
|
|
||||||
openedDatabase,
|
|
||||||
searchInfo,
|
|
||||||
false)
|
|
||||||
} else {
|
|
||||||
Toast.makeText(applicationContext,
|
|
||||||
R.string.autofill_read_only_save,
|
|
||||||
Toast.LENGTH_LONG)
|
|
||||||
.show()
|
|
||||||
}
|
|
||||||
} else if (searchShareForMagikeyboard) {
|
|
||||||
GroupActivity.launchForKeyboardSelectionResult(this,
|
|
||||||
openedDatabase,
|
|
||||||
searchInfo,
|
|
||||||
false)
|
|
||||||
} else {
|
|
||||||
GroupActivity.launchForSearchResult(this,
|
|
||||||
openedDatabase,
|
|
||||||
searchInfo,
|
|
||||||
false)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
// If database not open
|
|
||||||
if (searchInfo.otpString != null) {
|
|
||||||
FileDatabaseSelectActivity.launchForSaveResult(this,
|
|
||||||
searchInfo)
|
|
||||||
} else if (searchShareForMagikeyboard) {
|
|
||||||
FileDatabaseSelectActivity.launchForKeyboardSelectionResult(this,
|
|
||||||
searchInfo)
|
|
||||||
} else {
|
|
||||||
FileDatabaseSelectActivity.launchForSearchResult(this,
|
|
||||||
searchInfo)
|
|
||||||
}
|
}
|
||||||
|
} else if (searchShareForMagikeyboard) {
|
||||||
|
GroupActivity.launchForKeyboardSelectionResult(
|
||||||
|
this,
|
||||||
|
openedDatabase,
|
||||||
|
searchInfo,
|
||||||
|
false
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
GroupActivity.launchForSearchResult(
|
||||||
|
this,
|
||||||
|
openedDatabase,
|
||||||
|
searchInfo,
|
||||||
|
false
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
onDatabaseClosed = {
|
||||||
|
// If database not open
|
||||||
|
if (searchInfo.otpString != null) {
|
||||||
|
FileDatabaseSelectActivity.launchForSaveResult(
|
||||||
|
this,
|
||||||
|
searchInfo
|
||||||
|
)
|
||||||
|
} else if (searchShareForMagikeyboard) {
|
||||||
|
FileDatabaseSelectActivity.launchForKeyboardSelectionResult(
|
||||||
|
this,
|
||||||
|
searchInfo
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
FileDatabaseSelectActivity.launchForSearchResult(
|
||||||
|
this,
|
||||||
|
searchInfo
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,348 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2025 Jeremy Jamet / Kunzisoft.
|
||||||
|
*
|
||||||
|
* This file is part of KeePassDX.
|
||||||
|
*
|
||||||
|
* KeePassDX is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* KeePassDX is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
package com.kunzisoft.keepass.credentialprovider.activity
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.app.PendingIntent
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Build
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.activity.result.ActivityResultLauncher
|
||||||
|
import androidx.annotation.RequiresApi
|
||||||
|
import androidx.credentials.GetCredentialResponse
|
||||||
|
import androidx.credentials.exceptions.GetCredentialUnknownException
|
||||||
|
import androidx.credentials.provider.PendingIntentHandler
|
||||||
|
import com.kunzisoft.keepass.activities.FileDatabaseSelectActivity
|
||||||
|
import com.kunzisoft.keepass.activities.GroupActivity
|
||||||
|
import com.kunzisoft.keepass.activities.legacy.DatabaseModeActivity
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.addSpecialMode
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.buildActivityResultLauncher
|
||||||
|
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.util.OriginHelper
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.addAuthCode
|
||||||
|
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.removePasskey
|
||||||
|
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.retrievePasskeyCreationComponent
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.retrievePasskeyCreationRequestParameters
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.retrievePasskeyUsageRequestParameters
|
||||||
|
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.EntryInfoPasskey.getPasskey
|
||||||
|
import com.kunzisoft.keepass.model.Passkey
|
||||||
|
import com.kunzisoft.keepass.model.RegisterInfo
|
||||||
|
import com.kunzisoft.keepass.model.SearchInfo
|
||||||
|
import com.kunzisoft.keepass.utils.getParcelableExtraCompat
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
|
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
|
||||||
|
class PasskeyLauncherActivity : DatabaseModeActivity() {
|
||||||
|
|
||||||
|
private var mUsageParameters: PublicKeyCredentialUsageParameters? = null
|
||||||
|
private var mCreationParameters: PublicKeyCredentialCreationParameters? = null
|
||||||
|
private var mPasskey: Passkey? = null
|
||||||
|
|
||||||
|
private var mPasskeySelectionActivityResultLauncher: ActivityResultLauncher<Intent>? =
|
||||||
|
this.buildActivityResultLauncher(
|
||||||
|
lockDatabase = true,
|
||||||
|
dataTransformation = { intent ->
|
||||||
|
Log.d(TAG, "Passkey selection result")
|
||||||
|
val passkey = intent?.retrievePasskey()
|
||||||
|
intent?.removePasskey()
|
||||||
|
// Build a new formatted response from the selection response
|
||||||
|
val responseIntent = Intent()
|
||||||
|
passkey?.let {
|
||||||
|
mUsageParameters?.let { usageParameters ->
|
||||||
|
PendingIntentHandler.setGetCredentialResponse(
|
||||||
|
responseIntent,
|
||||||
|
GetCredentialResponse(
|
||||||
|
buildPasskeyPublicKeyCredential(
|
||||||
|
usageParameters = usageParameters,
|
||||||
|
passkey = passkey
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
} ?: run {
|
||||||
|
Log.e(TAG, "Unable to return passkey, usage parameters are empty")
|
||||||
|
}
|
||||||
|
} ?: run {
|
||||||
|
Log.e(TAG, "Unable to get the passkey for response")
|
||||||
|
}
|
||||||
|
// Return the response
|
||||||
|
responseIntent
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
private var mPasskeyRegistrationActivityResultLauncher: ActivityResultLauncher<Intent>? =
|
||||||
|
this.buildActivityResultLauncher(
|
||||||
|
lockDatabase = true,
|
||||||
|
dataTransformation = { intent ->
|
||||||
|
Log.d(TAG, "Passkey registration result")
|
||||||
|
val passkey = intent?.retrievePasskey()
|
||||||
|
intent?.removePasskey()
|
||||||
|
// Build a new formatted response from the creation response
|
||||||
|
val responseIntent = Intent()
|
||||||
|
// If registered passkey is the same as the one we want to validate,
|
||||||
|
if (mPasskey == passkey) {
|
||||||
|
mCreationParameters?.let {
|
||||||
|
PendingIntentHandler.setCreateCredentialResponse(
|
||||||
|
intent = responseIntent,
|
||||||
|
response = buildCreatePublicKeyCredentialResponse(
|
||||||
|
packageName = packageName,
|
||||||
|
publicKeyCredentialCreationParameters = it
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
responseIntent
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
override fun applyCustomStyle(): Boolean {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun finishActivityIfReloadRequested(): Boolean {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
||||||
|
super.onDatabaseRetrieved(database)
|
||||||
|
|
||||||
|
// TODO nodeId Really useful ? checkSecurity(intent, nodeId)
|
||||||
|
EntrySelectionHelper.retrieveSpecialModeFromIntent(intent).let { specialMode ->
|
||||||
|
val searchInfo = intent.retrieveSearchInfo() ?: SearchInfo()
|
||||||
|
when (specialMode) {
|
||||||
|
SpecialMode.SELECTION -> {
|
||||||
|
launchSelection(database, searchInfo)
|
||||||
|
}
|
||||||
|
SpecialMode.REGISTRATION -> {
|
||||||
|
launchRegistration(database, searchInfo)
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
Log.e(TAG, "Passkey launch mode not supported")
|
||||||
|
setResult(Activity.RESULT_CANCELED)
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun autoSelectPasskeyAndSetResult(
|
||||||
|
database: ContextualDatabase?,
|
||||||
|
nodeId: UUID
|
||||||
|
) {
|
||||||
|
mUsageParameters?.let { usageParameters ->
|
||||||
|
// To get the passkey from the database
|
||||||
|
val passkey = database
|
||||||
|
?.getEntryById(NodeIdUUID(nodeId))
|
||||||
|
?.getEntryInfo(database)
|
||||||
|
?.getPasskey()
|
||||||
|
?: throw GetCredentialUnknownException("no passkey with nodeId $nodeId found")
|
||||||
|
|
||||||
|
val result = Intent()
|
||||||
|
PendingIntentHandler.setGetCredentialResponse(
|
||||||
|
result,
|
||||||
|
GetCredentialResponse(
|
||||||
|
buildPasskeyPublicKeyCredential(
|
||||||
|
usageParameters = usageParameters,
|
||||||
|
passkey = passkey
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
setResult(RESULT_OK, result)
|
||||||
|
finish()
|
||||||
|
} ?: run {
|
||||||
|
Log.e(TAG, "Unable to auto select passkey, usage parameters are empty")
|
||||||
|
setResult(Activity.RESULT_CANCELED)
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun launchSelection(
|
||||||
|
database: ContextualDatabase?,
|
||||||
|
searchInfo: SearchInfo?
|
||||||
|
) {
|
||||||
|
Log.d(TAG, "Launch passkey selection")
|
||||||
|
retrievePasskeyUsageRequestParameters(this@PasskeyLauncherActivity, intent) { usageParameters ->
|
||||||
|
// Save the requested parameters
|
||||||
|
mUsageParameters = usageParameters
|
||||||
|
// Manage the passkey to use
|
||||||
|
intent.retrieveNodeId()?.let { nodeId ->
|
||||||
|
autoSelectPasskeyAndSetResult(database, nodeId)
|
||||||
|
} ?: 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 = true
|
||||||
|
)
|
||||||
|
},
|
||||||
|
onDatabaseClosed = {
|
||||||
|
Log.d(TAG, "Manual passkey selection in closed database")
|
||||||
|
FileDatabaseSelectActivity.launchForPasskeySelectionResult(
|
||||||
|
activity = this,
|
||||||
|
activityResultLauncher = mPasskeySelectionActivityResultLauncher,
|
||||||
|
searchInfo = searchInfo,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun autoRegisterPasskeyAndSetResult(
|
||||||
|
database: ContextualDatabase?,
|
||||||
|
nodeId: UUID
|
||||||
|
) {
|
||||||
|
// TODO Overwrite automatic selection
|
||||||
|
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(Activity.RESULT_CANCELED)
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun launchRegistration(
|
||||||
|
database: ContextualDatabase?,
|
||||||
|
searchInfo: SearchInfo
|
||||||
|
) {
|
||||||
|
Log.d(TAG, "Launch passkey registration")
|
||||||
|
PendingIntentHandler.retrieveProviderCreateCredentialRequest(intent)?.callingAppInfo?.let { callingAppInfo ->
|
||||||
|
retrievePasskeyCreationRequestParameters(
|
||||||
|
creationOptions = intent.retrievePasskeyCreationComponent(),
|
||||||
|
webOrigin = OriginHelper.getWebOrigin(callingAppInfo, assets),
|
||||||
|
apkSigningCertificate =
|
||||||
|
callingAppInfo
|
||||||
|
.signingInfo.apkContentsSigners
|
||||||
|
.getOrNull(0)?.toByteArray(),
|
||||||
|
passkeyCreated = { passkey, publicKeyCredentialParameters ->
|
||||||
|
// Save the requested parameters
|
||||||
|
mPasskey = passkey
|
||||||
|
mCreationParameters = publicKeyCredentialParameters
|
||||||
|
// Manage the passkey and create a register info
|
||||||
|
val registerInfo = RegisterInfo(
|
||||||
|
searchInfo = searchInfo,
|
||||||
|
username = null,
|
||||||
|
passkey = passkey
|
||||||
|
)
|
||||||
|
// If nodeId already provided
|
||||||
|
intent.retrieveNodeId()?.let { nodeId ->
|
||||||
|
autoRegisterPasskeyAndSetResult(database, nodeId)
|
||||||
|
} ?: run {
|
||||||
|
SearchHelper.checkAutoSearchInfo(
|
||||||
|
context = this,
|
||||||
|
database = database,
|
||||||
|
searchInfo = searchInfo,
|
||||||
|
onItemsFound = { openedDatabase, _ ->
|
||||||
|
Log.w(TAG, "Passkey found for registration, " +
|
||||||
|
"but launch manual registration for overwrite")
|
||||||
|
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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val TAG = PasskeyLauncherActivity::class.java.name
|
||||||
|
private const val EXTRA_SEARCH_INFO = "com.kunzisoft.keepass.extra.SEARCH_INFO"
|
||||||
|
|
||||||
|
fun Intent.retrieveSearchInfo(): SearchInfo? {
|
||||||
|
return this.getParcelableExtraCompat(EXTRA_SEARCH_INFO)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Intent.removeSearchInfo() {
|
||||||
|
return this.removeExtra(EXTRA_SEARCH_INFO)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getPendingIntent(
|
||||||
|
context: Context,
|
||||||
|
specialMode: SpecialMode,
|
||||||
|
searchInfo: SearchInfo? = null,
|
||||||
|
passkeyEntryNodeId: UUID? = null
|
||||||
|
): PendingIntent? {
|
||||||
|
return PendingIntent.getActivity(
|
||||||
|
context,
|
||||||
|
Math.random().toInt(),
|
||||||
|
Intent(context, PasskeyLauncherActivity::class.java).apply {
|
||||||
|
addSpecialMode(specialMode)
|
||||||
|
searchInfo?.let {
|
||||||
|
putExtra(EXTRA_SEARCH_INFO, searchInfo)
|
||||||
|
}
|
||||||
|
addAuthCode(passkeyEntryNodeId)
|
||||||
|
},
|
||||||
|
PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_CANCEL_CURRENT
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,236 +0,0 @@
|
|||||||
package com.kunzisoft.keepass.credentialprovider.activity
|
|
||||||
|
|
||||||
import android.content.Intent
|
|
||||||
import android.os.Build
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.util.Log
|
|
||||||
import androidx.annotation.RequiresApi
|
|
||||||
import androidx.biometric.BiometricManager
|
|
||||||
import androidx.biometric.BiometricPrompt
|
|
||||||
import androidx.credentials.GetCredentialResponse
|
|
||||||
import androidx.credentials.GetPublicKeyCredentialOption
|
|
||||||
import androidx.credentials.PublicKeyCredential
|
|
||||||
import androidx.credentials.exceptions.CreateCredentialUnknownException
|
|
||||||
import androidx.credentials.exceptions.GetCredentialUnknownException
|
|
||||||
import androidx.credentials.provider.PendingIntentHandler
|
|
||||||
import com.kunzisoft.asymmetric.Signature
|
|
||||||
import com.kunzisoft.keepass.R
|
|
||||||
import com.kunzisoft.keepass.activities.legacy.DatabaseActivity
|
|
||||||
import com.kunzisoft.keepass.credentialprovider.data.Passkey
|
|
||||||
import com.kunzisoft.keepass.credentialprovider.util.AppRelyingPartyRelation
|
|
||||||
import com.kunzisoft.keepass.credentialprovider.util.Base64Helper
|
|
||||||
import com.kunzisoft.keepass.credentialprovider.util.DatabaseHelper
|
|
||||||
import com.kunzisoft.keepass.credentialprovider.util.IntentHelper
|
|
||||||
import com.kunzisoft.keepass.credentialprovider.util.JsonHelper
|
|
||||||
import com.kunzisoft.keepass.credentialprovider.util.OriginHelper
|
|
||||||
import com.kunzisoft.keepass.credentialprovider.util.OriginHelper.Companion.DEFAULT_PROTOCOL
|
|
||||||
import com.kunzisoft.keepass.database.ContextualDatabase
|
|
||||||
|
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
|
|
||||||
class UsePasskeyActivity : DatabaseActivity() {
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
Log.d(javaClass.simpleName, "onCreate called")
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
|
||||||
Log.d(javaClass.simpleName, "onDatabaseRetrieved called")
|
|
||||||
super.onDatabaseRetrieved(database)
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (database == null) {
|
|
||||||
throw CreateCredentialUnknownException("retrievedDatabase is null, maybe database is locked")
|
|
||||||
}
|
|
||||||
|
|
||||||
if (intent == null) {
|
|
||||||
throw CreateCredentialUnknownException("intent is null")
|
|
||||||
}
|
|
||||||
usePasskeyAfterPrompt(database, intent)
|
|
||||||
} catch (e: CreateCredentialUnknownException) {
|
|
||||||
Log.e(this::class.java.simpleName, "CreateCredentialUnknownException was thrown", e)
|
|
||||||
setResult(RESULT_CANCELED)
|
|
||||||
finish()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(this::class.java.simpleName, "other exception was thrown", e)
|
|
||||||
setResult(RESULT_CANCELED)
|
|
||||||
finish()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun usePasskeyAfterPrompt(
|
|
||||||
database: ContextualDatabase,
|
|
||||||
intent: Intent
|
|
||||||
) {
|
|
||||||
val request = PendingIntentHandler.retrieveProviderGetCredentialRequest(intent)
|
|
||||||
?: throw CreateCredentialUnknownException("could not retrieve request from intent")
|
|
||||||
|
|
||||||
if (request.credentialOptions.size != 1) {
|
|
||||||
throw GetCredentialUnknownException("not exact one credentialOption")
|
|
||||||
}
|
|
||||||
|
|
||||||
if (request.credentialOptions[0] !is GetPublicKeyCredentialOption) {
|
|
||||||
throw CreateCredentialUnknownException("credentialOptions is of wrong type: ${request.credentialOptions[0]}")
|
|
||||||
}
|
|
||||||
|
|
||||||
val credentialOption = request.credentialOptions[0] as GetPublicKeyCredentialOption
|
|
||||||
val clientDataHash = credentialOption.clientDataHash
|
|
||||||
|
|
||||||
val requestOptions = JsonHelper.parseJsonToRequestOptions(credentialOption.requestJson)
|
|
||||||
|
|
||||||
val relyingParty = requestOptions.relyingParty
|
|
||||||
val challenge = Base64Helper.b64Decode(requestOptions.challengeString)
|
|
||||||
val packageName = request.callingAppInfo.packageName
|
|
||||||
val webOrigin = OriginHelper.getWebOrigin(request.callingAppInfo, assets)
|
|
||||||
|
|
||||||
val isPrivilegedApp =
|
|
||||||
(webOrigin != null && webOrigin == DEFAULT_PROTOCOL + relyingParty && clientDataHash != null)
|
|
||||||
|
|
||||||
Log.d(javaClass.simpleName, "isPrivilegedApp = $isPrivilegedApp")
|
|
||||||
|
|
||||||
if (!isPrivilegedApp) {
|
|
||||||
val apkSigners = request.callingAppInfo.signingInfo.apkContentsSigners
|
|
||||||
val apkSigningCertificate = apkSigners.getOrNull(0)?.toByteArray()
|
|
||||||
val isValid =
|
|
||||||
AppRelyingPartyRelation.isRelationValid(relyingParty, apkSigningCertificate)
|
|
||||||
if (!isValid) {
|
|
||||||
throw CreateCredentialUnknownException(
|
|
||||||
"could not verify relation between app " +
|
|
||||||
"and relyingParty $relyingParty"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val nodeId = IntentHelper.getVerifiedNodeId(intent)
|
|
||||||
?: throw GetCredentialUnknownException("could not get verified nodeId from intent")
|
|
||||||
|
|
||||||
val passkey = DatabaseHelper.searchPassKeyByNodeId(database, nodeId)
|
|
||||||
?: throw GetCredentialUnknownException("no passkey with nodeId $nodeId found")
|
|
||||||
|
|
||||||
usePasskeyAfterPromptWithParameters(
|
|
||||||
relyingParty,
|
|
||||||
packageName,
|
|
||||||
clientDataHash,
|
|
||||||
isPrivilegedApp,
|
|
||||||
challenge,
|
|
||||||
passkey
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun usePasskeyAfterPromptWithParameters(
|
|
||||||
relyingParty: String,
|
|
||||||
packageName: String,
|
|
||||||
clientDataHash: ByteArray?,
|
|
||||||
isPrivilegedApp: Boolean,
|
|
||||||
challenge: ByteArray,
|
|
||||||
passkey: Passkey
|
|
||||||
) {
|
|
||||||
val biometricPrompt = BiometricPrompt(
|
|
||||||
this,
|
|
||||||
object : BiometricPrompt.AuthenticationCallback() {
|
|
||||||
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
|
|
||||||
super.onAuthenticationError(errorCode, errString)
|
|
||||||
throw GetCredentialUnknownException("authentication error: errorCode = $errorCode, errString = $errString")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onAuthenticationFailed() {
|
|
||||||
super.onAuthenticationFailed()
|
|
||||||
throw GetCredentialUnknownException("authentication failed")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
|
|
||||||
super.onAuthenticationSucceeded(result)
|
|
||||||
createResponse(
|
|
||||||
relyingParty,
|
|
||||||
packageName,
|
|
||||||
clientDataHash,
|
|
||||||
isPrivilegedApp,
|
|
||||||
challenge,
|
|
||||||
passkey
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
val title = getString(R.string.passkey_usage_biometric_prompt_title)
|
|
||||||
val subtitle = getString(R.string.passkey_usage_biometric_prompt_subtitle, relyingParty)
|
|
||||||
val negativeButtonText =
|
|
||||||
getString(R.string.passkey_usage_biometric_prompt_negative_button_text)
|
|
||||||
val promptInfo = BiometricPrompt.PromptInfo.Builder()
|
|
||||||
.setTitle(title)
|
|
||||||
.setSubtitle(subtitle)
|
|
||||||
.setAllowedAuthenticators(BiometricManager.Authenticators.BIOMETRIC_STRONG)
|
|
||||||
.setNegativeButtonText(negativeButtonText)
|
|
||||||
.build()
|
|
||||||
biometricPrompt.authenticate(promptInfo)
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun createResponse(
|
|
||||||
relyingParty: String,
|
|
||||||
packageName: String,
|
|
||||||
clientDataHash: ByteArray?,
|
|
||||||
isPrivilegedApp: Boolean,
|
|
||||||
challenge: ByteArray,
|
|
||||||
passkey: Passkey
|
|
||||||
) {
|
|
||||||
|
|
||||||
// https://www.w3.org/TR/webauthn-3/#authdata-flags
|
|
||||||
val userPresent = true
|
|
||||||
val userVerified = true
|
|
||||||
val backupEligibility = true
|
|
||||||
val backupState = true
|
|
||||||
|
|
||||||
val authenticatorData = JsonHelper.generateAuthDataForUsage(
|
|
||||||
relyingParty.toByteArray(),
|
|
||||||
userPresent,
|
|
||||||
userVerified,
|
|
||||||
backupEligibility,
|
|
||||||
backupState
|
|
||||||
)
|
|
||||||
|
|
||||||
val clientDataJson: String
|
|
||||||
val dataToSign: ByteArray
|
|
||||||
if (isPrivilegedApp) {
|
|
||||||
clientDataJson = JsonHelper.generateClientDataJsonPrivileged()
|
|
||||||
dataToSign =
|
|
||||||
JsonHelper.generateDataToSignPrivileged(clientDataHash!!, authenticatorData)
|
|
||||||
} else {
|
|
||||||
val origin = DEFAULT_PROTOCOL + relyingParty
|
|
||||||
clientDataJson = JsonHelper.generateClientDataJsonNonPrivileged(
|
|
||||||
challenge,
|
|
||||||
origin,
|
|
||||||
packageName,
|
|
||||||
isGet = true,
|
|
||||||
isCrossOriginAdded = false
|
|
||||||
)
|
|
||||||
dataToSign =
|
|
||||||
JsonHelper.generateDataTosSignNonPrivileged(clientDataJson, authenticatorData)
|
|
||||||
}
|
|
||||||
|
|
||||||
val signature = Signature.sign(passkey.privateKeyPem, dataToSign)
|
|
||||||
?: throw GetCredentialUnknownException("signing failed")
|
|
||||||
|
|
||||||
val getCredentialResponse =
|
|
||||||
JsonHelper.generateGetCredentialResponse(
|
|
||||||
clientDataJson.toByteArray(),
|
|
||||||
authenticatorData,
|
|
||||||
signature,
|
|
||||||
passkey.userHandle,
|
|
||||||
passkey.credId
|
|
||||||
)
|
|
||||||
|
|
||||||
val result = Intent()
|
|
||||||
val passkeyCredential = PublicKeyCredential(getCredentialResponse)
|
|
||||||
PendingIntentHandler.setGetCredentialResponse(
|
|
||||||
result, GetCredentialResponse(passkeyCredential)
|
|
||||||
)
|
|
||||||
setResult(RESULT_OK, result)
|
|
||||||
finish()
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.kunzisoft.keepass.autofill
|
package com.kunzisoft.keepass.credentialprovider.autofill
|
||||||
|
|
||||||
import android.app.assist.AssistStructure
|
import android.app.assist.AssistStructure
|
||||||
|
|
||||||
@@ -17,7 +17,7 @@
|
|||||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.autofill
|
package com.kunzisoft.keepass.credentialprovider.autofill
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
@@ -40,17 +40,13 @@ import android.view.autofill.AutofillValue
|
|||||||
import android.widget.RemoteViews
|
import android.widget.RemoteViews
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import android.widget.inline.InlinePresentationSpec
|
import android.widget.inline.InlinePresentationSpec
|
||||||
import androidx.activity.result.ActivityResultLauncher
|
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
|
||||||
import androidx.autofill.inline.UiVersions
|
import androidx.autofill.inline.UiVersions
|
||||||
import androidx.autofill.inline.v1.InlineSuggestionUi
|
import androidx.autofill.inline.v1.InlineSuggestionUi
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.activities.AutofillLauncherActivity
|
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.buildIcon
|
||||||
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
import com.kunzisoft.keepass.credentialprovider.activity.AutofillLauncherActivity
|
||||||
import com.kunzisoft.keepass.activities.helpers.SpecialMode
|
|
||||||
import com.kunzisoft.keepass.database.ContextualDatabase
|
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||||
import com.kunzisoft.keepass.database.element.icon.IconImage
|
import com.kunzisoft.keepass.database.element.icon.IconImage
|
||||||
import com.kunzisoft.keepass.database.element.template.TemplateField
|
import com.kunzisoft.keepass.database.element.template.TemplateField
|
||||||
@@ -58,7 +54,6 @@ import com.kunzisoft.keepass.model.EntryInfo
|
|||||||
import com.kunzisoft.keepass.model.SearchInfo
|
import com.kunzisoft.keepass.model.SearchInfo
|
||||||
import com.kunzisoft.keepass.settings.AutofillSettingsActivity
|
import com.kunzisoft.keepass.settings.AutofillSettingsActivity
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
import com.kunzisoft.keepass.utils.LOCK_ACTION
|
|
||||||
import com.kunzisoft.keepass.utils.getParcelableExtraCompat
|
import com.kunzisoft.keepass.utils.getParcelableExtraCompat
|
||||||
import kotlin.math.min
|
import kotlin.math.min
|
||||||
|
|
||||||
@@ -294,23 +289,6 @@ object AutofillHelper {
|
|||||||
return dataset
|
return dataset
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Method to assign a drawable to a new icon from a database icon
|
|
||||||
*/
|
|
||||||
private fun buildIconFromEntry(context: Context,
|
|
||||||
database: ContextualDatabase,
|
|
||||||
entryInfo: EntryInfo): Icon? {
|
|
||||||
try {
|
|
||||||
database.iconDrawableFactory.getBitmapFromIcon(context,
|
|
||||||
entryInfo.icon, ContextCompat.getColor(context, R.color.green))?.let { bitmap ->
|
|
||||||
return Icon.createWithBitmap(bitmap)
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(RemoteViews::class.java.name, "Unable to assign icon in remote view", e)
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressLint("RestrictedApi")
|
@SuppressLint("RestrictedApi")
|
||||||
@RequiresApi(Build.VERSION_CODES.R)
|
@RequiresApi(Build.VERSION_CODES.R)
|
||||||
private fun buildInlinePresentationForEntry(context: Context,
|
private fun buildInlinePresentationForEntry(context: Context,
|
||||||
@@ -353,7 +331,7 @@ object AutofillHelper {
|
|||||||
Icon.createWithResource(context, R.mipmap.ic_launcher_round).apply {
|
Icon.createWithResource(context, R.mipmap.ic_launcher_round).apply {
|
||||||
setTintBlendMode(BlendMode.DST)
|
setTintBlendMode(BlendMode.DST)
|
||||||
})
|
})
|
||||||
buildIconFromEntry(context, database, entryInfo)?.let { icon ->
|
entryInfo.buildIcon(context, database)?.let { icon ->
|
||||||
setEndIcon(icon.apply {
|
setEndIcon(icon.apply {
|
||||||
setTintBlendMode(BlendMode.DST)
|
setTintBlendMode(BlendMode.DST)
|
||||||
})
|
})
|
||||||
@@ -534,7 +512,9 @@ object AutofillHelper {
|
|||||||
StructureParser(structure).parse()?.let { result ->
|
StructureParser(structure).parse()?.let { result ->
|
||||||
// New Response
|
// New Response
|
||||||
val response = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
val response = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||||
val compatInlineSuggestionsRequest = activity.intent?.getParcelableExtraCompat<CompatInlineSuggestionsRequest>(EXTRA_INLINE_SUGGESTIONS_REQUEST)
|
val compatInlineSuggestionsRequest = activity.intent?.getParcelableExtraCompat<CompatInlineSuggestionsRequest>(
|
||||||
|
EXTRA_INLINE_SUGGESTIONS_REQUEST
|
||||||
|
)
|
||||||
if (compatInlineSuggestionsRequest != null) {
|
if (compatInlineSuggestionsRequest != null) {
|
||||||
Toast.makeText(activity.applicationContext, R.string.autofill_inline_suggestions_keyboard, Toast.LENGTH_SHORT).show()
|
Toast.makeText(activity.applicationContext, R.string.autofill_inline_suggestions_keyboard, Toast.LENGTH_SHORT).show()
|
||||||
}
|
}
|
||||||
@@ -558,45 +538,14 @@ object AutofillHelper {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun buildActivityResultLauncher(activity: AppCompatActivity,
|
fun Intent.addAutofillComponent(context: Context, autofillComponent: AutofillComponent) {
|
||||||
lockDatabase: Boolean = false): ActivityResultLauncher<Intent> {
|
this.putExtra(EXTRA_ASSIST_STRUCTURE, autofillComponent.assistStructure)
|
||||||
return activity.registerForActivityResult(
|
|
||||||
ActivityResultContracts.StartActivityForResult()
|
|
||||||
) {
|
|
||||||
// Utility method to loop and close each activity with return data
|
|
||||||
if (it.resultCode == Activity.RESULT_OK) {
|
|
||||||
activity.setResult(it.resultCode, it.data)
|
|
||||||
}
|
|
||||||
if (it.resultCode == Activity.RESULT_CANCELED) {
|
|
||||||
activity.setResult(Activity.RESULT_CANCELED)
|
|
||||||
}
|
|
||||||
activity.finish()
|
|
||||||
|
|
||||||
if (lockDatabase && PreferencesUtil.isAutofillCloseDatabaseEnable(activity)) {
|
|
||||||
// Close the database
|
|
||||||
activity.sendBroadcast(Intent(LOCK_ACTION))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Utility method to start an activity with an Autofill for result
|
|
||||||
*/
|
|
||||||
fun startActivityForAutofillResult(activity: AppCompatActivity,
|
|
||||||
intent: Intent,
|
|
||||||
activityResultLauncher: ActivityResultLauncher<Intent>?,
|
|
||||||
autofillComponent: AutofillComponent,
|
|
||||||
searchInfo: SearchInfo?) {
|
|
||||||
EntrySelectionHelper.addSpecialModeInIntent(intent, SpecialMode.SELECTION)
|
|
||||||
intent.putExtra(EXTRA_ASSIST_STRUCTURE, autofillComponent.assistStructure)
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
|
||||||
&& PreferencesUtil.isAutofillInlineSuggestionsEnable(activity)) {
|
&& PreferencesUtil.isAutofillInlineSuggestionsEnable(context)) {
|
||||||
autofillComponent.compatInlineSuggestionsRequest?.let {
|
autofillComponent.compatInlineSuggestionsRequest?.let {
|
||||||
intent.putExtra(EXTRA_INLINE_SUGGESTIONS_REQUEST, it)
|
this.putExtra(EXTRA_INLINE_SUGGESTIONS_REQUEST, it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
EntrySelectionHelper.addSearchInfoInIntent(intent, searchInfo)
|
|
||||||
activityResultLauncher?.launch(intent)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private val TAG = AutofillHelper::class.java.name
|
private val TAG = AutofillHelper::class.java.name
|
||||||
@@ -17,7 +17,7 @@
|
|||||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.autofill
|
package com.kunzisoft.keepass.credentialprovider.autofill
|
||||||
|
|
||||||
import android.annotation.TargetApi
|
import android.annotation.TargetApi
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
@@ -17,7 +17,7 @@
|
|||||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.autofill
|
package com.kunzisoft.keepass.credentialprovider.autofill
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.app.PendingIntent
|
import android.app.PendingIntent
|
||||||
@@ -43,8 +43,8 @@ import androidx.annotation.RequiresApi
|
|||||||
import androidx.autofill.inline.UiVersions
|
import androidx.autofill.inline.UiVersions
|
||||||
import androidx.autofill.inline.v1.InlineSuggestionUi
|
import androidx.autofill.inline.v1.InlineSuggestionUi
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.activities.AutofillLauncherActivity
|
import com.kunzisoft.keepass.credentialprovider.activity.AutofillLauncherActivity
|
||||||
import com.kunzisoft.keepass.autofill.StructureParser.Companion.APPLICATION_ID_POPUP_WINDOW
|
import com.kunzisoft.keepass.credentialprovider.autofill.StructureParser.Companion.APPLICATION_ID_POPUP_WINDOW
|
||||||
import com.kunzisoft.keepass.database.ContextualDatabase
|
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||||
import com.kunzisoft.keepass.database.DatabaseTaskProvider
|
import com.kunzisoft.keepass.database.DatabaseTaskProvider
|
||||||
import com.kunzisoft.keepass.database.helper.SearchHelper
|
import com.kunzisoft.keepass.database.helper.SearchHelper
|
||||||
@@ -143,25 +143,28 @@ class KeeAutofillService : AutofillService() {
|
|||||||
parseResult: StructureParser.Result,
|
parseResult: StructureParser.Result,
|
||||||
inlineSuggestionsRequest: CompatInlineSuggestionsRequest?,
|
inlineSuggestionsRequest: CompatInlineSuggestionsRequest?,
|
||||||
callback: FillCallback) {
|
callback: FillCallback) {
|
||||||
SearchHelper.checkAutoSearchInfo(this,
|
SearchHelper.checkAutoSearchInfo(
|
||||||
database,
|
context = this,
|
||||||
searchInfo,
|
database = database,
|
||||||
{ openedDatabase, items ->
|
searchInfo = searchInfo,
|
||||||
callback.onSuccess(
|
onItemsFound = { openedDatabase, items ->
|
||||||
AutofillHelper.buildResponse(this, openedDatabase,
|
callback.onSuccess(
|
||||||
items, parseResult, inlineSuggestionsRequest)
|
AutofillHelper.buildResponse(
|
||||||
|
this, openedDatabase,
|
||||||
|
items, parseResult, inlineSuggestionsRequest
|
||||||
)
|
)
|
||||||
},
|
)
|
||||||
{ openedDatabase ->
|
},
|
||||||
// Show UI if no search result
|
onItemNotFound = { openedDatabase ->
|
||||||
showUIForEntrySelection(parseResult, openedDatabase,
|
// Show UI if no search result
|
||||||
searchInfo, inlineSuggestionsRequest, callback)
|
showUIForEntrySelection(parseResult, openedDatabase,
|
||||||
},
|
searchInfo, inlineSuggestionsRequest, callback)
|
||||||
{
|
},
|
||||||
// Show UI if database not open
|
onDatabaseClosed = {
|
||||||
showUIForEntrySelection(parseResult, null,
|
// Show UI if database not open
|
||||||
searchInfo, inlineSuggestionsRequest, callback)
|
showUIForEntrySelection(parseResult, null,
|
||||||
}
|
searchInfo, inlineSuggestionsRequest, callback)
|
||||||
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -385,19 +388,21 @@ class KeeAutofillService : AutofillService() {
|
|||||||
|
|
||||||
// Show UI to save data
|
// Show UI to save data
|
||||||
val registerInfo = RegisterInfo(
|
val registerInfo = RegisterInfo(
|
||||||
SearchInfo().apply {
|
searchInfo = SearchInfo().apply {
|
||||||
applicationId = parseResult.applicationId
|
applicationId = parseResult.applicationId
|
||||||
webDomain = parseResult.webDomain
|
webDomain = parseResult.webDomain
|
||||||
webScheme = parseResult.webScheme
|
webScheme = parseResult.webScheme
|
||||||
},
|
},
|
||||||
parseResult.usernameValue?.textValue?.toString(),
|
username = parseResult.usernameValue?.textValue?.toString(),
|
||||||
parseResult.passwordValue?.textValue?.toString(),
|
password = parseResult.passwordValue?.textValue?.toString(),
|
||||||
|
creditCard =
|
||||||
CreditCard(
|
CreditCard(
|
||||||
parseResult.creditCardHolder,
|
parseResult.creditCardHolder,
|
||||||
parseResult.creditCardNumber,
|
parseResult.creditCardNumber,
|
||||||
expiration,
|
expiration,
|
||||||
parseResult.cardVerificationValue
|
parseResult.cardVerificationValue
|
||||||
))
|
)
|
||||||
|
)
|
||||||
|
|
||||||
// TODO Callback in each activity #765
|
// TODO Callback in each activity #765
|
||||||
//if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
//if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||||
@@ -16,7 +16,7 @@
|
|||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.autofill
|
package com.kunzisoft.keepass.credentialprovider.autofill
|
||||||
|
|
||||||
import android.app.assist.AssistStructure
|
import android.app.assist.AssistStructure
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
package com.kunzisoft.keepass.credentialprovider.data
|
|
||||||
|
|
||||||
import com.kunzisoft.keepass.database.element.Entry
|
|
||||||
|
|
||||||
data class Passkey(
|
|
||||||
val nodeId: String,
|
|
||||||
val username: String,
|
|
||||||
val displayName: String,
|
|
||||||
val privateKeyPem: String,
|
|
||||||
val credId: String,
|
|
||||||
val userHandle: String,
|
|
||||||
val relyingParty: String,
|
|
||||||
val databaseEntry: Entry?
|
|
||||||
)
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
package com.kunzisoft.keepass.credentialprovider.data
|
|
||||||
|
|
||||||
data class PublicKeyCredentialCreationOptions(
|
|
||||||
val relyingParty: String,
|
|
||||||
val challenge: ByteArray,
|
|
||||||
val username: String,
|
|
||||||
val userId: ByteArray,
|
|
||||||
val keyTypeIdList: List<Long>
|
|
||||||
)
|
|
||||||
@@ -14,7 +14,7 @@
|
|||||||
* the License.
|
* the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package com.kunzisoft.keepass.magikeyboard;
|
package com.kunzisoft.keepass.credentialprovider.magikeyboard;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.res.Resources;
|
import android.content.res.Resources;
|
||||||
@@ -14,14 +14,14 @@
|
|||||||
* the License.
|
* the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package com.kunzisoft.keepass.magikeyboard;
|
package com.kunzisoft.keepass.credentialprovider.magikeyboard;
|
||||||
|
|
||||||
import static com.kunzisoft.keepass.magikeyboard.MagikeyboardService.KEY_BACK_KEYBOARD;
|
import static com.kunzisoft.keepass.credentialprovider.magikeyboard.MagikeyboardService.KEY_BACK_KEYBOARD;
|
||||||
import static com.kunzisoft.keepass.magikeyboard.MagikeyboardService.KEY_CHANGE_KEYBOARD;
|
import static com.kunzisoft.keepass.credentialprovider.magikeyboard.MagikeyboardService.KEY_CHANGE_KEYBOARD;
|
||||||
import static com.kunzisoft.keepass.magikeyboard.MagikeyboardService.KEY_ENTRY;
|
import static com.kunzisoft.keepass.credentialprovider.magikeyboard.MagikeyboardService.KEY_ENTRY;
|
||||||
import static com.kunzisoft.keepass.magikeyboard.MagikeyboardService.KEY_ENTRY_ALT;
|
import static com.kunzisoft.keepass.credentialprovider.magikeyboard.MagikeyboardService.KEY_ENTRY_ALT;
|
||||||
import static com.kunzisoft.keepass.magikeyboard.MagikeyboardService.KEY_OTP;
|
import static com.kunzisoft.keepass.credentialprovider.magikeyboard.MagikeyboardService.KEY_OTP;
|
||||||
import static com.kunzisoft.keepass.magikeyboard.MagikeyboardService.KEY_OTP_ALT;
|
import static com.kunzisoft.keepass.credentialprovider.magikeyboard.MagikeyboardService.KEY_OTP_ALT;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.res.TypedArray;
|
import android.content.res.TypedArray;
|
||||||
@@ -52,7 +52,7 @@ import android.widget.TextView;
|
|||||||
import androidx.annotation.RequiresApi;
|
import androidx.annotation.RequiresApi;
|
||||||
|
|
||||||
import com.kunzisoft.keepass.R;
|
import com.kunzisoft.keepass.R;
|
||||||
import com.kunzisoft.keepass.magikeyboard.Keyboard.Key;
|
import com.kunzisoft.keepass.credentialprovider.magikeyboard.Keyboard.Key;
|
||||||
|
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
@@ -18,7 +18,7 @@
|
|||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package com.kunzisoft.keepass.magikeyboard
|
package com.kunzisoft.keepass.credentialprovider.magikeyboard
|
||||||
|
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
@@ -41,8 +41,8 @@ import androidx.core.graphics.BlendModeCompat
|
|||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.activities.EntrySelectionLauncherActivity
|
import com.kunzisoft.keepass.credentialprovider.activity.EntrySelectionLauncherActivity
|
||||||
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper
|
||||||
import com.kunzisoft.keepass.adapters.FieldsAdapter
|
import com.kunzisoft.keepass.adapters.FieldsAdapter
|
||||||
import com.kunzisoft.keepass.database.ContextualDatabase
|
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||||
import com.kunzisoft.keepass.database.DatabaseTaskProvider
|
import com.kunzisoft.keepass.database.DatabaseTaskProvider
|
||||||
@@ -341,10 +341,11 @@ class MagikeyboardService : InputMethodService(), KeyboardView.OnKeyboardActionL
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun actionKeyEntry(searchInfo: SearchInfo? = null) {
|
private fun actionKeyEntry(searchInfo: SearchInfo? = null) {
|
||||||
SearchHelper.checkAutoSearchInfo(this,
|
SearchHelper.checkAutoSearchInfo(
|
||||||
mDatabase,
|
context = this,
|
||||||
searchInfo,
|
database = mDatabase,
|
||||||
{ _, items ->
|
searchInfo = searchInfo,
|
||||||
|
onItemsFound = { _, items ->
|
||||||
performSelection(
|
performSelection(
|
||||||
items,
|
items,
|
||||||
{
|
{
|
||||||
@@ -361,11 +362,11 @@ class MagikeyboardService : InputMethodService(), KeyboardView.OnKeyboardActionL
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
{
|
onItemNotFound = {
|
||||||
// Select if not found
|
// Select if not found
|
||||||
launchEntrySelection(searchInfo)
|
launchEntrySelection(searchInfo)
|
||||||
},
|
},
|
||||||
{
|
onDatabaseClosed = {
|
||||||
// Select if database not opened
|
// Select if database not opened
|
||||||
removeEntryInfo()
|
removeEntryInfo()
|
||||||
launchEntrySelection(searchInfo)
|
launchEntrySelection(searchInfo)
|
||||||
@@ -463,21 +464,18 @@ class MagikeyboardService : InputMethodService(), KeyboardView.OnKeyboardActionL
|
|||||||
fun performSelection(items: List<EntryInfo>,
|
fun performSelection(items: List<EntryInfo>,
|
||||||
actionPopulateKeyboard: (entryInfo: EntryInfo) -> Unit,
|
actionPopulateKeyboard: (entryInfo: EntryInfo) -> Unit,
|
||||||
actionEntrySelection: (autoSearch: Boolean) -> Unit) {
|
actionEntrySelection: (autoSearch: Boolean) -> Unit) {
|
||||||
if (items.size == 1) {
|
EntrySelectionHelper.performSelection(
|
||||||
val itemFound = items[0]
|
items = items,
|
||||||
if (entryUUID != itemFound.id) {
|
actionPopulateCredentialProvider = { itemFound ->
|
||||||
actionPopulateKeyboard.invoke(itemFound)
|
if (entryUUID != itemFound.id) {
|
||||||
} else {
|
actionPopulateKeyboard.invoke(itemFound)
|
||||||
// Force selection if magikeyboard already populated
|
} else {
|
||||||
actionEntrySelection.invoke(false)
|
// Force selection if magikeyboard already populated
|
||||||
}
|
actionEntrySelection.invoke(false)
|
||||||
} else if (items.size > 1) {
|
}
|
||||||
// Select the one we want in the selection
|
},
|
||||||
actionEntrySelection.invoke(true)
|
actionEntrySelection = actionEntrySelection
|
||||||
} else {
|
)
|
||||||
// Select an arbitrary one
|
|
||||||
actionEntrySelection.invoke(false)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun populateKeyboardAndMoveAppToBackground(activity: Activity,
|
fun populateKeyboardAndMoveAppToBackground(activity: Activity,
|
||||||
@@ -0,0 +1,320 @@
|
|||||||
|
package com.kunzisoft.keepass.credentialprovider.passkey
|
||||||
|
|
||||||
|
import android.graphics.BlendMode
|
||||||
|
import android.graphics.drawable.Icon
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.CancellationSignal
|
||||||
|
import android.os.OutcomeReceiver
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.annotation.RequiresApi
|
||||||
|
import androidx.credentials.exceptions.ClearCredentialException
|
||||||
|
import androidx.credentials.exceptions.CreateCredentialException
|
||||||
|
import androidx.credentials.exceptions.CreateCredentialUnknownException
|
||||||
|
import androidx.credentials.exceptions.GetCredentialException
|
||||||
|
import androidx.credentials.exceptions.GetCredentialUnknownException
|
||||||
|
import androidx.credentials.provider.BeginCreateCredentialRequest
|
||||||
|
import androidx.credentials.provider.BeginCreateCredentialResponse
|
||||||
|
import androidx.credentials.provider.BeginCreatePublicKeyCredentialRequest
|
||||||
|
import androidx.credentials.provider.BeginGetCredentialRequest
|
||||||
|
import androidx.credentials.provider.BeginGetCredentialResponse
|
||||||
|
import androidx.credentials.provider.BeginGetPublicKeyCredentialOption
|
||||||
|
import androidx.credentials.provider.CreateEntry
|
||||||
|
import androidx.credentials.provider.CredentialEntry
|
||||||
|
import androidx.credentials.provider.CredentialProviderService
|
||||||
|
import androidx.credentials.provider.ProviderClearCredentialStateRequest
|
||||||
|
import androidx.credentials.provider.PublicKeyCredentialEntry
|
||||||
|
import com.kunzisoft.keepass.R
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.buildIcon
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.SpecialMode
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.activity.PasskeyLauncherActivity
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.passkey.util.JsonHelper
|
||||||
|
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||||
|
import com.kunzisoft.keepass.database.DatabaseTaskProvider
|
||||||
|
import com.kunzisoft.keepass.database.helper.SearchHelper
|
||||||
|
import com.kunzisoft.keepass.model.EntryInfoPasskey.getPasskey
|
||||||
|
import com.kunzisoft.keepass.model.SearchInfo
|
||||||
|
import java.time.Instant
|
||||||
|
|
||||||
|
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
|
||||||
|
class PasskeyProviderService : CredentialProviderService() {
|
||||||
|
|
||||||
|
private var mDatabaseTaskProvider: DatabaseTaskProvider? = null
|
||||||
|
private var mDatabase: ContextualDatabase? = null
|
||||||
|
private lateinit var defaultIcon: Icon
|
||||||
|
|
||||||
|
override fun onCreate() {
|
||||||
|
super.onCreate()
|
||||||
|
|
||||||
|
mDatabaseTaskProvider = DatabaseTaskProvider(this)
|
||||||
|
mDatabaseTaskProvider?.registerProgressTask()
|
||||||
|
mDatabaseTaskProvider?.onDatabaseRetrieved = { database ->
|
||||||
|
this.mDatabase = database
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultIcon = Icon.createWithResource(
|
||||||
|
this@PasskeyProviderService,
|
||||||
|
R.mipmap.ic_launcher_round
|
||||||
|
).apply {
|
||||||
|
setTintBlendMode(BlendMode.DST)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
mDatabaseTaskProvider?.unregisterProgressTask()
|
||||||
|
super.onDestroy()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBeginGetCredentialRequest(
|
||||||
|
request: BeginGetCredentialRequest,
|
||||||
|
cancellationSignal: CancellationSignal,
|
||||||
|
callback: OutcomeReceiver<BeginGetCredentialResponse, GetCredentialException>,
|
||||||
|
) {
|
||||||
|
Log.d(javaClass.simpleName, "onBeginGetCredentialRequest called")
|
||||||
|
processGetCredentialsRequest(request)?.let { response ->
|
||||||
|
callback.onResult(response)
|
||||||
|
} ?: run {
|
||||||
|
callback.onError(GetCredentialUnknownException())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun processGetCredentialsRequest(request: BeginGetCredentialRequest): BeginGetCredentialResponse? {
|
||||||
|
val credentialEntries: MutableList<CredentialEntry> = mutableListOf()
|
||||||
|
|
||||||
|
for (option in request.beginGetCredentialOptions) {
|
||||||
|
when (option) {
|
||||||
|
is BeginGetPublicKeyCredentialOption -> {
|
||||||
|
credentialEntries.addAll(
|
||||||
|
populatePasskeyData(option)
|
||||||
|
)
|
||||||
|
return BeginGetCredentialResponse(credentialEntries)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Log.w(javaClass.simpleName, "unknown beginGetCredentialOption")
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun populatePasskeyData(option: BeginGetPublicKeyCredentialOption): List<CredentialEntry> {
|
||||||
|
|
||||||
|
val passkeyEntries: MutableList<CredentialEntry> = mutableListOf()
|
||||||
|
|
||||||
|
val relyingPartyJson = JsonHelper
|
||||||
|
.parseJsonToRequestOptions(option.requestJson)
|
||||||
|
.relyingParty
|
||||||
|
val searchInfo = SearchInfo().apply {
|
||||||
|
relyingParty = relyingPartyJson
|
||||||
|
}
|
||||||
|
Log.d(TAG, "Build passkey search for relying party $relyingPartyJson")
|
||||||
|
SearchHelper.checkAutoSearchInfo(
|
||||||
|
context = this,
|
||||||
|
database = mDatabase,
|
||||||
|
searchInfo = searchInfo,
|
||||||
|
onItemsFound = { database, items ->
|
||||||
|
Log.d(TAG, "Add pending intent for passkey selection with found items")
|
||||||
|
for (passkeyEntry in items) {
|
||||||
|
PasskeyLauncherActivity.getPendingIntent(
|
||||||
|
context = applicationContext,
|
||||||
|
specialMode = SpecialMode.SELECTION,
|
||||||
|
passkeyEntryNodeId = passkeyEntry.id
|
||||||
|
)?.let { usagePendingIntent ->
|
||||||
|
val passkey = passkeyEntry.getPasskey()
|
||||||
|
passkeyEntries.add(
|
||||||
|
PublicKeyCredentialEntry(
|
||||||
|
context = applicationContext,
|
||||||
|
username = passkey?.username ?: "Unknown",
|
||||||
|
icon = passkeyEntry.buildIcon(this@PasskeyProviderService, database)?.apply {
|
||||||
|
setTintBlendMode(BlendMode.DST)
|
||||||
|
} ?: defaultIcon,
|
||||||
|
pendingIntent = usagePendingIntent,
|
||||||
|
beginGetPublicKeyCredentialOption = option,
|
||||||
|
displayName = passkey?.displayName,
|
||||||
|
isAutoSelectAllowed = false
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onItemNotFound = { _ ->
|
||||||
|
Log.w(TAG, "No passkey found in the database with this relying party : $relyingPartyJson")
|
||||||
|
Log.d(TAG, "Add pending intent for passkey selection in opened database")
|
||||||
|
PasskeyLauncherActivity.getPendingIntent(
|
||||||
|
context = applicationContext,
|
||||||
|
specialMode = SpecialMode.SELECTION,
|
||||||
|
searchInfo = searchInfo
|
||||||
|
)?.let { pendingIntent ->
|
||||||
|
passkeyEntries.add(
|
||||||
|
PublicKeyCredentialEntry(
|
||||||
|
context = applicationContext,
|
||||||
|
username = getString(R.string.passkey_locked_database_username),
|
||||||
|
displayName = getString(R.string.passkey_selection_description),
|
||||||
|
icon = defaultIcon,
|
||||||
|
pendingIntent = pendingIntent,
|
||||||
|
beginGetPublicKeyCredentialOption = option,
|
||||||
|
lastUsedTime = Instant.now(),
|
||||||
|
isAutoSelectAllowed = false
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onDatabaseClosed = {
|
||||||
|
Log.d(TAG, "Add pending intent for passkey selection in closed database")
|
||||||
|
// Database is locked, a public key credential entry is shown to unlock it
|
||||||
|
PasskeyLauncherActivity.getPendingIntent(
|
||||||
|
context = applicationContext,
|
||||||
|
specialMode = SpecialMode.SELECTION,
|
||||||
|
searchInfo = searchInfo
|
||||||
|
)?.let { pendingIntent ->
|
||||||
|
passkeyEntries.add(
|
||||||
|
PublicKeyCredentialEntry(
|
||||||
|
context = applicationContext,
|
||||||
|
username = getString(R.string.passkey_locked_database_username),
|
||||||
|
displayName = getString(R.string.passkey_locked_database_description),
|
||||||
|
icon = defaultIcon,
|
||||||
|
pendingIntent = pendingIntent,
|
||||||
|
beginGetPublicKeyCredentialOption = option,
|
||||||
|
lastUsedTime = Instant.now(),
|
||||||
|
isAutoSelectAllowed = true
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return passkeyEntries
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBeginCreateCredentialRequest(
|
||||||
|
request: BeginCreateCredentialRequest,
|
||||||
|
cancellationSignal: CancellationSignal,
|
||||||
|
callback: OutcomeReceiver<BeginCreateCredentialResponse, CreateCredentialException>,
|
||||||
|
) {
|
||||||
|
Log.d(javaClass.simpleName, "onBeginCreateCredentialRequest called")
|
||||||
|
processCreateCredentialRequest(request)?.let { response ->
|
||||||
|
callback.onResult(response)
|
||||||
|
} ?: let {
|
||||||
|
callback.onError(CreateCredentialUnknownException())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun processCreateCredentialRequest(request: BeginCreateCredentialRequest): BeginCreateCredentialResponse? {
|
||||||
|
when (request) {
|
||||||
|
is BeginCreatePublicKeyCredentialRequest -> {
|
||||||
|
// Request is passkey type
|
||||||
|
return handleCreatePasskeyQuery(request)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// request type not supported
|
||||||
|
Log.w(javaClass.simpleName, "unknown type of BeginCreateCredentialRequest")
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun MutableList<CreateEntry>.addPendingIntentCreationNewEntry(
|
||||||
|
accountName: String,
|
||||||
|
searchInfo: SearchInfo?
|
||||||
|
) {
|
||||||
|
Log.d(TAG, "Add pending intent for registration in opened database to create new item")
|
||||||
|
// TODO add a setting to directly store in a specific group
|
||||||
|
PasskeyLauncherActivity.getPendingIntent(
|
||||||
|
context = applicationContext,
|
||||||
|
specialMode = SpecialMode.REGISTRATION,
|
||||||
|
searchInfo = searchInfo
|
||||||
|
)?.let { pendingIntent ->
|
||||||
|
this.add(
|
||||||
|
CreateEntry(
|
||||||
|
accountName = accountName,
|
||||||
|
icon = defaultIcon,
|
||||||
|
pendingIntent = pendingIntent,
|
||||||
|
description = getString(R.string.passkey_creation_description)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleCreatePasskeyQuery(request: BeginCreatePublicKeyCredentialRequest): BeginCreateCredentialResponse {
|
||||||
|
|
||||||
|
val accountName = mDatabase?.name ?: getString(R.string.passkey_locked_database_username)
|
||||||
|
val createEntries: MutableList<CreateEntry> = mutableListOf()
|
||||||
|
val searchInfo = SearchInfo().apply {
|
||||||
|
relyingParty = JsonHelper
|
||||||
|
.parseJsonToCreateOptions(request.requestJson)
|
||||||
|
.relyingParty
|
||||||
|
}
|
||||||
|
SearchHelper.checkAutoSearchInfo(
|
||||||
|
context = this,
|
||||||
|
database = mDatabase,
|
||||||
|
searchInfo = searchInfo,
|
||||||
|
onItemsFound = { database, items ->
|
||||||
|
if (database.isReadOnly) {
|
||||||
|
throw CreateCredentialUnknownException(
|
||||||
|
"Unable to register or overwrite a passkey in a database that is read only"
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
// To create a new entry
|
||||||
|
createEntries.addPendingIntentCreationNewEntry(accountName, searchInfo)
|
||||||
|
/* TODO Overwrite
|
||||||
|
// To select an existing entry and permit an overwrite
|
||||||
|
Log.w(TAG, "Passkey already registered")
|
||||||
|
for (entryInfo in items) {
|
||||||
|
PasskeyHelper.getPendingIntent(
|
||||||
|
context = applicationContext,
|
||||||
|
specialMode = SpecialMode.REGISTRATION,
|
||||||
|
searchInfo = searchInfo,
|
||||||
|
passkeyEntryNodeId = entryInfo.id
|
||||||
|
)?.let { createPendingIntent ->
|
||||||
|
createEntries.add(
|
||||||
|
CreateEntry(
|
||||||
|
accountName = accountName,
|
||||||
|
pendingIntent = createPendingIntent,
|
||||||
|
description = getString(
|
||||||
|
R.string.passkey_update_description,
|
||||||
|
entryInfo.getPasskey()?.displayName
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}*/
|
||||||
|
}
|
||||||
|
},
|
||||||
|
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"
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
createEntries.addPendingIntentCreationNewEntry(accountName, searchInfo)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onDatabaseClosed = {
|
||||||
|
// Launch the passkey launcher activity to open the database
|
||||||
|
Log.d(TAG, "Add pending intent for passkey registration in closed database")
|
||||||
|
PasskeyLauncherActivity.getPendingIntent(
|
||||||
|
context = applicationContext,
|
||||||
|
specialMode = SpecialMode.REGISTRATION
|
||||||
|
)?.let { pendingIntent ->
|
||||||
|
createEntries.add(
|
||||||
|
CreateEntry(
|
||||||
|
accountName = accountName,
|
||||||
|
icon = defaultIcon,
|
||||||
|
pendingIntent = pendingIntent,
|
||||||
|
description = getString(R.string.passkey_locked_database_description)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return BeginCreateCredentialResponse(createEntries)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onClearCredentialStateRequest(
|
||||||
|
request: ProviderClearCredentialStateRequest,
|
||||||
|
cancellationSignal: CancellationSignal,
|
||||||
|
callback: OutcomeReceiver<Void?, ClearCredentialException>
|
||||||
|
) {
|
||||||
|
// nothing to do
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val TAG = PasskeyProviderService::class.java.simpleName
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package com.kunzisoft.keepass.credentialprovider.passkey.data
|
||||||
|
|
||||||
|
data class PublicKeyCredentialCreationOptions(
|
||||||
|
val relyingParty: String,
|
||||||
|
val challenge: ByteArray, // TODO Equals Hashcode
|
||||||
|
val username: String,
|
||||||
|
val userId: ByteArray, // TODO Equals Hashcode
|
||||||
|
val keyTypeIdList: List<Long>
|
||||||
|
)
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package com.kunzisoft.keepass.credentialprovider.passkey.data
|
||||||
|
|
||||||
|
import java.security.KeyPair
|
||||||
|
|
||||||
|
data class PublicKeyCredentialCreationParameters(
|
||||||
|
val relyingParty: String,
|
||||||
|
val credentialId: ByteArray, // TODO Equals Hashcode
|
||||||
|
val signatureKey: Pair<KeyPair, Long>,
|
||||||
|
val isPrivilegedApp: Boolean,
|
||||||
|
val challenge: ByteArray, // TODO Equals Hashcode
|
||||||
|
)
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
package com.kunzisoft.keepass.credentialprovider.data
|
package com.kunzisoft.keepass.credentialprovider.passkey.data
|
||||||
|
|
||||||
data class PublicKeyCredentialRequestOptions(
|
data class PublicKeyCredentialRequestOptions(
|
||||||
val relyingParty: String,
|
val relyingParty: String,
|
||||||
val challengeString: String
|
val challengeString: String
|
||||||
) {
|
)
|
||||||
}
|
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package com.kunzisoft.keepass.credentialprovider.passkey.data
|
||||||
|
|
||||||
|
data class PublicKeyCredentialUsageParameters(
|
||||||
|
val relyingParty: String,
|
||||||
|
val packageName: String? = null,
|
||||||
|
val clientDataHash: ByteArray?, // TODO Equals Hashcode
|
||||||
|
val isPrivilegedApp: Boolean,
|
||||||
|
val challenge: ByteArray, // TODO Equals Hashcode
|
||||||
|
)
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.kunzisoft.keepass.credentialprovider.util
|
package com.kunzisoft.keepass.credentialprovider.passkey.util
|
||||||
|
|
||||||
class AppRelyingPartyRelation {
|
class AppRelyingPartyRelation {
|
||||||
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.kunzisoft.keepass.credentialprovider.util
|
package com.kunzisoft.keepass.credentialprovider.passkey.util
|
||||||
|
|
||||||
import org.apache.commons.codec.binary.Base64
|
import org.apache.commons.codec.binary.Base64
|
||||||
|
|
||||||
@@ -1,12 +1,12 @@
|
|||||||
package com.kunzisoft.keepass.credentialprovider.util
|
package com.kunzisoft.keepass.credentialprovider.passkey.util
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import androidx.credentials.webauthn.Cbor
|
import androidx.credentials.webauthn.Cbor
|
||||||
import com.kunzisoft.encrypt.HashManager
|
import com.kunzisoft.encrypt.HashManager
|
||||||
import com.kunzisoft.keepass.credentialprovider.data.PublicKeyCredentialCreationOptions
|
import com.kunzisoft.keepass.credentialprovider.passkey.data.PublicKeyCredentialCreationOptions
|
||||||
import com.kunzisoft.keepass.credentialprovider.data.PublicKeyCredentialRequestOptions
|
import com.kunzisoft.keepass.credentialprovider.passkey.data.PublicKeyCredentialRequestOptions
|
||||||
import com.kunzisoft.keepass.credentialprovider.util.Base64Helper.Companion.b64Decode
|
import com.kunzisoft.keepass.credentialprovider.passkey.util.Base64Helper.Companion.b64Decode
|
||||||
import com.kunzisoft.keepass.credentialprovider.util.Base64Helper.Companion.b64Encode
|
import com.kunzisoft.keepass.credentialprovider.passkey.util.Base64Helper.Companion.b64Encode
|
||||||
import org.json.JSONArray
|
import org.json.JSONArray
|
||||||
import org.json.JSONObject
|
import org.json.JSONObject
|
||||||
|
|
||||||
@@ -245,7 +245,5 @@ class JsonHelper {
|
|||||||
keyTypeIdList.distinct()
|
keyTypeIdList.distinct()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.kunzisoft.keepass.credentialprovider.util
|
package com.kunzisoft.keepass.credentialprovider.passkey.util
|
||||||
|
|
||||||
import android.content.res.AssetManager
|
import android.content.res.AssetManager
|
||||||
import androidx.credentials.provider.CallingAppInfo
|
import androidx.credentials.provider.CallingAppInfo
|
||||||
@@ -9,13 +9,12 @@ class OriginHelper {
|
|||||||
|
|
||||||
const val DEFAULT_PROTOCOL = "https://"
|
const val DEFAULT_PROTOCOL = "https://"
|
||||||
|
|
||||||
fun getWebOrigin(callingAppInfo: CallingAppInfo, assets: AssetManager): String? {
|
fun getWebOrigin(callingAppInfo: CallingAppInfo?, assets: AssetManager): String? {
|
||||||
val privilegedAllowlist = assets.open("trustedPackages.json").bufferedReader().use {
|
val privilegedAllowlist = assets.open("trustedPackages.json").bufferedReader().use {
|
||||||
it.readText()
|
it.readText()
|
||||||
}
|
}
|
||||||
// for trusted browsers like Chrome and Firefox
|
// for trusted browsers like Chrome and Firefox
|
||||||
val origin = callingAppInfo.getOrigin(privilegedAllowlist)?.removeSuffix("/")
|
return callingAppInfo?.getOrigin(privilegedAllowlist)?.removeSuffix("/")
|
||||||
return origin
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,428 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2025 Jeremy Jamet / Kunzisoft.
|
||||||
|
*
|
||||||
|
* This file is part of KeePassDX.
|
||||||
|
*
|
||||||
|
* KeePassDX is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* KeePassDX is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
package com.kunzisoft.keepass.credentialprovider.passkey.util
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.os.ParcelUuid
|
||||||
|
import android.security.keystore.KeyGenParameterSpec
|
||||||
|
import android.security.keystore.KeyProperties
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.annotation.RequiresApi
|
||||||
|
import androidx.credentials.CreatePublicKeyCredentialRequest
|
||||||
|
import androidx.credentials.CreatePublicKeyCredentialResponse
|
||||||
|
import androidx.credentials.GetPublicKeyCredentialOption
|
||||||
|
import androidx.credentials.PublicKeyCredential
|
||||||
|
import androidx.credentials.exceptions.CreateCredentialUnknownException
|
||||||
|
import androidx.credentials.exceptions.GetCredentialUnknownException
|
||||||
|
import androidx.credentials.provider.PendingIntentHandler
|
||||||
|
import com.kunzisoft.asymmetric.Signature
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.passkey.data.PublicKeyCredentialCreationOptions
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.passkey.data.PublicKeyCredentialCreationParameters
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.passkey.data.PublicKeyCredentialUsageParameters
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.passkey.util.OriginHelper.Companion.DEFAULT_PROTOCOL
|
||||||
|
import com.kunzisoft.keepass.model.EntryInfo
|
||||||
|
import com.kunzisoft.keepass.model.EntryInfoPasskey.getPasskey
|
||||||
|
import com.kunzisoft.keepass.model.Passkey
|
||||||
|
import com.kunzisoft.keepass.utils.StringUtil.toHexString
|
||||||
|
import com.kunzisoft.keepass.utils.getParcelableExtraCompat
|
||||||
|
import com.kunzisoft.random.KeePassDXRandom
|
||||||
|
import java.security.KeyStore
|
||||||
|
import java.security.MessageDigest
|
||||||
|
import java.time.Instant
|
||||||
|
import java.util.UUID
|
||||||
|
import javax.crypto.KeyGenerator
|
||||||
|
import javax.crypto.Mac
|
||||||
|
import javax.crypto.SecretKey
|
||||||
|
|
||||||
|
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
|
||||||
|
object PasskeyHelper {
|
||||||
|
|
||||||
|
private const val EXTRA_PASSKEY_ELEMENT = "com.kunzisoft.keepass.passkey.extra.EXTRA_PASSKEY_ELEMENT"
|
||||||
|
|
||||||
|
private const val HMAC_TYPE = "HmacSHA256"
|
||||||
|
|
||||||
|
private const val KEY_NODE_ID = "nodeId"
|
||||||
|
private const val KEY_TIMESTAMP = "timestamp"
|
||||||
|
private const val KEY_AUTHENTICATION_CODE = "authenticationCode"
|
||||||
|
|
||||||
|
private const val SEPARATOR = "_"
|
||||||
|
|
||||||
|
private const val NAME_OF_HMAC_KEY = "KeePassDXCredentialProviderHMACKey"
|
||||||
|
|
||||||
|
private const val KEYSTORE_TYPE = "AndroidKeyStore"
|
||||||
|
|
||||||
|
private val PLACEHOLDER_FOR_NEW_NODE_ID = "0".repeat(32)
|
||||||
|
|
||||||
|
private val REGEX_TIMESTAMP = "[0-9]{10}".toRegex()
|
||||||
|
private val REGEX_AUTHENTICATION_CODE = "[A-F0-9]{64}".toRegex() // 256 bits = 64 hex chars
|
||||||
|
|
||||||
|
private const val MAX_DIFF_IN_SECONDS = 60
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build the Passkey response for one entry
|
||||||
|
*/
|
||||||
|
fun Activity.buildPasskeyResponseAndSetResult(
|
||||||
|
entryInfo: EntryInfo,
|
||||||
|
extras: Bundle? = null
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
entryInfo.getPasskey()?.let {
|
||||||
|
val mReplyIntent = Intent()
|
||||||
|
Log.d(javaClass.name, "Success Passkey manual selection")
|
||||||
|
mReplyIntent.putExtra(EXTRA_PASSKEY_ELEMENT, entryInfo.getPasskey())
|
||||||
|
extras?.let {
|
||||||
|
mReplyIntent.putExtras(it)
|
||||||
|
}
|
||||||
|
setResult(Activity.RESULT_OK, mReplyIntent)
|
||||||
|
} ?: run {
|
||||||
|
Log.w(javaClass.name, "Failed Passkey manual selection")
|
||||||
|
setResult(Activity.RESULT_CANCELED)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(javaClass.name, "Cant add passkey entry as result", e)
|
||||||
|
setResult(Activity.RESULT_CANCELED)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Intent.addAuthCode(passkeyEntryNodeId: UUID? = null) {
|
||||||
|
passkeyEntryNodeId?.let {
|
||||||
|
putExtras(Bundle().apply {
|
||||||
|
val timestamp = Instant.now().epochSecond
|
||||||
|
putParcelable(KEY_NODE_ID, ParcelUuid(passkeyEntryNodeId))
|
||||||
|
putString(KEY_TIMESTAMP, timestamp.toString())
|
||||||
|
putString(
|
||||||
|
KEY_AUTHENTICATION_CODE, generatedAuthenticationCode(
|
||||||
|
passkeyEntryNodeId, timestamp
|
||||||
|
).toHexString()
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Intent.retrievePasskey(): Passkey? {
|
||||||
|
return this.getParcelableExtraCompat(EXTRA_PASSKEY_ELEMENT)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Intent.removePasskey() {
|
||||||
|
return this.removeExtra(EXTRA_PASSKEY_ELEMENT)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Intent.retrieveNodeId(): UUID? {
|
||||||
|
return getParcelableExtraCompat<ParcelUuid>(KEY_NODE_ID)?.uuid
|
||||||
|
}
|
||||||
|
|
||||||
|
fun checkSecurity(intent: Intent, nodeId: UUID?) {
|
||||||
|
val timestampString = intent.getStringExtra(KEY_TIMESTAMP)
|
||||||
|
if (timestampString.isNullOrEmpty())
|
||||||
|
throw CreateCredentialUnknownException("Timestamp null")
|
||||||
|
if (timestampString.matches(REGEX_TIMESTAMP).not()) {
|
||||||
|
throw CreateCredentialUnknownException("Timestamp not valid")
|
||||||
|
}
|
||||||
|
val timestamp = timestampString.toLong()
|
||||||
|
val diff = Instant.now().epochSecond - timestamp
|
||||||
|
if (diff < 0 || diff > MAX_DIFF_IN_SECONDS) {
|
||||||
|
throw CreateCredentialUnknownException("Out of time")
|
||||||
|
}
|
||||||
|
|
||||||
|
verifyAuthenticationCode(
|
||||||
|
intent.getStringExtra(KEY_AUTHENTICATION_CODE),
|
||||||
|
generatedAuthenticationCode(nodeId, timestamp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun generatedAuthenticationCode(nodeId: UUID?, timestamp: Long): ByteArray {
|
||||||
|
return generateAuthenticationCode(
|
||||||
|
(nodeId?.toString() ?: PLACEHOLDER_FOR_NEW_NODE_ID) + SEPARATOR + timestamp.toString()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun verifyAuthenticationCode(
|
||||||
|
valueToCheck: String?,
|
||||||
|
authenticationCode: ByteArray
|
||||||
|
) {
|
||||||
|
if (valueToCheck.isNullOrEmpty())
|
||||||
|
throw CreateCredentialUnknownException("Authentication code empty")
|
||||||
|
if (valueToCheck.matches(REGEX_AUTHENTICATION_CODE).not())
|
||||||
|
throw CreateCredentialUnknownException("Authentication not valid")
|
||||||
|
if (MessageDigest.isEqual(authenticationCode, generateAuthenticationCode(valueToCheck)))
|
||||||
|
throw CreateCredentialUnknownException("Authentication code incorrect")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun generateAuthenticationCode(message: String): ByteArray {
|
||||||
|
val keyStore = KeyStore.getInstance(KEYSTORE_TYPE)
|
||||||
|
keyStore.load(null)
|
||||||
|
val hmacKey = try {
|
||||||
|
keyStore.getKey(NAME_OF_HMAC_KEY, null) as SecretKey
|
||||||
|
} catch (e: Exception) {
|
||||||
|
// key not found
|
||||||
|
generateKey()
|
||||||
|
}
|
||||||
|
|
||||||
|
val mac = Mac.getInstance(HMAC_TYPE)
|
||||||
|
mac.init(hmacKey)
|
||||||
|
val authenticationCode = mac.doFinal(message.toByteArray())
|
||||||
|
return authenticationCode
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun generateKey(): SecretKey? {
|
||||||
|
val keyGenerator = KeyGenerator.getInstance(
|
||||||
|
KeyProperties.KEY_ALGORITHM_HMAC_SHA256, KEYSTORE_TYPE
|
||||||
|
)
|
||||||
|
val keySizeInBits = 128
|
||||||
|
keyGenerator.init(
|
||||||
|
KeyGenParameterSpec.Builder(NAME_OF_HMAC_KEY, KeyProperties.PURPOSE_SIGN)
|
||||||
|
.setKeySize(keySizeInBits)
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
val key = keyGenerator.generateKey()
|
||||||
|
return key
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun String.decodeHexToByteArray(): ByteArray {
|
||||||
|
if (length % 2 != 0) {
|
||||||
|
throw IllegalArgumentException("Must have an even length")
|
||||||
|
}
|
||||||
|
return chunked(2).map { it.toInt(16).toByte() }.toByteArray()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Intent.retrievePasskeyCreationComponent(): PublicKeyCredentialCreationOptions {
|
||||||
|
val request = PendingIntentHandler.retrieveProviderCreateCredentialRequest(this)
|
||||||
|
?: throw CreateCredentialUnknownException("could not retrieve request from intent")
|
||||||
|
if (request.callingRequest !is CreatePublicKeyCredentialRequest) {
|
||||||
|
throw CreateCredentialUnknownException("callingRequest is of wrong type: ${request.callingRequest.type}")
|
||||||
|
}
|
||||||
|
return JsonHelper.parseJsonToCreateOptions(
|
||||||
|
(request.callingRequest as CreatePublicKeyCredentialRequest).requestJson
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Intent.retrievePasskeyUsageComponent(): GetPublicKeyCredentialOption {
|
||||||
|
val request = PendingIntentHandler.retrieveProviderGetCredentialRequest(this)
|
||||||
|
?: throw CreateCredentialUnknownException("could not retrieve request from intent")
|
||||||
|
if (request.credentialOptions.size != 1) {
|
||||||
|
throw GetCredentialUnknownException("not exact one credentialOption")
|
||||||
|
}
|
||||||
|
if (request.credentialOptions[0] !is GetPublicKeyCredentialOption) {
|
||||||
|
throw CreateCredentialUnknownException("credentialOptions is of wrong type: ${request.credentialOptions[0]}")
|
||||||
|
}
|
||||||
|
return request.credentialOptions[0] as GetPublicKeyCredentialOption
|
||||||
|
}
|
||||||
|
|
||||||
|
fun retrievePasskeyCreationRequestParameters(
|
||||||
|
creationOptions: PublicKeyCredentialCreationOptions,
|
||||||
|
webOrigin: String?,
|
||||||
|
apkSigningCertificate: ByteArray?,
|
||||||
|
passkeyCreated: (Passkey, PublicKeyCredentialCreationParameters) -> Unit
|
||||||
|
) {
|
||||||
|
val relyingParty = creationOptions.relyingParty
|
||||||
|
val username = creationOptions.username
|
||||||
|
val userHandle = creationOptions.userId
|
||||||
|
val keyTypeIdList = creationOptions.keyTypeIdList
|
||||||
|
val challenge = creationOptions.challenge
|
||||||
|
|
||||||
|
val isPrivilegedApp =
|
||||||
|
(webOrigin != null && webOrigin == DEFAULT_PROTOCOL + relyingParty)
|
||||||
|
Log.d(this::class.java.simpleName, "isPrivilegedApp = $isPrivilegedApp")
|
||||||
|
|
||||||
|
if (!isPrivilegedApp) {
|
||||||
|
val isValid =
|
||||||
|
AppRelyingPartyRelation.isRelationValid(relyingParty, apkSigningCertificate)
|
||||||
|
if (!isValid) {
|
||||||
|
throw CreateCredentialUnknownException(
|
||||||
|
"could not verify relation between app " +
|
||||||
|
"and relyingParty $relyingParty"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val credentialId = KeePassDXRandom.generateCredentialId()
|
||||||
|
|
||||||
|
val (keyPair, keyTypeId) = Signature.generateKeyPair(keyTypeIdList)
|
||||||
|
?: throw CreateCredentialUnknownException("no known public key type found")
|
||||||
|
val privateKeyPem = Signature.convertPrivateKeyToPem(keyPair.private)
|
||||||
|
|
||||||
|
// create new entry in database
|
||||||
|
passkeyCreated.invoke(
|
||||||
|
Passkey(
|
||||||
|
username = username,
|
||||||
|
displayName = "$relyingParty (Passkey)",
|
||||||
|
privateKeyPem = privateKeyPem,
|
||||||
|
credentialId = Base64Helper.b64Encode(credentialId),
|
||||||
|
userHandle = Base64Helper.b64Encode(userHandle),
|
||||||
|
relyingParty = DEFAULT_PROTOCOL + relyingParty
|
||||||
|
),
|
||||||
|
PublicKeyCredentialCreationParameters(
|
||||||
|
relyingParty = relyingParty,
|
||||||
|
challenge = challenge,
|
||||||
|
credentialId = credentialId,
|
||||||
|
signatureKey = Pair(keyPair, keyTypeId),
|
||||||
|
isPrivilegedApp = isPrivilegedApp
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun buildCreatePublicKeyCredentialResponse(
|
||||||
|
packageName: String?,
|
||||||
|
publicKeyCredentialCreationParameters: PublicKeyCredentialCreationParameters
|
||||||
|
): CreatePublicKeyCredentialResponse {
|
||||||
|
|
||||||
|
val keyPair = publicKeyCredentialCreationParameters.signatureKey.first
|
||||||
|
val keyTypeId = publicKeyCredentialCreationParameters.signatureKey.second
|
||||||
|
|
||||||
|
val publicKeyEncoded = Signature.convertPublicKey(keyPair.public, keyTypeId)
|
||||||
|
val publicKeyMap = Signature.convertPublicKeyToMap(keyPair.public, keyTypeId)
|
||||||
|
|
||||||
|
val authData = JsonHelper.generateAuthDataForCreate(
|
||||||
|
userPresent = true,
|
||||||
|
userVerified = true,
|
||||||
|
backupEligibility = true,
|
||||||
|
backupState = true,
|
||||||
|
rpId = publicKeyCredentialCreationParameters.relyingParty.toByteArray(),
|
||||||
|
credentialId = publicKeyCredentialCreationParameters.credentialId,
|
||||||
|
credentialPublicKey = JsonHelper.generateCborFromMap(publicKeyMap!!)
|
||||||
|
)
|
||||||
|
|
||||||
|
val attestationObject = JsonHelper.generateAttestationObject(authData)
|
||||||
|
|
||||||
|
val clientJson: String
|
||||||
|
if (publicKeyCredentialCreationParameters.isPrivilegedApp) {
|
||||||
|
clientJson = JsonHelper.generateClientDataJsonPrivileged()
|
||||||
|
} else {
|
||||||
|
val origin = DEFAULT_PROTOCOL + publicKeyCredentialCreationParameters.relyingParty
|
||||||
|
clientJson = JsonHelper.generateClientDataJsonNonPrivileged(
|
||||||
|
publicKeyCredentialCreationParameters.challenge,
|
||||||
|
origin,
|
||||||
|
packageName,
|
||||||
|
isCrossOriginAdded = true,
|
||||||
|
isGet = false
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val responseJson = JsonHelper.createAuthenticatorAttestationResponseJSON(
|
||||||
|
publicKeyCredentialCreationParameters.credentialId,
|
||||||
|
clientJson,
|
||||||
|
attestationObject,
|
||||||
|
publicKeyEncoded!!,
|
||||||
|
authData,
|
||||||
|
keyTypeId
|
||||||
|
)
|
||||||
|
|
||||||
|
// log only the length to prevent logging sensitive information
|
||||||
|
Log.d(javaClass.simpleName, "responseJson with length ${responseJson.length} created")
|
||||||
|
return CreatePublicKeyCredentialResponse(responseJson)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun retrievePasskeyUsageRequestParameters(
|
||||||
|
context: Context,
|
||||||
|
intent: Intent,
|
||||||
|
result: (PublicKeyCredentialUsageParameters) -> Unit
|
||||||
|
) {
|
||||||
|
val callingAppInfo = PendingIntentHandler.retrieveProviderGetCredentialRequest(intent)?.callingAppInfo
|
||||||
|
val credentialOption = intent.retrievePasskeyUsageComponent()
|
||||||
|
val clientDataHash = credentialOption.clientDataHash
|
||||||
|
|
||||||
|
val requestOptions = JsonHelper.parseJsonToRequestOptions(credentialOption.requestJson)
|
||||||
|
|
||||||
|
val relyingParty = requestOptions.relyingParty
|
||||||
|
val challenge = Base64Helper.b64Decode(requestOptions.challengeString)
|
||||||
|
val packageName = callingAppInfo?.packageName
|
||||||
|
val webOrigin = OriginHelper.getWebOrigin(callingAppInfo, context.assets)
|
||||||
|
|
||||||
|
val isPrivilegedApp =
|
||||||
|
(webOrigin != null && webOrigin == DEFAULT_PROTOCOL + relyingParty && clientDataHash != null)
|
||||||
|
|
||||||
|
Log.d(javaClass.simpleName, "isPrivilegedApp = $isPrivilegedApp")
|
||||||
|
|
||||||
|
if (!isPrivilegedApp) {
|
||||||
|
if (!AppRelyingPartyRelation.isRelationValid(
|
||||||
|
relyingParty,
|
||||||
|
apkSigningCertificate = callingAppInfo?.signingInfo?.apkContentsSigners
|
||||||
|
?.getOrNull(0)?.toByteArray()
|
||||||
|
)) {
|
||||||
|
throw CreateCredentialUnknownException(
|
||||||
|
"could not verify relation between app " +
|
||||||
|
"and relyingParty $relyingParty"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result.invoke(
|
||||||
|
PublicKeyCredentialUsageParameters(
|
||||||
|
relyingParty = relyingParty,
|
||||||
|
packageName = packageName,
|
||||||
|
clientDataHash = clientDataHash,
|
||||||
|
isPrivilegedApp = isPrivilegedApp,
|
||||||
|
challenge = challenge
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun buildPasskeyPublicKeyCredential(
|
||||||
|
usageParameters: PublicKeyCredentialUsageParameters,
|
||||||
|
passkey: Passkey
|
||||||
|
): PublicKeyCredential {
|
||||||
|
|
||||||
|
// https://www.w3.org/TR/webauthn-3/#authdata-flags
|
||||||
|
val authenticatorData = JsonHelper.generateAuthDataForUsage(
|
||||||
|
usageParameters.relyingParty.toByteArray(),
|
||||||
|
userPresent = true,
|
||||||
|
userVerified = true,
|
||||||
|
backupEligibility = true,
|
||||||
|
backupState = true
|
||||||
|
)
|
||||||
|
|
||||||
|
val clientDataJson: String
|
||||||
|
val dataToSign: ByteArray
|
||||||
|
if (usageParameters.isPrivilegedApp) {
|
||||||
|
clientDataJson = JsonHelper.generateClientDataJsonPrivileged()
|
||||||
|
dataToSign =
|
||||||
|
JsonHelper.generateDataToSignPrivileged(usageParameters.clientDataHash!!, authenticatorData)
|
||||||
|
} else {
|
||||||
|
val origin = DEFAULT_PROTOCOL + usageParameters.relyingParty
|
||||||
|
clientDataJson = JsonHelper.generateClientDataJsonNonPrivileged(
|
||||||
|
usageParameters.challenge,
|
||||||
|
origin,
|
||||||
|
usageParameters.packageName,
|
||||||
|
isGet = true,
|
||||||
|
isCrossOriginAdded = false
|
||||||
|
)
|
||||||
|
dataToSign =
|
||||||
|
JsonHelper.generateDataTosSignNonPrivileged(clientDataJson, authenticatorData)
|
||||||
|
}
|
||||||
|
|
||||||
|
val signature = Signature.sign(passkey.privateKeyPem, dataToSign)
|
||||||
|
?: throw GetCredentialUnknownException("signing failed")
|
||||||
|
|
||||||
|
val getCredentialResponse =
|
||||||
|
JsonHelper.generateGetCredentialResponse(
|
||||||
|
clientDataJson.toByteArray(),
|
||||||
|
authenticatorData,
|
||||||
|
signature,
|
||||||
|
passkey.userHandle,
|
||||||
|
passkey.credentialId
|
||||||
|
)
|
||||||
|
return PublicKeyCredential(getCredentialResponse)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,219 +0,0 @@
|
|||||||
package com.kunzisoft.keepass.credentialprovider.service
|
|
||||||
|
|
||||||
import android.os.Build
|
|
||||||
import android.os.CancellationSignal
|
|
||||||
import android.os.OutcomeReceiver
|
|
||||||
import android.util.Log
|
|
||||||
import androidx.annotation.RequiresApi
|
|
||||||
import androidx.credentials.exceptions.ClearCredentialException
|
|
||||||
import androidx.credentials.exceptions.CreateCredentialException
|
|
||||||
import androidx.credentials.exceptions.CreateCredentialUnknownException
|
|
||||||
import androidx.credentials.exceptions.GetCredentialException
|
|
||||||
import androidx.credentials.exceptions.GetCredentialUnknownException
|
|
||||||
import androidx.credentials.provider.BeginCreateCredentialRequest
|
|
||||||
import androidx.credentials.provider.BeginCreateCredentialResponse
|
|
||||||
import androidx.credentials.provider.BeginCreatePublicKeyCredentialRequest
|
|
||||||
import androidx.credentials.provider.BeginGetCredentialRequest
|
|
||||||
import androidx.credentials.provider.BeginGetCredentialResponse
|
|
||||||
import androidx.credentials.provider.BeginGetPublicKeyCredentialOption
|
|
||||||
import androidx.credentials.provider.CreateEntry
|
|
||||||
import androidx.credentials.provider.CredentialEntry
|
|
||||||
import androidx.credentials.provider.CredentialProviderService
|
|
||||||
import androidx.credentials.provider.ProviderClearCredentialStateRequest
|
|
||||||
import androidx.credentials.provider.PublicKeyCredentialEntry
|
|
||||||
import com.kunzisoft.keepass.R
|
|
||||||
import com.kunzisoft.keepass.credentialprovider.data.Passkey
|
|
||||||
import com.kunzisoft.keepass.credentialprovider.util.DatabaseHelper
|
|
||||||
import com.kunzisoft.keepass.credentialprovider.util.IntentHelper
|
|
||||||
import com.kunzisoft.keepass.credentialprovider.util.JsonHelper
|
|
||||||
import com.kunzisoft.keepass.database.DatabaseTaskProvider
|
|
||||||
import com.kunzisoft.keepass.database.element.Database
|
|
||||||
import java.time.Instant
|
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
|
|
||||||
class KeePassDXCredentialProviderService : CredentialProviderService() {
|
|
||||||
|
|
||||||
private var mDatabaseTaskProvider: DatabaseTaskProvider? = null
|
|
||||||
private var mDatabase: Database? = null
|
|
||||||
|
|
||||||
override fun onCreate() {
|
|
||||||
super.onCreate()
|
|
||||||
|
|
||||||
mDatabaseTaskProvider = DatabaseTaskProvider(this)
|
|
||||||
mDatabaseTaskProvider?.registerProgressTask()
|
|
||||||
mDatabaseTaskProvider?.onDatabaseRetrieved = { database ->
|
|
||||||
this.mDatabase = database
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDestroy() {
|
|
||||||
mDatabaseTaskProvider?.unregisterProgressTask()
|
|
||||||
super.onDestroy()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onBeginCreateCredentialRequest(
|
|
||||||
request: BeginCreateCredentialRequest,
|
|
||||||
cancellationSignal: CancellationSignal,
|
|
||||||
callback: OutcomeReceiver<BeginCreateCredentialResponse, CreateCredentialException>,
|
|
||||||
) {
|
|
||||||
Log.d(javaClass.simpleName, "onBeginCreateCredentialRequest called")
|
|
||||||
processCreateCredentialRequest(request)?.let { response ->
|
|
||||||
callback.onResult(response)
|
|
||||||
} ?: let {
|
|
||||||
callback.onError(CreateCredentialUnknownException())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun processCreateCredentialRequest(request: BeginCreateCredentialRequest): BeginCreateCredentialResponse? {
|
|
||||||
when (request) {
|
|
||||||
is BeginCreatePublicKeyCredentialRequest -> {
|
|
||||||
// Request is passkey type
|
|
||||||
return handleCreatePasskeyQuery(request)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// request type not supported
|
|
||||||
Log.w(javaClass.simpleName, "unknown type of BeginCreateCredentialRequest")
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun handleCreatePasskeyQuery(request: BeginCreatePublicKeyCredentialRequest): BeginCreateCredentialResponse {
|
|
||||||
|
|
||||||
val accountName = mDatabase?.name ?: getString(R.string.passkey_locked_database_account_name)
|
|
||||||
val createEntries: MutableList<CreateEntry> = mutableListOf()
|
|
||||||
|
|
||||||
mDatabase?.let { database ->
|
|
||||||
// To create a new entry
|
|
||||||
IntentHelper.generateCreatePendingIntent(applicationContext)
|
|
||||||
?.let { pendingIntentNewEntry ->
|
|
||||||
createEntries.add(
|
|
||||||
CreateEntry(
|
|
||||||
accountName = accountName,
|
|
||||||
pendingIntent = pendingIntentNewEntry,
|
|
||||||
description = getString(R.string.passkey_creation_description)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// To select an existing entry
|
|
||||||
for (passkey in getCredentialsFromDb(
|
|
||||||
relyingPartyId = JsonHelper.parseJsonToCreateOptions(request.requestJson).relyingParty,
|
|
||||||
database = database
|
|
||||||
)) {
|
|
||||||
IntentHelper.generateCreatePendingIntent(applicationContext, passkey.nodeId)
|
|
||||||
?.let { createPendingIntent ->
|
|
||||||
createEntries.add(
|
|
||||||
CreateEntry(
|
|
||||||
accountName = accountName,
|
|
||||||
pendingIntent = createPendingIntent,
|
|
||||||
description = getString(
|
|
||||||
R.string.passkey_update_description,
|
|
||||||
passkey.displayName
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} ?: run {
|
|
||||||
// Database is locked, an entry is shown to unlock it
|
|
||||||
createEntries.add(
|
|
||||||
CreateEntry(
|
|
||||||
accountName = accountName,
|
|
||||||
pendingIntent = IntentHelper.generateUnlockPendingIntent(applicationContext),
|
|
||||||
description = getString(R.string.passkey_locked_database_description)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return BeginCreateCredentialResponse(createEntries)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onBeginGetCredentialRequest(
|
|
||||||
request: BeginGetCredentialRequest,
|
|
||||||
cancellationSignal: CancellationSignal,
|
|
||||||
callback: OutcomeReceiver<BeginGetCredentialResponse, GetCredentialException>,
|
|
||||||
) {
|
|
||||||
Log.d(javaClass.simpleName, "onBeginGetCredentialRequest called")
|
|
||||||
processGetCredentialsRequest(request)?.let { response ->
|
|
||||||
callback.onResult(response)
|
|
||||||
} ?: run {
|
|
||||||
callback.onError(GetCredentialUnknownException())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun processGetCredentialsRequest(request: BeginGetCredentialRequest): BeginGetCredentialResponse? {
|
|
||||||
val credentialEntries: MutableList<CredentialEntry> = mutableListOf()
|
|
||||||
|
|
||||||
for (option in request.beginGetCredentialOptions) {
|
|
||||||
when (option) {
|
|
||||||
is BeginGetPublicKeyCredentialOption -> {
|
|
||||||
credentialEntries.addAll(
|
|
||||||
populatePasskeyData(option)
|
|
||||||
)
|
|
||||||
return BeginGetCredentialResponse(credentialEntries)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Log.w(javaClass.simpleName, "unknown beginGetCredentialOption")
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun populatePasskeyData(option: BeginGetPublicKeyCredentialOption): List<CredentialEntry> {
|
|
||||||
|
|
||||||
val passkeyEntries: MutableList<CredentialEntry> = mutableListOf()
|
|
||||||
|
|
||||||
mDatabase?.let { database ->
|
|
||||||
// Retrieve passkeys entries from database
|
|
||||||
val relyingParty = JsonHelper.parseJsonToRequestOptions(option.requestJson).relyingParty
|
|
||||||
if (relyingParty.isBlank()) {
|
|
||||||
throw CreateCredentialUnknownException("relying party id is null or blank")
|
|
||||||
}
|
|
||||||
for (passkey in getCredentialsFromDb(
|
|
||||||
relyingPartyId = relyingParty,
|
|
||||||
database = database
|
|
||||||
)) {
|
|
||||||
IntentHelper.generateUsagePendingIntent(applicationContext, passkey.nodeId)
|
|
||||||
?.let { usagePendingIntent ->
|
|
||||||
passkeyEntries.add(
|
|
||||||
PublicKeyCredentialEntry(
|
|
||||||
context = applicationContext,
|
|
||||||
username = passkey.username,
|
|
||||||
pendingIntent = usagePendingIntent,
|
|
||||||
beginGetPublicKeyCredentialOption = option,
|
|
||||||
displayName = passkey.displayName,
|
|
||||||
isAutoSelectAllowed = false
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} ?: run {
|
|
||||||
// Database is locked, a public key credential entry is shown to unlock it
|
|
||||||
passkeyEntries.add(
|
|
||||||
PublicKeyCredentialEntry(
|
|
||||||
context = applicationContext,
|
|
||||||
username = getString(R.string.passkey_locked_database_account_name),
|
|
||||||
pendingIntent = IntentHelper.generateUnlockPendingIntent(applicationContext),
|
|
||||||
beginGetPublicKeyCredentialOption = option,
|
|
||||||
displayName = getString(R.string.passkey_locked_database_description),
|
|
||||||
lastUsedTime = Instant.now(),
|
|
||||||
isAutoSelectAllowed = true
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return passkeyEntries
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getCredentialsFromDb(relyingPartyId: String, database: Database): List<Passkey> {
|
|
||||||
val passkeys = DatabaseHelper.getAllPasskeys(database)
|
|
||||||
val passkeysMatching = passkeys.filter { p -> p.relyingParty == relyingPartyId }
|
|
||||||
return passkeysMatching
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onClearCredentialStateRequest(
|
|
||||||
request: ProviderClearCredentialStateRequest,
|
|
||||||
cancellationSignal: CancellationSignal,
|
|
||||||
callback: OutcomeReceiver<Void?, ClearCredentialException>
|
|
||||||
) {
|
|
||||||
// nothing to do
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,98 +0,0 @@
|
|||||||
package com.kunzisoft.keepass.credentialprovider.util
|
|
||||||
|
|
||||||
import android.os.Build
|
|
||||||
import android.util.Log
|
|
||||||
import androidx.annotation.RequiresApi
|
|
||||||
import com.kunzisoft.keepass.credentialprovider.data.Passkey
|
|
||||||
import com.kunzisoft.keepass.database.ContextualDatabase
|
|
||||||
import com.kunzisoft.keepass.database.DatabaseTaskProvider
|
|
||||||
import com.kunzisoft.keepass.database.element.Database
|
|
||||||
import com.kunzisoft.keepass.database.element.Entry
|
|
||||||
import com.kunzisoft.keepass.database.element.node.NodeIdUUID
|
|
||||||
import com.kunzisoft.keepass.database.search.SearchHelper
|
|
||||||
import com.kunzisoft.keepass.database.search.SearchParameters
|
|
||||||
import com.kunzisoft.keepass.utils.UuidUtil
|
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.O)
|
|
||||||
class DatabaseHelper {
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
|
|
||||||
fun getAllPasskeys(database: Database): List<Passkey> {
|
|
||||||
val searchHelper = SearchHelper()
|
|
||||||
val searchParameters = SearchParameters().apply {
|
|
||||||
searchQuery = PasskeyConverter.PASSKEY_TAG
|
|
||||||
searchInTitles = false
|
|
||||||
searchInUsernames = false
|
|
||||||
searchInPasswords = false
|
|
||||||
searchInUrls = false
|
|
||||||
searchInNotes = false
|
|
||||||
searchInOTP = false
|
|
||||||
searchInOther = false
|
|
||||||
searchInUUIDs = false
|
|
||||||
searchInTags = true
|
|
||||||
searchInCurrentGroup = false
|
|
||||||
searchInSearchableGroup = false
|
|
||||||
searchInRecycleBin = false
|
|
||||||
searchInTemplates = false
|
|
||||||
}
|
|
||||||
val fromGroup = null
|
|
||||||
val max = Int.MAX_VALUE
|
|
||||||
val searchResult = searchHelper.createVirtualGroupWithSearchResult(
|
|
||||||
database,
|
|
||||||
searchParameters,
|
|
||||||
fromGroup,
|
|
||||||
max
|
|
||||||
)
|
|
||||||
?: return emptyList()
|
|
||||||
|
|
||||||
return PasskeyConverter.convertEntriesListToPasskeys(searchResult.getChildEntries())
|
|
||||||
}
|
|
||||||
|
|
||||||
fun searchPassKeyByNodeId(database: Database, nodeId: String): Passkey? {
|
|
||||||
val uuidToSearch = UuidUtil.fromHexString(nodeId) ?: return null
|
|
||||||
val nodeIdUUIDToSearch = NodeIdUUID(uuidToSearch)
|
|
||||||
val entry = database.getEntryById(nodeIdUUIDToSearch) ?: return null
|
|
||||||
return PasskeyConverter.convertEntryToPasskey(entry)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun updateEntry(
|
|
||||||
database: Database,
|
|
||||||
databaseTaskProvider: DatabaseTaskProvider,
|
|
||||||
updatedPasskey: Passkey
|
|
||||||
) {
|
|
||||||
val oldEntry = Entry(updatedPasskey.databaseEntry!!)
|
|
||||||
val entryToUpdate = Entry(updatedPasskey.databaseEntry)
|
|
||||||
|
|
||||||
PasskeyConverter.setPasskeyInEntry(updatedPasskey, entryToUpdate)
|
|
||||||
|
|
||||||
entryToUpdate.setEntryInfo(
|
|
||||||
database,
|
|
||||||
entryToUpdate.getEntryInfo(
|
|
||||||
database,
|
|
||||||
raw = true,
|
|
||||||
removeTemplateConfiguration = false
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
val save = true
|
|
||||||
databaseTaskProvider.startDatabaseUpdateEntry(oldEntry, entryToUpdate, save)
|
|
||||||
Log.d(this::class.java.simpleName, "passkey in entry ${oldEntry.title} updated")
|
|
||||||
}
|
|
||||||
|
|
||||||
fun saveNewEntry(
|
|
||||||
database: ContextualDatabase,
|
|
||||||
databaseTaskProvider: DatabaseTaskProvider,
|
|
||||||
newPasskey: Passkey
|
|
||||||
) {
|
|
||||||
val newEntry = database.createEntry() ?: throw Exception("can not create new entry")
|
|
||||||
PasskeyConverter.setPasskeyInEntry(newPasskey, newEntry)
|
|
||||||
val save = true
|
|
||||||
|
|
||||||
val group = database.rootGroup
|
|
||||||
?: throw Exception("can not save new entry in database, because rootGroup is null")
|
|
||||||
databaseTaskProvider.startDatabaseCreateEntry(newEntry, group, save)
|
|
||||||
Log.d(this::class.java.simpleName, "new entry saved")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,195 +0,0 @@
|
|||||||
package com.kunzisoft.keepass.credentialprovider.util
|
|
||||||
|
|
||||||
import android.app.Activity
|
|
||||||
import android.app.PendingIntent
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import android.os.Build
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.security.keystore.KeyGenParameterSpec
|
|
||||||
import android.security.keystore.KeyProperties
|
|
||||||
import android.util.Log
|
|
||||||
import androidx.annotation.RequiresApi
|
|
||||||
import com.kunzisoft.keepass.activities.FileDatabaseSelectActivity
|
|
||||||
import com.kunzisoft.keepass.credentialprovider.activity.CreatePasskeyActivity
|
|
||||||
import com.kunzisoft.keepass.credentialprovider.activity.UsePasskeyActivity
|
|
||||||
import com.kunzisoft.keepass.utils.StringUtil.toHexString
|
|
||||||
import java.security.KeyStore
|
|
||||||
import java.security.MessageDigest
|
|
||||||
import java.time.Instant
|
|
||||||
import javax.crypto.KeyGenerator
|
|
||||||
import javax.crypto.Mac
|
|
||||||
import javax.crypto.SecretKey
|
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.O)
|
|
||||||
class IntentHelper {
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
private const val HMAC_TYPE = "HmacSHA256"
|
|
||||||
|
|
||||||
private const val KEY_NODE_ID = "nodeId"
|
|
||||||
private const val KEY_TIMESTAMP = "timestamp"
|
|
||||||
private const val KEY_AUTHENTICATION_CODE = "authenticationCode"
|
|
||||||
|
|
||||||
private const val SEPARATOR = "_"
|
|
||||||
|
|
||||||
private const val NAME_OF_HMAC_KEY = "KeePassDXCredentialProviderHMACKey"
|
|
||||||
|
|
||||||
private const val KEYSTORE_TYPE = "AndroidKeyStore"
|
|
||||||
|
|
||||||
private val PLACEHOLDER_FOR_NEW_NODE_ID = "0".repeat(32)
|
|
||||||
|
|
||||||
private val REGEX_NODE_ID = "[A-F0-9]{32}".toRegex()
|
|
||||||
private val REGEX_TIMESTAMP = "[0-9]{10}".toRegex()
|
|
||||||
private val REGEX_AUTHENTICATION_CODE = "[A-F0-9]{64}".toRegex() // 256 bits = 64 hex chars
|
|
||||||
|
|
||||||
private const val MAX_DIFF_IN_SECONDS = 60
|
|
||||||
|
|
||||||
private var currentRequestCode: Int = 0
|
|
||||||
|
|
||||||
private fun <T : Activity> createPendingIntent(
|
|
||||||
clazz: Class<T>,
|
|
||||||
applicationContext: Context,
|
|
||||||
data: Bundle? = null
|
|
||||||
): PendingIntent {
|
|
||||||
val intent = Intent().setClass(applicationContext, clazz)
|
|
||||||
|
|
||||||
data?.let { intent.putExtras(data) }
|
|
||||||
|
|
||||||
val requestCode = currentRequestCode
|
|
||||||
// keeps the requestCodes unique, the limit is arbitrary
|
|
||||||
currentRequestCode = (currentRequestCode + 1) % 1000
|
|
||||||
return PendingIntent.getActivity(
|
|
||||||
applicationContext,
|
|
||||||
requestCode,
|
|
||||||
intent,
|
|
||||||
PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun <T : Activity> createPendingIntentWithAuthenticationCode(
|
|
||||||
clazz: Class<T>,
|
|
||||||
applicationContext: Context,
|
|
||||||
nodeId: String
|
|
||||||
): PendingIntent? {
|
|
||||||
if (nodeId.matches(REGEX_NODE_ID).not()) return null
|
|
||||||
|
|
||||||
val data = Bundle()
|
|
||||||
val timestamp = Instant.now().epochSecond.toString()
|
|
||||||
data.putString(KEY_NODE_ID, nodeId)
|
|
||||||
data.putString(KEY_TIMESTAMP, timestamp)
|
|
||||||
|
|
||||||
val message = nodeId + SEPARATOR + timestamp
|
|
||||||
val authenticationCode = generateAuthenticationCode(message).toHexString()
|
|
||||||
|
|
||||||
data.putString(KEY_AUTHENTICATION_CODE, authenticationCode)
|
|
||||||
return createPendingIntent(clazz, applicationContext, data)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun generateUnlockPendingIntent(applicationContext: Context): PendingIntent {
|
|
||||||
// TODO after the database is unlocked by the user, return to the flow
|
|
||||||
return createPendingIntent(FileDatabaseSelectActivity::class.java, applicationContext)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun generateCreatePendingIntent(
|
|
||||||
applicationContext: Context,
|
|
||||||
nodeId: String = PLACEHOLDER_FOR_NEW_NODE_ID
|
|
||||||
): PendingIntent? {
|
|
||||||
return createPendingIntentWithAuthenticationCode(
|
|
||||||
CreatePasskeyActivity::class.java,
|
|
||||||
applicationContext,
|
|
||||||
nodeId
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun generateUsagePendingIntent(
|
|
||||||
applicationContext: Context,
|
|
||||||
nodeId: String
|
|
||||||
): PendingIntent? {
|
|
||||||
return createPendingIntentWithAuthenticationCode(
|
|
||||||
UsePasskeyActivity::class.java,
|
|
||||||
applicationContext,
|
|
||||||
nodeId
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getVerifiedNodeId(intent: Intent): String? {
|
|
||||||
val nodeId = intent.getStringExtra(KEY_NODE_ID) ?: return null
|
|
||||||
val timestampString = intent.getStringExtra(KEY_TIMESTAMP) ?: return null
|
|
||||||
val authenticationCode = intent.getStringExtra(KEY_AUTHENTICATION_CODE) ?: return null
|
|
||||||
|
|
||||||
if (nodeId.matches(REGEX_NODE_ID).not() ||
|
|
||||||
timestampString.matches(REGEX_TIMESTAMP).not() ||
|
|
||||||
authenticationCode.matches(REGEX_AUTHENTICATION_CODE).not()
|
|
||||||
) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
val diff = Instant.now().epochSecond - timestampString.toLong()
|
|
||||||
if (diff < 0 || diff > MAX_DIFF_IN_SECONDS) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
val message = (nodeId + SEPARATOR + timestampString)
|
|
||||||
if (verifyAuthenticationCode(
|
|
||||||
message,
|
|
||||||
authenticationCode.decodeHexToByteArray()
|
|
||||||
).not()
|
|
||||||
) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
Log.d(this::class.java.simpleName, "nodeId $nodeId verified")
|
|
||||||
return nodeId
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun verifyAuthenticationCode(
|
|
||||||
message: String,
|
|
||||||
authenticationCodeIn: ByteArray
|
|
||||||
): Boolean {
|
|
||||||
val authenticationCode = generateAuthenticationCode(message)
|
|
||||||
return MessageDigest.isEqual(authenticationCodeIn, authenticationCode)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun generateAuthenticationCode(message: String): ByteArray {
|
|
||||||
val keyStore = KeyStore.getInstance(KEYSTORE_TYPE)
|
|
||||||
keyStore.load(null)
|
|
||||||
val hmacKey = try {
|
|
||||||
keyStore.getKey(NAME_OF_HMAC_KEY, null) as SecretKey
|
|
||||||
} catch (e: Exception) {
|
|
||||||
// key not found
|
|
||||||
generateKey()
|
|
||||||
}
|
|
||||||
|
|
||||||
val mac = Mac.getInstance(HMAC_TYPE)
|
|
||||||
mac.init(hmacKey)
|
|
||||||
val authenticationCode = mac.doFinal(message.toByteArray())
|
|
||||||
return authenticationCode
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun generateKey(): SecretKey? {
|
|
||||||
val keyGenerator = KeyGenerator.getInstance(
|
|
||||||
KeyProperties.KEY_ALGORITHM_HMAC_SHA256, KEYSTORE_TYPE
|
|
||||||
)
|
|
||||||
val keySizeInBits = 128
|
|
||||||
keyGenerator.init(
|
|
||||||
KeyGenParameterSpec.Builder(NAME_OF_HMAC_KEY, KeyProperties.PURPOSE_SIGN)
|
|
||||||
.setKeySize(keySizeInBits)
|
|
||||||
.build()
|
|
||||||
)
|
|
||||||
val key = keyGenerator.generateKey()
|
|
||||||
return key
|
|
||||||
}
|
|
||||||
|
|
||||||
fun isPlaceholderNodeId(nodeId: String): Boolean {
|
|
||||||
return nodeId == PLACEHOLDER_FOR_NEW_NODE_ID
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun String.decodeHexToByteArray(): ByteArray {
|
|
||||||
if (length % 2 != 0) {
|
|
||||||
throw IllegalArgumentException("Must have an even length")
|
|
||||||
}
|
|
||||||
return chunked(2).map { it.toInt(16).toByte() }.toByteArray()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,120 +0,0 @@
|
|||||||
package com.kunzisoft.keepass.credentialprovider.util
|
|
||||||
|
|
||||||
import android.os.Build
|
|
||||||
import androidx.annotation.RequiresApi
|
|
||||||
import com.kunzisoft.keepass.credentialprovider.data.Passkey
|
|
||||||
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.security.ProtectedString
|
|
||||||
import com.kunzisoft.keepass.utils.UuidUtil
|
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.O)
|
|
||||||
class PasskeyConverter {
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
|
|
||||||
// field names from KeypassXC are used
|
|
||||||
private const val FIELD_USERNAME = "KPEX_PASSKEY_USERNAME"
|
|
||||||
private const val FIELD_PRIVATE_KEY = "KPEX_PASSKEY_PRIVATE_KEY_PEM"
|
|
||||||
private const val FIELD_CREDENTIAL_ID = "KPEX_PASSKEY_CREDENTIAL_ID"
|
|
||||||
private const val FIELD_USER_HANDLE = "KPEX_PASSKEY_USER_HANDLE"
|
|
||||||
private const val FIELD_RELYING_PARTY = "KPEX_PASSKEY_RELYING_PARTY"
|
|
||||||
|
|
||||||
const val PASSKEY_TAG = "Passkey"
|
|
||||||
|
|
||||||
fun convertEntryToPasskey(entry: Entry): Passkey? {
|
|
||||||
if (entry.tags.toList().contains(PASSKEY_TAG).not()) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
val nodeId = UuidUtil.toHexString(entry.nodeId.id) ?: return null
|
|
||||||
|
|
||||||
val displayName = entry.getVisualTitle()
|
|
||||||
|
|
||||||
var username = ""
|
|
||||||
var privateKeyPem = ""
|
|
||||||
var credId = ""
|
|
||||||
var userHandle = ""
|
|
||||||
var relyingParty = ""
|
|
||||||
|
|
||||||
for (field in entry.getExtraFields()) {
|
|
||||||
val fieldName = field.name
|
|
||||||
|
|
||||||
if (fieldName == FIELD_USERNAME) {
|
|
||||||
username = field.protectedValue.stringValue
|
|
||||||
} else if (field.name == FIELD_PRIVATE_KEY) {
|
|
||||||
privateKeyPem = field.protectedValue.stringValue
|
|
||||||
} else if (field.name == FIELD_CREDENTIAL_ID) {
|
|
||||||
credId = field.protectedValue.stringValue
|
|
||||||
} else if (field.name == FIELD_USER_HANDLE) {
|
|
||||||
userHandle = field.protectedValue.stringValue
|
|
||||||
} else if (field.name == FIELD_RELYING_PARTY) {
|
|
||||||
relyingParty = field.protectedValue.stringValue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return Passkey(
|
|
||||||
nodeId,
|
|
||||||
username,
|
|
||||||
displayName,
|
|
||||||
privateKeyPem,
|
|
||||||
credId,
|
|
||||||
userHandle,
|
|
||||||
relyingParty,
|
|
||||||
entry
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun convertEntriesListToPasskeys(entries: List<Entry>): List<Passkey> {
|
|
||||||
return entries.mapNotNull { e -> convertEntryToPasskey(e) }
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
fun setPasskeyInEntry(passkey: Passkey, entry: Entry) {
|
|
||||||
entry.tags.put(PASSKEY_TAG)
|
|
||||||
|
|
||||||
entry.title = passkey.displayName
|
|
||||||
entry.lastModificationTime = DateInstant()
|
|
||||||
|
|
||||||
entry.username = passkey.username
|
|
||||||
|
|
||||||
entry.url = OriginHelper.DEFAULT_PROTOCOL + passkey.relyingParty
|
|
||||||
|
|
||||||
val protected = true
|
|
||||||
val unProtected = false
|
|
||||||
|
|
||||||
entry.putExtraField(
|
|
||||||
Field(
|
|
||||||
FIELD_USERNAME,
|
|
||||||
ProtectedString(unProtected, passkey.username)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
entry.putExtraField(
|
|
||||||
Field(
|
|
||||||
FIELD_PRIVATE_KEY,
|
|
||||||
ProtectedString(protected, passkey.privateKeyPem)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
entry.putExtraField(
|
|
||||||
Field(
|
|
||||||
FIELD_CREDENTIAL_ID,
|
|
||||||
ProtectedString(protected, passkey.credId)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
entry.putExtraField(
|
|
||||||
Field(
|
|
||||||
FIELD_USER_HANDLE,
|
|
||||||
ProtectedString(protected, passkey.userHandle)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
entry.putExtraField(
|
|
||||||
Field(
|
|
||||||
FIELD_RELYING_PARTY,
|
|
||||||
ProtectedString(unProtected, passkey.relyingParty)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -43,13 +43,15 @@ object SearchHelper {
|
|||||||
/**
|
/**
|
||||||
* Utility method to perform actions if item is found or not after an auto search in [database]
|
* Utility method to perform actions if item is found or not after an auto search in [database]
|
||||||
*/
|
*/
|
||||||
fun checkAutoSearchInfo(context: Context,
|
fun checkAutoSearchInfo(
|
||||||
database: ContextualDatabase?,
|
context: Context,
|
||||||
searchInfo: SearchInfo?,
|
database: ContextualDatabase?,
|
||||||
onItemsFound: (openedDatabase: ContextualDatabase,
|
searchInfo: SearchInfo?,
|
||||||
items: List<EntryInfo>) -> Unit,
|
onItemsFound: (openedDatabase: ContextualDatabase,
|
||||||
onItemNotFound: (openedDatabase: ContextualDatabase) -> Unit,
|
items: List<EntryInfo>) -> Unit,
|
||||||
onDatabaseClosed: () -> Unit) {
|
onItemNotFound: (openedDatabase: ContextualDatabase) -> Unit,
|
||||||
|
onDatabaseClosed: () -> Unit
|
||||||
|
) {
|
||||||
if (database == null || !database.loaded) {
|
if (database == null || !database.loaded) {
|
||||||
onDatabaseClosed.invoke()
|
onDatabaseClosed.invoke()
|
||||||
} else if (TimeoutHelper.checkTime(context)) {
|
} else if (TimeoutHelper.checkTime(context)) {
|
||||||
@@ -59,8 +61,7 @@ object SearchHelper {
|
|||||||
&& !searchInfo.containsOnlyNullValues()) {
|
&& !searchInfo.containsOnlyNullValues()) {
|
||||||
// If search provide results
|
// If search provide results
|
||||||
database.createVirtualGroupFromSearchInfo(
|
database.createVirtualGroupFromSearchInfo(
|
||||||
searchInfo.toString(),
|
searchInfo,
|
||||||
searchInfo.isASearchByDomain(),
|
|
||||||
MAX_SEARCH_ENTRY
|
MAX_SEARCH_ENTRY
|
||||||
)?.let { searchGroup ->
|
)?.let { searchGroup ->
|
||||||
if (searchGroup.numberOfChildEntries > 0) {
|
if (searchGroup.numberOfChildEntries > 0) {
|
||||||
|
|||||||
@@ -1,13 +1,9 @@
|
|||||||
package com.kunzisoft.keepass.receivers
|
package com.kunzisoft.keepass.receivers
|
||||||
|
|
||||||
import android.content.BroadcastReceiver
|
import android.content.BroadcastReceiver
|
||||||
import android.content.ComponentName
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.pm.PackageManager
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.kunzisoft.keepass.magikeyboard.MagikeyboardService
|
|
||||||
import com.kunzisoft.keepass.utils.DexUtil
|
|
||||||
import com.kunzisoft.keepass.utils.MagikeyboardUtil
|
import com.kunzisoft.keepass.utils.MagikeyboardUtil
|
||||||
|
|
||||||
class DexModeReceiver : BroadcastReceiver() {
|
class DexModeReceiver : BroadcastReceiver() {
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ import android.util.Log
|
|||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.preference.PreferenceManager
|
import androidx.preference.PreferenceManager
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.magikeyboard.MagikeyboardService
|
import com.kunzisoft.keepass.credentialprovider.magikeyboard.MagikeyboardService
|
||||||
import com.kunzisoft.keepass.model.EntryInfo
|
import com.kunzisoft.keepass.model.EntryInfo
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ import android.util.Log
|
|||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.database.ContextualDatabase
|
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||||
import com.kunzisoft.keepass.magikeyboard.MagikeyboardService
|
import com.kunzisoft.keepass.credentialprovider.magikeyboard.MagikeyboardService
|
||||||
import com.kunzisoft.keepass.services.ClipboardEntryNotificationService
|
import com.kunzisoft.keepass.services.ClipboardEntryNotificationService
|
||||||
import com.kunzisoft.keepass.services.KeyboardEntryNotificationService
|
import com.kunzisoft.keepass.services.KeyboardEntryNotificationService
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import android.content.ComponentName
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.kunzisoft.keepass.magikeyboard.MagikeyboardService
|
import com.kunzisoft.keepass.credentialprovider.magikeyboard.MagikeyboardService
|
||||||
|
|
||||||
object MagikeyboardUtil {
|
object MagikeyboardUtil {
|
||||||
private val TAG = MagikeyboardUtil::class.java.name
|
private val TAG = MagikeyboardUtil::class.java.name
|
||||||
|
|||||||
@@ -93,7 +93,7 @@
|
|||||||
android:maxLines="1"
|
android:maxLines="1"
|
||||||
android:ellipsize="end"
|
android:ellipsize="end"
|
||||||
android:textColor="@color/grey_blue_slighter"/>
|
android:textColor="@color/grey_blue_slighter"/>
|
||||||
<com.kunzisoft.keepass.magikeyboard.KeyboardView
|
<com.kunzisoft.keepass.credentialprovider.magikeyboard.KeyboardView
|
||||||
android:id="@+id/magikeyboard_view"
|
android:id="@+id/magikeyboard_view"
|
||||||
style="@style/KeepassDXStyle.Keyboard"
|
style="@style/KeepassDXStyle.Keyboard"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
|
|||||||
@@ -739,14 +739,12 @@
|
|||||||
<string name="hide_expired_entries_summary">Expired entries are not shown</string>
|
<string name="hide_expired_entries_summary">Expired entries are not shown</string>
|
||||||
<string name="hide_templates_title">Hide templates</string>
|
<string name="hide_templates_title">Hide templates</string>
|
||||||
<string name="hide_templates_summary">Templates are not shown</string>
|
<string name="hide_templates_summary">Templates are not shown</string>
|
||||||
<string name="passkey_usage_biometric_prompt_title">Confirm passkey usage</string>
|
<string name="passkey">Passkey</string>
|
||||||
<string name="passkey_usage_biometric_prompt_subtitle">for %1$s</string>
|
<string name="passkey_service_name">KeePassDX Credential Provider</string>
|
||||||
<string name="passkey_usage_biometric_prompt_negative_button_text">Cancel</string>
|
|
||||||
<string name="passkey_creation_biometric_prompt_title">Confirm passkey creation</string>
|
|
||||||
<string name="passkey_creation_biometric_prompt_subtitle">for %1$s</string>
|
|
||||||
<string name="passkey_creation_biometric_prompt_negative_button_text">Cancel</string>
|
|
||||||
<string name="passkey_creation_description">Save passkey in new entry</string>
|
<string name="passkey_creation_description">Save passkey in new entry</string>
|
||||||
<string name="passkey_update_description">Update passkey in "%1$s"</string>
|
<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_locked_database_description">Select to unlock</string>
|
<string name="passkey_locked_database_description">Select to unlock</string>
|
||||||
<string name="passkey_locked_database_account_name">KeePassDX Database Locked</string>
|
|
||||||
</resources>
|
</resources>
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
apply plugin: 'com.android.library'
|
apply plugin: 'com.android.library'
|
||||||
apply plugin: 'kotlin-android'
|
apply plugin: 'kotlin-android'
|
||||||
|
apply plugin: 'kotlin-parcelize'
|
||||||
|
|
||||||
android {
|
android {
|
||||||
namespace 'com.kunzisoft.keepass.database'
|
namespace 'com.kunzisoft.keepass.database'
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ import com.kunzisoft.keepass.database.merge.DatabaseKDBXMerger
|
|||||||
import com.kunzisoft.keepass.database.search.SearchHelper
|
import com.kunzisoft.keepass.database.search.SearchHelper
|
||||||
import com.kunzisoft.keepass.database.search.SearchParameters
|
import com.kunzisoft.keepass.database.search.SearchParameters
|
||||||
import com.kunzisoft.keepass.hardware.HardwareKey
|
import com.kunzisoft.keepass.hardware.HardwareKey
|
||||||
|
import com.kunzisoft.keepass.model.SearchInfo
|
||||||
import com.kunzisoft.keepass.tasks.ProgressTaskUpdater
|
import com.kunzisoft.keepass.tasks.ProgressTaskUpdater
|
||||||
import com.kunzisoft.keepass.utils.SingletonHolder
|
import com.kunzisoft.keepass.utils.SingletonHolder
|
||||||
import com.kunzisoft.keepass.utils.StringUtil.toFormattedColorInt
|
import com.kunzisoft.keepass.utils.StringUtil.toFormattedColorInt
|
||||||
@@ -885,28 +886,15 @@ open class Database {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun createVirtualGroupFromSearchInfo(
|
fun createVirtualGroupFromSearchInfo(
|
||||||
searchInfoString: String,
|
searchInfo: SearchInfo,
|
||||||
searchInfoByDomain: Boolean,
|
|
||||||
max: Int = Integer.MAX_VALUE
|
max: Int = Integer.MAX_VALUE
|
||||||
): Group? {
|
): Group? {
|
||||||
return mSearchHelper.createVirtualGroupWithSearchResult(this,
|
return mSearchHelper.createVirtualGroupWithSearchResult(
|
||||||
SearchParameters().apply {
|
database = this,
|
||||||
searchQuery = searchInfoString
|
searchParameters = searchInfo.buildSearchParameters(),
|
||||||
searchInTitles = true
|
fromGroup = null,
|
||||||
searchInUsernames = false
|
max = max
|
||||||
searchInPasswords = false
|
)
|
||||||
searchInUrls = true
|
|
||||||
searchByDomain = searchInfoByDomain
|
|
||||||
searchInNotes = true
|
|
||||||
searchInOTP = false
|
|
||||||
searchInOther = true
|
|
||||||
searchInUUIDs = false
|
|
||||||
searchInTags = false
|
|
||||||
searchInCurrentGroup = false
|
|
||||||
searchInSearchableGroup = true
|
|
||||||
searchInRecycleBin = false
|
|
||||||
searchInTemplates = false
|
|
||||||
}, null, max)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val tagPool: Tags
|
val tagPool: Tags
|
||||||
|
|||||||
@@ -49,6 +49,10 @@ class Tags: Parcelable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun contains(tag: String): Boolean {
|
||||||
|
return mTags.contains(tag)
|
||||||
|
}
|
||||||
|
|
||||||
fun isEmpty(): Boolean {
|
fun isEmpty(): Boolean {
|
||||||
return mTags.isEmpty()
|
return mTags.isEmpty()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import com.kunzisoft.keepass.database.element.Entry
|
|||||||
import com.kunzisoft.keepass.database.element.Group
|
import com.kunzisoft.keepass.database.element.Group
|
||||||
import com.kunzisoft.keepass.database.element.node.NodeHandler
|
import com.kunzisoft.keepass.database.element.node.NodeHandler
|
||||||
import com.kunzisoft.keepass.database.element.node.NodeId
|
import com.kunzisoft.keepass.database.element.node.NodeId
|
||||||
|
import com.kunzisoft.keepass.model.EntryInfoPasskey.FIELD_RELYING_PARTY
|
||||||
import com.kunzisoft.keepass.otp.OtpEntryFields.OTP_FIELD
|
import com.kunzisoft.keepass.otp.OtpEntryFields.OTP_FIELD
|
||||||
import com.kunzisoft.keepass.utils.UuidUtil
|
import com.kunzisoft.keepass.utils.UuidUtil
|
||||||
import com.kunzisoft.keepass.utils.inTheSameDomainAs
|
import com.kunzisoft.keepass.utils.inTheSameDomainAs
|
||||||
@@ -171,8 +172,10 @@ class SearchHelper {
|
|||||||
}
|
}
|
||||||
if (searchParameters.searchInOther) {
|
if (searchParameters.searchInOther) {
|
||||||
entry.getExtraFields().forEach { field ->
|
entry.getExtraFields().forEach { field ->
|
||||||
if (field.name != OTP_FIELD
|
if ( (field.name != OTP_FIELD && field.name != FIELD_RELYING_PARTY)
|
||||||
|| (field.name == OTP_FIELD && searchParameters.searchInOTP)) {
|
|| (searchParameters.searchInOTP && field.name == OTP_FIELD)
|
||||||
|
|| (searchParameters.searchInRelyingParty && field.name == FIELD_RELYING_PARTY)
|
||||||
|
) {
|
||||||
if (checkSearchQuery(field.protectedValue.toString(), searchParameters))
|
if (checkSearchQuery(field.protectedValue.toString(), searchParameters))
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ class SearchParameters() : Parcelable{
|
|||||||
var searchInExpired = false
|
var searchInExpired = false
|
||||||
var searchInNotes = true
|
var searchInNotes = true
|
||||||
var searchInOTP = false
|
var searchInOTP = false
|
||||||
|
var searchInRelyingParty = false
|
||||||
var searchInOther = true
|
var searchInOther = true
|
||||||
var searchInUUIDs = false
|
var searchInUUIDs = false
|
||||||
var searchInTags = false
|
var searchInTags = false
|
||||||
|
|||||||
@@ -22,10 +22,15 @@ package com.kunzisoft.keepass.model
|
|||||||
import android.os.Parcel
|
import android.os.Parcel
|
||||||
import android.os.ParcelUuid
|
import android.os.ParcelUuid
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import com.kunzisoft.keepass.database.element.*
|
import com.kunzisoft.keepass.database.element.Attachment
|
||||||
|
import com.kunzisoft.keepass.database.element.Database
|
||||||
|
import com.kunzisoft.keepass.database.element.DateInstant
|
||||||
|
import com.kunzisoft.keepass.database.element.Field
|
||||||
|
import com.kunzisoft.keepass.database.element.Tags
|
||||||
import com.kunzisoft.keepass.database.element.entry.AutoType
|
import com.kunzisoft.keepass.database.element.entry.AutoType
|
||||||
import com.kunzisoft.keepass.database.element.security.ProtectedString
|
import com.kunzisoft.keepass.database.element.security.ProtectedString
|
||||||
import com.kunzisoft.keepass.database.element.template.TemplateField
|
import com.kunzisoft.keepass.database.element.template.TemplateField
|
||||||
|
import com.kunzisoft.keepass.model.EntryInfoPasskey.setPasskey
|
||||||
import com.kunzisoft.keepass.otp.OtpElement
|
import com.kunzisoft.keepass.otp.OtpElement
|
||||||
import com.kunzisoft.keepass.otp.OtpEntryFields
|
import com.kunzisoft.keepass.otp.OtpEntryFields
|
||||||
import com.kunzisoft.keepass.otp.OtpEntryFields.OTP_TOKEN_FIELD
|
import com.kunzisoft.keepass.otp.OtpEntryFields.OTP_TOKEN_FIELD
|
||||||
@@ -33,7 +38,8 @@ import com.kunzisoft.keepass.utils.readBooleanCompat
|
|||||||
import com.kunzisoft.keepass.utils.readListCompat
|
import com.kunzisoft.keepass.utils.readListCompat
|
||||||
import com.kunzisoft.keepass.utils.readParcelableCompat
|
import com.kunzisoft.keepass.utils.readParcelableCompat
|
||||||
import com.kunzisoft.keepass.utils.writeBooleanCompat
|
import com.kunzisoft.keepass.utils.writeBooleanCompat
|
||||||
import java.util.*
|
import java.util.Locale
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
class EntryInfo : NodeInfo {
|
class EntryInfo : NodeInfo {
|
||||||
|
|
||||||
@@ -113,6 +119,14 @@ class EntryInfo : NodeInfo {
|
|||||||
return customFields.lastOrNull { it.name == label }?.protectedValue?.toString() ?: ""
|
return customFields.lastOrNull { it.name == label }?.protectedValue?.toString() ?: ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun addOrReplaceField(field: Field) {
|
||||||
|
customFields.lastOrNull { it.name == field.name }?.let {
|
||||||
|
it.apply {
|
||||||
|
protectedValue = field.protectedValue
|
||||||
|
}
|
||||||
|
} ?: customFields.add(field)
|
||||||
|
}
|
||||||
|
|
||||||
// Return true if modified
|
// Return true if modified
|
||||||
private fun addUniqueField(field: Field, number: Int = 0) {
|
private fun addUniqueField(field: Field, number: Int = 0) {
|
||||||
var sameName = false
|
var sameName = false
|
||||||
@@ -226,6 +240,7 @@ class EntryInfo : NodeInfo {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (database?.allowEntryCustomFields() == true) {
|
if (database?.allowEntryCustomFields() == true) {
|
||||||
|
// TODO Move in a dedicated creditcard class
|
||||||
val creditCard: CreditCard? = registerInfo.creditCard
|
val creditCard: CreditCard? = registerInfo.creditCard
|
||||||
creditCard?.cardholder?.let {
|
creditCard?.cardholder?.let {
|
||||||
addUniqueField(Field(TemplateField.LABEL_HOLDER, ProtectedString(false, it)))
|
addUniqueField(Field(TemplateField.LABEL_HOLDER, ProtectedString(false, it)))
|
||||||
@@ -240,6 +255,9 @@ class EntryInfo : NodeInfo {
|
|||||||
creditCard?.cvv?.let {
|
creditCard?.cvv?.let {
|
||||||
addUniqueField(Field(TemplateField.LABEL_CVV, ProtectedString(true, it)))
|
addUniqueField(Field(TemplateField.LABEL_CVV, ProtectedString(true, it)))
|
||||||
}
|
}
|
||||||
|
registerInfo.passkey?.let {
|
||||||
|
setPasskey(it)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,91 @@
|
|||||||
|
package com.kunzisoft.keepass.model
|
||||||
|
|
||||||
|
import com.kunzisoft.keepass.database.element.Field
|
||||||
|
import com.kunzisoft.keepass.database.element.security.ProtectedString
|
||||||
|
|
||||||
|
object EntryInfoPasskey {
|
||||||
|
|
||||||
|
// field names from KeypassXC are used
|
||||||
|
private const val FIELD_USERNAME = "KPEX_PASSKEY_USERNAME"
|
||||||
|
private const val FIELD_PRIVATE_KEY = "KPEX_PASSKEY_PRIVATE_KEY_PEM"
|
||||||
|
private const val FIELD_CREDENTIAL_ID = "KPEX_PASSKEY_CREDENTIAL_ID"
|
||||||
|
private const val FIELD_USER_HANDLE = "KPEX_PASSKEY_USER_HANDLE"
|
||||||
|
const val FIELD_RELYING_PARTY = "KPEX_PASSKEY_RELYING_PARTY"
|
||||||
|
|
||||||
|
private const val PASSKEY_TAG = "Passkey"
|
||||||
|
|
||||||
|
fun EntryInfo.getPasskey(): Passkey? {
|
||||||
|
if (this.tags.toList().contains(PASSKEY_TAG).not()) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
var username = ""
|
||||||
|
var privateKeyPem = ""
|
||||||
|
var credId = ""
|
||||||
|
var userHandle = ""
|
||||||
|
var relyingParty = ""
|
||||||
|
for (field in this.customFields) {
|
||||||
|
when (field.name) {
|
||||||
|
FIELD_USERNAME -> {
|
||||||
|
username = field.protectedValue.stringValue
|
||||||
|
}
|
||||||
|
FIELD_PRIVATE_KEY -> {
|
||||||
|
privateKeyPem = field.protectedValue.stringValue
|
||||||
|
}
|
||||||
|
FIELD_CREDENTIAL_ID -> {
|
||||||
|
credId = field.protectedValue.stringValue
|
||||||
|
}
|
||||||
|
FIELD_USER_HANDLE -> {
|
||||||
|
userHandle = field.protectedValue.stringValue
|
||||||
|
}
|
||||||
|
FIELD_RELYING_PARTY -> {
|
||||||
|
relyingParty = field.protectedValue.stringValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Passkey(
|
||||||
|
username = username,
|
||||||
|
displayName = this.getVisualTitle(),
|
||||||
|
privateKeyPem = privateKeyPem,
|
||||||
|
credentialId = credId,
|
||||||
|
userHandle = userHandle,
|
||||||
|
relyingParty = relyingParty
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun EntryInfo.setPasskey(passkey: Passkey) {
|
||||||
|
tags.put(PASSKEY_TAG)
|
||||||
|
title = passkey.displayName
|
||||||
|
username = passkey.username
|
||||||
|
url = passkey.relyingParty
|
||||||
|
addOrReplaceField(
|
||||||
|
Field(
|
||||||
|
FIELD_USERNAME,
|
||||||
|
ProtectedString(enableProtection = false, passkey.username)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
addOrReplaceField(
|
||||||
|
Field(
|
||||||
|
FIELD_PRIVATE_KEY,
|
||||||
|
ProtectedString(enableProtection = true, passkey.privateKeyPem)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
addOrReplaceField(
|
||||||
|
Field(
|
||||||
|
FIELD_CREDENTIAL_ID,
|
||||||
|
ProtectedString(enableProtection = true, passkey.credentialId)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
addOrReplaceField(
|
||||||
|
Field(
|
||||||
|
FIELD_USER_HANDLE,
|
||||||
|
ProtectedString(enableProtection = true, passkey.userHandle)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
addOrReplaceField(
|
||||||
|
Field(
|
||||||
|
FIELD_RELYING_PARTY,
|
||||||
|
ProtectedString(enableProtection = false, passkey.relyingParty)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package com.kunzisoft.keepass.model
|
||||||
|
|
||||||
|
import android.os.Parcelable
|
||||||
|
import kotlinx.parcelize.Parcelize
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
data class Passkey(
|
||||||
|
val username: String,
|
||||||
|
val displayName: String,
|
||||||
|
val privateKeyPem: String,
|
||||||
|
val credentialId: String,
|
||||||
|
val userHandle: String,
|
||||||
|
val relyingParty: String,
|
||||||
|
): Parcelable
|
||||||
@@ -4,15 +4,19 @@ import android.os.Parcel
|
|||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import com.kunzisoft.keepass.utils.readParcelableCompat
|
import com.kunzisoft.keepass.utils.readParcelableCompat
|
||||||
|
|
||||||
data class RegisterInfo(val searchInfo: SearchInfo,
|
data class RegisterInfo(
|
||||||
val username: String?,
|
val searchInfo: SearchInfo,
|
||||||
val password: String?,
|
val username: String?,
|
||||||
val creditCard: CreditCard?): Parcelable {
|
val password: String? = null,
|
||||||
|
val creditCard: CreditCard? = null,
|
||||||
|
val passkey: Passkey? = null
|
||||||
|
): Parcelable {
|
||||||
|
|
||||||
constructor(parcel: Parcel) : this(
|
constructor(parcel: Parcel) : this(
|
||||||
parcel.readParcelableCompat() ?: SearchInfo(),
|
parcel.readParcelableCompat() ?: SearchInfo(),
|
||||||
parcel.readString() ?: "",
|
parcel.readString() ?: "",
|
||||||
parcel.readString() ?: "",
|
parcel.readString() ?: "",
|
||||||
|
parcel.readParcelableCompat(),
|
||||||
parcel.readParcelableCompat()) {
|
parcel.readParcelableCompat()) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -21,6 +25,7 @@ data class RegisterInfo(val searchInfo: SearchInfo,
|
|||||||
parcel.writeString(username)
|
parcel.writeString(username)
|
||||||
parcel.writeString(password)
|
parcel.writeString(password)
|
||||||
parcel.writeParcelable(creditCard, flags)
|
parcel.writeParcelable(creditCard, flags)
|
||||||
|
parcel.writeParcelable(passkey, flags)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun describeContents(): Int {
|
override fun describeContents(): Int {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import android.content.res.Resources
|
|||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Parcel
|
import android.os.Parcel
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
|
import com.kunzisoft.keepass.database.search.SearchParameters
|
||||||
import com.kunzisoft.keepass.otp.OtpEntryFields
|
import com.kunzisoft.keepass.otp.OtpEntryFields
|
||||||
import com.kunzisoft.keepass.utils.ObjectNameResource
|
import com.kunzisoft.keepass.utils.ObjectNameResource
|
||||||
import com.kunzisoft.keepass.utils.readBooleanCompat
|
import com.kunzisoft.keepass.utils.readBooleanCompat
|
||||||
@@ -33,6 +34,7 @@ class SearchInfo : ObjectNameResource, Parcelable {
|
|||||||
get() {
|
get() {
|
||||||
return if (webDomain == null) null else field
|
return if (webDomain == null) null else field
|
||||||
}
|
}
|
||||||
|
var relyingParty: String? = null
|
||||||
var otpString: String? = null
|
var otpString: String? = null
|
||||||
|
|
||||||
constructor()
|
constructor()
|
||||||
@@ -42,6 +44,7 @@ class SearchInfo : ObjectNameResource, Parcelable {
|
|||||||
applicationId = toCopy?.applicationId
|
applicationId = toCopy?.applicationId
|
||||||
webDomain = toCopy?.webDomain
|
webDomain = toCopy?.webDomain
|
||||||
webScheme = toCopy?.webScheme
|
webScheme = toCopy?.webScheme
|
||||||
|
relyingParty = toCopy?.relyingParty
|
||||||
otpString = toCopy?.otpString
|
otpString = toCopy?.otpString
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,6 +56,8 @@ class SearchInfo : ObjectNameResource, Parcelable {
|
|||||||
webDomain = if (readDomain.isNullOrEmpty()) null else readDomain
|
webDomain = if (readDomain.isNullOrEmpty()) null else readDomain
|
||||||
val readScheme = parcel.readString()
|
val readScheme = parcel.readString()
|
||||||
webScheme = if (readScheme.isNullOrEmpty()) null else readScheme
|
webScheme = if (readScheme.isNullOrEmpty()) null else readScheme
|
||||||
|
val readRelyingParty = parcel.readString()
|
||||||
|
relyingParty = if (readRelyingParty.isNullOrEmpty()) null else readRelyingParty
|
||||||
val readOtp = parcel.readString()
|
val readOtp = parcel.readString()
|
||||||
otpString = if (readOtp.isNullOrEmpty()) null else readOtp
|
otpString = if (readOtp.isNullOrEmpty()) null else readOtp
|
||||||
}
|
}
|
||||||
@@ -66,6 +71,7 @@ class SearchInfo : ObjectNameResource, Parcelable {
|
|||||||
parcel.writeString(applicationId ?: "")
|
parcel.writeString(applicationId ?: "")
|
||||||
parcel.writeString(webDomain ?: "")
|
parcel.writeString(webDomain ?: "")
|
||||||
parcel.writeString(webScheme ?: "")
|
parcel.writeString(webScheme ?: "")
|
||||||
|
parcel.writeString(relyingParty ?: "")
|
||||||
parcel.writeString(otpString ?: "")
|
parcel.writeString(otpString ?: "")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -82,12 +88,55 @@ class SearchInfo : ObjectNameResource, Parcelable {
|
|||||||
return applicationId == null
|
return applicationId == null
|
||||||
&& webDomain == null
|
&& webDomain == null
|
||||||
&& webScheme == null
|
&& webScheme == null
|
||||||
|
&& relyingParty == null
|
||||||
&& otpString == null
|
&& otpString == null
|
||||||
}
|
}
|
||||||
|
|
||||||
fun isASearchByDomain(): Boolean {
|
private fun isASearchByDomain(): Boolean {
|
||||||
return toString() == webDomain && webDomain != null
|
return toString() == webDomain && webDomain != null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun isAPasskeySearch(): Boolean {
|
||||||
|
return toString() == relyingParty && relyingParty != null
|
||||||
|
}
|
||||||
|
|
||||||
|
fun buildSearchParameters(): SearchParameters {
|
||||||
|
return SearchParameters().apply {
|
||||||
|
if (isAPasskeySearch()) {
|
||||||
|
searchQuery = toString()
|
||||||
|
searchInTitles = false
|
||||||
|
searchInUsernames = false
|
||||||
|
searchInPasswords = false
|
||||||
|
searchInUrls = false
|
||||||
|
searchInNotes = false
|
||||||
|
searchInOTP = false
|
||||||
|
searchInOther = false
|
||||||
|
searchInUUIDs = false
|
||||||
|
searchInTags = false
|
||||||
|
searchInRelyingParty = true
|
||||||
|
searchInCurrentGroup = false
|
||||||
|
searchInSearchableGroup = false
|
||||||
|
searchInRecycleBin = false
|
||||||
|
searchInTemplates = false
|
||||||
|
} else {
|
||||||
|
searchQuery = toString()
|
||||||
|
searchInTitles = true
|
||||||
|
searchInUsernames = false
|
||||||
|
searchInPasswords = false
|
||||||
|
searchInUrls = true
|
||||||
|
searchByDomain = isASearchByDomain()
|
||||||
|
searchInNotes = true
|
||||||
|
searchInOTP = false
|
||||||
|
searchInOther = true
|
||||||
|
searchInUUIDs = false
|
||||||
|
searchInTags = false
|
||||||
|
searchInCurrentGroup = false
|
||||||
|
searchInSearchableGroup = true
|
||||||
|
searchInRecycleBin = false
|
||||||
|
searchInTemplates = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun equals(other: Any?): Boolean {
|
override fun equals(other: Any?): Boolean {
|
||||||
if (this === other) return true
|
if (this === other) return true
|
||||||
@@ -97,6 +146,7 @@ class SearchInfo : ObjectNameResource, Parcelable {
|
|||||||
if (applicationId != other.applicationId) return false
|
if (applicationId != other.applicationId) return false
|
||||||
if (webDomain != other.webDomain) return false
|
if (webDomain != other.webDomain) return false
|
||||||
if (webScheme != other.webScheme) return false
|
if (webScheme != other.webScheme) return false
|
||||||
|
if (relyingParty != other.relyingParty) return false
|
||||||
if (otpString != other.otpString) return false
|
if (otpString != other.otpString) return false
|
||||||
|
|
||||||
return true
|
return true
|
||||||
@@ -107,12 +157,13 @@ class SearchInfo : ObjectNameResource, Parcelable {
|
|||||||
result = 31 * result + (applicationId?.hashCode() ?: 0)
|
result = 31 * result + (applicationId?.hashCode() ?: 0)
|
||||||
result = 31 * result + (webDomain?.hashCode() ?: 0)
|
result = 31 * result + (webDomain?.hashCode() ?: 0)
|
||||||
result = 31 * result + (webScheme?.hashCode() ?: 0)
|
result = 31 * result + (webScheme?.hashCode() ?: 0)
|
||||||
|
result = 31 * result + (relyingParty?.hashCode() ?: 0)
|
||||||
result = 31 * result + (otpString?.hashCode() ?: 0)
|
result = 31 * result + (otpString?.hashCode() ?: 0)
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun toString(): String {
|
override fun toString(): String {
|
||||||
return otpString ?: webDomain ?: applicationId ?: ""
|
return otpString ?: webDomain ?: applicationId ?: relyingParty ?: ""
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|||||||
Reference in New Issue
Block a user