mirror of
https://github.com/Kunzisoft/KeePassDX.git
synced 2025-12-04 15:49:33 +01:00
Compare commits
15 Commits
master
...
5fd25c6150
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5fd25c6150 | ||
|
|
c1cfddddbe | ||
|
|
9146315001 | ||
|
|
609b536898 | ||
|
|
f9051ce787 | ||
|
|
d90d175bd8 | ||
|
|
c17fba8ef7 | ||
|
|
ed095ad0a7 | ||
|
|
82a8776911 | ||
|
|
753e9c4721 | ||
|
|
b64094ed20 | ||
|
|
bc854c63f7 | ||
|
|
3b793a72b8 | ||
|
|
f19afbdb2e | ||
|
|
622e9cefdd |
@@ -1,3 +1,9 @@
|
|||||||
|
KeePassDX(4.3.0)
|
||||||
|
* Manual change of app language #1884 #1990
|
||||||
|
* Add Passkey User Verification #2283
|
||||||
|
* Fix autofill username detection #2276
|
||||||
|
* Fix Passkey in passwordless mode #2282
|
||||||
|
|
||||||
KeePassDX(4.2.4)
|
KeePassDX(4.2.4)
|
||||||
* Fix remembering database location #2262
|
* Fix remembering database location #2262
|
||||||
|
|
||||||
|
|||||||
@@ -11,8 +11,8 @@ android {
|
|||||||
applicationId "com.kunzisoft.keepass"
|
applicationId "com.kunzisoft.keepass"
|
||||||
minSdkVersion 19
|
minSdkVersion 19
|
||||||
targetSdkVersion 35
|
targetSdkVersion 35
|
||||||
versionCode = 149
|
versionCode = 150
|
||||||
versionName = "4.2.4"
|
versionName = "4.3.0"
|
||||||
multiDexEnabled true
|
multiDexEnabled true
|
||||||
|
|
||||||
testApplicationId = "com.kunzisoft.keepass.tests"
|
testApplicationId = "com.kunzisoft.keepass.tests"
|
||||||
@@ -110,6 +110,10 @@ android {
|
|||||||
// Bouncy castle bug https://github.com/bcgit/bc-java/issues/1685
|
// Bouncy castle bug https://github.com/bcgit/bc-java/issues/1685
|
||||||
resources.pickFirsts.add('META-INF/versions/9/OSGI-INF/MANIFEST.MF')
|
resources.pickFirsts.add('META-INF/versions/9/OSGI-INF/MANIFEST.MF')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
androidResources {
|
||||||
|
generateLocaleConfig true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
def room_version = "2.5.1"
|
def room_version = "2.5.1"
|
||||||
|
|||||||
@@ -31,6 +31,8 @@ import androidx.activity.result.contract.ActivityResultContracts
|
|||||||
import androidx.activity.viewModels
|
import androidx.activity.viewModels
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
import androidx.biometric.BiometricPrompt
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.activities.FileDatabaseSelectActivity
|
import com.kunzisoft.keepass.activities.FileDatabaseSelectActivity
|
||||||
@@ -44,8 +46,15 @@ import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.setActivity
|
|||||||
import com.kunzisoft.keepass.credentialprovider.SpecialMode
|
import com.kunzisoft.keepass.credentialprovider.SpecialMode
|
||||||
import com.kunzisoft.keepass.credentialprovider.TypeMode
|
import com.kunzisoft.keepass.credentialprovider.TypeMode
|
||||||
import com.kunzisoft.keepass.credentialprovider.passkey.data.AndroidPrivilegedApp
|
import com.kunzisoft.keepass.credentialprovider.passkey.data.AndroidPrivilegedApp
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.passkey.data.UserVerificationRequirement
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.ALLOWED_AUTHENTICATORS
|
||||||
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.addAppOrigin
|
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.addAppOrigin
|
||||||
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.addAuthCode
|
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.addAuthCode
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.addUserVerification
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.getUserVerificationCondition
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.getUserVerifiedWithAuth
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.isAuthenticatorsAllowed
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.removeUserVerification
|
||||||
import com.kunzisoft.keepass.credentialprovider.viewmodel.CredentialLauncherViewModel
|
import com.kunzisoft.keepass.credentialprovider.viewmodel.CredentialLauncherViewModel
|
||||||
import com.kunzisoft.keepass.credentialprovider.viewmodel.PasskeyLauncherViewModel
|
import com.kunzisoft.keepass.credentialprovider.viewmodel.PasskeyLauncherViewModel
|
||||||
import com.kunzisoft.keepass.database.ContextualDatabase
|
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||||
@@ -54,6 +63,7 @@ import com.kunzisoft.keepass.model.SearchInfo
|
|||||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_ENTRY_TASK
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_ENTRY_TASK
|
||||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||||
import com.kunzisoft.keepass.utils.AppUtil.randomRequestCode
|
import com.kunzisoft.keepass.utils.AppUtil.randomRequestCode
|
||||||
|
import com.kunzisoft.keepass.utils.LOCK_ACTION
|
||||||
import com.kunzisoft.keepass.view.toastError
|
import com.kunzisoft.keepass.view.toastError
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
@@ -82,10 +92,64 @@ class PasskeyLauncherActivity : DatabaseLockActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
// To manage https://github.com/Kunzisoft/KeePassDX/issues/2283
|
||||||
|
val userVerificationCondition = intent.getUserVerificationCondition()
|
||||||
|
if (userVerificationCondition) {
|
||||||
|
if (isAuthenticatorsAllowed().not()) {
|
||||||
|
intent.removeUserVerification()
|
||||||
|
sendBroadcast(Intent(LOCK_ACTION))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// super.onCreate must be after UserVerification to allow database lock
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
// Biometric must be after super.onCreate
|
||||||
|
if (userVerificationCondition) {
|
||||||
|
if (isAuthenticatorsAllowed()) {
|
||||||
|
BiometricPrompt(
|
||||||
|
this, ContextCompat.getMainExecutor(this),
|
||||||
|
object : BiometricPrompt.AuthenticationCallback() {
|
||||||
|
override fun onAuthenticationError(
|
||||||
|
errorCode: Int,
|
||||||
|
errString: CharSequence
|
||||||
|
) {
|
||||||
|
super.onAuthenticationError(errorCode, errString)
|
||||||
|
when (errorCode) {
|
||||||
|
BiometricPrompt.ERROR_CANCELED,
|
||||||
|
BiometricPrompt.ERROR_NEGATIVE_BUTTON,
|
||||||
|
BiometricPrompt.ERROR_USER_CANCELED -> {
|
||||||
|
// No operation
|
||||||
|
Log.i(TAG, "$errString")
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
toastError(SecurityException("Authentication error: $errString"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
passkeyLauncherViewModel.cancelResult()
|
||||||
|
}
|
||||||
|
override fun onAuthenticationSucceeded(
|
||||||
|
result: BiometricPrompt.AuthenticationResult
|
||||||
|
) {
|
||||||
|
super.onAuthenticationSucceeded(result)
|
||||||
|
passkeyLauncherViewModel.launchAction(userVerified = true, intent, mSpecialMode)
|
||||||
|
}
|
||||||
|
override fun onAuthenticationFailed() {
|
||||||
|
super.onAuthenticationFailed()
|
||||||
|
toastError(SecurityException(getString(R.string.device_unlock_not_recognized)))
|
||||||
|
passkeyLauncherViewModel.cancelResult()
|
||||||
|
}
|
||||||
|
}).authenticate(
|
||||||
|
BiometricPrompt.PromptInfo.Builder()
|
||||||
|
.setTitle(getString(R.string.user_verification_required))
|
||||||
|
.setAllowedAuthenticators(ALLOWED_AUTHENTICATORS)
|
||||||
|
.setConfirmationRequired(false)
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
// Initialize the parameters
|
// Initialize the parameters
|
||||||
passkeyLauncherViewModel.initialize()
|
passkeyLauncherViewModel.initialize(userVerified = intent.getUserVerifiedWithAuth())
|
||||||
// Retrieve the UI
|
// Retrieve the UI
|
||||||
passkeyLauncherViewModel.uiState.collect { uiState ->
|
passkeyLauncherViewModel.uiState.collect { uiState ->
|
||||||
when (uiState) {
|
when (uiState) {
|
||||||
@@ -278,7 +342,9 @@ class PasskeyLauncherActivity : DatabaseLockActivity() {
|
|||||||
specialMode: SpecialMode,
|
specialMode: SpecialMode,
|
||||||
searchInfo: SearchInfo? = null,
|
searchInfo: SearchInfo? = null,
|
||||||
appOrigin: AppOrigin? = null,
|
appOrigin: AppOrigin? = null,
|
||||||
nodeId: UUID? = null
|
nodeId: UUID? = null,
|
||||||
|
userVerification: UserVerificationRequirement = UserVerificationRequirement.PREFERRED,
|
||||||
|
userVerifiedWithAuth: Boolean = true
|
||||||
): PendingIntent? {
|
): PendingIntent? {
|
||||||
return PendingIntent.getActivity(
|
return PendingIntent.getActivity(
|
||||||
context,
|
context,
|
||||||
@@ -290,6 +356,7 @@ class PasskeyLauncherActivity : DatabaseLockActivity() {
|
|||||||
addAppOrigin(appOrigin)
|
addAppOrigin(appOrigin)
|
||||||
addNodeId(nodeId)
|
addNodeId(nodeId)
|
||||||
addAuthCode(nodeId)
|
addAuthCode(nodeId)
|
||||||
|
addUserVerification(userVerification, userVerifiedWithAuth)
|
||||||
},
|
},
|
||||||
PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
|
PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -154,7 +154,7 @@ class StructureParser(private val structure: AssistStructure) {
|
|||||||
it.contains(View.AUTOFILL_HINT_PASSWORD, true) -> {
|
it.contains(View.AUTOFILL_HINT_PASSWORD, true) -> {
|
||||||
// Password Id changed if it's the second times we are here,
|
// Password Id changed if it's the second times we are here,
|
||||||
// So the last username candidate is most appropriate
|
// So the last username candidate is most appropriate
|
||||||
if (result?.passwordId != null) {
|
if (result?.passwordId != null && usernameIdCandidate != null) {
|
||||||
result?.usernameId = usernameIdCandidate
|
result?.usernameId = usernameIdCandidate
|
||||||
result?.usernameValue = usernameValueCandidate
|
result?.usernameValue = usernameValueCandidate
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,12 +42,15 @@ import androidx.credentials.provider.CredentialEntry
|
|||||||
import androidx.credentials.provider.CredentialProviderService
|
import androidx.credentials.provider.CredentialProviderService
|
||||||
import androidx.credentials.provider.ProviderClearCredentialStateRequest
|
import androidx.credentials.provider.ProviderClearCredentialStateRequest
|
||||||
import androidx.credentials.provider.PublicKeyCredentialEntry
|
import androidx.credentials.provider.PublicKeyCredentialEntry
|
||||||
|
import com.kunzisoft.encrypt.Base64Helper.Companion.b64Encode
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.buildIcon
|
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.buildIcon
|
||||||
import com.kunzisoft.keepass.credentialprovider.SpecialMode
|
import com.kunzisoft.keepass.credentialprovider.SpecialMode
|
||||||
import com.kunzisoft.keepass.credentialprovider.activity.PasskeyLauncherActivity
|
import com.kunzisoft.keepass.credentialprovider.activity.PasskeyLauncherActivity
|
||||||
import com.kunzisoft.keepass.credentialprovider.passkey.data.PublicKeyCredentialCreationOptions
|
import com.kunzisoft.keepass.credentialprovider.passkey.data.PublicKeyCredentialCreationOptions
|
||||||
import com.kunzisoft.keepass.credentialprovider.passkey.data.PublicKeyCredentialRequestOptions
|
import com.kunzisoft.keepass.credentialprovider.passkey.data.PublicKeyCredentialRequestOptions
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.passkey.data.UserVerificationRequirement
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.isAuthenticatorsAllowed
|
||||||
import com.kunzisoft.keepass.database.ContextualDatabase
|
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||||
import com.kunzisoft.keepass.database.DatabaseTaskProvider
|
import com.kunzisoft.keepass.database.DatabaseTaskProvider
|
||||||
import com.kunzisoft.keepass.database.exception.RegisterInReadOnlyDatabaseException
|
import com.kunzisoft.keepass.database.exception.RegisterInReadOnlyDatabaseException
|
||||||
@@ -64,6 +67,7 @@ class PasskeyProviderService : CredentialProviderService() {
|
|||||||
private var mDatabaseTaskProvider: DatabaseTaskProvider? = null
|
private var mDatabaseTaskProvider: DatabaseTaskProvider? = null
|
||||||
private var mDatabase: ContextualDatabase? = null
|
private var mDatabase: ContextualDatabase? = null
|
||||||
private lateinit var defaultIcon: Icon
|
private lateinit var defaultIcon: Icon
|
||||||
|
private lateinit var relaunchIcon: Icon
|
||||||
private var isAutoSelectAllowed: Boolean = false
|
private var isAutoSelectAllowed: Boolean = false
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
@@ -82,6 +86,11 @@ class PasskeyProviderService : CredentialProviderService() {
|
|||||||
setTintBlendMode(BlendMode.DST)
|
setTintBlendMode(BlendMode.DST)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
relaunchIcon = Icon.createWithResource(
|
||||||
|
this@PasskeyProviderService,
|
||||||
|
R.drawable.ic_clock_loader_red_24dp
|
||||||
|
)
|
||||||
|
|
||||||
isAutoSelectAllowed = isPasskeyAutoSelectEnable(this)
|
isAutoSelectAllowed = isPasskeyAutoSelectEnable(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -90,9 +99,13 @@ class PasskeyProviderService : CredentialProviderService() {
|
|||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun buildPasskeySearchInfo(relyingParty: String): SearchInfo {
|
private fun buildPasskeySearchInfo(
|
||||||
|
relyingParty: String,
|
||||||
|
credentialIds: List<String> = listOf()
|
||||||
|
): SearchInfo {
|
||||||
return SearchInfo().apply {
|
return SearchInfo().apply {
|
||||||
this.relyingParty = relyingParty
|
this.relyingParty = relyingParty
|
||||||
|
this.credentialIds = credentialIds
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -108,6 +121,7 @@ class PasskeyProviderService : CredentialProviderService() {
|
|||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(javaClass.simpleName, "onBeginGetCredentialRequest error", e)
|
Log.e(javaClass.simpleName, "onBeginGetCredentialRequest error", e)
|
||||||
|
toastError(e)
|
||||||
callback.onError(GetCredentialUnknownException())
|
callback.onError(GetCredentialUnknownException())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -136,65 +150,100 @@ class PasskeyProviderService : CredentialProviderService() {
|
|||||||
option: BeginGetPublicKeyCredentialOption,
|
option: BeginGetPublicKeyCredentialOption,
|
||||||
callback: (List<CredentialEntry>) -> Unit
|
callback: (List<CredentialEntry>) -> Unit
|
||||||
) {
|
) {
|
||||||
|
|
||||||
val passkeyEntries: MutableList<CredentialEntry> = mutableListOf()
|
val passkeyEntries: MutableList<CredentialEntry> = mutableListOf()
|
||||||
|
|
||||||
val relyingPartyId = PublicKeyCredentialRequestOptions(option.requestJson).rpId
|
val publicKeyCredentialRequestOptions = PublicKeyCredentialRequestOptions(option.requestJson)
|
||||||
val searchInfo = buildPasskeySearchInfo(relyingPartyId)
|
val relyingPartyId = publicKeyCredentialRequestOptions.rpId
|
||||||
Log.d(TAG, "Build passkey search for relying party $relyingPartyId")
|
val credentialIdList = publicKeyCredentialRequestOptions.allowCredentials
|
||||||
|
.map { b64Encode(it.id) }
|
||||||
|
val searchInfo = buildPasskeySearchInfo(relyingPartyId, credentialIdList)
|
||||||
|
val userVerification = publicKeyCredentialRequestOptions.userVerification
|
||||||
|
Log.d(TAG, "Build passkey search for UV $userVerification, " +
|
||||||
|
"RP $relyingPartyId and Credential IDs $credentialIdList")
|
||||||
SearchHelper.checkAutoSearchInfo(
|
SearchHelper.checkAutoSearchInfo(
|
||||||
context = this,
|
context = this,
|
||||||
database = mDatabase,
|
database = mDatabase,
|
||||||
searchInfo = searchInfo,
|
searchInfo = searchInfo,
|
||||||
onItemsFound = { database, items ->
|
onItemsFound = { database, items ->
|
||||||
Log.d(TAG, "Add pending intent for passkey selection with found items")
|
manageUserVerification(
|
||||||
for (passkeyEntry in items) {
|
passkeyEntries = passkeyEntries,
|
||||||
PasskeyLauncherActivity.getPendingIntent(
|
searchInfo = searchInfo,
|
||||||
context = applicationContext,
|
option = option,
|
||||||
specialMode = SpecialMode.SELECTION,
|
userVerification = userVerification
|
||||||
nodeId = passkeyEntry.id,
|
) {
|
||||||
appOrigin = passkeyEntry.appOrigin
|
Log.d(TAG, "Add pending intent for passkey selection with found items")
|
||||||
)?.let { usagePendingIntent ->
|
for (passkeyEntry in items) {
|
||||||
val passkey = passkeyEntry.passkey
|
PasskeyLauncherActivity.getPendingIntent(
|
||||||
passkeyEntries.add(
|
context = applicationContext,
|
||||||
PublicKeyCredentialEntry(
|
specialMode = SpecialMode.SELECTION,
|
||||||
context = applicationContext,
|
nodeId = passkeyEntry.id,
|
||||||
username = passkey?.username ?: "Unknown",
|
appOrigin = passkeyEntry.appOrigin,
|
||||||
icon = passkeyEntry.buildIcon(this@PasskeyProviderService, database)?.apply {
|
userVerification = userVerification,
|
||||||
setTintBlendMode(BlendMode.DST)
|
userVerifiedWithAuth = false
|
||||||
} ?: defaultIcon,
|
)?.let { usagePendingIntent ->
|
||||||
pendingIntent = usagePendingIntent,
|
val passkey = passkeyEntry.passkey
|
||||||
beginGetPublicKeyCredentialOption = option,
|
passkeyEntries.add(
|
||||||
displayName = passkeyEntry.getVisualTitle(),
|
PublicKeyCredentialEntry(
|
||||||
isAutoSelectAllowed = isAutoSelectAllowed
|
context = applicationContext,
|
||||||
|
username = passkey?.username ?: "Unknown",
|
||||||
|
icon = passkeyEntry.buildIcon(
|
||||||
|
this@PasskeyProviderService,
|
||||||
|
database
|
||||||
|
)?.apply {
|
||||||
|
setTintBlendMode(BlendMode.DST)
|
||||||
|
} ?: defaultIcon,
|
||||||
|
pendingIntent = usagePendingIntent,
|
||||||
|
beginGetPublicKeyCredentialOption = option,
|
||||||
|
displayName = passkeyEntry.getVisualTitle(),
|
||||||
|
isAutoSelectAllowed = isAutoSelectAllowed
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
callback(passkeyEntries)
|
callback(passkeyEntries)
|
||||||
},
|
},
|
||||||
onItemNotFound = { _ ->
|
onItemNotFound = { _ ->
|
||||||
Log.w(TAG, "No passkey found in the database with this relying party : $relyingPartyId")
|
manageUserVerification(
|
||||||
Log.d(TAG, "Add pending intent for passkey selection in opened database")
|
passkeyEntries = passkeyEntries,
|
||||||
PasskeyLauncherActivity.getPendingIntent(
|
searchInfo = searchInfo,
|
||||||
context = applicationContext,
|
option = option,
|
||||||
specialMode = SpecialMode.SELECTION,
|
userVerification = userVerification,
|
||||||
searchInfo = searchInfo
|
) {
|
||||||
)?.let { pendingIntent ->
|
Log.w(TAG, "No passkey found in the database with this relying party : $relyingPartyId")
|
||||||
passkeyEntries.add(
|
if (credentialIdList.isEmpty()) {
|
||||||
PublicKeyCredentialEntry(
|
Log.d(TAG, "Add pending intent for passkey selection in opened database")
|
||||||
|
PasskeyLauncherActivity.getPendingIntent(
|
||||||
context = applicationContext,
|
context = applicationContext,
|
||||||
username = getString(R.string.passkey_database_username),
|
specialMode = SpecialMode.SELECTION,
|
||||||
displayName = getString(R.string.passkey_selection_description),
|
searchInfo = searchInfo,
|
||||||
icon = defaultIcon,
|
userVerification = userVerification,
|
||||||
pendingIntent = pendingIntent,
|
userVerifiedWithAuth = false
|
||||||
beginGetPublicKeyCredentialOption = option,
|
)?.let { pendingIntent ->
|
||||||
lastUsedTime = Instant.now(),
|
passkeyEntries.add(
|
||||||
isAutoSelectAllowed = isAutoSelectAllowed
|
PublicKeyCredentialEntry(
|
||||||
|
context = applicationContext,
|
||||||
|
username = getString(R.string.passkey_database_username),
|
||||||
|
displayName = getString(R.string.passkey_selection_description),
|
||||||
|
icon = defaultIcon,
|
||||||
|
pendingIntent = pendingIntent,
|
||||||
|
beginGetPublicKeyCredentialOption = option,
|
||||||
|
lastUsedTime = Instant.now(),
|
||||||
|
isAutoSelectAllowed = isAutoSelectAllowed
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
callback(passkeyEntries)
|
||||||
|
} else {
|
||||||
|
throw IOException(
|
||||||
|
getString(
|
||||||
|
R.string.error_passkey_credential_id,
|
||||||
|
relyingPartyId,
|
||||||
|
credentialIdList
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
}
|
||||||
}
|
}
|
||||||
callback(passkeyEntries)
|
|
||||||
},
|
},
|
||||||
onDatabaseClosed = {
|
onDatabaseClosed = {
|
||||||
Log.d(TAG, "Add pending intent for passkey selection in closed database")
|
Log.d(TAG, "Add pending intent for passkey selection in closed database")
|
||||||
@@ -202,7 +251,8 @@ class PasskeyProviderService : CredentialProviderService() {
|
|||||||
PasskeyLauncherActivity.getPendingIntent(
|
PasskeyLauncherActivity.getPendingIntent(
|
||||||
context = applicationContext,
|
context = applicationContext,
|
||||||
specialMode = SpecialMode.SELECTION,
|
specialMode = SpecialMode.SELECTION,
|
||||||
searchInfo = searchInfo
|
searchInfo = searchInfo,
|
||||||
|
userVerifiedWithAuth = true
|
||||||
)?.let { pendingIntent ->
|
)?.let { pendingIntent ->
|
||||||
passkeyEntries.add(
|
passkeyEntries.add(
|
||||||
PublicKeyCredentialEntry(
|
PublicKeyCredentialEntry(
|
||||||
@@ -222,6 +272,42 @@ class PasskeyProviderService : CredentialProviderService() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* To easily manage user verification condition
|
||||||
|
*/
|
||||||
|
private fun manageUserVerification(
|
||||||
|
passkeyEntries: MutableList<CredentialEntry>,
|
||||||
|
searchInfo: SearchInfo,
|
||||||
|
option: BeginGetPublicKeyCredentialOption,
|
||||||
|
userVerification: UserVerificationRequirement,
|
||||||
|
standardAction: () -> Unit
|
||||||
|
) {
|
||||||
|
if (userVerification == UserVerificationRequirement.REQUIRED && isAuthenticatorsAllowed().not()) {
|
||||||
|
PasskeyLauncherActivity.getPendingIntent(
|
||||||
|
context = applicationContext,
|
||||||
|
specialMode = SpecialMode.SELECTION,
|
||||||
|
searchInfo = searchInfo,
|
||||||
|
userVerification = userVerification,
|
||||||
|
userVerifiedWithAuth = true
|
||||||
|
)?.let { pendingIntent ->
|
||||||
|
passkeyEntries.add(
|
||||||
|
PublicKeyCredentialEntry(
|
||||||
|
context = applicationContext,
|
||||||
|
username = getString(R.string.passkey_database_username),
|
||||||
|
displayName = getString(R.string.passkey_relaunch_database_description),
|
||||||
|
icon = relaunchIcon,
|
||||||
|
pendingIntent = pendingIntent,
|
||||||
|
beginGetPublicKeyCredentialOption = option,
|
||||||
|
lastUsedTime = Instant.now(),
|
||||||
|
isAutoSelectAllowed = isAutoSelectAllowed
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
standardAction()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onBeginCreateCredentialRequest(
|
override fun onBeginCreateCredentialRequest(
|
||||||
request: BeginCreateCredentialRequest,
|
request: BeginCreateCredentialRequest,
|
||||||
cancellationSignal: CancellationSignal,
|
cancellationSignal: CancellationSignal,
|
||||||
@@ -257,14 +343,17 @@ class PasskeyProviderService : CredentialProviderService() {
|
|||||||
|
|
||||||
private fun MutableList<CreateEntry>.addPendingIntentCreationNewEntry(
|
private fun MutableList<CreateEntry>.addPendingIntentCreationNewEntry(
|
||||||
accountName: String,
|
accountName: String,
|
||||||
searchInfo: SearchInfo?
|
searchInfo: SearchInfo?,
|
||||||
|
userVerification: UserVerificationRequirement
|
||||||
) {
|
) {
|
||||||
Log.d(TAG, "Add pending intent for registration in opened database to create new item")
|
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
|
// TODO add a setting to directly store in a specific group
|
||||||
PasskeyLauncherActivity.getPendingIntent(
|
PasskeyLauncherActivity.getPendingIntent(
|
||||||
context = applicationContext,
|
context = applicationContext,
|
||||||
specialMode = SpecialMode.REGISTRATION,
|
specialMode = SpecialMode.REGISTRATION,
|
||||||
searchInfo = searchInfo
|
searchInfo = searchInfo,
|
||||||
|
userVerification = userVerification,
|
||||||
|
userVerifiedWithAuth = false
|
||||||
)?.let { pendingIntent ->
|
)?.let { pendingIntent ->
|
||||||
this.add(
|
this.add(
|
||||||
CreateEntry(
|
CreateEntry(
|
||||||
@@ -287,11 +376,13 @@ class PasskeyProviderService : CredentialProviderService() {
|
|||||||
getString(R.string.passkey_database_username)
|
getString(R.string.passkey_database_username)
|
||||||
else databaseName
|
else databaseName
|
||||||
val createEntries: MutableList<CreateEntry> = mutableListOf()
|
val createEntries: MutableList<CreateEntry> = mutableListOf()
|
||||||
val relyingPartyId = PublicKeyCredentialCreationOptions(
|
val publicKeyCredentialCreationOptions = PublicKeyCredentialCreationOptions(
|
||||||
requestJson = request.requestJson,
|
requestJson = request.requestJson,
|
||||||
clientDataHash = request.clientDataHash
|
clientDataHash = request.clientDataHash
|
||||||
).relyingPartyEntity.id
|
)
|
||||||
|
val relyingPartyId = publicKeyCredentialCreationOptions.relyingPartyEntity.id
|
||||||
val searchInfo = buildPasskeySearchInfo(relyingPartyId)
|
val searchInfo = buildPasskeySearchInfo(relyingPartyId)
|
||||||
|
val userVerification = publicKeyCredentialCreationOptions.authenticatorSelection.userVerification
|
||||||
Log.d(TAG, "Build passkey search for relying party $relyingPartyId")
|
Log.d(TAG, "Build passkey search for relying party $relyingPartyId")
|
||||||
SearchHelper.checkAutoSearchInfo(
|
SearchHelper.checkAutoSearchInfo(
|
||||||
context = this,
|
context = this,
|
||||||
@@ -302,7 +393,11 @@ class PasskeyProviderService : CredentialProviderService() {
|
|||||||
throw RegisterInReadOnlyDatabaseException()
|
throw RegisterInReadOnlyDatabaseException()
|
||||||
} else {
|
} else {
|
||||||
// To create a new entry
|
// To create a new entry
|
||||||
createEntries.addPendingIntentCreationNewEntry(accountName, searchInfo)
|
createEntries.addPendingIntentCreationNewEntry(
|
||||||
|
accountName = accountName,
|
||||||
|
searchInfo = searchInfo,
|
||||||
|
userVerification = userVerification
|
||||||
|
)
|
||||||
/* TODO Overwrite
|
/* TODO Overwrite
|
||||||
// To select an existing entry and permit an overwrite
|
// To select an existing entry and permit an overwrite
|
||||||
Log.w(TAG, "Passkey already registered")
|
Log.w(TAG, "Passkey already registered")
|
||||||
@@ -333,7 +428,11 @@ class PasskeyProviderService : CredentialProviderService() {
|
|||||||
if (database.isReadOnly) {
|
if (database.isReadOnly) {
|
||||||
throw RegisterInReadOnlyDatabaseException()
|
throw RegisterInReadOnlyDatabaseException()
|
||||||
} else {
|
} else {
|
||||||
createEntries.addPendingIntentCreationNewEntry(accountName, searchInfo)
|
createEntries.addPendingIntentCreationNewEntry(
|
||||||
|
accountName = accountName,
|
||||||
|
searchInfo = searchInfo,
|
||||||
|
userVerification = userVerification
|
||||||
|
)
|
||||||
}
|
}
|
||||||
callback(createEntries)
|
callback(createEntries)
|
||||||
},
|
},
|
||||||
@@ -342,7 +441,8 @@ class PasskeyProviderService : CredentialProviderService() {
|
|||||||
Log.d(TAG, "Add pending intent for passkey registration in closed database")
|
Log.d(TAG, "Add pending intent for passkey registration in closed database")
|
||||||
PasskeyLauncherActivity.getPendingIntent(
|
PasskeyLauncherActivity.getPendingIntent(
|
||||||
context = applicationContext,
|
context = applicationContext,
|
||||||
specialMode = SpecialMode.REGISTRATION
|
specialMode = SpecialMode.REGISTRATION,
|
||||||
|
userVerifiedWithAuth = true
|
||||||
)?.let { pendingIntent ->
|
)?.let { pendingIntent ->
|
||||||
createEntries.add(
|
createEntries.add(
|
||||||
CreateEntry(
|
CreateEntry(
|
||||||
|
|||||||
@@ -16,7 +16,25 @@
|
|||||||
|
|
||||||
package com.kunzisoft.keepass.credentialprovider.passkey.data
|
package com.kunzisoft.keepass.credentialprovider.passkey.data
|
||||||
|
|
||||||
data class PublicKeyCredentialRpEntity(val name: String, val id: String)
|
import com.kunzisoft.encrypt.Base64Helper
|
||||||
|
import org.json.JSONObject
|
||||||
|
|
||||||
|
data class PublicKeyCredentialRpEntity(
|
||||||
|
val name: String,
|
||||||
|
val id: String
|
||||||
|
) {
|
||||||
|
companion object {
|
||||||
|
fun JSONObject.getPublicKeyCredentialRpEntity(
|
||||||
|
parameterName: String
|
||||||
|
): PublicKeyCredentialRpEntity {
|
||||||
|
val rpJson = this.getJSONObject(parameterName)
|
||||||
|
return PublicKeyCredentialRpEntity(
|
||||||
|
rpJson.getString("name"),
|
||||||
|
rpJson.getString("id")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
data class PublicKeyCredentialUserEntity(
|
data class PublicKeyCredentialUserEntity(
|
||||||
val name: String,
|
val name: String,
|
||||||
@@ -42,9 +60,41 @@ data class PublicKeyCredentialUserEntity(
|
|||||||
result = 31 * result + displayName.hashCode()
|
result = 31 * result + displayName.hashCode()
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun JSONObject.getPublicKeyCredentialUserEntity(
|
||||||
|
parameterName: String
|
||||||
|
): PublicKeyCredentialUserEntity {
|
||||||
|
val rpUser = this.getJSONObject(parameterName)
|
||||||
|
return PublicKeyCredentialUserEntity(
|
||||||
|
rpUser.getString("name"),
|
||||||
|
Base64Helper.b64Decode(rpUser.getString("id")),
|
||||||
|
rpUser.getString("displayName")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
data class PublicKeyCredentialParameters(val type: String, val alg: Long)
|
data class PublicKeyCredentialParameters(
|
||||||
|
val type: String,
|
||||||
|
val alg: Long
|
||||||
|
) {
|
||||||
|
companion object {
|
||||||
|
fun JSONObject.getPublicKeyCredentialParametersList(
|
||||||
|
parameterName: String
|
||||||
|
): List<PublicKeyCredentialParameters> {
|
||||||
|
val pubKeyCredParamsJson = this.getJSONArray(parameterName)
|
||||||
|
val pubKeyCredParamsTmp: MutableList<PublicKeyCredentialParameters> = mutableListOf()
|
||||||
|
for (i in 0 until pubKeyCredParamsJson.length()) {
|
||||||
|
val e = pubKeyCredParamsJson.getJSONObject(i)
|
||||||
|
pubKeyCredParamsTmp.add(
|
||||||
|
PublicKeyCredentialParameters(e.getString("type"), e.getLong("alg"))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return pubKeyCredParamsTmp.toList()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
data class PublicKeyCredentialDescriptor(
|
data class PublicKeyCredentialDescriptor(
|
||||||
val type: String,
|
val type: String,
|
||||||
@@ -70,11 +120,104 @@ data class PublicKeyCredentialDescriptor(
|
|||||||
result = 31 * result + transports.hashCode()
|
result = 31 * result + transports.hashCode()
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun JSONObject.getPublicKeyCredentialDescriptorList(
|
||||||
|
parameterName: String
|
||||||
|
): List<PublicKeyCredentialDescriptor> {
|
||||||
|
val credentialsJson = this.getJSONArray(parameterName)
|
||||||
|
val credentialsTmp: MutableList<PublicKeyCredentialDescriptor> = mutableListOf()
|
||||||
|
for (i in 0 until credentialsJson.length()) {
|
||||||
|
val credentialJson = credentialsJson.getJSONObject(i)
|
||||||
|
|
||||||
|
val transports: MutableList<String> = mutableListOf()
|
||||||
|
val transportsJson = credentialJson.getJSONArray("transports")
|
||||||
|
for (j in 0 until transportsJson.length()) {
|
||||||
|
transports.add(transportsJson.getString(j))
|
||||||
|
}
|
||||||
|
credentialsTmp.add(
|
||||||
|
PublicKeyCredentialDescriptor(
|
||||||
|
type = credentialJson.getString("type"),
|
||||||
|
id = Base64Helper.b64Decode(credentialJson.getString("id")),
|
||||||
|
transports = transports
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return credentialsTmp.toList()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// https://www.w3.org/TR/webauthn-3/#dictdef-authenticatorselectioncriteria
|
||||||
data class AuthenticatorSelectionCriteria(
|
data class AuthenticatorSelectionCriteria(
|
||||||
val authenticatorAttachment: String,
|
val authenticatorAttachment: String? = null,
|
||||||
val residentKey: String,
|
val residentKey: ResidentKeyRequirement? = null,
|
||||||
val requireResidentKey: Boolean = false,
|
val requireResidentKey: Boolean?,
|
||||||
val userVerification: String = "preferred"
|
val userVerification: UserVerificationRequirement = UserVerificationRequirement.PREFERRED
|
||||||
)
|
) {
|
||||||
|
companion object {
|
||||||
|
fun JSONObject.getAuthenticatorSelectionCriteria(
|
||||||
|
parameterName: String
|
||||||
|
): AuthenticatorSelectionCriteria {
|
||||||
|
val authenticatorSelection = this.optJSONObject(parameterName)
|
||||||
|
?: return AuthenticatorSelectionCriteria(requireResidentKey = null)
|
||||||
|
val authenticatorAttachment = if (!authenticatorSelection.isNull("authenticatorAttachment"))
|
||||||
|
authenticatorSelection.getString("authenticatorAttachment") else null
|
||||||
|
var residentKey = if (!authenticatorSelection.isNull("residentKey"))
|
||||||
|
ResidentKeyRequirement.fromString(authenticatorSelection.getString("residentKey"))
|
||||||
|
else null
|
||||||
|
val requireResidentKey = authenticatorSelection.optBoolean("requireResidentKey", false)
|
||||||
|
val userVerification = UserVerificationRequirement
|
||||||
|
.fromString(authenticatorSelection.optString("userVerification", "preferred"))
|
||||||
|
?: UserVerificationRequirement.PREFERRED
|
||||||
|
// https://www.w3.org/TR/webauthn-3/#enumdef-residentkeyrequirement
|
||||||
|
if (residentKey == null) {
|
||||||
|
residentKey = if (requireResidentKey) {
|
||||||
|
ResidentKeyRequirement.REQUIRED
|
||||||
|
} else {
|
||||||
|
ResidentKeyRequirement.DISCOURAGED
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return AuthenticatorSelectionCriteria(
|
||||||
|
authenticatorAttachment = authenticatorAttachment,
|
||||||
|
residentKey = residentKey,
|
||||||
|
requireResidentKey = requireResidentKey,
|
||||||
|
userVerification = userVerification
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://www.w3.org/TR/webauthn-3/#enumdef-residentkeyrequirement
|
||||||
|
enum class ResidentKeyRequirement(val value: String) {
|
||||||
|
DISCOURAGED("discouraged"),
|
||||||
|
PREFERRED("preferred"),
|
||||||
|
REQUIRED("required");
|
||||||
|
override fun toString(): String {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
companion object {
|
||||||
|
fun fromString(value: String): ResidentKeyRequirement? {
|
||||||
|
return ResidentKeyRequirement.entries.firstOrNull {
|
||||||
|
it.value.equals(other = value, ignoreCase = true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://www.w3.org/TR/webauthn-3/#enumdef-userverificationrequirement
|
||||||
|
enum class UserVerificationRequirement(val value: String) {
|
||||||
|
REQUIRED("required"),
|
||||||
|
PREFERRED("preferred"),
|
||||||
|
DISCOURAGED("discouraged");
|
||||||
|
override fun toString(): String {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
companion object {
|
||||||
|
fun fromString(value: String): UserVerificationRequirement? {
|
||||||
|
return UserVerificationRequirement.entries.firstOrNull {
|
||||||
|
it.value.equals(other = value, ignoreCase = true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -20,52 +20,42 @@
|
|||||||
package com.kunzisoft.keepass.credentialprovider.passkey.data
|
package com.kunzisoft.keepass.credentialprovider.passkey.data
|
||||||
|
|
||||||
import com.kunzisoft.encrypt.Base64Helper
|
import com.kunzisoft.encrypt.Base64Helper
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.passkey.data.AuthenticatorSelectionCriteria.Companion.getAuthenticatorSelectionCriteria
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.passkey.data.PublicKeyCredentialDescriptor.Companion.getPublicKeyCredentialDescriptorList
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.passkey.data.PublicKeyCredentialParameters.Companion.getPublicKeyCredentialParametersList
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.passkey.data.PublicKeyCredentialRpEntity.Companion.getPublicKeyCredentialRpEntity
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.passkey.data.PublicKeyCredentialUserEntity.Companion.getPublicKeyCredentialUserEntity
|
||||||
import org.json.JSONObject
|
import org.json.JSONObject
|
||||||
|
|
||||||
class PublicKeyCredentialCreationOptions(
|
class PublicKeyCredentialCreationOptions(
|
||||||
requestJson: String,
|
requestJson: String,
|
||||||
var clientDataHash: ByteArray?
|
var clientDataHash: ByteArray?
|
||||||
) {
|
) {
|
||||||
val json: JSONObject = JSONObject(requestJson)
|
private val json: JSONObject = JSONObject(requestJson)
|
||||||
|
|
||||||
val relyingPartyEntity: PublicKeyCredentialRpEntity
|
val relyingPartyEntity: PublicKeyCredentialRpEntity =
|
||||||
val userEntity: PublicKeyCredentialUserEntity
|
json.getPublicKeyCredentialRpEntity("rp")
|
||||||
val challenge: ByteArray
|
|
||||||
val pubKeyCredParams: List<PublicKeyCredentialParameters>
|
|
||||||
|
|
||||||
var timeout: Long
|
val userEntity: PublicKeyCredentialUserEntity =
|
||||||
var excludeCredentials: List<PublicKeyCredentialDescriptor>
|
json.getPublicKeyCredentialUserEntity("user")
|
||||||
var authenticatorSelection: AuthenticatorSelectionCriteria
|
|
||||||
var attestation: String
|
|
||||||
|
|
||||||
init {
|
val challenge: ByteArray =
|
||||||
val rpJson = json.getJSONObject("rp")
|
Base64Helper.b64Decode(json.getString("challenge"))
|
||||||
relyingPartyEntity = PublicKeyCredentialRpEntity(rpJson.getString("name"), rpJson.getString("id"))
|
|
||||||
val rpUser = json.getJSONObject("user")
|
|
||||||
val userId = Base64Helper.b64Decode(rpUser.getString("id"))
|
|
||||||
userEntity =
|
|
||||||
PublicKeyCredentialUserEntity(
|
|
||||||
rpUser.getString("name"),
|
|
||||||
userId,
|
|
||||||
rpUser.getString("displayName")
|
|
||||||
)
|
|
||||||
challenge = Base64Helper.b64Decode(json.getString("challenge"))
|
|
||||||
val pubKeyCredParamsJson = json.getJSONArray("pubKeyCredParams")
|
|
||||||
val pubKeyCredParamsTmp: MutableList<PublicKeyCredentialParameters> = mutableListOf()
|
|
||||||
for (i in 0 until pubKeyCredParamsJson.length()) {
|
|
||||||
val e = pubKeyCredParamsJson.getJSONObject(i)
|
|
||||||
pubKeyCredParamsTmp.add(
|
|
||||||
PublicKeyCredentialParameters(e.getString("type"), e.getLong("alg"))
|
|
||||||
)
|
|
||||||
}
|
|
||||||
pubKeyCredParams = pubKeyCredParamsTmp.toList()
|
|
||||||
|
|
||||||
timeout = json.optLong("timeout", 0)
|
val pubKeyCredParams: List<PublicKeyCredentialParameters> =
|
||||||
// TODO: Fix excludeCredentials and authenticatorSelection
|
json.getPublicKeyCredentialParametersList("pubKeyCredParams")
|
||||||
excludeCredentials = emptyList()
|
|
||||||
authenticatorSelection = AuthenticatorSelectionCriteria("platform", "required")
|
var timeout: Long =
|
||||||
attestation = json.optString("attestation", "none")
|
json.optLong("timeout", 0)
|
||||||
}
|
|
||||||
|
var excludeCredentials: List<PublicKeyCredentialDescriptor> =
|
||||||
|
json.getPublicKeyCredentialDescriptorList("excludeCredentials")
|
||||||
|
|
||||||
|
var authenticatorSelection: AuthenticatorSelectionCriteria =
|
||||||
|
json.getAuthenticatorSelectionCriteria("authenticatorSelection")
|
||||||
|
|
||||||
|
var attestation: String =
|
||||||
|
json.optString("attestation", "none")
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val TAG = PublicKeyCredentialCreationOptions::class.simpleName
|
private val TAG = PublicKeyCredentialCreationOptions::class.simpleName
|
||||||
|
|||||||
@@ -20,12 +20,33 @@
|
|||||||
package com.kunzisoft.keepass.credentialprovider.passkey.data
|
package com.kunzisoft.keepass.credentialprovider.passkey.data
|
||||||
|
|
||||||
import com.kunzisoft.encrypt.Base64Helper
|
import com.kunzisoft.encrypt.Base64Helper
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.passkey.data.PublicKeyCredentialDescriptor.Companion.getPublicKeyCredentialDescriptorList
|
||||||
import org.json.JSONObject
|
import org.json.JSONObject
|
||||||
|
|
||||||
|
// https://www.w3.org/TR/webauthn-3/#enumdef-residentkeyrequirement
|
||||||
class PublicKeyCredentialRequestOptions(requestJson: String) {
|
class PublicKeyCredentialRequestOptions(requestJson: String) {
|
||||||
val json: JSONObject = JSONObject(requestJson)
|
private val json: JSONObject = JSONObject(requestJson)
|
||||||
val challenge: ByteArray = Base64Helper.b64Decode(json.getString("challenge"))
|
|
||||||
val timeout: Long = json.optLong("timeout", 0)
|
val challenge: ByteArray =
|
||||||
val rpId: String = json.optString("rpId", "")
|
Base64Helper.b64Decode(json.getString("challenge"))
|
||||||
val userVerification: String = json.optString("userVerification", "preferred")
|
|
||||||
|
val timeout: Long =
|
||||||
|
json.optLong("timeout", 0)
|
||||||
|
|
||||||
|
val rpId: String =
|
||||||
|
json.optString("rpId", "")
|
||||||
|
|
||||||
|
val allowCredentials: List<PublicKeyCredentialDescriptor> =
|
||||||
|
json.getPublicKeyCredentialDescriptorList("allowCredentials")
|
||||||
|
|
||||||
|
val userVerification: UserVerificationRequirement =
|
||||||
|
UserVerificationRequirement.fromString(
|
||||||
|
json.optString("userVerification", "preferred"))
|
||||||
|
?: UserVerificationRequirement.PREFERRED
|
||||||
|
|
||||||
|
// TODO Hints
|
||||||
|
val hints: List<String> = listOf()
|
||||||
|
|
||||||
|
// TODO Extensions
|
||||||
|
// val extensions: AuthenticationExtensionsClientInputs
|
||||||
}
|
}
|
||||||
@@ -22,6 +22,7 @@ package com.kunzisoft.keepass.credentialprovider.passkey.util
|
|||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.content.res.Resources
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.security.keystore.KeyGenParameterSpec
|
import android.security.keystore.KeyGenParameterSpec
|
||||||
@@ -29,6 +30,10 @@ import android.security.keystore.KeyProperties
|
|||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
|
import androidx.biometric.BiometricManager
|
||||||
|
import androidx.biometric.BiometricManager.Authenticators.BIOMETRIC_WEAK
|
||||||
|
import androidx.biometric.BiometricManager.Authenticators.DEVICE_CREDENTIAL
|
||||||
|
import androidx.biometric.BiometricManager.BIOMETRIC_SUCCESS
|
||||||
import androidx.credentials.CreatePublicKeyCredentialRequest
|
import androidx.credentials.CreatePublicKeyCredentialRequest
|
||||||
import androidx.credentials.CreatePublicKeyCredentialResponse
|
import androidx.credentials.CreatePublicKeyCredentialResponse
|
||||||
import androidx.credentials.GetPublicKeyCredentialOption
|
import androidx.credentials.GetPublicKeyCredentialOption
|
||||||
@@ -55,6 +60,7 @@ import com.kunzisoft.keepass.credentialprovider.passkey.data.PublicKeyCredential
|
|||||||
import com.kunzisoft.keepass.credentialprovider.passkey.data.PublicKeyCredentialCreationParameters
|
import com.kunzisoft.keepass.credentialprovider.passkey.data.PublicKeyCredentialCreationParameters
|
||||||
import com.kunzisoft.keepass.credentialprovider.passkey.data.PublicKeyCredentialRequestOptions
|
import com.kunzisoft.keepass.credentialprovider.passkey.data.PublicKeyCredentialRequestOptions
|
||||||
import com.kunzisoft.keepass.credentialprovider.passkey.data.PublicKeyCredentialUsageParameters
|
import com.kunzisoft.keepass.credentialprovider.passkey.data.PublicKeyCredentialUsageParameters
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.passkey.data.UserVerificationRequirement
|
||||||
import com.kunzisoft.keepass.credentialprovider.passkey.util.PrivilegedAllowLists.getOriginFromPrivilegedAllowLists
|
import com.kunzisoft.keepass.credentialprovider.passkey.util.PrivilegedAllowLists.getOriginFromPrivilegedAllowLists
|
||||||
import com.kunzisoft.keepass.model.AndroidOrigin
|
import com.kunzisoft.keepass.model.AndroidOrigin
|
||||||
import com.kunzisoft.keepass.model.AppOrigin
|
import com.kunzisoft.keepass.model.AppOrigin
|
||||||
@@ -62,7 +68,9 @@ import com.kunzisoft.keepass.model.EntryInfo
|
|||||||
import com.kunzisoft.keepass.model.Passkey
|
import com.kunzisoft.keepass.model.Passkey
|
||||||
import com.kunzisoft.keepass.utils.AppUtil
|
import com.kunzisoft.keepass.utils.AppUtil
|
||||||
import com.kunzisoft.keepass.utils.StringUtil.toHexString
|
import com.kunzisoft.keepass.utils.StringUtil.toHexString
|
||||||
|
import com.kunzisoft.keepass.utils.getEnumExtra
|
||||||
import com.kunzisoft.keepass.utils.getParcelableExtraCompat
|
import com.kunzisoft.keepass.utils.getParcelableExtraCompat
|
||||||
|
import com.kunzisoft.keepass.utils.putEnumExtra
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
@@ -90,6 +98,8 @@ object PasskeyHelper {
|
|||||||
private const val EXTRA_APP_ORIGIN = "com.kunzisoft.keepass.extra.appOrigin"
|
private const val EXTRA_APP_ORIGIN = "com.kunzisoft.keepass.extra.appOrigin"
|
||||||
private const val EXTRA_TIMESTAMP = "com.kunzisoft.keepass.extra.timestamp"
|
private const val EXTRA_TIMESTAMP = "com.kunzisoft.keepass.extra.timestamp"
|
||||||
private const val EXTRA_AUTHENTICATION_CODE = "com.kunzisoft.keepass.extra.authenticationCode"
|
private const val EXTRA_AUTHENTICATION_CODE = "com.kunzisoft.keepass.extra.authenticationCode"
|
||||||
|
private const val EXTRA_USER_VERIFICATION = "com.kunzisoft.keepass.extra.userVerification"
|
||||||
|
private const val EXTRA_USER_VERIFIED_WITH_AUTH = "com.kunzisoft.keepass.extra.userVerifiedWithAuth"
|
||||||
|
|
||||||
private const val SEPARATOR = "_"
|
private const val SEPARATOR = "_"
|
||||||
|
|
||||||
@@ -106,6 +116,60 @@ object PasskeyHelper {
|
|||||||
|
|
||||||
private val internalSecureRandom: SecureRandom = SecureRandom()
|
private val internalSecureRandom: SecureRandom = SecureRandom()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add the User Verification to the intent
|
||||||
|
*/
|
||||||
|
fun Intent.addUserVerification(
|
||||||
|
userVerification: UserVerificationRequirement,
|
||||||
|
userVerifiedWithAuth: Boolean
|
||||||
|
) {
|
||||||
|
putEnumExtra(EXTRA_USER_VERIFICATION, userVerification)
|
||||||
|
putExtra(EXTRA_USER_VERIFIED_WITH_AUTH, userVerifiedWithAuth)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the User Verification from the intent
|
||||||
|
*/
|
||||||
|
fun Intent.getUserVerificationCondition(): Boolean {
|
||||||
|
return (getEnumExtra<UserVerificationRequirement>(EXTRA_USER_VERIFICATION)
|
||||||
|
?: UserVerificationRequirement.PREFERRED) == UserVerificationRequirement.REQUIRED
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Define if the User is verified with authentification from the intent
|
||||||
|
*/
|
||||||
|
fun Intent.getUserVerifiedWithAuth(): Boolean {
|
||||||
|
return getBooleanExtra(EXTRA_USER_VERIFIED_WITH_AUTH, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove the User Verification from the intent
|
||||||
|
*/
|
||||||
|
fun Intent.removeUserVerification() {
|
||||||
|
removeExtra(EXTRA_USER_VERIFICATION)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove the User verified with auth from the intent
|
||||||
|
*/
|
||||||
|
fun Intent.removeUserVerifiedWithAuth() {
|
||||||
|
removeExtra(EXTRA_USER_VERIFIED_WITH_AUTH)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Allowed authenticators for the User Verification
|
||||||
|
*/
|
||||||
|
const val ALLOWED_AUTHENTICATORS = BIOMETRIC_WEAK or DEVICE_CREDENTIAL
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the device supports the biometric prompt for User Verification
|
||||||
|
*/
|
||||||
|
fun Context.isAuthenticatorsAllowed(): Boolean {
|
||||||
|
return BiometricManager.from(this)
|
||||||
|
.canAuthenticate(ALLOWED_AUTHENTICATORS) == BIOMETRIC_SUCCESS
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add an authentication code generated by an entry to the intent
|
* Add an authentication code generated by an entry to the intent
|
||||||
*/
|
*/
|
||||||
@@ -200,6 +264,28 @@ object PasskeyHelper {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build the Passkey error response
|
||||||
|
*/
|
||||||
|
fun Activity.buildPasskeyErrorAndSetResult(
|
||||||
|
resources: Resources,
|
||||||
|
relyingPartyId: String?,
|
||||||
|
credentialIds: List<String>
|
||||||
|
) {
|
||||||
|
val error = resources.getString(
|
||||||
|
R.string.error_passkey_credential_id,
|
||||||
|
relyingPartyId,
|
||||||
|
credentialIds
|
||||||
|
)
|
||||||
|
Log.e(javaClass.name, error)
|
||||||
|
Toast.makeText(
|
||||||
|
this,
|
||||||
|
error,
|
||||||
|
Toast.LENGTH_SHORT
|
||||||
|
).show()
|
||||||
|
setResult(Activity.RESULT_CANCELED)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check the timestamp and authentication code transmitted via PendingIntent
|
* Check the timestamp and authentication code transmitted via PendingIntent
|
||||||
*/
|
*/
|
||||||
@@ -471,6 +557,7 @@ object PasskeyHelper {
|
|||||||
*/
|
*/
|
||||||
fun buildCreatePublicKeyCredentialResponse(
|
fun buildCreatePublicKeyCredentialResponse(
|
||||||
publicKeyCredentialCreationParameters: PublicKeyCredentialCreationParameters,
|
publicKeyCredentialCreationParameters: PublicKeyCredentialCreationParameters,
|
||||||
|
userVerified: Boolean,
|
||||||
backupEligibility: Boolean,
|
backupEligibility: Boolean,
|
||||||
backupState: Boolean
|
backupState: Boolean
|
||||||
): CreatePublicKeyCredentialResponse {
|
): CreatePublicKeyCredentialResponse {
|
||||||
@@ -488,7 +575,7 @@ object PasskeyHelper {
|
|||||||
keyTypeId = keyTypeId
|
keyTypeId = keyTypeId
|
||||||
) ?: mapOf<Int, Any>()),
|
) ?: mapOf<Int, Any>()),
|
||||||
userPresent = true,
|
userPresent = true,
|
||||||
userVerified = true,
|
userVerified = userVerified,
|
||||||
backupEligibility = backupEligibility,
|
backupEligibility = backupEligibility,
|
||||||
backupState = backupState,
|
backupState = backupState,
|
||||||
publicKeyTypeId = keyTypeId,
|
publicKeyTypeId = keyTypeId,
|
||||||
@@ -560,6 +647,7 @@ object PasskeyHelper {
|
|||||||
requestOptions: PublicKeyCredentialRequestOptions,
|
requestOptions: PublicKeyCredentialRequestOptions,
|
||||||
clientDataResponse: ClientDataResponse,
|
clientDataResponse: ClientDataResponse,
|
||||||
passkey: Passkey,
|
passkey: Passkey,
|
||||||
|
userVerified: Boolean,
|
||||||
defaultBackupEligibility: Boolean,
|
defaultBackupEligibility: Boolean,
|
||||||
defaultBackupState: Boolean
|
defaultBackupState: Boolean
|
||||||
): PublicKeyCredential {
|
): PublicKeyCredential {
|
||||||
@@ -568,7 +656,7 @@ object PasskeyHelper {
|
|||||||
response = AuthenticatorAssertionResponse(
|
response = AuthenticatorAssertionResponse(
|
||||||
requestOptions = requestOptions,
|
requestOptions = requestOptions,
|
||||||
userPresent = true,
|
userPresent = true,
|
||||||
userVerified = true,
|
userVerified = userVerified,
|
||||||
backupEligibility = passkey.backupEligibility ?: defaultBackupEligibility,
|
backupEligibility = passkey.backupEligibility ?: defaultBackupEligibility,
|
||||||
backupState = passkey.backupState ?: defaultBackupState,
|
backupState = passkey.backupState ?: defaultBackupState,
|
||||||
userHandle = passkey.userHandle,
|
userHandle = passkey.userHandle,
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ abstract class CredentialLauncherViewModel(application: Application): AndroidVie
|
|||||||
|
|
||||||
protected var isResultLauncherRegistered: Boolean = false
|
protected var isResultLauncherRegistered: Boolean = false
|
||||||
private var mSelectionResult: ActivityResult? = null
|
private var mSelectionResult: ActivityResult? = null
|
||||||
|
|
||||||
protected val mCredentialUiState = MutableStateFlow<CredentialState>(CredentialState.Loading)
|
protected val mCredentialUiState = MutableStateFlow<CredentialState>(CredentialState.Loading)
|
||||||
val credentialUiState: StateFlow<CredentialState> = mCredentialUiState
|
val credentialUiState: StateFlow<CredentialState> = mCredentialUiState
|
||||||
|
|
||||||
@@ -56,7 +55,7 @@ abstract class CredentialLauncherViewModel(application: Application): AndroidVie
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onDatabaseRetrieved(database: ContextualDatabase) {
|
fun onDatabaseRetrieved(database: ContextualDatabase) {
|
||||||
mDatabase = database
|
mDatabase = database
|
||||||
mSelectionResult?.let { selectionResult ->
|
mSelectionResult?.let { selectionResult ->
|
||||||
manageSelectionResult(database, selectionResult)
|
manageSelectionResult(database, selectionResult)
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import com.kunzisoft.keepass.credentialprovider.passkey.data.PublicKeyCredential
|
|||||||
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.buildCreatePublicKeyCredentialResponse
|
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.buildPasskeyPublicKeyCredential
|
||||||
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.checkSecurity
|
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.checkSecurity
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.getUserVerificationCondition
|
||||||
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.getVerifiedGETClientDataResponse
|
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.getVerifiedGETClientDataResponse
|
||||||
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.removeAppOrigin
|
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.removeAppOrigin
|
||||||
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.removePasskey
|
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.removePasskey
|
||||||
@@ -64,14 +65,16 @@ class PasskeyLauncherViewModel(application: Application): CredentialLauncherView
|
|||||||
private var mPasskey: Passkey? = null
|
private var mPasskey: Passkey? = null
|
||||||
|
|
||||||
private var mLockDatabaseAfterSelection: Boolean = false
|
private var mLockDatabaseAfterSelection: Boolean = false
|
||||||
|
private var mUserVerified: Boolean = true
|
||||||
private var mBackupEligibility: Boolean = true
|
private var mBackupEligibility: Boolean = true
|
||||||
private var mBackupState: Boolean = false
|
private var mBackupState: Boolean = false
|
||||||
|
|
||||||
private val mUiState = MutableStateFlow<UIState>(UIState.Loading)
|
private val mUiState = MutableStateFlow<UIState>(UIState.Loading)
|
||||||
val uiState: StateFlow<UIState> = mUiState
|
val uiState: StateFlow<UIState> = mUiState
|
||||||
|
|
||||||
fun initialize() {
|
fun initialize(userVerified: Boolean) {
|
||||||
mLockDatabaseAfterSelection = PreferencesUtil.isPasskeyCloseDatabaseEnable(getApplication())
|
mLockDatabaseAfterSelection = PreferencesUtil.isPasskeyCloseDatabaseEnable(getApplication())
|
||||||
|
mUserVerified = userVerified
|
||||||
mBackupEligibility = PreferencesUtil.isPasskeyBackupEligibilityEnable(getApplication())
|
mBackupEligibility = PreferencesUtil.isPasskeyBackupEligibilityEnable(getApplication())
|
||||||
mBackupState = PreferencesUtil.isPasskeyBackupStateEnable(getApplication())
|
mBackupState = PreferencesUtil.isPasskeyBackupStateEnable(getApplication())
|
||||||
}
|
}
|
||||||
@@ -149,14 +152,29 @@ class PasskeyLauncherViewModel(application: Application): CredentialLauncherView
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun launchAction(
|
||||||
|
userVerified: Boolean,
|
||||||
|
intent: Intent,
|
||||||
|
specialMode: SpecialMode,
|
||||||
|
) {
|
||||||
|
this.mUserVerified = userVerified
|
||||||
|
super.launchActionIfNeeded(intent, specialMode, mDatabase)
|
||||||
|
}
|
||||||
|
|
||||||
override fun launchActionIfNeeded(
|
override fun launchActionIfNeeded(
|
||||||
intent: Intent,
|
intent: Intent,
|
||||||
specialMode: SpecialMode,
|
specialMode: SpecialMode,
|
||||||
database: ContextualDatabase?
|
database: ContextualDatabase?
|
||||||
) {
|
) {
|
||||||
// Launch with database when a nodeId is present
|
if (intent.getUserVerificationCondition()) {
|
||||||
if ((database != null && database.loaded) || intent.retrieveNodeId() == null) {
|
if (database != null) {
|
||||||
super.launchActionIfNeeded(intent, specialMode, database)
|
onDatabaseRetrieved(database)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Launch with database when a nodeId is present
|
||||||
|
if ((database != null && database.loaded) || intent.retrieveNodeId() == null) {
|
||||||
|
super.launchActionIfNeeded(intent, specialMode, database)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -307,6 +325,7 @@ class PasskeyLauncherViewModel(application: Application): CredentialLauncherView
|
|||||||
appOrigin = appOrigin
|
appOrigin = appOrigin
|
||||||
),
|
),
|
||||||
passkey = passkey,
|
passkey = passkey,
|
||||||
|
userVerified = mUserVerified,
|
||||||
defaultBackupEligibility = mBackupEligibility,
|
defaultBackupEligibility = mBackupEligibility,
|
||||||
defaultBackupState = mBackupState
|
defaultBackupState = mBackupState
|
||||||
)
|
)
|
||||||
@@ -363,6 +382,7 @@ class PasskeyLauncherViewModel(application: Application): CredentialLauncherView
|
|||||||
appOrigin = appOrigin
|
appOrigin = appOrigin
|
||||||
),
|
),
|
||||||
passkey = passkey,
|
passkey = passkey,
|
||||||
|
userVerified = mUserVerified,
|
||||||
defaultBackupEligibility = mBackupEligibility,
|
defaultBackupEligibility = mBackupEligibility,
|
||||||
defaultBackupState = mBackupState
|
defaultBackupState = mBackupState
|
||||||
)
|
)
|
||||||
@@ -505,6 +525,7 @@ class PasskeyLauncherViewModel(application: Application): CredentialLauncherView
|
|||||||
intent = responseIntent,
|
intent = responseIntent,
|
||||||
response = buildCreatePublicKeyCredentialResponse(
|
response = buildCreatePublicKeyCredentialResponse(
|
||||||
publicKeyCredentialCreationParameters = it,
|
publicKeyCredentialCreationParameters = it,
|
||||||
|
userVerified = mUserVerified,
|
||||||
backupEligibility = passkey?.backupEligibility
|
backupEligibility = passkey?.backupEligibility
|
||||||
?: mBackupEligibility,
|
?: mBackupEligibility,
|
||||||
backupState = passkey?.backupState
|
backupState = passkey?.backupState
|
||||||
|
|||||||
@@ -94,6 +94,7 @@ object SearchHelper {
|
|||||||
callback.invoke(
|
callback.invoke(
|
||||||
SearchParameters().apply {
|
SearchParameters().apply {
|
||||||
searchQuery = query
|
searchQuery = query
|
||||||
|
searchOptions = optionsString()
|
||||||
allowEmptyQuery = false
|
allowEmptyQuery = false
|
||||||
searchInTitles = false
|
searchInTitles = false
|
||||||
searchInUsernames = false
|
searchInUsernames = false
|
||||||
|
|||||||
9
app/src/main/res/drawable/ic_clock_loader_red_24dp.xml
Normal file
9
app/src/main/res/drawable/ic_clock_loader_red_24dp.xml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:height="24dp"
|
||||||
|
android:width="24dp"
|
||||||
|
android:viewportHeight="960"
|
||||||
|
android:viewportWidth="960">
|
||||||
|
<path
|
||||||
|
android:fillColor="#E50808"
|
||||||
|
android:pathData="M480,880Q397,880 324,848.5Q251,817 197,763Q143,709 111.5,636Q80,563 80,480Q80,397 111.5,324Q143,251 197,197Q251,143 324,111.5Q397,80 480,80Q563,80 636,111.5Q709,143 763,197Q817,251 848.5,324Q880,397 880,480Q880,563 848.5,636Q817,709 763,763Q709,817 636,848.5Q563,880 480,880ZM253,707L480,480L480,160Q346,160 253,253Q160,346 160,480Q160,544 184,603Q208,662 253,707Z"/>
|
||||||
|
</vector>
|
||||||
1
app/src/main/res/resources.properties
Normal file
1
app/src/main/res/resources.properties
Normal file
@@ -0,0 +1 @@
|
|||||||
|
unqualifiedResLocale=en-US
|
||||||
@@ -767,6 +767,7 @@
|
|||||||
<string name="passkey_selection_description">Select an existing passkey</string>
|
<string name="passkey_selection_description">Select an existing passkey</string>
|
||||||
<string name="passkey_database_username">KeePassDX Database</string>
|
<string name="passkey_database_username">KeePassDX Database</string>
|
||||||
<string name="passkey_locked_database_description">Select to unlock</string>
|
<string name="passkey_locked_database_description">Select to unlock</string>
|
||||||
|
<string name="passkey_relaunch_database_description">Reauthenticate (No Device Auth)</string>
|
||||||
<string name="passkey_username">Passkey Username</string>
|
<string name="passkey_username">Passkey Username</string>
|
||||||
<string name="passkey_private_key">Passkey Private Key</string>
|
<string name="passkey_private_key">Passkey Private Key</string>
|
||||||
<string name="passkey_credential_id">Passkey Credential Id</string>
|
<string name="passkey_credential_id">Passkey Credential Id</string>
|
||||||
@@ -775,4 +776,6 @@
|
|||||||
<string name="passkey_backup_eligibility">Passkey Backup Eligibility</string>
|
<string name="passkey_backup_eligibility">Passkey Backup Eligibility</string>
|
||||||
<string name="passkey_backup_state">Passkey Backup State</string>
|
<string name="passkey_backup_state">Passkey Backup State</string>
|
||||||
<string name="error_passkey_result">Unable to return the passkey</string>
|
<string name="error_passkey_result">Unable to return the passkey</string>
|
||||||
|
<string name="error_passkey_credential_id">No passkey found with relying party %1$s and credentialIds %2$s</string>
|
||||||
|
<string name="user_verification_required">User verification required</string>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -27,6 +27,7 @@ import com.kunzisoft.keepass.database.element.node.NodeId
|
|||||||
import com.kunzisoft.keepass.model.AppOriginEntryField.isAppId
|
import com.kunzisoft.keepass.model.AppOriginEntryField.isAppId
|
||||||
import com.kunzisoft.keepass.model.AppOriginEntryField.isAppIdSignature
|
import com.kunzisoft.keepass.model.AppOriginEntryField.isAppIdSignature
|
||||||
import com.kunzisoft.keepass.model.AppOriginEntryField.isWebDomain
|
import com.kunzisoft.keepass.model.AppOriginEntryField.isWebDomain
|
||||||
|
import com.kunzisoft.keepass.model.PasskeyEntryFields.isCredentialId
|
||||||
import com.kunzisoft.keepass.model.PasskeyEntryFields.isPasskey
|
import com.kunzisoft.keepass.model.PasskeyEntryFields.isPasskey
|
||||||
import com.kunzisoft.keepass.model.PasskeyEntryFields.isRelyingParty
|
import com.kunzisoft.keepass.model.PasskeyEntryFields.isRelyingParty
|
||||||
import com.kunzisoft.keepass.otp.OtpEntryFields.isOTP
|
import com.kunzisoft.keepass.otp.OtpEntryFields.isOTP
|
||||||
@@ -176,11 +177,29 @@ class SearchHelper {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (searchParameters.searchInRelyingParty) {
|
if (searchParameters.searchInRelyingParty) {
|
||||||
if(entry.getExtraFields().any { field ->
|
val relyingParty = searchParameters.searchQuery
|
||||||
|
val credentialIds = searchParameters.searchOptions
|
||||||
|
val containsRelyingParty = entry.getExtraFields().any { field ->
|
||||||
field.isRelyingParty()
|
field.isRelyingParty()
|
||||||
&& checkSearchQuery(field.protectedValue.stringValue, searchParameters)
|
&& field.protectedValue.stringValue
|
||||||
})
|
.equals(relyingParty, ignoreCase = true)
|
||||||
return true
|
}
|
||||||
|
// Check empty to allow any credential if not defined
|
||||||
|
val containsCredentialId = if(credentialIds.isEmpty()) true
|
||||||
|
else entry.getExtraFields().any { field ->
|
||||||
|
field.isCredentialId()
|
||||||
|
&& credentialIds.any { credentialId ->
|
||||||
|
checkSearchQuery(
|
||||||
|
stringToCheck = field.protectedValue.stringValue,
|
||||||
|
searchParameters = SearchParameters().apply {
|
||||||
|
searchQuery = credentialId
|
||||||
|
caseSensitive = false
|
||||||
|
isRegex = false
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return containsRelyingParty && containsCredentialId
|
||||||
}
|
}
|
||||||
if (searchParameters.searchInNotes) {
|
if (searchParameters.searchInNotes) {
|
||||||
if (checkSearchQuery(entry.notes, searchParameters))
|
if (checkSearchQuery(entry.notes, searchParameters))
|
||||||
|
|||||||
@@ -27,6 +27,8 @@ import android.os.Parcelable
|
|||||||
*/
|
*/
|
||||||
class SearchParameters() : Parcelable{
|
class SearchParameters() : Parcelable{
|
||||||
var searchQuery: String = ""
|
var searchQuery: String = ""
|
||||||
|
// Add an optional string to search with the main search query
|
||||||
|
var searchOptions: List<String> = listOf()
|
||||||
var allowEmptyQuery = true
|
var allowEmptyQuery = true
|
||||||
var caseSensitive = false
|
var caseSensitive = false
|
||||||
var isRegex = false
|
var isRegex = false
|
||||||
|
|||||||
@@ -176,6 +176,13 @@ object PasskeyEntryFields {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Detect if the current field is a Passkey credential id
|
||||||
|
*/
|
||||||
|
fun Field.isCredentialId(): Boolean {
|
||||||
|
return name == FIELD_CREDENTIAL_ID
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Detect if the current field is a Passkey relying party
|
* Detect if the current field is a Passkey relying party
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ class SearchInfo : ObjectNameResource, Parcelable {
|
|||||||
return if (webDomain == null) null else field
|
return if (webDomain == null) null else field
|
||||||
}
|
}
|
||||||
var relyingParty: String? = null
|
var relyingParty: String? = null
|
||||||
|
var credentialIds: List<String> = listOf()
|
||||||
var otpString: String? = null
|
var otpString: String? = null
|
||||||
|
|
||||||
constructor()
|
constructor()
|
||||||
@@ -46,6 +47,7 @@ class SearchInfo : ObjectNameResource, Parcelable {
|
|||||||
webDomain = toCopy?.webDomain
|
webDomain = toCopy?.webDomain
|
||||||
webScheme = toCopy?.webScheme
|
webScheme = toCopy?.webScheme
|
||||||
relyingParty = toCopy?.relyingParty
|
relyingParty = toCopy?.relyingParty
|
||||||
|
credentialIds = toCopy?.credentialIds ?: listOf()
|
||||||
otpString = toCopy?.otpString
|
otpString = toCopy?.otpString
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,6 +63,9 @@ class SearchInfo : ObjectNameResource, Parcelable {
|
|||||||
webScheme = if (readScheme.isNullOrEmpty()) null else readScheme
|
webScheme = if (readScheme.isNullOrEmpty()) null else readScheme
|
||||||
val readRelyingParty = parcel.readString()
|
val readRelyingParty = parcel.readString()
|
||||||
relyingParty = if (readRelyingParty.isNullOrEmpty()) null else readRelyingParty
|
relyingParty = if (readRelyingParty.isNullOrEmpty()) null else readRelyingParty
|
||||||
|
val readCredentialIdList = mutableListOf<String>()
|
||||||
|
parcel.readStringList(readCredentialIdList)
|
||||||
|
credentialIds = readCredentialIdList.toList()
|
||||||
val readOtp = parcel.readString()
|
val readOtp = parcel.readString()
|
||||||
otpString = if (readOtp.isNullOrEmpty()) null else readOtp
|
otpString = if (readOtp.isNullOrEmpty()) null else readOtp
|
||||||
}
|
}
|
||||||
@@ -76,6 +81,7 @@ class SearchInfo : ObjectNameResource, Parcelable {
|
|||||||
parcel.writeString(webDomain ?: "")
|
parcel.writeString(webDomain ?: "")
|
||||||
parcel.writeString(webScheme ?: "")
|
parcel.writeString(webScheme ?: "")
|
||||||
parcel.writeString(relyingParty ?: "")
|
parcel.writeString(relyingParty ?: "")
|
||||||
|
parcel.writeStringList(credentialIds)
|
||||||
parcel.writeString(otpString ?: "")
|
parcel.writeString(otpString ?: "")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,6 +100,7 @@ class SearchInfo : ObjectNameResource, Parcelable {
|
|||||||
&& webDomain == null
|
&& webDomain == null
|
||||||
&& webScheme == null
|
&& webScheme == null
|
||||||
&& relyingParty == null
|
&& relyingParty == null
|
||||||
|
&& credentialIds.isEmpty()
|
||||||
&& otpString == null
|
&& otpString == null
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -127,6 +134,7 @@ class SearchInfo : ObjectNameResource, Parcelable {
|
|||||||
if (webDomain != other.webDomain) return false
|
if (webDomain != other.webDomain) return false
|
||||||
if (webScheme != other.webScheme) return false
|
if (webScheme != other.webScheme) return false
|
||||||
if (relyingParty != other.relyingParty) return false
|
if (relyingParty != other.relyingParty) return false
|
||||||
|
if (credentialIds != other.credentialIds) return false
|
||||||
if (otpString != other.otpString) return false
|
if (otpString != other.otpString) return false
|
||||||
|
|
||||||
return true
|
return true
|
||||||
@@ -139,6 +147,7 @@ class SearchInfo : ObjectNameResource, Parcelable {
|
|||||||
result = 31 * result + (webDomain?.hashCode() ?: 0)
|
result = 31 * result + (webDomain?.hashCode() ?: 0)
|
||||||
result = 31 * result + (webScheme?.hashCode() ?: 0)
|
result = 31 * result + (webScheme?.hashCode() ?: 0)
|
||||||
result = 31 * result + (relyingParty?.hashCode() ?: 0)
|
result = 31 * result + (relyingParty?.hashCode() ?: 0)
|
||||||
|
result = 31 * result + (credentialIds.hashCode())
|
||||||
result = 31 * result + (otpString?.hashCode() ?: 0)
|
result = 31 * result + (otpString?.hashCode() ?: 0)
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
@@ -147,6 +156,10 @@ class SearchInfo : ObjectNameResource, Parcelable {
|
|||||||
return otpString ?: webDomain ?: applicationId ?: relyingParty ?: tag ?: ""
|
return otpString ?: webDomain ?: applicationId ?: relyingParty ?: tag ?: ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun optionsString(): List<String> {
|
||||||
|
return if (isPasskeySearch && credentialIds.isNotEmpty()) credentialIds else listOf()
|
||||||
|
}
|
||||||
|
|
||||||
fun toRegisterInfo(): RegisterInfo {
|
fun toRegisterInfo(): RegisterInfo {
|
||||||
return RegisterInfo(this)
|
return RegisterInfo(this)
|
||||||
}
|
}
|
||||||
|
|||||||
4
fastlane/metadata/android/en-US/changelogs/150.txt
Normal file
4
fastlane/metadata/android/en-US/changelogs/150.txt
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
* Manual change of app language #1884 #1990
|
||||||
|
* Add Passkey User Verification #2283
|
||||||
|
* Fix autofill username detection #2276
|
||||||
|
* Fix Passkey in passwordless mode #2282
|
||||||
4
fastlane/metadata/android/fr-FR/changelogs/150.txt
Normal file
4
fastlane/metadata/android/fr-FR/changelogs/150.txt
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
* Changement manuel de la langue de l'appli #1884 #1990
|
||||||
|
* Ajout de la Verification Utilisateur pour Passkey #2283
|
||||||
|
* Correction de la détection du nom d'utilisateur pour le remplissage auto #2276
|
||||||
|
* Correction de Passkey en mode passwordless #2282
|
||||||
Reference in New Issue
Block a user