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
|
||||
android:name="com.kunzisoft.keepass.settings.SettingsActivity" />
|
||||
<activity
|
||||
android:name="com.kunzisoft.keepass.activities.AutofillLauncherActivity"
|
||||
android:name="com.kunzisoft.keepass.credentialprovider.activity.AutofillLauncherActivity"
|
||||
android:theme="@style/Theme.Transparent"
|
||||
android:configChanges="keyboardHidden"
|
||||
android:excludeFromRecents="true"/>
|
||||
@@ -175,7 +175,7 @@
|
||||
android:name="com.kunzisoft.keepass.hardware.HardwareKeyActivity"
|
||||
android:theme="@style/Theme.Transparent" />
|
||||
<activity
|
||||
android:name="com.kunzisoft.keepass.activities.EntrySelectionLauncherActivity"
|
||||
android:name="com.kunzisoft.keepass.credentialprovider.activity.EntrySelectionLauncherActivity"
|
||||
android:theme="@style/Theme.Transparent"
|
||||
android:launchMode="singleInstance"
|
||||
android:exported="true">
|
||||
@@ -201,19 +201,13 @@
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name="com.kunzisoft.keepass.credentialprovider.activity.CreatePasskeyActivity"
|
||||
android:label="CreatePasskeyActivity"
|
||||
android:exported="true"
|
||||
android:name="com.kunzisoft.keepass.credentialprovider.activity.PasskeyLauncherActivity"
|
||||
android:theme="@style/Theme.Transparent"
|
||||
android:configChanges="keyboardHidden"
|
||||
android:excludeFromRecents="true"
|
||||
android:exported="false"
|
||||
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
|
||||
android:name="com.kunzisoft.keepass.services.DatabaseTaskNotificationService"
|
||||
android:foregroundServiceType="dataSync"
|
||||
@@ -241,7 +235,7 @@
|
||||
android:exported="false" />
|
||||
<!-- Receiver for Autofill -->
|
||||
<service
|
||||
android:name="com.kunzisoft.keepass.autofill.KeeAutofillService"
|
||||
android:name="com.kunzisoft.keepass.credentialprovider.autofill.KeeAutofillService"
|
||||
android:label="@string/autofill_service_name"
|
||||
android:exported="true"
|
||||
android:permission="android.permission.BIND_AUTOFILL_SERVICE">
|
||||
@@ -253,7 +247,7 @@
|
||||
</intent-filter>
|
||||
</service>
|
||||
<service
|
||||
android:name="com.kunzisoft.keepass.magikeyboard.MagikeyboardService"
|
||||
android:name="com.kunzisoft.keepass.credentialprovider.magikeyboard.MagikeyboardService"
|
||||
android:label="@string/keyboard_label"
|
||||
android:exported="true"
|
||||
android:permission="android.permission.BIND_INPUT_METHOD" >
|
||||
@@ -263,12 +257,11 @@
|
||||
<action android:name="android.view.InputMethod" />
|
||||
</intent-filter>
|
||||
</service>
|
||||
|
||||
<service
|
||||
android:name="com.kunzisoft.keepass.credentialprovider.service.KeePassDXCredentialProviderService"
|
||||
android:name="com.kunzisoft.keepass.credentialprovider.passkey.PasskeyProviderService"
|
||||
android:enabled="true"
|
||||
android:exported="true"
|
||||
android:label="KeePassDX Credential Provider"
|
||||
android:label="@string/passkey_service_name"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:permission="android.permission.BIND_CREDENTIAL_PROVIDER_SERVICE"
|
||||
tools:targetApi="upside_down_cake">
|
||||
|
||||
@@ -23,7 +23,6 @@ import android.content.Intent
|
||||
import android.graphics.Color
|
||||
import android.graphics.drawable.ColorDrawable
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
@@ -38,13 +37,10 @@ import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.activity.viewModels
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
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.BlendModeCompat
|
||||
import androidx.core.graphics.ColorUtils
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.WindowCompat
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
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.activities.fragments.EntryFragment
|
||||
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.adapters.TagsAdapter
|
||||
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.node.NodeId
|
||||
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.otp.OtpType
|
||||
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.SetOTPDialogFragment
|
||||
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.legacy.DatabaseLockActivity
|
||||
import com.kunzisoft.keepass.adapters.TemplatesSelectorAdapter
|
||||
import com.kunzisoft.keepass.autofill.AutofillComponent
|
||||
import com.kunzisoft.keepass.autofill.AutofillHelper
|
||||
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper
|
||||
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.element.Attachment
|
||||
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.template.Template
|
||||
import com.kunzisoft.keepass.education.EntryEditActivityEducation
|
||||
import com.kunzisoft.keepass.magikeyboard.MagikeyboardService
|
||||
import com.kunzisoft.keepass.model.AttachmentState
|
||||
import com.kunzisoft.keepass.model.DataTime
|
||||
import com.kunzisoft.keepass.model.EntryAttachmentState
|
||||
@@ -376,18 +378,25 @@ class EntryEditActivity : DatabaseLockActivity(),
|
||||
|
||||
// Don't wait for saving if it's to provide autofill
|
||||
mDatabase?.let { database ->
|
||||
EntrySelectionHelper.doSpecialAction(intent,
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{
|
||||
EntrySelectionHelper.doSpecialAction(
|
||||
intent = intent,
|
||||
defaultAction = {},
|
||||
searchAction = {},
|
||||
saveAction = {},
|
||||
keyboardSelectionAction = {
|
||||
entryValidatedForKeyboardSelection(database, entrySave.newEntry)
|
||||
},
|
||||
{ _, _ ->
|
||||
autofillSelectionAction = { _, _ ->
|
||||
entryValidatedForAutofillSelection(database, entrySave.newEntry)
|
||||
},
|
||||
{
|
||||
autofillRegistrationAction = {
|
||||
entryValidatedForAutofillRegistration(entrySave.newEntry)
|
||||
},
|
||||
passkeySelectionAction = {
|
||||
entryValidatedForPasskeySelection(database, entrySave.newEntry)
|
||||
},
|
||||
passkeyRegistrationAction = {
|
||||
entryValidatedForPasskeyRegistration(database, entrySave.newEntry)
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -430,25 +439,32 @@ class EntryEditActivity : DatabaseLockActivity(),
|
||||
}
|
||||
if (newNodes.size == 1) {
|
||||
(newNodes[0] as? Entry?)?.let { entry ->
|
||||
EntrySelectionHelper.doSpecialAction(intent,
|
||||
{
|
||||
EntrySelectionHelper.doSpecialAction(
|
||||
intent = intent,
|
||||
defaultAction = {
|
||||
// Finish naturally
|
||||
finishForEntryResult(entry)
|
||||
},
|
||||
{
|
||||
searchAction = {
|
||||
// Nothing when search retrieved
|
||||
},
|
||||
{
|
||||
saveAction = {
|
||||
entryValidatedForSave(entry)
|
||||
},
|
||||
{
|
||||
keyboardSelectionAction = {
|
||||
entryValidatedForKeyboardSelection(database, entry)
|
||||
},
|
||||
{ _, _ ->
|
||||
autofillSelectionAction = { _, _ ->
|
||||
entryValidatedForAutofillSelection(database, entry)
|
||||
},
|
||||
{
|
||||
autofillRegistrationAction = {
|
||||
entryValidatedForAutofillRegistration(entry)
|
||||
},
|
||||
passkeySelectionAction = {
|
||||
entryValidatedForPasskeySelection(database, entry)
|
||||
},
|
||||
passkeyRegistrationAction = {
|
||||
entryValidatedForPasskeyRegistration(database, entry)
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -488,9 +504,33 @@ class EntryEditActivity : DatabaseLockActivity(),
|
||||
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()
|
||||
finishForEntryResult(entry)
|
||||
}
|
||||
|
||||
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) {
|
||||
// Assign entry callback as a result
|
||||
try {
|
||||
val bundle = Bundle()
|
||||
val bundle = buildEntryResult(entry)
|
||||
val intentEntry = Intent()
|
||||
bundle.putParcelable(ADD_OR_UPDATE_ENTRY_KEY, entry.nodeId)
|
||||
intentEntry.putExtras(bundle)
|
||||
setResult(Activity.RESULT_OK, intentEntry)
|
||||
super.finish()
|
||||
@@ -892,7 +937,7 @@ class EntryEditActivity : DatabaseLockActivity(),
|
||||
if (TimeoutHelper.checkTimeAndLockIfTimeout(activity)) {
|
||||
val intent = Intent(activity, EntryEditActivity::class.java)
|
||||
intent.putExtra(KEY_PARENT, groupId)
|
||||
AutofillHelper.startActivityForAutofillResult(
|
||||
EntrySelectionHelper.startActivityForAutofillSelectionModeResult(
|
||||
activity,
|
||||
intent,
|
||||
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)
|
||||
*/
|
||||
fun launchToUpdateForRegistration(context: Context,
|
||||
database: ContextualDatabase,
|
||||
activityResultLauncher: ActivityResultLauncher<Intent>?,
|
||||
entryId: NodeId<UUID>,
|
||||
registerInfo: RegisterInfo? = null) {
|
||||
registerInfo: RegisterInfo?,
|
||||
typeMode: TypeMode) {
|
||||
if (database.loaded && !database.isReadOnly) {
|
||||
if (TimeoutHelper.checkTimeAndLockIfTimeout(context)) {
|
||||
val intent = Intent(context, EntryEditActivity::class.java)
|
||||
intent.putExtra(KEY_ENTRY, entryId)
|
||||
EntrySelectionHelper.startActivityForRegistrationModeResult(
|
||||
context,
|
||||
activityResultLauncher,
|
||||
intent,
|
||||
registerInfo
|
||||
registerInfo,
|
||||
typeMode
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -928,16 +1000,20 @@ class EntryEditActivity : DatabaseLockActivity(),
|
||||
*/
|
||||
fun launchToCreateForRegistration(context: Context,
|
||||
database: ContextualDatabase,
|
||||
activityResultLauncher: ActivityResultLauncher<Intent>?,
|
||||
groupId: NodeId<*>,
|
||||
registerInfo: RegisterInfo? = null) {
|
||||
registerInfo: RegisterInfo? = null,
|
||||
typeMode: TypeMode) {
|
||||
if (database.loaded && !database.isReadOnly) {
|
||||
if (TimeoutHelper.checkTimeAndLockIfTimeout(context)) {
|
||||
val intent = Intent(context, EntryEditActivity::class.java)
|
||||
intent.putExtra(KEY_PARENT, groupId)
|
||||
EntrySelectionHelper.startActivityForRegistrationModeResult(
|
||||
context,
|
||||
activityResultLauncher,
|
||||
intent,
|
||||
registerInfo
|
||||
registerInfo,
|
||||
typeMode
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,15 +44,16 @@ import androidx.recyclerview.widget.SimpleItemAnimator
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.kunzisoft.keepass.R
|
||||
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.SpecialMode
|
||||
import com.kunzisoft.keepass.credentialprovider.SpecialMode
|
||||
import com.kunzisoft.keepass.activities.helpers.setOpenDocumentClickListener
|
||||
import com.kunzisoft.keepass.activities.legacy.DatabaseModeActivity
|
||||
import com.kunzisoft.keepass.adapters.FileDatabaseHistoryAdapter
|
||||
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
|
||||
import com.kunzisoft.keepass.autofill.AutofillComponent
|
||||
import com.kunzisoft.keepass.autofill.AutofillHelper
|
||||
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.buildActivityResultLauncher
|
||||
import com.kunzisoft.keepass.credentialprovider.TypeMode
|
||||
import com.kunzisoft.keepass.credentialprovider.autofill.AutofillComponent
|
||||
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||
import com.kunzisoft.keepass.database.MainCredential
|
||||
import com.kunzisoft.keepass.education.FileDatabaseSelectActivityEducation
|
||||
@@ -99,10 +100,8 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
|
||||
|
||||
private var mExternalFileHelper: ExternalFileHelper? = null
|
||||
|
||||
private var mAutofillActivityResultLauncher: ActivityResultLauncher<Intent>? =
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
|
||||
AutofillHelper.buildActivityResultLauncher(this)
|
||||
else null
|
||||
private var mCredentialActivityResultLauncher: ActivityResultLauncher<Intent>? =
|
||||
this.buildActivityResultLauncher()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
@@ -299,7 +298,7 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
|
||||
},
|
||||
{ onCancelSpecialMode() },
|
||||
{ onLaunchActivitySpecialMode() },
|
||||
mAutofillActivityResultLauncher)
|
||||
mCredentialActivityResultLauncher)
|
||||
}
|
||||
|
||||
private fun launchGroupActivityIfLoaded(database: ContextualDatabase) {
|
||||
@@ -309,7 +308,7 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
|
||||
{ onValidateSpecialMode() },
|
||||
{ onCancelSpecialMode() },
|
||||
{ onLaunchActivitySpecialMode() },
|
||||
mAutofillActivityResultLauncher)
|
||||
mCredentialActivityResultLauncher)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -493,23 +492,46 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
|
||||
activityResultLauncher: ActivityResultLauncher<Intent>?,
|
||||
autofillComponent: AutofillComponent,
|
||||
searchInfo: SearchInfo? = null) {
|
||||
AutofillHelper.startActivityForAutofillResult(activity,
|
||||
EntrySelectionHelper.startActivityForAutofillSelectionModeResult(activity,
|
||||
Intent(activity, FileDatabaseSelectActivity::class.java),
|
||||
activityResultLauncher,
|
||||
autofillComponent,
|
||||
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
|
||||
* -------------------------
|
||||
*/
|
||||
fun launchForRegistration(context: Context,
|
||||
registerInfo: RegisterInfo? = null) {
|
||||
EntrySelectionHelper.startActivityForRegistrationModeResult(context,
|
||||
Intent(context, FileDatabaseSelectActivity::class.java),
|
||||
registerInfo)
|
||||
activityResultLauncher: ActivityResultLauncher<Intent>?,
|
||||
registerInfo: RegisterInfo? = null,
|
||||
typeMode: TypeMode) {
|
||||
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.SortDialogFragment
|
||||
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.SpecialMode
|
||||
import com.kunzisoft.keepass.credentialprovider.SpecialMode
|
||||
import com.kunzisoft.keepass.activities.legacy.DatabaseLockActivity
|
||||
import com.kunzisoft.keepass.adapters.BreadcrumbAdapter
|
||||
import com.kunzisoft.keepass.autofill.AutofillComponent
|
||||
import com.kunzisoft.keepass.autofill.AutofillHelper
|
||||
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.buildActivityResultLauncher
|
||||
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.MainCredential
|
||||
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.search.SearchParameters
|
||||
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.GroupInfo
|
||||
import com.kunzisoft.keepass.model.RegisterInfo
|
||||
@@ -264,10 +267,8 @@ class GroupActivity : DatabaseLockActivity(),
|
||||
mGroupEditViewModel.selectIcon(icon)
|
||||
}
|
||||
|
||||
private var mAutofillActivityResultLauncher: ActivityResultLauncher<Intent>? =
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
|
||||
AutofillHelper.buildActivityResultLauncher(this)
|
||||
else null
|
||||
private var mCredentialActivityResultLauncher: ActivityResultLauncher<Intent>? =
|
||||
this.buildActivityResultLauncher()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
@@ -484,59 +485,87 @@ class GroupActivity : DatabaseLockActivity(),
|
||||
addNodeButtonView?.setAddEntryClickListener {
|
||||
mDatabase?.let { database ->
|
||||
mMainGroup?.let { currentGroup ->
|
||||
EntrySelectionHelper.doSpecialAction(intent,
|
||||
{
|
||||
EntrySelectionHelper.doSpecialAction(
|
||||
intent = intent,
|
||||
defaultAction = {
|
||||
mMainGroup?.nodeId?.let { currentParentGroupId ->
|
||||
EntryEditActivity.launchToCreate(
|
||||
this@GroupActivity,
|
||||
database,
|
||||
currentParentGroupId,
|
||||
mEntryActivityResultLauncher
|
||||
activity = this@GroupActivity,
|
||||
database = database,
|
||||
groupId = currentParentGroupId,
|
||||
activityResultLauncher = mEntryActivityResultLauncher
|
||||
)
|
||||
}
|
||||
},
|
||||
{
|
||||
searchAction = {
|
||||
// Search not used
|
||||
},
|
||||
{ searchInfo ->
|
||||
saveAction = { searchInfo ->
|
||||
EntryEditActivity.launchToCreateForSave(
|
||||
this@GroupActivity,
|
||||
database,
|
||||
currentGroup.nodeId,
|
||||
searchInfo
|
||||
context = this@GroupActivity,
|
||||
database = database,
|
||||
groupId = currentGroup.nodeId,
|
||||
searchInfo = searchInfo
|
||||
)
|
||||
onLaunchActivitySpecialMode()
|
||||
},
|
||||
{ searchInfo ->
|
||||
keyboardSelectionAction = { searchInfo ->
|
||||
EntryEditActivity.launchForKeyboardSelectionResult(
|
||||
this@GroupActivity,
|
||||
database,
|
||||
currentGroup.nodeId,
|
||||
searchInfo
|
||||
context = this@GroupActivity,
|
||||
database = database,
|
||||
groupId = currentGroup.nodeId,
|
||||
searchInfo = searchInfo
|
||||
)
|
||||
onLaunchActivitySpecialMode()
|
||||
},
|
||||
{ searchInfo, autofillComponent ->
|
||||
autofillSelectionAction = { searchInfo, autofillComponent ->
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
EntryEditActivity.launchForAutofillResult(
|
||||
this@GroupActivity,
|
||||
database,
|
||||
mAutofillActivityResultLauncher,
|
||||
autofillComponent,
|
||||
currentGroup.nodeId,
|
||||
searchInfo
|
||||
activity = this@GroupActivity,
|
||||
database = database,
|
||||
activityResultLauncher = mCredentialActivityResultLauncher,
|
||||
autofillComponent = autofillComponent,
|
||||
groupId = currentGroup.nodeId,
|
||||
searchInfo = searchInfo
|
||||
)
|
||||
onLaunchActivitySpecialMode()
|
||||
} else {
|
||||
onCancelSpecialMode()
|
||||
}
|
||||
},
|
||||
{ searchInfo ->
|
||||
autofillRegistrationAction = { registerInfo ->
|
||||
EntryEditActivity.launchToCreateForRegistration(
|
||||
this@GroupActivity,
|
||||
database,
|
||||
currentGroup.nodeId,
|
||||
searchInfo
|
||||
context = this@GroupActivity,
|
||||
database = database,
|
||||
activityResultLauncher = null,
|
||||
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()
|
||||
}
|
||||
@@ -679,30 +708,40 @@ class GroupActivity : DatabaseLockActivity(),
|
||||
when (actionTask) {
|
||||
ACTION_DATABASE_UPDATE_ENTRY_TASK -> {
|
||||
if (result.isSuccess) {
|
||||
EntrySelectionHelper.doSpecialAction(intent,
|
||||
{
|
||||
EntrySelectionHelper.doSpecialAction(
|
||||
intent = intent,
|
||||
defaultAction = {
|
||||
// Standard not used after task
|
||||
},
|
||||
{
|
||||
searchAction = {
|
||||
// Search not used
|
||||
},
|
||||
{
|
||||
saveAction = {
|
||||
// Save not used
|
||||
},
|
||||
{
|
||||
keyboardSelectionAction = {
|
||||
// Keyboard selection
|
||||
entry?.let {
|
||||
entrySelectedForKeyboardSelection(database, it)
|
||||
}
|
||||
},
|
||||
{ _, _ ->
|
||||
autofillSelectionAction = { _, _ ->
|
||||
// Autofill selection
|
||||
entry?.let {
|
||||
entrySelectedForAutofillSelection(database, it)
|
||||
}
|
||||
},
|
||||
{
|
||||
autofillRegistrationAction = {
|
||||
// Not use
|
||||
},
|
||||
passkeySelectionAction = {
|
||||
// Passkey selection
|
||||
entry?.let {
|
||||
entrySelectedForPasskeySelection(database, it)
|
||||
}
|
||||
},
|
||||
passkeyRegistrationAction = {
|
||||
// TODO Passkey Registration
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -846,27 +885,28 @@ class GroupActivity : DatabaseLockActivity(),
|
||||
|
||||
Type.ENTRY -> try {
|
||||
val entryVersioned = node as Entry
|
||||
EntrySelectionHelper.doSpecialAction(intent,
|
||||
{
|
||||
EntrySelectionHelper.doSpecialAction(
|
||||
intent = intent,
|
||||
defaultAction = {
|
||||
EntryActivity.launch(
|
||||
this@GroupActivity,
|
||||
database,
|
||||
entryVersioned.nodeId,
|
||||
mEntryActivityResultLauncher
|
||||
activity = this@GroupActivity,
|
||||
database = database,
|
||||
entryId = entryVersioned.nodeId,
|
||||
activityResultLauncher = mEntryActivityResultLauncher
|
||||
)
|
||||
// Do not reload group here
|
||||
},
|
||||
{
|
||||
searchAction = {
|
||||
// Nothing here, a search is simply performed
|
||||
},
|
||||
{ searchInfo ->
|
||||
saveAction = { searchInfo ->
|
||||
if (!database.isReadOnly) {
|
||||
entrySelectedForSave(database, entryVersioned, searchInfo)
|
||||
loadGroup()
|
||||
} else
|
||||
finish()
|
||||
},
|
||||
{ searchInfo ->
|
||||
keyboardSelectionAction = { searchInfo ->
|
||||
if (!database.isReadOnly
|
||||
&& searchInfo != null
|
||||
&& PreferencesUtil.isKeyboardSaveSearchInfoEnable(this@GroupActivity)
|
||||
@@ -876,7 +916,7 @@ class GroupActivity : DatabaseLockActivity(),
|
||||
entrySelectedForKeyboardSelection(database, entryVersioned)
|
||||
loadGroup()
|
||||
},
|
||||
{ searchInfo, _ ->
|
||||
autofillSelectionAction = { searchInfo, _ ->
|
||||
if (!database.isReadOnly
|
||||
&& searchInfo != null
|
||||
&& PreferencesUtil.isAutofillSaveSearchInfoEnable(this@GroupActivity)
|
||||
@@ -886,9 +926,39 @@ class GroupActivity : DatabaseLockActivity(),
|
||||
entrySelectedForAutofillSelection(database, entryVersioned)
|
||||
loadGroup()
|
||||
},
|
||||
{ registerInfo ->
|
||||
autofillRegistrationAction = { registerInfo ->
|
||||
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()
|
||||
} else
|
||||
finish()
|
||||
@@ -934,18 +1004,33 @@ class GroupActivity : DatabaseLockActivity(),
|
||||
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(
|
||||
database: ContextualDatabase,
|
||||
entry: Entry,
|
||||
registerInfo: RegisterInfo?
|
||||
activityResultLauncher: ActivityResultLauncher<Intent>?,
|
||||
registerInfo: RegisterInfo?,
|
||||
typeMode: TypeMode
|
||||
) {
|
||||
removeSearch()
|
||||
// Registration to update the entry
|
||||
EntryEditActivity.launchToUpdateForRegistration(
|
||||
this@GroupActivity,
|
||||
database,
|
||||
entry.nodeId,
|
||||
registerInfo
|
||||
context = this@GroupActivity,
|
||||
database = database,
|
||||
activityResultLauncher = activityResultLauncher,
|
||||
entryId = entry.nodeId,
|
||||
registerInfo = registerInfo,
|
||||
typeMode = typeMode
|
||||
)
|
||||
onLaunchActivitySpecialMode()
|
||||
}
|
||||
@@ -1569,19 +1654,19 @@ class GroupActivity : DatabaseLockActivity(),
|
||||
* -------------------------
|
||||
*/
|
||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||
fun launchForAutofillResult(activity: AppCompatActivity,
|
||||
database: ContextualDatabase,
|
||||
activityResultLaunch: ActivityResultLauncher<Intent>?,
|
||||
autofillComponent: AutofillComponent,
|
||||
searchInfo: SearchInfo? = null,
|
||||
autoSearch: Boolean = false) {
|
||||
fun launchForAutofillSelectionResult(activity: AppCompatActivity,
|
||||
database: ContextualDatabase,
|
||||
activityResultLauncher: ActivityResultLauncher<Intent>?,
|
||||
autofillComponent: AutofillComponent,
|
||||
searchInfo: SearchInfo? = null,
|
||||
autoSearch: Boolean = false) {
|
||||
if (database.loaded) {
|
||||
checkTimeAndBuildIntent(activity, null) { intent ->
|
||||
intent.putExtra(AUTO_SEARCH_KEY, autoSearch)
|
||||
AutofillHelper.startActivityForAutofillResult(
|
||||
EntrySelectionHelper.startActivityForAutofillSelectionModeResult(
|
||||
activity,
|
||||
intent,
|
||||
activityResultLaunch,
|
||||
activityResultLauncher,
|
||||
autofillComponent,
|
||||
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
|
||||
* -------------------------
|
||||
*/
|
||||
fun launchForRegistration(context: Context,
|
||||
activityResultLauncher: ActivityResultLauncher<Intent>?,
|
||||
database: ContextualDatabase,
|
||||
registerInfo: RegisterInfo? = null) {
|
||||
registerInfo: RegisterInfo? = null,
|
||||
typeMode: TypeMode) {
|
||||
if (database.loaded && !database.isReadOnly) {
|
||||
checkTimeAndBuildIntent(context, null) { intent ->
|
||||
intent.putExtra(AUTO_SEARCH_KEY, false)
|
||||
EntrySelectionHelper.startActivityForRegistrationModeResult(
|
||||
context,
|
||||
activityResultLauncher,
|
||||
intent,
|
||||
registerInfo
|
||||
registerInfo,
|
||||
typeMode
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1619,153 +1732,231 @@ class GroupActivity : DatabaseLockActivity(),
|
||||
onValidateSpecialMode: () -> Unit,
|
||||
onCancelSpecialMode: () -> Unit,
|
||||
onLaunchActivitySpecialMode: () -> Unit,
|
||||
autofillActivityResultLauncher: ActivityResultLauncher<Intent>?) {
|
||||
EntrySelectionHelper.doSpecialAction(activity.intent,
|
||||
{
|
||||
// Default action
|
||||
launch(
|
||||
activity,
|
||||
activityResultLauncher: ActivityResultLauncher<Intent>?) {
|
||||
EntrySelectionHelper.doSpecialAction(
|
||||
intent = activity.intent,
|
||||
defaultAction = {
|
||||
// Default action
|
||||
launch(
|
||||
activity,
|
||||
database,
|
||||
true
|
||||
)
|
||||
},
|
||||
searchAction = { searchInfo ->
|
||||
// Search action
|
||||
if (database.loaded) {
|
||||
launchForSearchResult(activity,
|
||||
database,
|
||||
true
|
||||
)
|
||||
},
|
||||
{ searchInfo ->
|
||||
// Search action
|
||||
if (database.loaded) {
|
||||
launchForSearchResult(activity,
|
||||
searchInfo,
|
||||
true)
|
||||
onLaunchActivitySpecialMode()
|
||||
} else {
|
||||
// Simply close if database not opened
|
||||
onCancelSpecialMode()
|
||||
}
|
||||
},
|
||||
saveAction = { searchInfo ->
|
||||
// Save info
|
||||
if (database.loaded) {
|
||||
if (!database.isReadOnly) {
|
||||
launchForSaveResult(
|
||||
activity,
|
||||
database,
|
||||
searchInfo,
|
||||
true)
|
||||
false
|
||||
)
|
||||
onLaunchActivitySpecialMode()
|
||||
} else {
|
||||
// Simply close if database not opened
|
||||
Toast.makeText(
|
||||
activity.applicationContext,
|
||||
R.string.autofill_read_only_save,
|
||||
Toast.LENGTH_LONG
|
||||
)
|
||||
.show()
|
||||
onCancelSpecialMode()
|
||||
}
|
||||
},
|
||||
{ searchInfo ->
|
||||
// Save info
|
||||
if (database.loaded) {
|
||||
if (!database.isReadOnly) {
|
||||
launchForSaveResult(
|
||||
activity,
|
||||
database,
|
||||
searchInfo,
|
||||
false
|
||||
)
|
||||
onLaunchActivitySpecialMode()
|
||||
} else {
|
||||
Toast.makeText(
|
||||
activity.applicationContext,
|
||||
R.string.autofill_read_only_save,
|
||||
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()
|
||||
}
|
||||
}
|
||||
},
|
||||
keyboardSelectionAction = { searchInfo ->
|
||||
// Keyboard selection
|
||||
SearchHelper.checkAutoSearchInfo(
|
||||
context = activity,
|
||||
database = database,
|
||||
searchInfo = searchInfo,
|
||||
onItemsFound = { _, items ->
|
||||
MagikeyboardService.performSelection(
|
||||
items,
|
||||
{ entryInfo ->
|
||||
// Keyboard populated
|
||||
MagikeyboardService.populateKeyboardAndMoveAppToBackground(
|
||||
activity,
|
||||
entryInfo
|
||||
)
|
||||
onValidateSpecialMode()
|
||||
},
|
||||
{
|
||||
// Here no search info found, disable auto search
|
||||
{ autoSearch ->
|
||||
launchForKeyboardSelectionResult(activity,
|
||||
database,
|
||||
searchInfo,
|
||||
false)
|
||||
autoSearch)
|
||||
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()
|
||||
}
|
||||
)
|
||||
},
|
||||
{ searchInfo, autofillComponent ->
|
||||
// Autofill selection
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
SearchHelper.checkAutoSearchInfo(activity,
|
||||
database,
|
||||
searchInfo,
|
||||
{ openedDatabase, items ->
|
||||
// Response is build
|
||||
AutofillHelper.buildResponseAndSetResult(activity, openedDatabase, items)
|
||||
} else {
|
||||
onCancelSpecialMode()
|
||||
}
|
||||
},
|
||||
autofillRegistrationAction = { registerInfo ->
|
||||
// Autofill registration
|
||||
if (!database.isReadOnly) {
|
||||
SearchHelper.checkAutoSearchInfo(
|
||||
context = activity,
|
||||
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()
|
||||
},
|
||||
{
|
||||
// Here no search info found, disable auto search
|
||||
launchForAutofillResult(activity,
|
||||
database,
|
||||
autofillActivityResultLauncher,
|
||||
autofillComponent,
|
||||
searchInfo,
|
||||
false)
|
||||
actionEntrySelection = {
|
||||
launchForPasskeySelectionResult(
|
||||
context = activity,
|
||||
database = database,
|
||||
searchInfo = searchInfo,
|
||||
activityResultLauncher = activityResultLauncher
|
||||
)
|
||||
onLaunchActivitySpecialMode()
|
||||
},
|
||||
{
|
||||
// Simply close if database not opened, normally not happened
|
||||
onCancelSpecialMode()
|
||||
}
|
||||
)
|
||||
} else {
|
||||
onCancelSpecialMode()
|
||||
}
|
||||
},
|
||||
{ registerInfo ->
|
||||
// Autofill registration
|
||||
if (!database.isReadOnly) {
|
||||
SearchHelper.checkAutoSearchInfo(activity,
|
||||
database,
|
||||
registerInfo?.searchInfo,
|
||||
{ _, _ ->
|
||||
// No auto search, it's a registration
|
||||
launchForRegistration(activity,
|
||||
database,
|
||||
registerInfo)
|
||||
onLaunchActivitySpecialMode()
|
||||
},
|
||||
{
|
||||
// Here no search info found, disable auto search
|
||||
launchForRegistration(activity,
|
||||
database,
|
||||
registerInfo)
|
||||
onLaunchActivitySpecialMode()
|
||||
},
|
||||
{
|
||||
// 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()
|
||||
}
|
||||
})
|
||||
)
|
||||
},
|
||||
onItemNotFound = {
|
||||
// Here no search info found, disable auto search
|
||||
launchForPasskeySelectionResult(
|
||||
context = activity,
|
||||
database = database,
|
||||
searchInfo = searchInfo,
|
||||
activityResultLauncher = activityResultLauncher
|
||||
)
|
||||
onLaunchActivitySpecialMode()
|
||||
},
|
||||
onDatabaseClosed = {
|
||||
// Simply close if database not opened, normally not happened
|
||||
onCancelSpecialMode()
|
||||
}
|
||||
)
|
||||
} else {
|
||||
onCancelSpecialMode()
|
||||
}
|
||||
},
|
||||
passkeyRegistrationAction = { registerInfo ->
|
||||
// Passkey registration
|
||||
if (!database.isReadOnly) {
|
||||
launchForRegistration(
|
||||
context = activity,
|
||||
activityResultLauncher = activityResultLauncher,
|
||||
database = database,
|
||||
registerInfo = registerInfo,
|
||||
typeMode = TypeMode.PASSKEY
|
||||
)
|
||||
onLaunchActivitySpecialMode()
|
||||
} else {
|
||||
Toast.makeText(activity.applicationContext,
|
||||
R.string.autofill_read_only_save,
|
||||
Toast.LENGTH_LONG)
|
||||
.show()
|
||||
onCancelSpecialMode()
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,15 +46,16 @@ import androidx.fragment.app.commit
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.kunzisoft.keepass.R
|
||||
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.SpecialMode
|
||||
import com.kunzisoft.keepass.activities.legacy.DatabaseLockActivity
|
||||
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.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.MainCredential
|
||||
import com.kunzisoft.keepass.database.exception.DuplicateUuidDatabaseException
|
||||
@@ -113,10 +114,8 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
|
||||
private var mReadOnly: Boolean = false
|
||||
private var mForceReadOnly: Boolean = false
|
||||
|
||||
private var mAutofillActivityResultLauncher: ActivityResultLauncher<Intent>? =
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
|
||||
AutofillHelper.buildActivityResultLauncher(this)
|
||||
else null
|
||||
private var mCredentialActivityResultLauncher: ActivityResultLauncher<Intent>? =
|
||||
this.buildActivityResultLauncher()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
@@ -395,7 +394,7 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
|
||||
{ onValidateSpecialMode() },
|
||||
{ onCancelSpecialMode() },
|
||||
{ onLaunchActivitySpecialMode() },
|
||||
mAutofillActivityResultLauncher
|
||||
mCredentialActivityResultLauncher
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -806,14 +805,14 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
|
||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||
@Throws(FileNotFoundException::class)
|
||||
fun launchForAutofillResult(activity: AppCompatActivity,
|
||||
activityResultLauncher: ActivityResultLauncher<Intent>?,
|
||||
databaseFile: Uri,
|
||||
keyFile: Uri?,
|
||||
hardwareKey: HardwareKey?,
|
||||
activityResultLauncher: ActivityResultLauncher<Intent>?,
|
||||
autofillComponent: AutofillComponent,
|
||||
searchInfo: SearchInfo?) {
|
||||
buildAndLaunchIntent(activity, databaseFile, keyFile, hardwareKey) { intent ->
|
||||
AutofillHelper.startActivityForAutofillResult(
|
||||
EntrySelectionHelper.startActivityForAutofillSelectionModeResult(
|
||||
activity,
|
||||
intent,
|
||||
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
|
||||
* -------------------------
|
||||
*/
|
||||
fun launchForRegistration(activity: Activity,
|
||||
databaseFile: Uri,
|
||||
keyFile: Uri?,
|
||||
hardwareKey: HardwareKey?,
|
||||
registerInfo: RegisterInfo?) {
|
||||
fun launchForRegistration(
|
||||
activity: Activity,
|
||||
activityResultLauncher: ActivityResultLauncher<Intent>?,
|
||||
databaseFile: Uri,
|
||||
keyFile: Uri?,
|
||||
hardwareKey: HardwareKey?,
|
||||
typeMode: TypeMode,
|
||||
registerInfo: RegisterInfo?
|
||||
) {
|
||||
buildAndLaunchIntent(activity, databaseFile, keyFile, hardwareKey) { intent ->
|
||||
EntrySelectionHelper.startActivityForRegistrationModeResult(
|
||||
activity,
|
||||
intent,
|
||||
registerInfo)
|
||||
context = activity,
|
||||
activityResultLauncher = activityResultLauncher,
|
||||
intent = intent,
|
||||
typeMode = typeMode,
|
||||
registerInfo = registerInfo
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -852,74 +881,104 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
|
||||
fileNoFoundAction: (exception: FileNotFoundException) -> Unit,
|
||||
onCancelSpecialMode: () -> Unit,
|
||||
onLaunchActivitySpecialMode: () -> Unit,
|
||||
autofillActivityResultLauncher: ActivityResultLauncher<Intent>?) {
|
||||
activityResultLauncher: ActivityResultLauncher<Intent>?) {
|
||||
|
||||
try {
|
||||
EntrySelectionHelper.doSpecialAction(activity.intent,
|
||||
{
|
||||
launch(
|
||||
activity,
|
||||
databaseUri,
|
||||
keyFile,
|
||||
hardwareKey
|
||||
)
|
||||
},
|
||||
{ searchInfo -> // Search Action
|
||||
launchForSearchResult(
|
||||
activity,
|
||||
databaseUri,
|
||||
keyFile,
|
||||
hardwareKey,
|
||||
searchInfo
|
||||
)
|
||||
onLaunchActivitySpecialMode()
|
||||
},
|
||||
{ searchInfo -> // Save Action
|
||||
launchForSaveResult(
|
||||
activity,
|
||||
databaseUri,
|
||||
keyFile,
|
||||
hardwareKey,
|
||||
searchInfo
|
||||
)
|
||||
onLaunchActivitySpecialMode()
|
||||
},
|
||||
{ searchInfo -> // Keyboard Selection Action
|
||||
launchForKeyboardResult(
|
||||
activity,
|
||||
databaseUri,
|
||||
keyFile,
|
||||
hardwareKey,
|
||||
searchInfo
|
||||
)
|
||||
onLaunchActivitySpecialMode()
|
||||
},
|
||||
{ searchInfo, autofillComponent -> // Autofill Selection Action
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
launchForAutofillResult(
|
||||
activity,
|
||||
databaseUri,
|
||||
keyFile,
|
||||
hardwareKey,
|
||||
autofillActivityResultLauncher,
|
||||
autofillComponent,
|
||||
searchInfo
|
||||
)
|
||||
onLaunchActivitySpecialMode()
|
||||
} else {
|
||||
onCancelSpecialMode()
|
||||
}
|
||||
},
|
||||
{ registerInfo -> // Registration Action
|
||||
launchForRegistration(
|
||||
activity,
|
||||
databaseUri,
|
||||
keyFile,
|
||||
hardwareKey,
|
||||
registerInfo
|
||||
EntrySelectionHelper.doSpecialAction(
|
||||
intent = activity.intent,
|
||||
defaultAction = {
|
||||
launch(
|
||||
activity = activity,
|
||||
databaseFile = databaseUri,
|
||||
keyFile = keyFile,
|
||||
hardwareKey = hardwareKey
|
||||
)
|
||||
},
|
||||
searchAction = { searchInfo ->
|
||||
launchForSearchResult(
|
||||
activity = activity,
|
||||
databaseFile = databaseUri,
|
||||
keyFile = keyFile,
|
||||
hardwareKey = hardwareKey,
|
||||
searchInfo = searchInfo
|
||||
)
|
||||
onLaunchActivitySpecialMode()
|
||||
},
|
||||
saveAction = { searchInfo ->
|
||||
launchForSaveResult(
|
||||
activity = activity,
|
||||
databaseFile = databaseUri,
|
||||
keyFile = keyFile,
|
||||
hardwareKey = hardwareKey,
|
||||
searchInfo = searchInfo
|
||||
)
|
||||
onLaunchActivitySpecialMode()
|
||||
},
|
||||
keyboardSelectionAction = { searchInfo ->
|
||||
launchForKeyboardResult(
|
||||
activity = activity,
|
||||
databaseFile = databaseUri,
|
||||
keyFile = keyFile,
|
||||
hardwareKey = hardwareKey,
|
||||
searchInfo = searchInfo
|
||||
)
|
||||
onLaunchActivitySpecialMode()
|
||||
},
|
||||
autofillSelectionAction = { searchInfo, autofillComponent ->
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
launchForAutofillResult(
|
||||
activity = activity,
|
||||
activityResultLauncher = activityResultLauncher,
|
||||
databaseFile = databaseUri,
|
||||
keyFile = keyFile,
|
||||
hardwareKey = hardwareKey,
|
||||
autofillComponent = autofillComponent,
|
||||
searchInfo = searchInfo
|
||||
)
|
||||
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) {
|
||||
fileNoFoundAction(e)
|
||||
|
||||
@@ -36,8 +36,8 @@ import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.recyclerview.widget.RecyclerView.SCROLL_STATE_IDLE
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.activities.dialogs.SortDialogFragment
|
||||
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
||||
import com.kunzisoft.keepass.activities.helpers.SpecialMode
|
||||
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper
|
||||
import com.kunzisoft.keepass.credentialprovider.SpecialMode
|
||||
import com.kunzisoft.keepass.adapters.NodesAdapter
|
||||
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||
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.activities.dialogs.DeleteNodesDialogFragment
|
||||
import com.kunzisoft.keepass.activities.dialogs.PasswordEncodingDialogFragment
|
||||
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
||||
import com.kunzisoft.keepass.activities.helpers.SpecialMode
|
||||
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper
|
||||
import com.kunzisoft.keepass.credentialprovider.SpecialMode
|
||||
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||
import com.kunzisoft.keepass.database.MainCredential
|
||||
import com.kunzisoft.keepass.database.element.Entry
|
||||
|
||||
@@ -5,9 +5,10 @@ import android.view.View
|
||||
import android.widget.Toast
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
||||
import com.kunzisoft.keepass.activities.helpers.SpecialMode
|
||||
import com.kunzisoft.keepass.activities.helpers.TypeMode
|
||||
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper
|
||||
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.isIntentSenderMode
|
||||
import com.kunzisoft.keepass.credentialprovider.SpecialMode
|
||||
import com.kunzisoft.keepass.credentialprovider.TypeMode
|
||||
import com.kunzisoft.keepass.model.SearchInfo
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import com.kunzisoft.keepass.view.ToolbarSpecial
|
||||
@@ -42,18 +43,15 @@ abstract class DatabaseModeActivity : DatabaseActivity() {
|
||||
/**
|
||||
* Intent sender uses special retains data in callback
|
||||
*/
|
||||
private fun isIntentSender(): Boolean {
|
||||
return (mSpecialMode == SpecialMode.SELECTION
|
||||
&& mTypeMode == TypeMode.AUTOFILL)
|
||||
/* TODO Registration callback #765
|
||||
|| (mSpecialMode == SpecialMode.REGISTRATION
|
||||
&& mTypeMode == TypeMode.AUTOFILL
|
||||
&& Build.VERSION.SDK_INT >= Build.VERSION_CODES.P)
|
||||
*/
|
||||
protected fun isIntentSender(): Boolean {
|
||||
return isIntentSenderMode(mSpecialMode, mTypeMode)
|
||||
}
|
||||
|
||||
fun onLaunchActivitySpecialMode() {
|
||||
if (!isIntentSender()) {
|
||||
// TODO Verify behavior for Autofill Callback #765
|
||||
if (isIntentSender()) {
|
||||
onValidateSpecialMode()
|
||||
} else {
|
||||
EntrySelectionHelper.removeModesFromIntent(intent)
|
||||
EntrySelectionHelper.removeInfoFromIntent(intent)
|
||||
finish()
|
||||
@@ -136,6 +134,7 @@ abstract class DatabaseModeActivity : DatabaseActivity() {
|
||||
TypeMode.DEFAULT, // Not important because hidden
|
||||
TypeMode.MAGIKEYBOARD -> R.string.magic_keyboard_title
|
||||
TypeMode.AUTOFILL -> R.string.autofill
|
||||
TypeMode.PASSKEY -> R.string.passkey
|
||||
}
|
||||
title = getString(selectionModeStringId)
|
||||
if (mTypeMode != TypeMode.DEFAULT)
|
||||
|
||||
@@ -17,17 +17,32 @@
|
||||
* 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.Intent
|
||||
import android.graphics.drawable.Icon
|
||||
import android.os.Build
|
||||
import com.kunzisoft.keepass.autofill.AutofillComponent
|
||||
import com.kunzisoft.keepass.autofill.AutofillHelper
|
||||
import android.util.Log
|
||||
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.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.getParcelableExtraCompat
|
||||
import com.kunzisoft.keepass.utils.putEnumExtra
|
||||
|
||||
object EntrySelectionHelper {
|
||||
@@ -37,6 +52,33 @@ object EntrySelectionHelper {
|
||||
private const val KEY_SEARCH_INFO = "com.kunzisoft.keepass.extra.SEARCH_INFO"
|
||||
private const val KEY_REGISTER_INFO = "com.kunzisoft.keepass.extra.REGISTER_INFO"
|
||||
|
||||
/**
|
||||
* 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,
|
||||
intent: Intent,
|
||||
searchInfo: SearchInfo) {
|
||||
@@ -66,15 +108,52 @@ object EntrySelectionHelper {
|
||||
context.startActivity(intent)
|
||||
}
|
||||
|
||||
fun startActivityForRegistrationModeResult(context: Context,
|
||||
intent: Intent,
|
||||
registerInfo: RegisterInfo?) {
|
||||
addSpecialModeInIntent(intent, SpecialMode.REGISTRATION)
|
||||
// At the moment, only autofill for registration
|
||||
/**
|
||||
* Utility method to start an activity with an Autofill for result
|
||||
*/
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
fun startActivityForAutofillSelectionModeResult(
|
||||
context: Context,
|
||||
intent: Intent,
|
||||
activityResultLauncher: ActivityResultLauncher<Intent>?,
|
||||
autofillComponent: AutofillComponent,
|
||||
searchInfo: SearchInfo?
|
||||
) {
|
||||
addSpecialModeInIntent(intent, SpecialMode.SELECTION)
|
||||
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)
|
||||
intent.flags = intent.flags or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||
context.startActivity(intent)
|
||||
if (activityResultLauncher == null) {
|
||||
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?) {
|
||||
@@ -103,8 +182,13 @@ object EntrySelectionHelper {
|
||||
}
|
||||
|
||||
fun addSpecialModeInIntent(intent: Intent, specialMode: SpecialMode) {
|
||||
// TODO Replace by Intent.addSpecialMode
|
||||
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 {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
@@ -131,6 +215,17 @@ object EntrySelectionHelper {
|
||||
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,
|
||||
defaultAction: () -> Unit,
|
||||
searchAction: (searchInfo: SearchInfo) -> Unit,
|
||||
@@ -138,7 +233,9 @@ object EntrySelectionHelper {
|
||||
keyboardSelectionAction: (searchInfo: SearchInfo?) -> Unit,
|
||||
autofillSelectionAction: (searchInfo: SearchInfo?,
|
||||
autofillComponent: AutofillComponent) -> Unit,
|
||||
autofillRegistrationAction: (registerInfo: RegisterInfo?) -> Unit) {
|
||||
autofillRegistrationAction: (registerInfo: RegisterInfo?) -> Unit,
|
||||
passkeySelectionAction: (searchInfo: SearchInfo?) -> Unit,
|
||||
passkeyRegistrationAction: (registerInfo: RegisterInfo?) -> Unit) {
|
||||
|
||||
when (retrieveSpecialModeFromIntent(intent)) {
|
||||
SpecialMode.DEFAULT -> {
|
||||
@@ -186,6 +283,7 @@ object EntrySelectionHelper {
|
||||
defaultAction.invoke()
|
||||
}
|
||||
TypeMode.MAGIKEYBOARD -> keyboardSelectionAction.invoke(searchInfo)
|
||||
TypeMode.PASSKEY -> passkeySelectionAction.invoke(searchInfo)
|
||||
else -> {
|
||||
// In this case, error
|
||||
removeModesFromIntent(intent)
|
||||
@@ -202,10 +300,59 @@ object EntrySelectionHelper {
|
||||
}
|
||||
SpecialMode.REGISTRATION -> {
|
||||
val registerInfo: RegisterInfo? = retrieveRegisterInfoFromIntent(intent)
|
||||
removeModesFromIntent(intent)
|
||||
removeInfoFromIntent(intent)
|
||||
autofillRegistrationAction.invoke(registerInfo)
|
||||
if (!isIntentSenderMode(
|
||||
specialMode = retrieveSpecialModeFromIntent(intent),
|
||||
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 {
|
||||
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/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.activities
|
||||
package com.kunzisoft.keepass.credentialprovider.activity
|
||||
|
||||
import android.app.Activity
|
||||
import android.app.PendingIntent
|
||||
@@ -30,13 +30,17 @@ import android.widget.Toast
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.annotation.RequiresApi
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
||||
import com.kunzisoft.keepass.activities.helpers.SpecialMode
|
||||
import com.kunzisoft.keepass.activities.FileDatabaseSelectActivity
|
||||
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.autofill.AutofillComponent
|
||||
import com.kunzisoft.keepass.autofill.AutofillHelper
|
||||
import com.kunzisoft.keepass.autofill.CompatInlineSuggestionsRequest
|
||||
import com.kunzisoft.keepass.autofill.KeeAutofillService
|
||||
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.buildActivityResultLauncher
|
||||
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.autofill.CompatInlineSuggestionsRequest
|
||||
import com.kunzisoft.keepass.credentialprovider.autofill.KeeAutofillService
|
||||
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||
import com.kunzisoft.keepass.database.helper.SearchHelper
|
||||
import com.kunzisoft.keepass.model.RegisterInfo
|
||||
@@ -48,10 +52,8 @@ import com.kunzisoft.keepass.utils.getParcelableExtraCompat
|
||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||
class AutofillLauncherActivity : DatabaseModeActivity() {
|
||||
|
||||
private var mAutofillActivityResultLauncher: ActivityResultLauncher<Intent>? =
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
|
||||
AutofillHelper.buildActivityResultLauncher(this, true)
|
||||
else null
|
||||
private var mCredentialActivityResultLauncher: ActivityResultLauncher<Intent>? =
|
||||
this.buildActivityResultLauncher(lockDatabase = true)
|
||||
|
||||
override fun applyCustomStyle(): Boolean {
|
||||
return false
|
||||
@@ -72,7 +74,9 @@ class AutofillLauncherActivity : DatabaseModeActivity() {
|
||||
// To pass extra inline request
|
||||
var compatInlineSuggestionsRequest: CompatInlineSuggestionsRequest? = null
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
compatInlineSuggestionsRequest = bundle.getParcelableCompat(KEY_INLINE_SUGGESTION)
|
||||
compatInlineSuggestionsRequest = bundle.getParcelableCompat(
|
||||
KEY_INLINE_SUGGESTION
|
||||
)
|
||||
}
|
||||
// Build search param
|
||||
bundle.getParcelableCompat<SearchInfo>(KEY_SEARCH_INFO)?.let { searchInfo ->
|
||||
@@ -102,7 +106,9 @@ class AutofillLauncherActivity : DatabaseModeActivity() {
|
||||
}
|
||||
SpecialMode.REGISTRATION -> {
|
||||
// To register info
|
||||
val registerInfo = intent.getParcelableExtraCompat<RegisterInfo>(KEY_REGISTER_INFO)
|
||||
val registerInfo = intent.getParcelableExtraCompat<RegisterInfo>(
|
||||
KEY_REGISTER_INFO
|
||||
)
|
||||
val searchInfo = SearchInfo(registerInfo?.searchInfo)
|
||||
WebDomain.getConcreteWebDomain(this, searchInfo.webDomain) { concreteWebDomain ->
|
||||
searchInfo.webDomain = concreteWebDomain
|
||||
@@ -134,30 +140,35 @@ class AutofillLauncherActivity : DatabaseModeActivity() {
|
||||
finish()
|
||||
} else {
|
||||
// If database is open
|
||||
SearchHelper.checkAutoSearchInfo(this,
|
||||
database,
|
||||
searchInfo,
|
||||
{ openedDatabase, items ->
|
||||
// Items found
|
||||
AutofillHelper.buildResponseAndSetResult(this, openedDatabase, items)
|
||||
finish()
|
||||
},
|
||||
{ openedDatabase ->
|
||||
// Show the database UI to select the entry
|
||||
GroupActivity.launchForAutofillResult(this,
|
||||
openedDatabase,
|
||||
mAutofillActivityResultLauncher,
|
||||
autofillComponent,
|
||||
searchInfo,
|
||||
false)
|
||||
},
|
||||
{
|
||||
// If database not open
|
||||
FileDatabaseSelectActivity.launchForAutofillResult(this,
|
||||
mAutofillActivityResultLauncher,
|
||||
autofillComponent,
|
||||
searchInfo)
|
||||
}
|
||||
SearchHelper.checkAutoSearchInfo(
|
||||
context = this,
|
||||
database = database,
|
||||
searchInfo = searchInfo,
|
||||
onItemsFound = { openedDatabase, items ->
|
||||
// Items found
|
||||
AutofillHelper.buildResponseAndSetResult(this, openedDatabase, items)
|
||||
finish()
|
||||
},
|
||||
onItemNotFound = { openedDatabase ->
|
||||
// Show the database UI to select the entry
|
||||
GroupActivity.launchForAutofillSelectionResult(
|
||||
this,
|
||||
openedDatabase,
|
||||
mCredentialActivityResultLauncher,
|
||||
autofillComponent,
|
||||
searchInfo,
|
||||
false
|
||||
)
|
||||
},
|
||||
onDatabaseClosed = {
|
||||
// If database not open
|
||||
FileDatabaseSelectActivity.launchForAutofillResult(
|
||||
this,
|
||||
mCredentialActivityResultLauncher,
|
||||
autofillComponent,
|
||||
searchInfo
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -174,34 +185,47 @@ class AutofillLauncherActivity : DatabaseModeActivity() {
|
||||
setResult(Activity.RESULT_CANCELED)
|
||||
} else {
|
||||
val readOnly = database?.isReadOnly != false
|
||||
SearchHelper.checkAutoSearchInfo(this,
|
||||
database,
|
||||
searchInfo,
|
||||
{ openedDatabase, _ ->
|
||||
if (!readOnly) {
|
||||
// Show the database UI to select the entry
|
||||
GroupActivity.launchForRegistration(this,
|
||||
openedDatabase,
|
||||
registerInfo)
|
||||
} else {
|
||||
showReadOnlySaveMessage()
|
||||
}
|
||||
},
|
||||
{ openedDatabase ->
|
||||
if (!readOnly) {
|
||||
// Show the database UI to select the entry
|
||||
GroupActivity.launchForRegistration(this,
|
||||
openedDatabase,
|
||||
registerInfo)
|
||||
} else {
|
||||
showReadOnlySaveMessage()
|
||||
}
|
||||
},
|
||||
{
|
||||
// If database not open
|
||||
FileDatabaseSelectActivity.launchForRegistration(this,
|
||||
registerInfo)
|
||||
SearchHelper.checkAutoSearchInfo(
|
||||
context = this,
|
||||
database = database,
|
||||
searchInfo = searchInfo,
|
||||
onItemsFound = { 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()
|
||||
}
|
||||
},
|
||||
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()
|
||||
@@ -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/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.activities
|
||||
package com.kunzisoft.keepass.credentialprovider.activity
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.widget.Toast
|
||||
import androidx.core.net.toUri
|
||||
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.credentialprovider.magikeyboard.MagikeyboardService
|
||||
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||
import com.kunzisoft.keepass.database.helper.SearchHelper
|
||||
import com.kunzisoft.keepass.magikeyboard.MagikeyboardService
|
||||
import com.kunzisoft.keepass.model.SearchInfo
|
||||
import com.kunzisoft.keepass.otp.OtpEntryFields
|
||||
import com.kunzisoft.keepass.utils.KeyboardUtil.isKeyboardActivatedInSettings
|
||||
import com.kunzisoft.keepass.utils.getParcelableCompat
|
||||
import com.kunzisoft.keepass.utils.WebDomain
|
||||
import com.kunzisoft.keepass.utils.getParcelableCompat
|
||||
|
||||
/**
|
||||
* Activity to search or select entry in database,
|
||||
@@ -73,7 +75,7 @@ class EntrySelectionLauncherActivity : DatabaseModeActivity() {
|
||||
if (OtpEntryFields.isOTPUri(extra))
|
||||
otpString = extra
|
||||
else
|
||||
sharedWebDomain = Uri.parse(extra).host
|
||||
sharedWebDomain = extra.toUri().host
|
||||
}
|
||||
}
|
||||
launchSelection(database, sharedWebDomain, otpString)
|
||||
@@ -121,87 +123,105 @@ class EntrySelectionLauncherActivity : DatabaseModeActivity() {
|
||||
|
||||
// If database is open
|
||||
val readOnly = database?.isReadOnly != false
|
||||
SearchHelper.checkAutoSearchInfo(this,
|
||||
database,
|
||||
searchInfo,
|
||||
{ openedDatabase, items ->
|
||||
// Items found
|
||||
if (searchInfo.otpString != null) {
|
||||
if (!readOnly) {
|
||||
GroupActivity.launchForSaveResult(
|
||||
SearchHelper.checkAutoSearchInfo(
|
||||
context = this,
|
||||
database = database,
|
||||
searchInfo = searchInfo,
|
||||
onItemsFound = { openedDatabase, items ->
|
||||
// Items found
|
||||
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) {
|
||||
MagikeyboardService.performSelection(
|
||||
items,
|
||||
{ entryInfo ->
|
||||
// Automatically populate keyboard
|
||||
MagikeyboardService.populateKeyboardAndMoveAppToBackground(
|
||||
this,
|
||||
entryInfo
|
||||
)
|
||||
},
|
||||
{ autoSearch ->
|
||||
GroupActivity.launchForKeyboardSelectionResult(
|
||||
this,
|
||||
openedDatabase,
|
||||
searchInfo,
|
||||
false)
|
||||
} else {
|
||||
Toast.makeText(applicationContext,
|
||||
R.string.autofill_read_only_save,
|
||||
Toast.LENGTH_LONG)
|
||||
.show()
|
||||
autoSearch
|
||||
)
|
||||
}
|
||||
} else if (searchShareForMagikeyboard) {
|
||||
MagikeyboardService.performSelection(
|
||||
items,
|
||||
{ entryInfo ->
|
||||
// Automatically populate keyboard
|
||||
MagikeyboardService.populateKeyboardAndMoveAppToBackground(
|
||||
this,
|
||||
entryInfo
|
||||
)
|
||||
},
|
||||
{ autoSearch ->
|
||||
GroupActivity.launchForKeyboardSelectionResult(this,
|
||||
openedDatabase,
|
||||
searchInfo,
|
||||
autoSearch)
|
||||
}
|
||||
)
|
||||
} else {
|
||||
GroupActivity.launchForSearchResult(
|
||||
this,
|
||||
openedDatabase,
|
||||
searchInfo,
|
||||
true
|
||||
)
|
||||
}
|
||||
},
|
||||
onItemNotFound = { openedDatabase ->
|
||||
// Show the database UI to select the entry
|
||||
if (searchInfo.otpString != null) {
|
||||
if (!readOnly) {
|
||||
GroupActivity.launchForSaveResult(
|
||||
this,
|
||||
openedDatabase,
|
||||
searchInfo,
|
||||
false
|
||||
)
|
||||
} else {
|
||||
GroupActivity.launchForSearchResult(this,
|
||||
openedDatabase,
|
||||
searchInfo,
|
||||
true)
|
||||
}
|
||||
},
|
||||
{ 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)
|
||||
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
|
||||
)
|
||||
}
|
||||
},
|
||||
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
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
* 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.app.Activity
|
||||
@@ -40,17 +40,13 @@ import android.view.autofill.AutofillValue
|
||||
import android.widget.RemoteViews
|
||||
import android.widget.Toast
|
||||
import android.widget.inline.InlinePresentationSpec
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.autofill.inline.UiVersions
|
||||
import androidx.autofill.inline.v1.InlineSuggestionUi
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.activities.AutofillLauncherActivity
|
||||
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
||||
import com.kunzisoft.keepass.activities.helpers.SpecialMode
|
||||
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.buildIcon
|
||||
import com.kunzisoft.keepass.credentialprovider.activity.AutofillLauncherActivity
|
||||
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||
import com.kunzisoft.keepass.database.element.icon.IconImage
|
||||
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.settings.AutofillSettingsActivity
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import com.kunzisoft.keepass.utils.LOCK_ACTION
|
||||
import com.kunzisoft.keepass.utils.getParcelableExtraCompat
|
||||
import kotlin.math.min
|
||||
|
||||
@@ -294,23 +289,6 @@ object AutofillHelper {
|
||||
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")
|
||||
@RequiresApi(Build.VERSION_CODES.R)
|
||||
private fun buildInlinePresentationForEntry(context: Context,
|
||||
@@ -353,7 +331,7 @@ object AutofillHelper {
|
||||
Icon.createWithResource(context, R.mipmap.ic_launcher_round).apply {
|
||||
setTintBlendMode(BlendMode.DST)
|
||||
})
|
||||
buildIconFromEntry(context, database, entryInfo)?.let { icon ->
|
||||
entryInfo.buildIcon(context, database)?.let { icon ->
|
||||
setEndIcon(icon.apply {
|
||||
setTintBlendMode(BlendMode.DST)
|
||||
})
|
||||
@@ -534,7 +512,9 @@ object AutofillHelper {
|
||||
StructureParser(structure).parse()?.let { result ->
|
||||
// New Response
|
||||
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) {
|
||||
Toast.makeText(activity.applicationContext, R.string.autofill_inline_suggestions_keyboard, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
@@ -558,45 +538,14 @@ object AutofillHelper {
|
||||
}
|
||||
}
|
||||
|
||||
fun buildActivityResultLauncher(activity: AppCompatActivity,
|
||||
lockDatabase: Boolean = false): ActivityResultLauncher<Intent> {
|
||||
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)
|
||||
fun Intent.addAutofillComponent(context: Context, autofillComponent: AutofillComponent) {
|
||||
this.putExtra(EXTRA_ASSIST_STRUCTURE, autofillComponent.assistStructure)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
|
||||
&& PreferencesUtil.isAutofillInlineSuggestionsEnable(activity)) {
|
||||
&& PreferencesUtil.isAutofillInlineSuggestionsEnable(context)) {
|
||||
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
|
||||
@@ -17,7 +17,7 @@
|
||||
* 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.os.Build
|
||||
@@ -17,7 +17,7 @@
|
||||
* 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.app.PendingIntent
|
||||
@@ -43,8 +43,8 @@ import androidx.annotation.RequiresApi
|
||||
import androidx.autofill.inline.UiVersions
|
||||
import androidx.autofill.inline.v1.InlineSuggestionUi
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.activities.AutofillLauncherActivity
|
||||
import com.kunzisoft.keepass.autofill.StructureParser.Companion.APPLICATION_ID_POPUP_WINDOW
|
||||
import com.kunzisoft.keepass.credentialprovider.activity.AutofillLauncherActivity
|
||||
import com.kunzisoft.keepass.credentialprovider.autofill.StructureParser.Companion.APPLICATION_ID_POPUP_WINDOW
|
||||
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||
import com.kunzisoft.keepass.database.DatabaseTaskProvider
|
||||
import com.kunzisoft.keepass.database.helper.SearchHelper
|
||||
@@ -143,25 +143,28 @@ class KeeAutofillService : AutofillService() {
|
||||
parseResult: StructureParser.Result,
|
||||
inlineSuggestionsRequest: CompatInlineSuggestionsRequest?,
|
||||
callback: FillCallback) {
|
||||
SearchHelper.checkAutoSearchInfo(this,
|
||||
database,
|
||||
searchInfo,
|
||||
{ openedDatabase, items ->
|
||||
callback.onSuccess(
|
||||
AutofillHelper.buildResponse(this, openedDatabase,
|
||||
items, parseResult, inlineSuggestionsRequest)
|
||||
SearchHelper.checkAutoSearchInfo(
|
||||
context = this,
|
||||
database = database,
|
||||
searchInfo = searchInfo,
|
||||
onItemsFound = { openedDatabase, items ->
|
||||
callback.onSuccess(
|
||||
AutofillHelper.buildResponse(
|
||||
this, openedDatabase,
|
||||
items, parseResult, inlineSuggestionsRequest
|
||||
)
|
||||
},
|
||||
{ openedDatabase ->
|
||||
// Show UI if no search result
|
||||
showUIForEntrySelection(parseResult, openedDatabase,
|
||||
searchInfo, inlineSuggestionsRequest, callback)
|
||||
},
|
||||
{
|
||||
// Show UI if database not open
|
||||
showUIForEntrySelection(parseResult, null,
|
||||
searchInfo, inlineSuggestionsRequest, callback)
|
||||
}
|
||||
)
|
||||
},
|
||||
onItemNotFound = { openedDatabase ->
|
||||
// Show UI if no search result
|
||||
showUIForEntrySelection(parseResult, openedDatabase,
|
||||
searchInfo, inlineSuggestionsRequest, callback)
|
||||
},
|
||||
onDatabaseClosed = {
|
||||
// Show UI if database not open
|
||||
showUIForEntrySelection(parseResult, null,
|
||||
searchInfo, inlineSuggestionsRequest, callback)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -385,19 +388,21 @@ class KeeAutofillService : AutofillService() {
|
||||
|
||||
// Show UI to save data
|
||||
val registerInfo = RegisterInfo(
|
||||
SearchInfo().apply {
|
||||
applicationId = parseResult.applicationId
|
||||
webDomain = parseResult.webDomain
|
||||
webScheme = parseResult.webScheme
|
||||
},
|
||||
parseResult.usernameValue?.textValue?.toString(),
|
||||
parseResult.passwordValue?.textValue?.toString(),
|
||||
searchInfo = SearchInfo().apply {
|
||||
applicationId = parseResult.applicationId
|
||||
webDomain = parseResult.webDomain
|
||||
webScheme = parseResult.webScheme
|
||||
},
|
||||
username = parseResult.usernameValue?.textValue?.toString(),
|
||||
password = parseResult.passwordValue?.textValue?.toString(),
|
||||
creditCard =
|
||||
CreditCard(
|
||||
parseResult.creditCardHolder,
|
||||
parseResult.creditCardNumber,
|
||||
expiration,
|
||||
parseResult.cardVerificationValue
|
||||
))
|
||||
)
|
||||
)
|
||||
|
||||
// TODO Callback in each activity #765
|
||||
//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
|
||||
* 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.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.
|
||||
*/
|
||||
|
||||
package com.kunzisoft.keepass.magikeyboard;
|
||||
package com.kunzisoft.keepass.credentialprovider.magikeyboard;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.Resources;
|
||||
@@ -14,14 +14,14 @@
|
||||
* 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.magikeyboard.MagikeyboardService.KEY_CHANGE_KEYBOARD;
|
||||
import static com.kunzisoft.keepass.magikeyboard.MagikeyboardService.KEY_ENTRY;
|
||||
import static com.kunzisoft.keepass.magikeyboard.MagikeyboardService.KEY_ENTRY_ALT;
|
||||
import static com.kunzisoft.keepass.magikeyboard.MagikeyboardService.KEY_OTP;
|
||||
import static com.kunzisoft.keepass.magikeyboard.MagikeyboardService.KEY_OTP_ALT;
|
||||
import static com.kunzisoft.keepass.credentialprovider.magikeyboard.MagikeyboardService.KEY_BACK_KEYBOARD;
|
||||
import static com.kunzisoft.keepass.credentialprovider.magikeyboard.MagikeyboardService.KEY_CHANGE_KEYBOARD;
|
||||
import static com.kunzisoft.keepass.credentialprovider.magikeyboard.MagikeyboardService.KEY_ENTRY;
|
||||
import static com.kunzisoft.keepass.credentialprovider.magikeyboard.MagikeyboardService.KEY_ENTRY_ALT;
|
||||
import static com.kunzisoft.keepass.credentialprovider.magikeyboard.MagikeyboardService.KEY_OTP;
|
||||
import static com.kunzisoft.keepass.credentialprovider.magikeyboard.MagikeyboardService.KEY_OTP_ALT;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.TypedArray;
|
||||
@@ -52,7 +52,7 @@ import android.widget.TextView;
|
||||
import androidx.annotation.RequiresApi;
|
||||
|
||||
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.HashMap;
|
||||
@@ -18,7 +18,7 @@
|
||||
*
|
||||
*/
|
||||
|
||||
package com.kunzisoft.keepass.magikeyboard
|
||||
package com.kunzisoft.keepass.credentialprovider.magikeyboard
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
@@ -41,8 +41,8 @@ import androidx.core.graphics.BlendModeCompat
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.activities.EntrySelectionLauncherActivity
|
||||
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
||||
import com.kunzisoft.keepass.credentialprovider.activity.EntrySelectionLauncherActivity
|
||||
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper
|
||||
import com.kunzisoft.keepass.adapters.FieldsAdapter
|
||||
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||
import com.kunzisoft.keepass.database.DatabaseTaskProvider
|
||||
@@ -341,10 +341,11 @@ class MagikeyboardService : InputMethodService(), KeyboardView.OnKeyboardActionL
|
||||
}
|
||||
|
||||
private fun actionKeyEntry(searchInfo: SearchInfo? = null) {
|
||||
SearchHelper.checkAutoSearchInfo(this,
|
||||
mDatabase,
|
||||
searchInfo,
|
||||
{ _, items ->
|
||||
SearchHelper.checkAutoSearchInfo(
|
||||
context = this,
|
||||
database = mDatabase,
|
||||
searchInfo = searchInfo,
|
||||
onItemsFound = { _, items ->
|
||||
performSelection(
|
||||
items,
|
||||
{
|
||||
@@ -361,11 +362,11 @@ class MagikeyboardService : InputMethodService(), KeyboardView.OnKeyboardActionL
|
||||
}
|
||||
)
|
||||
},
|
||||
{
|
||||
onItemNotFound = {
|
||||
// Select if not found
|
||||
launchEntrySelection(searchInfo)
|
||||
},
|
||||
{
|
||||
onDatabaseClosed = {
|
||||
// Select if database not opened
|
||||
removeEntryInfo()
|
||||
launchEntrySelection(searchInfo)
|
||||
@@ -463,21 +464,18 @@ class MagikeyboardService : InputMethodService(), KeyboardView.OnKeyboardActionL
|
||||
fun performSelection(items: List<EntryInfo>,
|
||||
actionPopulateKeyboard: (entryInfo: EntryInfo) -> Unit,
|
||||
actionEntrySelection: (autoSearch: Boolean) -> Unit) {
|
||||
if (items.size == 1) {
|
||||
val itemFound = items[0]
|
||||
if (entryUUID != itemFound.id) {
|
||||
actionPopulateKeyboard.invoke(itemFound)
|
||||
} else {
|
||||
// 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)
|
||||
} else {
|
||||
// Select an arbitrary one
|
||||
actionEntrySelection.invoke(false)
|
||||
}
|
||||
EntrySelectionHelper.performSelection(
|
||||
items = items,
|
||||
actionPopulateCredentialProvider = { itemFound ->
|
||||
if (entryUUID != itemFound.id) {
|
||||
actionPopulateKeyboard.invoke(itemFound)
|
||||
} else {
|
||||
// Force selection if magikeyboard already populated
|
||||
actionEntrySelection.invoke(false)
|
||||
}
|
||||
},
|
||||
actionEntrySelection = actionEntrySelection
|
||||
)
|
||||
}
|
||||
|
||||
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(
|
||||
val relyingParty: 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 {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.kunzisoft.keepass.credentialprovider.util
|
||||
package com.kunzisoft.keepass.credentialprovider.passkey.util
|
||||
|
||||
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 androidx.credentials.webauthn.Cbor
|
||||
import com.kunzisoft.encrypt.HashManager
|
||||
import com.kunzisoft.keepass.credentialprovider.data.PublicKeyCredentialCreationOptions
|
||||
import com.kunzisoft.keepass.credentialprovider.data.PublicKeyCredentialRequestOptions
|
||||
import com.kunzisoft.keepass.credentialprovider.util.Base64Helper.Companion.b64Decode
|
||||
import com.kunzisoft.keepass.credentialprovider.util.Base64Helper.Companion.b64Encode
|
||||
import com.kunzisoft.keepass.credentialprovider.passkey.data.PublicKeyCredentialCreationOptions
|
||||
import com.kunzisoft.keepass.credentialprovider.passkey.data.PublicKeyCredentialRequestOptions
|
||||
import com.kunzisoft.keepass.credentialprovider.passkey.util.Base64Helper.Companion.b64Decode
|
||||
import com.kunzisoft.keepass.credentialprovider.passkey.util.Base64Helper.Companion.b64Encode
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONObject
|
||||
|
||||
@@ -245,7 +245,5 @@ class JsonHelper {
|
||||
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 androidx.credentials.provider.CallingAppInfo
|
||||
@@ -9,13 +9,12 @@ class OriginHelper {
|
||||
|
||||
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 {
|
||||
it.readText()
|
||||
}
|
||||
// for trusted browsers like Chrome and Firefox
|
||||
val origin = callingAppInfo.getOrigin(privilegedAllowlist)?.removeSuffix("/")
|
||||
return origin
|
||||
return callingAppInfo?.getOrigin(privilegedAllowlist)?.removeSuffix("/")
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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]
|
||||
*/
|
||||
fun checkAutoSearchInfo(context: Context,
|
||||
database: ContextualDatabase?,
|
||||
searchInfo: SearchInfo?,
|
||||
onItemsFound: (openedDatabase: ContextualDatabase,
|
||||
items: List<EntryInfo>) -> Unit,
|
||||
onItemNotFound: (openedDatabase: ContextualDatabase) -> Unit,
|
||||
onDatabaseClosed: () -> Unit) {
|
||||
fun checkAutoSearchInfo(
|
||||
context: Context,
|
||||
database: ContextualDatabase?,
|
||||
searchInfo: SearchInfo?,
|
||||
onItemsFound: (openedDatabase: ContextualDatabase,
|
||||
items: List<EntryInfo>) -> Unit,
|
||||
onItemNotFound: (openedDatabase: ContextualDatabase) -> Unit,
|
||||
onDatabaseClosed: () -> Unit
|
||||
) {
|
||||
if (database == null || !database.loaded) {
|
||||
onDatabaseClosed.invoke()
|
||||
} else if (TimeoutHelper.checkTime(context)) {
|
||||
@@ -59,8 +61,7 @@ object SearchHelper {
|
||||
&& !searchInfo.containsOnlyNullValues()) {
|
||||
// If search provide results
|
||||
database.createVirtualGroupFromSearchInfo(
|
||||
searchInfo.toString(),
|
||||
searchInfo.isASearchByDomain(),
|
||||
searchInfo,
|
||||
MAX_SEARCH_ENTRY
|
||||
)?.let { searchGroup ->
|
||||
if (searchGroup.numberOfChildEntries > 0) {
|
||||
|
||||
@@ -1,13 +1,9 @@
|
||||
package com.kunzisoft.keepass.receivers
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.util.Log
|
||||
import com.kunzisoft.keepass.magikeyboard.MagikeyboardService
|
||||
import com.kunzisoft.keepass.utils.DexUtil
|
||||
import com.kunzisoft.keepass.utils.MagikeyboardUtil
|
||||
|
||||
class DexModeReceiver : BroadcastReceiver() {
|
||||
|
||||
@@ -27,7 +27,7 @@ import android.util.Log
|
||||
import android.widget.Toast
|
||||
import androidx.preference.PreferenceManager
|
||||
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.settings.PreferencesUtil
|
||||
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
||||
|
||||
@@ -32,7 +32,7 @@ import android.util.Log
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.kunzisoft.keepass.R
|
||||
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.KeyboardEntryNotificationService
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
|
||||
@@ -4,7 +4,7 @@ import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import android.util.Log
|
||||
import com.kunzisoft.keepass.magikeyboard.MagikeyboardService
|
||||
import com.kunzisoft.keepass.credentialprovider.magikeyboard.MagikeyboardService
|
||||
|
||||
object MagikeyboardUtil {
|
||||
private val TAG = MagikeyboardUtil::class.java.name
|
||||
|
||||
@@ -93,7 +93,7 @@
|
||||
android:maxLines="1"
|
||||
android:ellipsize="end"
|
||||
android:textColor="@color/grey_blue_slighter"/>
|
||||
<com.kunzisoft.keepass.magikeyboard.KeyboardView
|
||||
<com.kunzisoft.keepass.credentialprovider.magikeyboard.KeyboardView
|
||||
android:id="@+id/magikeyboard_view"
|
||||
style="@style/KeepassDXStyle.Keyboard"
|
||||
android:layout_width="match_parent"
|
||||
|
||||
@@ -739,14 +739,12 @@
|
||||
<string name="hide_expired_entries_summary">Expired entries are not shown</string>
|
||||
<string name="hide_templates_title">Hide templates</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_usage_biometric_prompt_subtitle">for %1$s</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">Passkey</string>
|
||||
<string name="passkey_service_name">KeePassDX Credential Provider</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_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_account_name">KeePassDX Database Locked</string>
|
||||
</resources>
|
||||
@@ -1,5 +1,6 @@
|
||||
apply plugin: 'com.android.library'
|
||||
apply plugin: 'kotlin-android'
|
||||
apply plugin: 'kotlin-parcelize'
|
||||
|
||||
android {
|
||||
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.SearchParameters
|
||||
import com.kunzisoft.keepass.hardware.HardwareKey
|
||||
import com.kunzisoft.keepass.model.SearchInfo
|
||||
import com.kunzisoft.keepass.tasks.ProgressTaskUpdater
|
||||
import com.kunzisoft.keepass.utils.SingletonHolder
|
||||
import com.kunzisoft.keepass.utils.StringUtil.toFormattedColorInt
|
||||
@@ -885,28 +886,15 @@ open class Database {
|
||||
}
|
||||
|
||||
fun createVirtualGroupFromSearchInfo(
|
||||
searchInfoString: String,
|
||||
searchInfoByDomain: Boolean,
|
||||
searchInfo: SearchInfo,
|
||||
max: Int = Integer.MAX_VALUE
|
||||
): Group? {
|
||||
return mSearchHelper.createVirtualGroupWithSearchResult(this,
|
||||
SearchParameters().apply {
|
||||
searchQuery = searchInfoString
|
||||
searchInTitles = true
|
||||
searchInUsernames = false
|
||||
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)
|
||||
return mSearchHelper.createVirtualGroupWithSearchResult(
|
||||
database = this,
|
||||
searchParameters = searchInfo.buildSearchParameters(),
|
||||
fromGroup = null,
|
||||
max = max
|
||||
)
|
||||
}
|
||||
|
||||
val tagPool: Tags
|
||||
|
||||
@@ -49,6 +49,10 @@ class Tags: Parcelable {
|
||||
}
|
||||
}
|
||||
|
||||
fun contains(tag: String): Boolean {
|
||||
return mTags.contains(tag)
|
||||
}
|
||||
|
||||
fun isEmpty(): Boolean {
|
||||
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.node.NodeHandler
|
||||
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.utils.UuidUtil
|
||||
import com.kunzisoft.keepass.utils.inTheSameDomainAs
|
||||
@@ -171,8 +172,10 @@ class SearchHelper {
|
||||
}
|
||||
if (searchParameters.searchInOther) {
|
||||
entry.getExtraFields().forEach { field ->
|
||||
if (field.name != OTP_FIELD
|
||||
|| (field.name == OTP_FIELD && searchParameters.searchInOTP)) {
|
||||
if ( (field.name != OTP_FIELD && field.name != FIELD_RELYING_PARTY)
|
||||
|| (searchParameters.searchInOTP && field.name == OTP_FIELD)
|
||||
|| (searchParameters.searchInRelyingParty && field.name == FIELD_RELYING_PARTY)
|
||||
) {
|
||||
if (checkSearchQuery(field.protectedValue.toString(), searchParameters))
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -37,6 +37,7 @@ class SearchParameters() : Parcelable{
|
||||
var searchInExpired = false
|
||||
var searchInNotes = true
|
||||
var searchInOTP = false
|
||||
var searchInRelyingParty = false
|
||||
var searchInOther = true
|
||||
var searchInUUIDs = false
|
||||
var searchInTags = false
|
||||
|
||||
@@ -22,10 +22,15 @@ package com.kunzisoft.keepass.model
|
||||
import android.os.Parcel
|
||||
import android.os.ParcelUuid
|
||||
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.security.ProtectedString
|
||||
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.OtpEntryFields
|
||||
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.readParcelableCompat
|
||||
import com.kunzisoft.keepass.utils.writeBooleanCompat
|
||||
import java.util.*
|
||||
import java.util.Locale
|
||||
import java.util.UUID
|
||||
|
||||
class EntryInfo : NodeInfo {
|
||||
|
||||
@@ -113,6 +119,14 @@ class EntryInfo : NodeInfo {
|
||||
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
|
||||
private fun addUniqueField(field: Field, number: Int = 0) {
|
||||
var sameName = false
|
||||
@@ -226,6 +240,7 @@ class EntryInfo : NodeInfo {
|
||||
}
|
||||
|
||||
if (database?.allowEntryCustomFields() == true) {
|
||||
// TODO Move in a dedicated creditcard class
|
||||
val creditCard: CreditCard? = registerInfo.creditCard
|
||||
creditCard?.cardholder?.let {
|
||||
addUniqueField(Field(TemplateField.LABEL_HOLDER, ProtectedString(false, it)))
|
||||
@@ -240,6 +255,9 @@ class EntryInfo : NodeInfo {
|
||||
creditCard?.cvv?.let {
|
||||
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 com.kunzisoft.keepass.utils.readParcelableCompat
|
||||
|
||||
data class RegisterInfo(val searchInfo: SearchInfo,
|
||||
val username: String?,
|
||||
val password: String?,
|
||||
val creditCard: CreditCard?): Parcelable {
|
||||
data class RegisterInfo(
|
||||
val searchInfo: SearchInfo,
|
||||
val username: String?,
|
||||
val password: String? = null,
|
||||
val creditCard: CreditCard? = null,
|
||||
val passkey: Passkey? = null
|
||||
): Parcelable {
|
||||
|
||||
constructor(parcel: Parcel) : this(
|
||||
parcel.readParcelableCompat() ?: SearchInfo(),
|
||||
parcel.readString() ?: "",
|
||||
parcel.readString() ?: "",
|
||||
parcel.readParcelableCompat(),
|
||||
parcel.readParcelableCompat()) {
|
||||
}
|
||||
|
||||
@@ -21,6 +25,7 @@ data class RegisterInfo(val searchInfo: SearchInfo,
|
||||
parcel.writeString(username)
|
||||
parcel.writeString(password)
|
||||
parcel.writeParcelable(creditCard, flags)
|
||||
parcel.writeParcelable(passkey, flags)
|
||||
}
|
||||
|
||||
override fun describeContents(): Int {
|
||||
|
||||
@@ -4,6 +4,7 @@ import android.content.res.Resources
|
||||
import android.net.Uri
|
||||
import android.os.Parcel
|
||||
import android.os.Parcelable
|
||||
import com.kunzisoft.keepass.database.search.SearchParameters
|
||||
import com.kunzisoft.keepass.otp.OtpEntryFields
|
||||
import com.kunzisoft.keepass.utils.ObjectNameResource
|
||||
import com.kunzisoft.keepass.utils.readBooleanCompat
|
||||
@@ -33,6 +34,7 @@ class SearchInfo : ObjectNameResource, Parcelable {
|
||||
get() {
|
||||
return if (webDomain == null) null else field
|
||||
}
|
||||
var relyingParty: String? = null
|
||||
var otpString: String? = null
|
||||
|
||||
constructor()
|
||||
@@ -42,6 +44,7 @@ class SearchInfo : ObjectNameResource, Parcelable {
|
||||
applicationId = toCopy?.applicationId
|
||||
webDomain = toCopy?.webDomain
|
||||
webScheme = toCopy?.webScheme
|
||||
relyingParty = toCopy?.relyingParty
|
||||
otpString = toCopy?.otpString
|
||||
}
|
||||
|
||||
@@ -53,6 +56,8 @@ class SearchInfo : ObjectNameResource, Parcelable {
|
||||
webDomain = if (readDomain.isNullOrEmpty()) null else readDomain
|
||||
val readScheme = parcel.readString()
|
||||
webScheme = if (readScheme.isNullOrEmpty()) null else readScheme
|
||||
val readRelyingParty = parcel.readString()
|
||||
relyingParty = if (readRelyingParty.isNullOrEmpty()) null else readRelyingParty
|
||||
val readOtp = parcel.readString()
|
||||
otpString = if (readOtp.isNullOrEmpty()) null else readOtp
|
||||
}
|
||||
@@ -66,6 +71,7 @@ class SearchInfo : ObjectNameResource, Parcelable {
|
||||
parcel.writeString(applicationId ?: "")
|
||||
parcel.writeString(webDomain ?: "")
|
||||
parcel.writeString(webScheme ?: "")
|
||||
parcel.writeString(relyingParty ?: "")
|
||||
parcel.writeString(otpString ?: "")
|
||||
}
|
||||
|
||||
@@ -82,12 +88,55 @@ class SearchInfo : ObjectNameResource, Parcelable {
|
||||
return applicationId == null
|
||||
&& webDomain == null
|
||||
&& webScheme == null
|
||||
&& relyingParty == null
|
||||
&& otpString == null
|
||||
}
|
||||
|
||||
fun isASearchByDomain(): Boolean {
|
||||
private fun isASearchByDomain(): Boolean {
|
||||
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 {
|
||||
if (this === other) return true
|
||||
@@ -97,6 +146,7 @@ class SearchInfo : ObjectNameResource, Parcelable {
|
||||
if (applicationId != other.applicationId) return false
|
||||
if (webDomain != other.webDomain) return false
|
||||
if (webScheme != other.webScheme) return false
|
||||
if (relyingParty != other.relyingParty) return false
|
||||
if (otpString != other.otpString) return false
|
||||
|
||||
return true
|
||||
@@ -107,12 +157,13 @@ class SearchInfo : ObjectNameResource, Parcelable {
|
||||
result = 31 * result + (applicationId?.hashCode() ?: 0)
|
||||
result = 31 * result + (webDomain?.hashCode() ?: 0)
|
||||
result = 31 * result + (webScheme?.hashCode() ?: 0)
|
||||
result = 31 * result + (relyingParty?.hashCode() ?: 0)
|
||||
result = 31 * result + (otpString?.hashCode() ?: 0)
|
||||
return result
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return otpString ?: webDomain ?: applicationId ?: ""
|
||||
return otpString ?: webDomain ?: applicationId ?: relyingParty ?: ""
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
Reference in New Issue
Block a user